A brief overview of Python-Java bridges in 2020

As the author of Puffinplot, a Java program whose users are probably, in general, more comfortable with Python, I have a strong interest in convenient ways to connect the two languages. PuffinPlot currently embeds Jython as a scripting language, but it’s not an ideal solution, since it’s restricted to Python 2 and doesn’t provide access to the enormous CPython ecosystem. I’ve therefore been evaluating other options for connecting the two languages. For my current use case, the main requirement is to be able to make use of classes in a jar from Python – preferably without any special tooling on the Java side and with full support for CPython packages on the Python side – but I’ve listed all the software that I could find that facilitates any kind of integration between the two languages.

This article includes brief code snippets to demonstrate the syntax used for each of the bridge packages. Full, runnable Java and Python demos are available in this GitHub repository.

GraalVM

Initial release: 2019
Latest release as of 2020-01-09: 2019-12-24

GraalVM is a JVM and JDK which supports various new and exciting features, including multi-language interoperability within the VM – thus, like Jython, it offers a reimplementation of Python rather than a bridge to an existing CPython runtime. In the longer term, it seems a very promising prospect for Java-Python interoperability. However, as of 2019, GraalVM is not yet always a viable drop-in replacement for the standard JVM (for instance, Windows support is currently described as ‘experimental’), and GraalVM Python is definitely not yet a drop-in replacement for CPython. The reference manual states:

GraalVM provides an early-stage implementation of Python 3.7. Warning: The support for Python is experimental. Experimental features might never be included in a production version, or might change significantly before being considered production-ready.

A primary goal is to support SciPy and its constituent libraries. At this point, the Python implementation is made available for experimentation and curious end-users.

Javabridge

A Python wrapper for the Java Native Interface. The JavaBridge website describes it like this:

The javabridge Python package makes it easy to start a Java virtual machine (JVM) from Python and interact with it. Python code can interact with the JVM using a low-level API or a more convenient high-level API.

The documentation seems a little sparse, but for calling jarred Java code from Python it seems to work smoothly. A comment on this issue mentions that the ‘two most useful classes’ are JWrapper and JClassWrapper. Of the two, JClassWrapper seems more convenient for instantiating a Java class from Python.

Installation: pip3 install javabridge.

Example of use:

javabridge.start_vm(run_headless=True, class_path=javabridge.JARS +
                    [jar_path])
try:
    # Bind a Java variable and run a script that uses it.
    print(javabridge.run_script(
        'java.lang.String.format("Hello, %s!", greetee);',
        dict(greetee="world")))

    # Wrap a Java object and call some of its methods.
    array_list = javabridge.JWrapper(javabridge.make_instance(
        "java/util/ArrayList", "()V"))
    array_list.add("ArrayList item 1")
    array_list.add("ArrayList item 2")
    print("ArrayList size:", array_list.size())
    print("First ArrayList item:", array_list.get(0))

    # Wrap a Java object from our jar and call a method.
    main1 = javabridge.JWrapper(javabridge.make_instance(
        "net/talvi/pythonjavabridgedemos/Main",
        "(Ljava/lang/String;)V", "Alice"))
    print(main1.greet("Hello"))

    # Wrap a Java object using JClassWrapper (no signature required)
    main2 = javabridge.JClassWrapper(
        "net.talvi.pythonjavabridgedemos.Main")("Bob")
    print(main2.greet("Hi there"))
    print("2 + 2 =", main2.add(2, 2))

finally:
    javabridge.kill_vm()

jep

Initial release: 2015
Latest release as of 2020-01-09: 2019-08-30

Jep uses JNI to embed a CPython interpreter in the JVM (thus it cannot be used from a standalone Python runtime). CPython extensions and Cython modules are supported, but not guaranteed to work in all cases.

Python installation: Make sure JAVA_HOME is set, then pip3 install jep. The jep executable assumes that the Jep jar is installed globally, so it will not work if the Jep package is installed using pip’s user scheme (pip3 install --user jep).

Maven configuration for Java installation:

<dependency>
  <groupId>black.ninia</groupId>
  <artifactId>jep</artifactId>
  <version>3.9.0</version>
</dependency>

