Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implot v0.9 binding #2

Open
wants to merge 2 commits into
base: upgrade/v1.79
Choose a base branch
from
Open

Conversation

hinxx
Copy link

@hinxx hinxx commented Mar 23, 2021

I'm just throwing this out here. This is supposed to be cython baed ImPlot (https://github.com/epezent/implot) binding for python. It is based off pyimgui in upgrade/v1.79 branch (v1.82 at this point).

I don't really know where to ask the questions about how to proceed with this; going for pyimplot as a separate project or offering it as an inclusion into pyimgui. Referencing the first mention of this in pyimgui#192 (comment).

Screenshot_2021-03-23_15-37-28

Read on if you are interested..

    **Warning: This is alpha quality code.**

Have a look at the doc/examples/integrations_glfw3_implot.py that includes ImPlot
demo. Note that the demo was NOT ported to python yet.

It is not clear to me if this is the best place for ImPlot python binding. The
question remains if it would be better to place this code into a separate project,
or provide it as part of pyimgui. As of now the latter has been chosen due to my
laziness, poor undestanding of cython and also due to some obstacles
encountered during the process of creating this binding.

Read on if you are interested in couple of details.

As per ImGui and ImPlot documentation using them both as shared library is not recomended.
In case of python binding this is exactly what is being done. The ImGui part ends up being
a shared library and plot becomes a separate shared library. Creating a single shared library
(with ImGui and ImPlot) does not sound like a good idea at all so lets not go there. Not going
with shared library is also something that we can not do; AFAICT due to the way cypthon does
its business (I might be wrong).

At the level of C++ that cython wraps into python module and functions, ImPlot want access
to imgui.h and imgui_internal.h. For example ImPlot uses ImTextureID, ImGuiCond,
ImGuiMouseButton, ImGuiKeyModFlags, ImGuiDragDropFlags, ImU32, ImDrawList, ImGuiContext,
ImVec2, ImVec4 and alike. These need to be exposed a cython level, too. Currently,
these come from cimgui and internal modules provided by pyimgui binding. Using them is
as simple as adding a cimport line to a cimplot.pxd. pyimgui (c)imports were requiered for
plot.pyx as well:

    cimport cimplot
    cimport cimgui
    cimport core
    cimport enums

If the ImPlot is placed in a separate project/repo these would need to be redefined.

When I tried to compile the ImPlot C++ code for cython binding (without adding ImGui sources)
for that shared library, doing import imgui in a user script fails with GImGui being undefined.
Have I missed something there during the library build?! I don't know.

If the ImPlot binding is separate from ImGui binding (cleanest approach), I'm not sure how
the user script would behave if pyimgui and pyimplot would be based of different ImGui C++ code.
Might be a non-issue, but I just do not know.

Have any ideas or suggestions on the above topics? Bring them up, please!

@KinoxKlark
Copy link
Owner

It looks awesome! I will try it when I'll have a little time ;)

Concerning the big question of releasing it as a separate project or merging it in pyimgui, I am not sure of the answer. In my humble opinion, it makes more sense to have them as separate projects so that the interested user can choose to take it or not. But as you pointed out this leads to lots of questions concerning the sharing of code at cython level. It is an important question and it feels really similar to the one about the 'docking' branch of DearImGui.

For now, in this upgrade branch, we have two submodules: pyimgui.core (which is import * in pyimgui namespace) as the main ImGui API and pyimgui.internal that contains a mapping of requested features from imgui_internal.h. If I recall it correctly both are compiled separately and thus both contain a separate statically linked version of DearImGui. This is suboptimal but I didn't find how to work around cython properly, I may invest more time in it in the future. Since both modules rely on different DearImGui codes, they each own their GImGui pointer to the context. I believe this is the reason for the failure you described.

