clj-python / libpython-clj

Python bindings for Clojure
Eclipse Public License 2.0
1.08k stars 68 forks source link

Integration with 3rd application python interpreters (e.g. blender) #94

Closed AlexAti closed 3 years ago

AlexAti commented 4 years ago

(I was looking forward to control blender with clojure, so the following discussion is blender-centered, but a lot of tools integrate python for scripting, such as krita or freecad, and might benefit from something like this.)

Blender integrates its own python interpreter, and I found out that an external prompt can be provided in the cli through:

/usr/bin/blender --python-console

Though when I tried to leverage it through the following code:

(py/initialize! :program-name "blender" :python-executable "/usr/bin/blender --python-console") (require-python 'bpy) ; fails

The bpy module could not be found by require-python. I've tried to look into libpython-clj internals to see if this was something easy to enable, but I'm a little bit lost. Replicating the query from libpython-clj.python.interpreter/python-system-data, blender returns:

{"platform": "linux", "prefix": "/usr", "base_prefix": "/usr", "executable": "/usr/bin/blender", "base_exec_prefix": "/usr", "exec_prefix": "/usr", "version": [3, 7, 5]}

It seems that with this, libpython-clj is unable to find a suitable place to attach to blender (possibly because blender's own python is part of the executable? I don't know). What is clear is that any parameter such as --python-console is lost there.

Could you please point me to how could I make this work? Happy to dedicate some time to it but if someone more experienced says this is going to need extensive refactoring of the lib I will cut my losses now.

(An alternative route is to recompile blender from scratch to be used as a module, but not everyone can recompile and install random software, it does not enable the GUI so it limits combining clojure and GUI, and I am personally experiencing issues on compiling it as a module, seems that works with 2.79 but not 2.8. Having libpython-clj be powerful/flexible enough for this scenario would be awesome.)

cnuernber commented 4 years ago

Hey, I can totally understand wanting to use blender and clojure. I think you are in for it though.

First, libpython-clj only works with python >= 3.5, and most likely >= 3.6.

Second, libpython-clj specifically is looking for a shared library it can load (it may be finding it) and then going from there. I am not sure how blender works but they may compile everything into the executable. Conda does this, for instance.

So this could be a tough road although it is a good fit in theory. Clojure would be a great way to automate some blender workflows.

jjtolton commented 4 years ago

Ahh this seems like such a cool idea! If you're looking for something practical, cut your losses. But if you want to shave some yacks, this could be a great time! Check out this function here: https://github.com/clj-python/libpython-clj/blob/master/src/libpython_clj/python/interpreter.clj#L29

make sure that

/usr/bin/blender --python-console -c <(cat << eof
import sys, json
print(json.dumps(
{'platform':          sys.platform,
  'prefix':           sys.prefix,
  'base_prefix':      sys.base_prefix,
  'executable':       sys.executable,
  'base_exec_prefix': sys.base_exec_prefix,
  'exec_prefix':      sys.exec_prefix,
  'version':          list(sys.version_info)[:3]}))
eof
)

returns something sensical from the console. If that doesn't work, nothing else will!
I would also test to make sure that libpython-clj even works on Python 3.4 individually. If Python3.4 works there is a small hope that maybe you can get something going :)

Again, practical? No. But think of the adventure!

jjtolton commented 4 years ago