Classes can be imported and used directly from the Java namespace. The Python Jep package must be installed, but does not need to be explicitly invoked in the Python script. The jep executable can be used to run a Python script directly, or the script can be invoked from Java. When running via the jep executable, third-party jars can be used by setting the CLASSPATH variable, but they cannot be specified within the Python script itself. Here is an example of usage from the Python side:

#!/usr/bin/env jep

from java.util import *
cal = GregorianCalendar(2008, Calendar.DECEMBER, 3)
weekday = cal.getDisplayName(Calendar.DAY_OF_WEEK,
                             Calendar.LONG_FORMAT,
                             Locale.ENGLISH)
print("Python 3 was released on a {}.".format(weekday))

Java code to call a Python script in Jep:

try (Interpreter interp = new SharedInterpreter()) {
    interp.set("variable_to_pass", someJavaVariable);
    interp.runScript(script_filename);
} catch (JepException ex) {
    System.err.println("Jep exception:\n"
            + ex.getMessage());
    System.exit(1);
}

jpy

Initial release: 2014
Latest release as of 2020-01-09: 2018-02-08 on GitHub, 2014-07-24 on PyPI

A bidirectional bridge with a focus on ‘maximum data transfer speed between the two languages’, written in support of the ESA’s SNAP toolbox. jpy is functional but seems a little unpolished (installation is by building from source, and documentation is somewhat sparse).

Installation: clone the repository, then

export JDK_HOME=<your-jdk-dir>
export JAVA_HOME=$JDK_HOME
python3 setup.py build maven bdist_wheel

This produces a .whl archive in the dist subdirectory, which can be installed with pip3. The build process also produces a jar file, which is placed in a subdirectory of the build directory. The project publishes pre-built jars to SNAP’s repository at http://nexus.senbox.net/, but it’s unclear whether this repository will remain available indefinitely.

Use from Python:

jpy.create_jvm(["-Djava.class.path=" + jar_path])
StringBuilder = jpy.get_type("java.lang.StringBuilder")
sb = StringBuilder()
sb.append("Demonstration of ")
sb.append("StringBuilder")
print(sb.toString())

Main = jpy.get_type("net.talvi.pythonjavabridgedemos.Main")
main = Main("Bob")
print(main.greet("Wotcha"))

For calling Python from Java, the documentation suggests that something like the following ought to work.

PyLib.startPython();
PyObject.executeCode("print('Hello!')", PyInputMode.STATEMENT);
PyLib.stopPython();

In practice, quite a lot of set-up is needed to invoke Python from Java via jpy. The jpy jar must of course be available at compile and run time. In addition, at runtime, the paths to jpy’s shared libraries must be specified. The Javadoc says that they should be passed via a configuration file in the user’s home directory; in practice, this doesn’t seem to work, but examination of the source code reveals that they can be passed via system properties. Doing this allows jpy to find the library, but it crashes at once with a java.lang.UnsatisfiedLinkError due to an undefined symbol PyFloat_Type.

JPype

Initial release: 2005
Latest release as of 2020-01-09: 2019-12-17

Summary, from the project page:

JPype is a Python module to provide full access to Java from within Python. It allows Python to make use of Java only libraries…

This is achieved not through re-implementing Python, as Jython has done, but rather through interfacing at the native level in both virtual machines. This shared memory based approach achieves decent computing performance, while providing the access to the entirety of CPython and Java libraries.

JPype has a long and slightly convoluted history. First conceived in (as far as I can tell) 2004, it had a few releases up to 2007, then went rather quiet for a few years. The old project site at SourceForge appears entirely dead, but a continuation on GitHub, dating from 2012, seems to be in good health these days.

Installation: pip3 install JPype1

Example Python script:

jpype.startJVM(jpype.getDefaultJVMPath(),
               "-ea",
               "-Djava.class.path=" + jar_path,
               convertStrings=False)

jpype.java.lang.System.out.println("This was printed via System.out.println.")

main = jpype.JPackage("net").talvi.pythonjavabridgedemos.Main("Jeff")
print(main.greet("Morning"))

jpype.shutdownJVM()

Jython

Releases announced on: http://fwierzbicki.blogspot.com/
Initial release: 1997 (as JPython)
Latest release as of 2020-01-09: 2017-07-01

Not a ‘bridge’ as such, but an implementation of Python on the JVM. This has the clear advantage of allowing Python support without any non-Java dependencies: a Java application can bundle Jython in order to support Python out of the box.