In order to be sure that each submodule constantly own a pointer to the correct context we have to propagate it from core to other submodules using a C function: cdef UpdateImGuiContext(cimgui.ImGuiContext* _ptr). This function is define in the submodule (here only in internal as internal.UpdateImGuiContext(ptr) but if more submodules are added they each should have one). This function is called every time the context is updated, i.e. in mapped function create_context(), destroy_context(), and set_current_context().

Having two copies of DearImGui is clearly a waste of memory and does not scale if we want to add more submodules, but I couldn't see a better way to deal with it at the time. This may change in the future.

I wonder if we could keep a centralized statically linked version of DearImGui and provide a way to point to it. In that case, submodules could ask it and, if we make it public, external modules will be able too. This raise question about how to implement this using cython and how to deal with DearImGui context management without starting a callback war.

I truly don't know what is best, these are my thoughts for now and any comment is welcome. I will try to invest time in that reflection. If the need for external addons like pyplot is here, we shall think about it!

@hinxx
Copy link
Author

hinxx commented Mar 24, 2021

@KinoxKlark thank you for your input!

But as you pointed out this leads to lots of questions concerning the sharing of code at cython level. It is an important question and it feels really similar to the one about the 'docking' branch of DearImGui.

Exactly this: sharing code on cython level, between projects, seems the main point of congestion, IMO. I'm not aware docking branch issue and how it relates, though.

For now, in this upgrade branch, we have two submodules: pyimgui.core (which is import * in pyimgui namespace) as the main ImGui API and pyimgui.internal that contains a mapping of requested features from imgui_internal.h.

Curious: why are the internals exposed over cython?

In order to be sure that each submodule constantly own a pointer to the correct context we have to propagate it from core to other submodules using a C function: cdef UpdateImGuiContext(cimgui.ImGuiContext* _ptr). This function is define in the

ImPlot has this on C++ level and I had to use it in order to make things work. In the doc/examples/integrations_glfw3_implot.py I do this:

    ctx = imgui.create_context()
    imgui.plot.create_context()
    imgui.plot.set_imgui_context(ctx)

Without this ImPlot reference to ImGui context remains NULL and segfaults are imminent.

I wonder if we could keep a centralized statically linked version of DearImGui and provide a way to point to it. In that case, submodules could ask it and, if we make it public, external modules will be able too. This raise question about how to implement this using cython and how to deal with DearImGui context management without starting a callback war.

If I imagine that the ImPlot would be a separate project, IMHO it means that access to cython parts of the pyimgui are off the table. Then I would expect that ImGui and ImPlot C++ sources need to be compiled, and cherry picked core, enums and possibly internals from pyimgui would need to be replicated (not sure how big of an amount).

Since the ImGui is not usually built into a shared library (i.e. for distribution) neither pyimgui nor pyimplot can avoid compiling ImGui separately into their binding. Do we want to postulate that the ImGui would need to be provided as a (system wide) shared library for these two bindings to work? I would guess not. The use cases for ImGui are quite different from other GUI toolkits.

OTOH, I'm also trying to understand how would pyimplot be able to access ImGui shared library if it would be provided by the pyimgui, instead of having it system wide. If this would somehow work, we would cut out lots of code duplication since ImGui core and internals would be in a form of a shared library. This still does not solve the access to pyimgui cython parts that pyimplot might need.

I truly don't know what is best, these are my thoughts for now and any comment is welcome. I will try to invest time in that reflection. If the need for external addons like pyplot is here, we shall think about it!

I can think of file dialogs, or some other addons that are kept outside of ImGui, but you're probably aware of those yourself. Not that these are something I actually use today in python, so no hurry there.

As an exercise, I'll try to split the ImPlot from pyimgui as seen in this PR, just to see how that would look like. I'm also intrigued to see if bundling shared ImGui with pyimgui would be of benefit for pyimplot. Last but not least, I need educate myself if cython code sharing between modules/projects is a thing to pursue.

Thank you for your time!

@hinxx
Copy link
Author

hinxx commented Mar 24, 2021

Splitting the ImPlot out of the pyimgui here. Looks promising. I had to define _ImGuiContext (copied the code from pyimgui over to pyimplot) in order to get the ImPlot::SetImGuiContext(ImGuiContext* ctx) work.