Okay I did some investigation because I was curious. Unfortunately blender is going the "wrong way" for libpython-clj :( :( :(

Blender is a C program that starts a Python interpreter. The only way to make this work would be for Blender to start a JVM which would execute Clojure and from there we could start libpython-clj. There are versions of blender with recent Python -- mine is Python3.7. You CAN run this with libpython-clj -- for me it would be

(py/initialize :python-executable /home/jay/programs/blender-2.80-linux-glibc217-x86_64/2.80/python/bin/python3.7m)

But unfortunately that's not going to start blender :(

/usr/bin/blender --python-console is not an actual Python interpreter command -- it's a compiled executable that launches blender and a Python interpreter simultaneously. It's probably misleading but we use python -c ... to have Python tell us the locations of its system files.

Now -- if you can find a way to start Blender FROM Python (instead of using the command line) -- THEN we can get it working. Because we would just have to run libpython-clj, then start blender.

I haven't had time to investigate further, but this looks promising: https://blender.stackexchange.com/questions/1365/how-can-i-run-blender-from-command-line-or-a-python-script-without-opening-a-gui

and https://wiki.blender.org/wiki/Building_Blender/Other/BlenderAsPyModule

So this indicates to me that it would be possible to user blender as a Python module in libpython-clj, but you would not be able to use the GUI -- so I don't know if that's what you're looking for :/

nahuel commented 4 years ago

I think Blender + Clojure REPL can be a killer combination. Asking without knowing about the internals of libpython-clj, how feasible do you think is to modify it to run a python script like:

import libpythonclj
# Now launch a JVM and take control of the Python environment from the CLJ side
libpythonclj.start("./test.clj")

so you can start the JVM from an unmodified Blender and not the other way around?

It seems like there are two posible routes:

Probably modifying Blender is more feasible. Note, maybe this link is more up to date than the BlenderAsPyModule, it mentions a make bpy step not present in the wiki: https://blender.stackexchange.com/a/127215

Another option can be some LD_PRELOAD trick before starting Blender...?

cnuernber commented 4 years ago

https://pythonhosted.org/javabridge/ may be another direction.

Or potentially using graal native image to load a shared library of things.

Both pretty interesting! :-).

jjtolton commented 4 years ago

I think Blender + Clojure REPL can be a killer combination. Asking without knowing about the internals of libpython-clj, how feasible do you think is to modify it to run a python script like:

import libpythonclj
# Now launch a JVM and take control of the Python environment from the CLJ side
libpythonclj.start("./test.clj")

The feasibility is "feasible" but not the amount of work it would take is not really possible while we're holding down full-time jobs :(

The three possible approaches that come to mind are:

1) an RPC style interface like you and @cnuernber described, 2) a surjective mapping of Python bytecode to JVM bytecode, 3) python bindings for JVM like in Rust: https://github.com/PyO3/pyo3

Unfortunately, all of these are outside the current scope of libpython-clj. But they are exciting thought experiments! --JJ

nahuel commented 4 years ago

FYI just found a project using an alternative route, CLJS inside V8 inside Python inside Blender (not using libpython-clj): https://github.com/darwin/blender-clojure

jjtolton commented 4 years ago

This is really fascinating. That looks fantastically complex. The technical analogy here would be to embed the JVM in Python somehow then inject Python API's into the JVM and then wrap THOSE with libpython-clj style APIs. (btw, NOT what libpython-clj was designed to do but could probably reuse a lot of techniques learned to do something similar in a different project w/a different scope)

I've never heard of embedded JVM in CPython but worth looking into!

nahuel commented 4 years ago

Another way: "Clojure to Python transpiler for scripting Blender" https://github.com/eltonlaw/donkeyblend

tristanstraub commented 4 years ago

Just randomly, I've gotten it working, and now found this conversation.

https://github.com/tristanstraub/blender-clj/

It builds blender as a python module, imports via libpython-clj. I patched blender just slightly to make the GUI run in the foreground. The hardest part is not breaking blender because of how it handles operator contexts, which happens to be a normal problem even when scripting in python, apparently.

I'm still working on the build on my laptop, but have it working on my desktop.

jjtolton commented 4 years ago

:O

When you say "gotten it working" I'm curious what you mean...?

tristanstraub commented 4 years ago

Running and scripted from emacs/cider. :)

https://github.com/tristanstraub/blender-clj/blob/master/screenshot.png

jjtolton commented 4 years ago

You're controlling Blender from libpython-clj?? Or are you sending a libpython-clj script into blender...???

On Mon, Jun 8, 2020 at 5:36 PM tristanstraub notifications@github.com wrote:

Running and scripted from emacs/cider. :)

https://github.com/tristanstraub/blender-clj/blob/master/screenshot.png

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/clj-python/libpython-clj/issues/94#issuecomment-640900612, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACPJX44HBZV7BRMPX6ATTSDRVVKVLANCNFSM4MJPXNWQ .

cnuernber commented 4 years ago

whoa that is sweet.

tristanstraub commented 4 years ago

I've been controlling blender from libpython-clj. Specifically, blender is built as a python module "bpy.so", ever so slightly patched, so the GUI will be visible. I load cider/emacs. I require libpython-clj. Then I get libpython-clj to pull in "bpy.so". From there, I can use libpython-clj to import "bpy", which gives me access to everything the python scripting layer has available.

It is possible to crash blender, just like from python scripts, if you don't setup the context it needs, but I managed to get this working nicely by using a blender timer to callback to clojure. This timer is setup from clojure. What is also nice is that the python console inside blender has the same python engine as the one in libpython-clj, so you can do things like libpython-clj/add-module and then from the python console, you can import it by name. You can even put a clojure function inside that "add-module" module (using set-attr!), and call it from the blender python console. So, it's a two way street.

jjtolton commented 4 years ago

whoaaaaaaaaaaaaaaa. Can you possibly write up the steps to reproduce this? This potentially has really significantly AWESOME consequences. It sounds like you've somehow discovered how to get a Clojure REPL bridged with a Python Interpreter, effectively creating some sort of bidrectional control. I'd really love to play around with this and see what else we can do with it!

