From 53c7b749b8dbaae5e712e4b89b6ccc6c6110138d Mon Sep 17 00:00:00 2001 From: Carsten Behring Date: Sun, 28 Nov 2021 19:41:30 +0100 Subject: [PATCH 1/5] instructions for using cljbridge package --- topics/embedded.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/topics/embedded.md b/topics/embedded.md index 9c13b8f..c89db20 100644 --- a/topics/embedded.md +++ b/topics/embedded.md @@ -49,10 +49,17 @@ If the installation cannot find 'jni.h' then most likely you have the Java runti ## From the Python REPL -The next step involves starting a python repl from our libpython-clj base directory. -This is only required because we have a special -[python script](https://github.com/clj-python/libpython-clj/blob/master/cljbridge.py) -that has code to start a Java VM with a correct classpath. So we start by importing +The next step involves starting a python repl. + +This requires a python library `cljbridge`, +which can be installed via + +``` +export JAVA_HOME=<--YOUR JAVA HOME--> +python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cljbridge +``` + +So we start by importing that script: @@ -60,7 +67,7 @@ that script: Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. ->>> import cljbridge +>>> from clojurebridge import cljbridge >>> test_var=10 >>> cljbridge.init_jvm(start_repl=True) Mar 11, 2021 9:08:47 AM clojure.tools.logging$eval3186$fn__3189 invoke From 6ae2b320dc593f69bea78a0f9d98d0c2da1ad535 Mon Sep 17 00:00:00 2001 From: Carsten Behring Date: Sun, 28 Nov 2021 22:12:53 +0100 Subject: [PATCH 2/5] remove instrucions for javabridge gets installed as deps of clojurebridge --- topics/embedded.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/topics/embedded.md b/topics/embedded.md index c89db20..b1ee085 100644 --- a/topics/embedded.md +++ b/topics/embedded.md @@ -27,24 +27,20 @@ Due to the above reasons there is a solid argument for, if possible, embedding Clojure into Python allowing the Python executable to be the host process. -## Enter: javabridge +## Enter: cljbridge Python already had a nascent system for embedding Java in Python - the -[javabridge module](https://pypi.org/project/javabridge/). You will need to have -javabridge installed in your Python environment. In order to compile `javabridge` -requires the JDK installed and **not just the JRE**. [tristanstraub](https://github.com/tristanstraub/) +[javabridge module](https://pypi.org/project/javabridge/). + +We went a step further and provide cljbridge. + +In order to compile `javabridge` +a JDK is required and **not just the JRE**. [tristanstraub](https://github.com/tristanstraub/) had found a way to use this in order to work with [Blender](https://github.com/tristanstraub/blender-clj/). We took a bit more time and worked out ways to smooth out these interactions and make sure they were supported throughout the system. -```console -python3 -m pip install javabridge -``` - -If the installation cannot find 'jni.h' then most likely you have the Java runtime -(JRE) installed as opposed to the Java development kit (JDK). - ## From the Python REPL @@ -59,6 +55,11 @@ export JAVA_HOME=<--YOUR JAVA HOME--> python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cljbridge ``` +This will install and eventually compile `javabridge` as well. + +If the installation cannot find 'jni.h' then most likely you have the Java runtime +(JRE) installed as opposed to the Java development kit (JDK). + So we start by importing that script: From bdc277646f7081bec8f0ec09a728794c4cd9f4f7 Mon Sep 17 00:00:00 2001 From: Carsten Behring Date: Sun, 28 Nov 2021 22:20:00 +0100 Subject: [PATCH 3/5] fixed wording --- topics/embedded.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topics/embedded.md b/topics/embedded.md index b1ee085..c2a8641 100644 --- a/topics/embedded.md +++ b/topics/embedded.md @@ -33,7 +33,7 @@ Clojure into Python allowing the Python executable to be the host process. Python already had a nascent system for embedding Java in Python - the [javabridge module](https://pypi.org/project/javabridge/). -We went a step further and provide cljbridge. +We went a step further and provide `cljbridge` python module. In order to compile `javabridge` a JDK is required and **not just the JRE**. [tristanstraub](https://github.com/tristanstraub/) From 32c8820b72a3c84dd94c8dd1d2bb5f75416b09cb Mon Sep 17 00:00:00 2001 From: Carsten Behring Date: Mon, 6 Dec 2021 19:47:36 +0100 Subject: [PATCH 4/5] cljbridge is now in pypi --- topics/embedded.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topics/embedded.md b/topics/embedded.md index c2a8641..4a0c520 100644 --- a/topics/embedded.md +++ b/topics/embedded.md @@ -52,7 +52,7 @@ which can be installed via ``` export JAVA_HOME=<--YOUR JAVA HOME--> -python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cljbridge +python3 -m pip install cljbridge ``` This will install and eventually compile `javabridge` as well. From df492a16a3969eddf678d9a1005b915bd15dafd8 Mon Sep 17 00:00:00 2001 From: Carsten Behring Date: Mon, 6 Dec 2021 21:16:53 +0100 Subject: [PATCH 5/5] deleted bridge.py --- cljbridge.py | 212 --------------------------------------------------- 1 file changed, 212 deletions(-) delete mode 100644 cljbridge.py diff --git a/cljbridge.py b/cljbridge.py deleted file mode 100644 index 720a857..0000000 --- a/cljbridge.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Python bindings to start a Clojure repl from a python process. Relies on -libpython-clj being in a deps.edn pathway as clojure is called from the command - line to build the classpath. Expects javabridge to be installed and functional. - -Javabridge will dynamically find the java library that corresponds with calling 'java' - from the command line and load it. We then initialize Clojure and provide pathways -to require namespaces, find symbols, and call functions. - -There are two import initialization methods - init_clojure and init_clojure_repl - these -take care of starting up everything in the correct order. -""" -import subprocess -import javabridge - -try: - from collections.abc import Callable # noqa -except ImportError: - from collections import Callable - - -def init_clojure_runtime(): - """Initialize the clojure runtime. This needs to happen at least once before -attempting to require a namespace or lookup a clojure var.""" - javabridge.static_call("clojure/lang/RT", "init", "()V") - - -def find_clj_var(fn_ns, fn_name): - """Use the clojure runtime to find a var. Clojure vars are placeholders in -namespaces that forward their operations to the data they point to. This allows -someone to hold a var and but recompile a namespace to get different behavior. They -implement both `clojure.lang.IFn` and `clojure.lang.IDeref` so they can act like a -function and you can dereference them to get to their original value.""" - return javabridge.static_call("clojure/lang/RT", - "var", - "(Ljava/lang/String;Ljava/lang/String;)Lclojure/lang/Var;", - fn_ns, - fn_name) - - -class CLJFn(Callable): - """Construct a python callable from a clojure object. This callable will forward -function calls to it's Clojure object expecting a clojure.lang.IFn interface.""" - applyTo = javabridge.make_method("applyTo", "(clojure/lang/ISeq;)Ljava/lang/Object;") - def __init__(self, ifn_obj): - self.o = ifn_obj - - def __call__(self, *args, **kw_args): - if not kw_args: - invoker = getattr(self, "invoke"+str(len(args))) - return invoker(*args) - else: - raise Exception("Unable to handle kw_args for now") - print(len(args), len(kw_args)) - - -for i in range(20): - opargs = "" - for j in range(i): - opargs += "Ljava/lang/Object;" - setattr(CLJFn, "invoke" + str(i), - javabridge.make_method("invoke", "(" + opargs + ")Ljava/lang/Object;" )) - - -def resolve_fn(namespaced_name): - """Resolve a clojure var given a fully qualified namespace name. The return value - is callable. Note that the namespace itself needs to be required first.""" - ns_name, sym_name = namespaced_name.split("/") - return CLJFn(find_clj_var(ns_name, sym_name)) - - -def resolve_call_fn(namespaced_fn_name, *args): - """Resolve a function given a fully qualified namespace name and call it.""" - return resolve_fn(namespaced_fn_name)(*args) - -def symbol(sym_name): - """Create a clojure symbol from a string""" - return javabridge.static_call("clojure/lang/Symbol", "intern", - "(Ljava/lang/String;)Lclojure/lang/Symbol;", sym_name) - -__REQUIRE_FN = None - -def require(ns_name): - """Require a clojure namespace. This needs to happen before you find symbols - in that namespace else you will be uninitialized var errors.""" - if not __REQUIRE_FN: - _REQUIRE_FN = resolve_fn("clojure.core/require") - return _REQUIRE_FN(symbol(ns_name)) - - -def init_libpy_embedded(): - """Initialize libpy on a mode where it looks for symbols in the local process and - it itself doesn't attempt to run the python intialization procedures but expects - the python system to be previously initialized.""" - require("libpython-clj2.embedded") - return resolve_call_fn("libpython-clj2.embedded/initialize!") - - -def classpath(classpath_args=[]): - """Call clojure at the command line and return the classpath in as a list of - strings. Clojure will pick up a local deps.edn or deps can be specified inline.""" - return subprocess.check_output(['clojure'] + list(classpath_args) + ['-Spath']).decode("utf-8").strip().split(':') - - -DEFAULT_NREPL_VERSION = "0.8.3" -DEFAULT_CIDER_NREPL_VERSION = "0.25.5" - - -def repl_classpath(nrepl_version=DEFAULT_NREPL_VERSION, - cider_nrepl_version=DEFAULT_CIDER_NREPL_VERSION, - classpath_args=[], **kw_args): - """Return the classpath with the correct deps to run nrepl and cider. - Positional arguments are added after the -Sdeps argument to start the - nrepl server.""" - return classpath(classpath_args=["-Sdeps", '{:deps {nrepl/nrepl {:mvn/version "%s"} cider/cider-nrepl {:mvn/version "%s"}}}' % (nrepl_version, cider_nrepl_version)] - + list(classpath_args)) - - -def init_clojure(classpath_args=[]): - """Initialize a vanilla clojure process using the clojure command line to output - the classpath to use for the java vm. At the return of this function clojure is - initialized and libpython-clj2.python's public functions will work. - - * classpath_args - List of arguments that will be passed to the clojure command - line process when building the classpath. """ - javabridge.start_vm(run_headless=True, - class_path=classpath(classpath_args=classpath_args)) - init_clojure_runtime() - init_libpy_embedded() - return True - - -def py_dict_to_keyword_map(py_dict): - hash_map = None - keyword = resolve_fn("clojure.core/keyword") - assoc = resolve_fn("clojure.core/assoc") - for k in py_dict: - hash_map = assoc(hash_map, keyword(k), py_dict[k]) - return hash_map - - -def init_jvm(**kw_args): - """Initialize clojure with extra arguments specifically for embedding a cider-nrepl - server. Then start an nrepl server. The port will both be printed to stdout and - output to a .nrepl_server file. This function does not return as it leaves the GIL - released so that repl threads have access to Python. libpython-clj2.python is - initialized 'require-python' pathways should work. - - - Keyword arguments in python are mapped to a hash-map of keyword options and - passed directly to clojure so any nrepl are valid arguments to this function. - - * `classpath_args` - List of additional arguments that be passed to the clojure - process when building the classpath. - * `port` - Integer port to open up repl. A random port will be found if not - provided. - * `bind` - Bind address. If you are having connectivity issues try - bind=\"0.0.0.0\"""" - javabridge.start_vm(run_headless=True, class_path=repl_classpath(**kw_args)) - init_clojure_runtime() - if "load_user_clj" in kw_args: - resolve_call_fn("clojure.core/load-file", - "user.clj") - init_libpy_embedded() - - if "start_repl" in kw_args: - resolve_call_fn("libpython-clj2.embedded/start-repl!", - py_dict_to_keyword_map(kw_args)) - - if "kill_vm_after" in kw_args: - javabridge.kill_vm() - -def load_clojure_file(**kw_args): - """Initializes clojure and loads and runs a Clojure file. This function load the specified clojure file - and kills then the jvm and returns. - - Keyword arguments are: - - * `classpath_args` - List of additional arguments that be passed to the clojure - process when building the classpath. - * `clj_file` Clojure file to be loaded with Clojure `load-file` fn - """ - javabridge.start_vm(run_headless=True, class_path=repl_classpath(**kw_args)) - init_clojure_runtime() - init_libpy_embedded() - try: - resolve_call_fn("clojure.core/load-file", - kw_args["clj_file"]) - finally: - javabridge.kill_vm() - -class GenericJavaObj: - __str__ = javabridge.make_method("toString", "()Ljava/lang/String;") - get_class = javabridge.make_method("getClass", "()Ljava/lang/Class;") - __repl__ = javabridge.make_method("toString", "()Ljava/lang/String;") - def __init__(self, jobj): - self.o = jobj - - -def longCast(jobj): - "Cast a java object to a primitive long value." - return javabridge.static_call("clojure/lang/RT", "longCast", - "(Ljava/lang/Object;)J", jobj) - - -def to_ptr(pyobj): - """Create a tech.v3.datatype.ffi.Pointer java object from a python object. This - allows you to pass python objects directly into libpython-clj2.python-derived - pathways (such as ->jvm). If java is going to hold onto the python data for - a long time and it will fall out of Python scope object should be - 'incref-tracked' - 'libpython-clj2.python.ffi/incref-track-pyobject'.""" - return javabridge.static_call("tech/v3/datatype/ffi/Pointer", "constructNonZero", - "(J)Ltech/v3/datatype/ffi/Pointer;", id(pyobj))