This is what happens:

$ python3
Python 3.9.2 (default, Mar 12 2021, 13:13:11) 
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import imgui
>>> import implot
>>> ctx = imgui.create_context()
>>> implot.create_context()
<implot.plot._ImPlotContext object at 0x7fe3807c4e90>
>>> implot.set_imgui_context(ctx)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Argument 'ctx' has incorrect type (expected implot.plot._ImGuiContext, got imgui.core._ImGuiContext)

This is similar to what you have done with UpdateImGuiContext(cimgui.ImGuiContext* _ptr). In my case the problem arises due to ImGuiContext being implemented in ImGui cython, but needs to be known to ImPlot as such, too.

Any ideas on how to overcome this?

@KinoxKlark
Copy link
Owner

Curious: why are the internals exposed over cython?

Some specific internal manipulation where requested. With official dearimgui it is possible to disable fields using internal flags. The internal exposition has been done to offer this possibility to python users, a detailed example here: pyimgui#192 (comment). Clearly, not all internals should be mapped, I think the idea was to start the foundations of submodules separation so that it would become possible to map other things as needed. Before that, there was no submodules separation.

This is similar to what you have done with UpdateImGuiContext(cimgui.ImGuiContext* _ptr). In my case the problem arises due to ImGuiContext being implemented in ImGui cython, but needs to be known to ImPlot as such, too.

Any ideas on how to overcome this?

I think it would require some adjustment on the pyimgui side but it seems feasible. The idea would be to extract the declaration of _ImGuiContext() in a .pxd file, then in your external module implementation (.pyx) you should be able to cimport and import pyimgui and get access to this class.

I designed a quick test to check the concept: I created two folders A/ and B/ each containing a basic cython setup.

My file tree looks like this:

A/setup.py
A/module_a.pxd
A/module_a.pyx
B/setup.py
B/module_a.pxd # only containing declaration
B/module_b.pyx

I had to copy the module_a.pxd containing declaration in both folders to give access to it via cimport. Note that it does not contain any implementation, only declaration. Maybe it could be kept as a separate dependency.

For reference here are my test files:

# A/setup.py

from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize(["module_a.pyx"]))
# A/module_a.pxd
cdef class CommonContext:
    cdef int ctx
# A/module_a.pyx
cdef class CommonContext:
    def __cinit__(self, int ctx):
        self.ctx = ctx

def create_context():
    cdef CommonContext cc = CommonContext(42)
    print("Context from A:", cc.ctx)
    return cc
# B/setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize(["module_b.pyx"]))
# B/module_b.pyx
cimport module_a
import module_a   # necessary to find the .pyd

def set_context(module_a.CommonContext cc):
    print("Context from B", cc.ctx)

I then compiled both projects. Note that for convenience I manually copied the compiled .pyd module_a to B/ folder in order to be able to import it.

A quick python test gives:

>>> import module_a as A
>>> import module_b as B
>>> ctx = A.create_context()
('Context from A:', 42)
>>> B.set_context(ctx)
('Context from B', 42)

Thus, the CommonContext() class is only cdefined in module_a and compiled only once. module_b only needs the declaration and directly call the definition in module_a which is not part of module_b. (Here again I manually copied both .pyd in the folder where I run the command to have access to the modules without installing them.)

This may not solve the static library duplication problem but it seems that it could help with cython specific sharing declaration. I will spend more time in the coming days investigating this.

What are the pyimgui cython definition that you may need to access other than _ImGuiContext()? Maybe we can come up with a clean way of sharing specific data and context between pyimgui and external extensions like implot. I'll try to gather my ideas and propose something when it gets clearer.

@hinxx
Copy link
Author

hinxx commented Mar 24, 2021

Thanks for the help. I can run the example and see it work as advertised.

I added this to core.pxd in pyimgui:

