Ghost Letters 幽霊文字

GraalVM: Java + Python

The Introduction to GraalVM starts with:

GraalVM is an advanced JDK with ahead-of-time Native Image compilation.

‘Native Image compilation’ means that a Java program - and its dependencies - can be compiled into a single executable. So instead of multiple .jar files that require an installed JDK, GraalVM creates a single file that works without JDK (but this file is now platform specific).

While the Native Image compilation is the most popular feature, the GraalVM project has some other tricks up its sleeve. GraalVM can run (and compile into an executable) other, non-JVM languages, for instance Python, Ruby, JavaScript and WebAssembly. After seeing the Devoxx talk “Supercharge your Java Applications with Python!” by Fabio Niephaus and Thomas Wuerthinger I wanted to try out the GraalPy project.

GraalPy allows to write Java code that calls Python code (and vice versa). The most interesting feature for me is the graalpy-maven-plugin. This maven plugin allows to install Python dependencies. Behind the scences it uses Pip, but this is fully transparent to the (Java) developer. Disclaimer: not each Python dependency is supported, yet. The GraalPy: Package Compatibility page tracks which dependency works and which not.

Niephaus and Wuerthinger gave a very informative and entertaining talk at Devoxx, but one thing struck me missing: what if I already have a small Python program that consists of a handful of files and uses third party dependencies? Most likely I would prefer to not copy over all the Python code into Java text blocks. Instead, I want to keep the Python code in .py files and call into it from the Java side (and do something with the result).

First, I took a look at the Graal Languages - Demos and Guides on GitHub. Unfortunately, I could not find an example for the combination Python files + Pip dependencies. However, the GraalVM documentation mentions a maven archetype that bootstraps exactly my desired setup. So on Linux you can run:

1
2
3
4
mvn archetype:generate \
  -DarchetypeGroupId=org.graalvm.python \
  -DarchetypeArtifactId=graalpy-archetype-polyglot-app \
  -DarchetypeVersion=24.1.0

(Same command works on Windows, but without the \ backslashes.)

This creates the following folder structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
my-app/
├─ src/
│  ├─ main/
│  │  ├─ java/
│  │  │  ├─ org/
│  │  │  │  ├─ example/
│  │  │  │  │  ├─ Main.java
│  │  ├─ resources/
│  │  │  ├─ org.graalvm.python.vfs/
│  │  │  │  ├─ src/
│  │  │  │  │  ├─ pythonMain.py
│  │  │  │  │  ├─ restService.py
├─ pom.xml

The files and Java package can have whatever name you prefer, but the path resources/org.graalvm.python.vfs/src is the default location where GraalPy will look for custom Python resources. The following Java code will call into the Python world:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
  public static final String PYTHON_DIR = "/org.graalvm.python.vfs/src";

  public static void main(String[] args)  throws IOException {
    try (Context context = GraalPyResources.contextBuilder().build()) {
      URL pythonFileUrl = Main.class.getResource(PYTHON_DIR + "/pythonMain.py");
      Source.Builder builder = Source.newBuilder("python", pythonFileUrl);
      context.eval(builder.build());
    }
  }
}

The pythonMain.py looks like the following:

1
2
3
4
5
6
7
from restService import call_api

response = call_api("https://jsonplaceholder.typicode.com/todos/1")
json = response.json()

print(json)
print("Run from a .py file!")

and the restService.py looks like this:

1
2
3
4
5
6
import requests

def call_api(url):
    api_url = url
    response = requests.get(api_url)
    return response

The imported requests is not part of the Python SDK, but a third party dependency. The mentioned graalpy-maven-plugin can download the dependency via Pip and makes it available at runtime. So in the pom.xml you would configure the Python dependency like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<build>
  <plugins>
    <plugin>
      <groupId>org.graalvm.python</groupId>
      <artifactId>graalpy-maven-plugin</artifactId>
      <version>${graalvm.polyglot.version}</version>
      <executions>
        <execution>
          <configuration>
            <packages>
              <package>requests==2.32.3</package>
            </packages>
          </configuration>
          <goals>
            <goal>process-graalpy-resources</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Finally, we add the exec-maven-plugin so we can run the code conveniently from the command line

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<build>
  <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>exec-maven-plugin</artifactId>
          <version>1.2.1</version>
          <executions>
              <execution>
                <goals>
                  <goal>java</goal>
                </goals>
              </execution>
          </executions>
          <configuration>
              <mainClass>org.example.Main</mainClass>
          </configuration>
        </plugin>
      </plugins>
  </pluginManagement>
</build>

Run the example via mvn clean compile && mvn exec:java. The expected output is:

1
2
{'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}
Run from a .py file!

You could even compile this into a single executable file, but be warned - the compilation of this trivial program takes several minutes and will result in a ~370 MB file.

You find the full code example here.

PS: The main reason I wrote this blog post was that I stumbled on my desired solution by accident. The Graal docu did not explain what to expect from the archetype, and all the GitHub examples covered ’too trivial’ examples. So I wished for a bit more advanced, real-world example.

PPS: While I could run the example also via the ‘Play’ button in IntelliJ (v2024.3.2.2), that required to restart IntelliJ at least 2 times. First IntelliJ did not recognize the GraalPy dependencies at all (even so it worked on the command line), then it did not find the .py files.


Alternative way to write the Java code

In case you want to augment the Python code from the Java side, an alternative would be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {

  public static final String API_URL = "https://jsonplaceholder.typicode.com/todos/1";

  public static void main(String[] args) {
    try (Context context = GraalPyResources.createContext()) {
      context.eval("python",
        // language=python
        """
          from restService import call_api
          
          response = call_api('%s')
          json = response.json()
          
          print(json)
          print("I run in a Java text block")
          """.formatted(API_URL)
      );
    }
  }
}

Here the code stays free of (default) path to the Python files.