The main disadvantage is equally clear: the CPython ecosystem is one of Python’s main attractions, and it is not available from Jython. In recent years, Jython has also suffered from a slow pace of development: the last release, version 2.7.1, was in 2017, and there is no clear timeline for Python 3 support. This will become increasingly awkward now that CPython 2.7 has reached its official end-of-life on 1 January 2020. The JyNI project aims to allow the use of CPython extensions from Jython, but its development appears to have stalled: the last release, v2.7-alpha.5, was in 2017 and the last commit was in 2018.

Jython can be used from within a Java project via the Java scripting API, or as a standalone script interpreter using the jython launch script or by running the jar.

Interactive installation: download the installer jar and exectute it: java -jar jython-installer-2.7.1.jar

Use without installation: download the standalone jar.

Maven dependency:

<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython-standalone</artifactId>
    <version>2.7.1</version>
</dependency>

Example of Jython use from Java:

try(PythonInterpreter interpreter = new PythonInterpreter()) {
    interpreter.exec("print('Hello world!')");
}

final ScriptEngine scriptEngine =
        new ScriptEngineManager().getEngineByName("python");
scriptEngine.put("variable_to_pass", someJavaVariable);
try (Reader reader = new FileReader(script_filename)) {
    scriptEngine.eval(reader);
}

Example of Java use from a standalone Jython script:

# Demonstrate use of classes from the Java runtime.
from java.util import ArrayList
al = ArrayList()
al.add(2)
al.add(2)
print "2 + 2 =", al.stream().mapToInt(lambda x: x).sum()

# Jython lets us add jar files to sys.path.
sys.path.append(jar_path)

# Import a Java class from the jar file added above.
from net.talvi.pythonjavabridgedemos import Main

# Demonstrate instantiation and use of a class from a third-party jar.
my_main = Main("Medea")
print my_main.greet("Good health")

The script can be run with the jython executable (which can also be specified on a shebang line), or by executing the Jython jar explicitly: java -jar jython-standalone-2.7.1.jar script_name.py.

Py4j

Initial release: 2009
Latest release as of 2020-01-09: 2018-10-21

Project description:

Py4J enables Python programs running in a Python interpreter to dynamically access Java objects in a Java Virtual Machine. Methods are called as if the Java objects resided in the Python interpreter and Java collections can be accessed through standard Python collection methods. Py4J also enables Java programs to call back Python objects. Py4J is distributed under the BSD license.

One caveat is that using it requires explicit support on the Java side (see https://www.py4j.org/getting_started.html), so you can't just drop it in as a shim to use any old JAR from a Python script: the Java process has to start a Py4J server before any communication can take place.

Here’s an example of starting the server on the Java side. Any object can be passed to the GatewayServer constructor as an entry point.

final Main main = new Main("Heliogabalus");
final GatewayServer server = new GatewayServer(main);
server.start(false);

With the server running, a Python script can communicate with the JVM and with the supplied entry point:

# Demonstrate use of standard Java classes.
cal = gateway.jvm.java.util.\
    GregorianCalendar(2008, gateway.jvm.java.util.Calendar.DECEMBER, 3)
weekday = cal.getDisplayName(gateway.jvm.java.util.Calendar.DAY_OF_WEEK,
                             gateway.jvm.java.util.Calendar.LONG_FORMAT,
                             gateway.jvm.java.util.Locale.ENGLISH)
print("Python 3 was released on a {}.".format(weekday))

# Demonstrate use of the class provided as an entry point.
print(gateway.entry_point.greet("Salutations"))

# Demonstrate instantiation and use of a third-party class.
main = gateway.jvm.net.talvi.pythonjavabridgedemos.Main("Trismestigus")
print(main.greet("Felicitations"))

It’s also possible to initiate communication from the Java side rather than the Python side, albeit a little more complicated.

PyJNIus

Initial release: 2012
Latest release as of 2020-01-09: 2019-12-05

PyJNIus uses JNI to allow access to Java classes from Python code.

Installation: pip3 install Cython, followed by pip3 install pyjnius.

Example script:

jnius_config.set_classpath(jar_path)

from jnius import autoclass

Main = autoclass("net.talvi.pythonjavabridgedemos.Main")
main = Main("John")
print(main.greet("G'day"))
print("55 + 89 =", main.add(55, 89))

links

social