cdef class _ImGuiContext(object):
    cdef cimgui.ImGuiContext* _ptr

    @staticmethod
    cdef from_ptr(cimgui.ImGuiContext* ptr)

Commented out the cdef cimgui.ImGuiContext* _ptr from cdef class _ImGuiContext(object): in core.pyx. Then I rebuilt the pyimgui.
Added core.pxd, to pyimplot and changed the plot.pyx set_imgui_context() to take core._ImGuiContext ctx as an argument. Also added from imgui import core to plot.pyx. Then I rebuilt the pyimplot.
The implementation of the _ImGuiContext class is only in pyimgui this time.
Copied the *.py and *.so (I'm on Linux) to newly created imgui and implot subfolders, in a new top folder. I also copied core.cpython-39-x86_64-linux-gnu.so file into the implot folder so that it can be imported.

Files/folders:

top/imgui/_compat.py
top/imgui/core.cpython-39-x86_64-linux-gnu.so
top/imgui/extra.py
top/imgui/__init__.py
top/imgui/integrations/
top/imgui/internal.cpython-39-x86_64-linux-gnu.so

top/implot/core.cpython-39-x86_64-linux-gnu.so
top/implot/__init__.py
top/implot/plot.cpython-39-x86_64-linux-gnu.so

Results:

$ python
Python 3.9.2 (default, Mar 12 2021, 13:13:11) 
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import imgui
>>> import implot
>>> ctx = imgui.create_context()
>>> implot.create_context()
<implot.plot._ImPlotContext object at 0x7fcc55c7e350>
>>> implot.set_imgui_context(ctx)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Argument 'ctx' has incorrect type (expected imgui.core._ImGuiContext, got imgui.core._ImGuiContext)

The names of the argument now match, but python seems confused and still thinks they are not the same :) ?! Need to dig deeper ..

@KinoxKlark
Copy link
Owner