tristanstraub commented 4 years ago

@jjtolton I wouldn't say I've discovered anything, but simply glued the bits together, and made very minor changes to blender to make it fit better. Blender could already be built as a standalone python module, but it didn't allow the GUI to be visible. I would say, this is probably the only innovation, as small as it is. libpython-clj is the secret sauce that makes this possible.

So, the link above to the repo I'm working on pretty much reproduces it. Here it is again:

https://github.com/tristanstraub/blender-clj/

The script "build-blender-fedora.sh" downloads, patches and builds bpy.so. It works on my system which is linux (fedora-31), but it pretty much just implements the steps found here https://wiki.blender.org/wiki/Building_Blender/Other/BlenderAsPyModule and here: https://wiki.blender.org/wiki/Building_Blender/Linux/Fedora

If you are on windows or macosx, I would suggest trying to build "bpy.so" by referring to https://wiki.blender.org/wiki/Building_Blender under the various operating system links. This is probably the biggest headache. Patching, rebuilding and loading with libpython-clj is the easiest part.

The usage of "bpy.so" can be found in "src/blender-clj/core.clj", and examples under "src/blender-clj/src/examples/*.clj"

jjtolton commented 4 years ago

This SUPER exciting! Can't wait to take a look. I'll be honest, as you can tell from my smug 3 bullet point response above, I didn't think this was possible, so I'm very excited to see how it works!! Great job sticking with this.

tristanstraub commented 4 years ago

I've transitioned to using https://github.com/tristanstraub/clj-python-trampoline which doesn't require a patched blender build.

It does need a small modification to libpython-clj. Perhaps, with some battle testing, libpython-clj itself could take on the minor change, so that it also executes from embedded python processes.

It is using javabridge.

[NOTE: So far, I've gotten patched blender, and javabridge from an unpatched blender working. There are definately cross-platform issues with both of those, but I think that is simply a matter of ironing those kinks out]

cnuernber commented 4 years ago

Wow so that is pretty interesting.

I am confused by couple things which is a good thing :-).

Is python shared library that libpython-clj attempts to load is the same shared library that is running as the host? They build a jvm and host it in python but how do you ensure the symbols and such all line up?

This is really interesting and a possible next avenue of research/development here.

tristanstraub commented 4 years ago

Yeah, it has to be the same library. I took a gamble with that since the jvm is in the same process as blender, the dynamic loading of "libpython.so" should be idempotent, which appears to be the case.

The output from libpython looks like this:

{:lib-version "3.7",
 :java-library-path-addendum "/usr/lib",
 :exec-prefix "/usr",
 :executable "/usr/bin/python3",
 :libnames ("python3.7m" "python3.7"),
 :prefix "/usr",
 :base-prefix "/usr",
 :base-exec-prefix "/usr",
 :python-home "/usr",
 :version [3 7 7],
 :platform "linux"}

And "lsof" of the blender process, which encapsulates the jvm (verified that "ps" doesn't show "java" running), shows "/usr/lib64/libpython3.7m.so.1.0", which matches.

I'm on linux, so it is using my local python install, as far as I can see. Windows and MacOS might be different story, but the idea should still work. I've seen that under windows, "pip install" can be done from the blender bundled python with a little command line magic (to install javabridge).

I realise my process has been quite adhoc, and aiming for just one particular OS, and who knows what I have broken. The call I make to "construct-main-interpreter" is a complete botch, since I found that jna/mapSharedLibraryNames was consistently crashing my process, I had to comment it out. And because the python process has already started elsewhere, PySys_SaveThreads, etc don't work in "initialize!".

I'm also getting a bunch of stack overflows in the "process reaper", which crashes the process on my laptop, but not on my desktop. Although, I've not retested my latest code back on my laptop.

Testing it is also possible from the standard "python" executable, instead of blender, or some other embedded process.

cnuernber commented 3 years ago

@tristanstraub - [clj-python/libpython-clj2 2.00-beta-1] now checks to see if python is initialized via Py_IsInitialized and skips library initialization if that is the case. The main namespace is now libpython-clj2.python. If you have a moment, could you test this out?

cnuernber commented 3 years ago

Closing this for now. The best answer is to better support tristan's pathway.

tristanstraub commented 3 years ago

@cnuernber I guess this might already be merged. I have been rather busy so haven't been keeping up to date. I'll try to test it. I just tried my old code and it seems to crash blender, so I'll test it against your changes.

Looks like you have been really busy on bringing libpython-clj further along. Really impressed!