I think I will need to look at it and play with it. Can you push this here or in a branch of your fork please? (Sorry if you already did, I didn't found it)

@hinxx
Copy link
Author

hinxx commented Mar 25, 2021

I've just pushed the code to https://github.com/hinxx/pyimplot. Also, https://github.com/hinxx/pyimgui/tree/changes-for-pyimplot holds the changes that are required for pyimplot to see _ImGuiContext.

What are the pyimgui cython definition that you may need to access other than _ImGuiContext()?

You can see them in the pyimplot's cimgui.pxd.

@KinoxKlark
Copy link
Owner

Thanks! I looked at it and it seems that you need to cimport the imgui.core module instead of simply core. No need to copy .so here since pyimgui is installed in your environment when compiled. And, my bad, it seems that the copy of the .pxd is not necessary and declarations are directly imported from the module.

I created a PR on your repo with my changes. Let me know if there are any issues! (Here for ref: hinxx/pyimplot#1)

If this is working, we now have to deal with context updates. ImGui context can be destroyed, created, or changed at any time. One option is to let the user manually signal the change in context. As I understand it is already the case for cpp pyplot and cpp imgui. Unfortunately this leads to the question of dealing with context destruction and pointer access violation. Another option would be to hide this from the user and register callbacks from imgui to implot.

Another question that should not be discarded is: How to manage pyimgui version requirements correctly? For now, any version of pyimgui can be used but (correct me if I am wrong) only one of them is used for compilation. This would imply that pyimplot should require exactly this version (e.g. in requirements.txt).

Note also that this seems to solve the linking problem between pyimgui and pyplot but you still have a copy of DearImGui compiled in your module.

@hinxx
Copy link
Author

hinxx commented Mar 25, 2021

I created a PR on your repo with my changes. Let me know if there are any issues!

As noted in hinxx/pyimplot#1 (comment) your fix solved lots of issues in one sweep. NICE! Thank you again! 👍

If this is working, we now have to deal with context updates. ..

You're way ahead of me on this one, and I sounds it needs addressing sooner or later. I'm assuming same context for the time being. Is there a simple use case you can describe when context would change and for what reason?

Another question that should not be discarded is: How to manage pyimgui version requirements correctly?

That was one of my early open questions. It might be that it does not matter (to an extent) if the versions differ, because both bindings carry their own compiled ImGui C++ source. I guess where it might break is if pyimgui passes over to pyimgui a data type that is different, and similar situations. Definitely needs more insight and addressing. Would use of ImGui shared library solve the issue? Some thoughts below.

Note also that this seems to solve the linking problem between pyimgui and pyplot but you still have a copy of DearImGui compiled in your module.

Right.

Working on this TypeError issue on my side I went and created libimgui.so and libimplot.so from C++ and used them instead of compiling C++ twice. I had to add GImGui to the implot.cpp in a hacky way to avoid missing symbol during import implot. If this approach were polished a bit it might allow us to use the same and single ImGui C++ code for both bindings at compile and runtime. At the moment it looks promising. New question: Should the pre-built shared libimgui.so and libimplot.so objects be distributed to the end user? Probably need to still allow building from source as well..

Let me play around with this a bit more and report back!

@hinxx
Copy link
Author

hinxx commented Mar 25, 2021

Here is the new branch for changes in pyimgui https://github.com/hinxx/pyimgui/tree/use-libimgui-so that uses shared library libimgui.so.
Accompanying hinxx/pyimplot@160f4c8 does the same for pyimplot. It also uses to pyimgui's libimgui.so to avod re-compiling the ImGui sources. It still needs those sources to build libimplot.so, though.

Both bindings still checkout the sources and compile them into a shared library that is placed into same folder as cython output. I had to export LD_LIBRARY_PATH before starting the python demo script (we can probably make this go away). Another pending optimization is the pyimplot setup.py's ability to figure out the path to pyimgui where the libimgui.so is located; currently hardcoded.

In the process I managed to get rid of all the pyimgui pxd file copies initially introduced to pyimplot.

Tested with the doc/examples/integrations_glfw3.py from the pyimplot that shows ImGui and ImPlot demo windows.

@hinxx
Copy link
Author

hinxx commented Mar 25, 2021

Introduced changes to pyimgui https://github.com/hinxx/pyimgui/tree/use-libimgui-so to void the need for setting the LD_LIBRARY_PATH with location of libimgui.so for python scripts. Same changes were made in pyimplot repo as well.

Additional changes in pyimplot commit hinxx/pyimplot@2b0db86 allow locating path to pyimgui provided libimgui.so used at cython compile time.

Tested with the doc/examples/integrations_glfw3.py from the pyimplot that shows ImGui and ImPlot demo windows.

@hinxx
Copy link
Author

hinxx commented Mar 26, 2021

If this is working, we now have to deal with context updates.

Looking at this now. I did not find many uses of the SetImGuiContext, except in ImGuiNET/ImGui.NET#218. AFAICT, those folks seem to be doing the manual context updates.

Here, I'm assuming the script has control of both contexts and knows when they are being created/destroyed. It feels natural that the developer would take of the context updating explicitly. I'm trying to think of a use case where something more sophisticated / automated would be required, as opposed to manual context setting, but I'm coming up with nothing.

Another option would be to hide this from the user and register callbacks from imgui to implot.

Can there exist multiple ImGui contexts in an app (script)? If so, could such "under the hood" pyimplot context changes lead to undesired effects?
Otherwise, with a single pyimgui context, having pyimgui make a callback to pyimplot might be an added value for the end user.

@KinoxKlark
Copy link
Owner

Is there a simple use case you can describe when context would change and for what reason?

I have no direct example but I would have thought when you want to manage different windows you would need to manage different contexts for different windows. (Just a thought, not a piece of actual knowledge, I may be wrong about that.)

I will need to go deeper into this to have a better idea. Hereafter a rapid search I found this: ocornut/imgui#586 (comment) (not sure if it can help the conversation but I drop them anyways).

I guess where it might break is if pyimgui passes over to pyimgui a data type that is different

That is my concern, if we don't know if the compiled Dear ImGui version is the same we can't be sure that the context we pass has the exact same definition. It may be updated in the future and using the newer version from the older code could lead to unexpected behavior.

Would use of ImGui shared library solve the issue?

I would say so. If ImGui was proposed as a shared library then we would be sure that everybody "speaks the same language".

I did not have time yet to test your separation in a shared library. I hope to be able to check it at the beginning of next week.

I was wondering if, instead of having a purely extern shared library, we couldn't compile ImGui only in imgui.core and cimport it from the other modules (imgui.internals or implot). I don't know if it is possible and I want to play a bit with that.

By compiling your module with a cimport of imgui.core it is like if imgui.core was used by your script as a dynamically linked library (correct me if I am wrong about that). Thus in a sense, you already are dependent on pyimgui version.

I don't know if it can help but in imgui.cpp before the definition of the global context you can read this:

// DLL users:
// - Heaps and globals are not shared across DLL boundaries!
// - You will need to call SetCurrentContext() + SetAllocatorFunctions() for each static/DLL boundary you are calling from.
// - Same apply for hot-reloading mechanisms that are reliant on reloading DLL (note that many hot-reloading mechanism works without DLL).
// - Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
// - Confused? In a debugger: add GImGui to your watch window and notice how its value changes depending on your current location (which DLL boundary you are in).

// Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be != NULL.
// - ImGui::CreateContext() will automatically set this pointer if it is NULL.
//   Change to a different context by calling ImGui::SetCurrentContext().
// - Important: Dear ImGui functions are not thread-safe because of this pointer.
//   If you want thread-safety to allow N threads to access N different contexts:
//   - Change this variable to use thread local storage so each thread can refer to a different context, in your imconfig.h:
//         struct ImGuiContext;
//         extern thread_local ImGuiContext* MyImGuiTLS;
//         #define GImGui MyImGuiTLS
//     And then define MyImGuiTLS in one of your cpp file. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword.
//   - Future development aim to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586
//   - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from different namespace.
// - DLL users: read comments above.

Here, I'm assuming the script has control of both contexts and knows when they are being created/destroyed. It feels natural that the developer would take of the context updating explicitly. I'm trying to think of a use case where something more sophisticated / automated would be required, as opposed to manual context setting, but I'm coming up with nothing.

I totally agree, context management responsibility should be given to the user. The problem is that (for now, in a non-shared version) the context has to be pass to each submodules. Changing the context in imgui.core have to update the context in imgui.internals since both shouldn't have different context as they should be seen as a unique module mapping Dear Imgui. It may indeed not be as important in the case of an external module like implot.

I really have no good insight for now and all this seems a bit blurry to me. I will need to dive deeper into it and do some tests. I also will try your shared solution.

Thanks for your help!

@hinxx
Copy link
Author

hinxx commented Mar 26, 2021

I was wondering if, instead of having a purely extern shared library, we couldn't compile ImGui only in imgui.core and cimport it from the other modules (imgui.internals or implot). I don't know if it is possible and I want to play a bit with that.

Interesting. This might work.
FYI, I already managed to "re-use" pycimgui pxd files, without having a copy of them, by doing:

cimport imgui.cimgui as cimgui
cimport imgui.core as core
cimport imgui.enums as enums

in plot.pyx. So now there are no pyimgui pxd files in pyimplot (except for common.pxd).

By compiling your module with a cimport of imgui.core it is like if imgui.core was used by your script as a dynamically linked library (correct me if I am wrong about that). Thus in a sense, you already are dependent on pyimgui version.

Sounds sane.
The thing is that when ImPlot needs to be compiled with cython, it needs ImGui sources, or at least headers. I think imgui.core shared object can not be utilized at that stage. If ImPlot is compiled into a shared library (i.e. libimplot.so), only ImGui headers are required; still creates dependency on the ImGui C++ code base that might differ compared to what pyimgui uses.

I did not have time yet to test your separation in a shared library. I hope to be able to check it at the beginning of next week.

No worries. Take you time!

Note to self. We are tackling couple of items:

  • avoid duplication of code on C++ level (by recompiling the C++ for each module),
  • avoid duplication of code on cython level (by not copying pxd files from one module to another),
  • make sure pyimgui and pyimplot use the same C++ and cython level API/code base,
  • NEW: make pyimgui share C++ headers for pymplot build (avoids pyimplot cloning ImGui source); assuming that libimgui.so shared library is already distributed with pyimgui

@KinoxKlark
Copy link
Owner

Hey! I didn't forget you but I have been a little bit overbooked with some projects at university. Sorry for the lack of news. I couldn't check it yet. Any news on your side?

@hinxx
Copy link
Author

hinxx commented Apr 19, 2021

I was literally thinking of dropping you a line on this subject today! 😆 Hope your university projects went OK!

I have not been looking into this for the last three weeks either. I'll need to remind myself of the state of the matters this week again. I recall last thing that I was doing is addressing the last bullet point:

NEW: make pyimgui share C++ headers for pymplot build (avoids pyimplot cloning ImGui source); assuming that libimgui.so shared library is already distributed with pyimgui

I've copied the ImGui C++ headers to installation folder of pyuimgui and was able to use them from pyimplot thus avoiding the whole ImGui rebuild. With this and the use of libimgui.so shared ImGui object, also from pyimgui, I feel like the proof of concept was achieved. The methods are crude but the approach works.

Essentially all four bullet points were addressed and I think all the changes for that are in my fork/branch and in pyimplot repo. Can you take a look at that? I'll have some time this week to revive this effort, too.

@KinoxKlark
Copy link
Owner

I checked your implementation and it sounds promising. Unfortunately, I work on windows and I didn't try to adapt the compilation to test it properly. We can try to set up different compilation pipelines to compile the dll on various platforms but I must admit that I am not very fond of compiling it with a separate pipeline. It seems to be a lot of work to ensure that it will always work on all targeted platforms simultaneously as the pyimgui module compilation.

Having it in an external dll also raises the question of distribution. I believe we could include the dll directly into the package using the 'package_data' field of 'setup()' function.

I think that it would be nicer if imgui was directly compiled into a 'core' pyimgui and accessed as an external library from other modules. Thus I took time to dig a bit into that. Here is what I have learned:

Cython offers the possibility to 'cimport' C declaration from other modules. As I understand it, it is equivalent to 'cimport' the module '.pxd' if it exists. Additional '.pxd' files can be made accessible by including them in the module using 'package_data' from 'setup()'. As you probably know, '.pxd' acts as a header file for cython. If I have a module 'A' with some C declaration in 'A.pxd' and the appropriate implementation in 'A.pyx', then, once compiled in a dll and packed as an importable module I can 'cimport' any C declaration of 'A.pxd' into another module 'B', let's say 'B.pyx'. Here 'B' can be compiled and packed and it will refer to the implementation of 'A' without duplicating the implementation. (This is what we already explore before for the context management from Cython).

The problem here is that only the C functions implemented in the 'A.pyx' are correctly linked and available from external modules. If you 'cdef extern "xxx.h"' a function in 'A.pxd' and compile the module 'A' statically linking the correct '.c' files you will be able to use this function in that compilation context (e.g. in 'A.pyx'), but not in external module 'B'. To be precise, in 'B' you will be able to 'cimport' the 'A.pxd' containing the prototype of the function and you will be able to use it (which is nice if this is the prototype of a class that you only want to make reference to), but as soon as you try to call it will crash as it can't find the function implementation, even if the implementation is compiled in 'A'.

Fortunately, there is a way of making it works. What should be understood is that the 'cdef extern "xxx.h"' does not create à conventional C declaration but defer an '#include' which will include the desired declaration from 'xxx.h'. It can in fact also be used on 'xxx.c' to force the inclusion of the implementation. The idea is thus to do a 'cdef extern "xxx.c"' into the '.pyx' (and not the '.pxd') forcing the external C function to be compiled inside the module 'A' as any 'cdef' cython function. To be able to import it in another module we have to declare it in 'A.pxd' as any 'cdef' cython function (Not as an 'extern' anymore!). Doing so, module 'A' can be 'cimport' from module 'B' and the external C function can be referred to and called in 'B' through 'A'. (For reference, here is where I found this trick: https://docs.cython.org/en/latest/src/userguide/external_C_code.html#implementing-functions-in-c)

For this to work the C function in 'xxx.c' has to be declared as 'static'.

Thus it should be possible to compile a unique version of imgui into the 'pyimgui.core' (or another submodule, to be discussed) and use directly pyimgui as the dll for accessing it in external modules. This can also be used to clean the code duplication in different submodules of pyimgui such as 'pyimgui.internals'. This is a proof of concept and it definitely needs a more careful investigation on how to do it for pyimgui. Unfortunately, that is all the time that I have this week and this will have to wait a little bit from my side.

I hope this is helpful!

@hinxx
Copy link
Author

hinxx commented Apr 23, 2021

I think I understand your idea behind going with 'cdef extern "xxx.c". It sounds plausible, but I have no experience in such approach.

For this to work the C function in 'xxx.c' has to be declared as 'static'.

I guess this would require some changes to original ImGui sources? Maybe just defining IMGUI_API to hold static is enough.

Having it in an external dll also raises the question of distribution. I believe we could include the dll directly into the package using the 'package_data' field of 'setup()' function.

Yes, this was my intent, too.

We can try to set up different compilation pipelines to compile the dll on various platforms but I must admit that I am not very fond of compiling it with a separate pipeline. It seems to be a lot of work to ensure that it will always work on all targeted platforms simultaneously as the pyimgui module compilation.

What is meant here with the 'separate pipeline'? Are you referring to adding another binary wheel in addition to those already supported by ie. https://pypi.org/project/imgui/? Or are you referring to some kind of CI pipeline?

This is kind of related to the distribution, in my opinion, let's see if I understand this how the shared library concept would work.
A regular Linux / Windows / OSX user might be more inclined to use a binary pyimgui wheel from pip. A more advanced Linux user might be prepared to go with source and compile the package using Cypthon, instead. What I'm proposing, with compiling ImGui as shared library, would require all users to compile the pyimgui or that pip to hold a truck load of prebuild wheels to accommodate all the different OS / environments. Not appealing, one way or the other, I agree.

Is there a middle ground?

If we imagine that pyimgui wheels distributed the precompiled ImGui shared library for a handful of OS / python combinations (as it does now), would that be acceptable to you? If the wheel is missing user needs to locally build the pyimgui using cython. No changes there, so far, except for adding a dll/so to the binary package.

Any depending modules, like pyimplot for example, would highly likely need to specify which pyimgui release they need to use (and implicitly which ImGui C++ code / API). Those external modules would need to setup their CI scripts such that their binary wheels are build with correct pyimgui / python (this is also the case is not using shared shared libraries). External modules are free to provide a handful of binary wheels, just as in case with pyimgui. For the missing OS / python combinations or specific pyimgui release (without a binary wheel) local compilation is required.

Anyway, I'm willing to go with whatever makes our lives easier and we should definitely explore the path you propose!

@hinxx
Copy link
Author

hinxx commented Apr 29, 2021

I took some time to quickly hack the pyimgui code as you proposed. I was able to run the demo doc/examples/integrations_glfw3.py on Linux at this stage. This is only proof-of-concept code change to move towards 'embedding' the imgui.cpp C++ code. I've left out the 'internal' part altogether. I have yet to try to see how this approach can be utilized for pyimplot.

See https://github.com/hinxx/pyimgui/tree/embedd if you're interested.

EDIT: As soon as I started adding declarations to *.pxd for pyimplot to use I seem to be getting to the hard part; the C++ functions need to be declared static.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants