boomslang embeds CPython 3.14 in Java by running a WASI build with Chicory. Python execution stays inside the JVM, so callers do not need JNI, subprocess management, or a system Python install.
The default artifact includes:
- CPython 3.14 built for
wasm32-wasip1 - Python stdlib plus NumPy, Pandas, Matplotlib, Pydantic, ijson, and Jinja2
python/bin/boomslang.wasm- generated Chicory AOT classes for the bundled WASM
- copy-on-write memory snapshots for fast
PythonInstancecreation boomslang_host, a small bridge for calling Java functions from Python
Use the default artifact when you want the bundled Python runtime:
<dependency>
<groupId>com.hubspot</groupId>
<artifactId>boomslang</artifactId>
<version>${boomslang.version}</version>
</dependency>Create one factory and reuse it. The stdlib path is a host directory where boomslang extracts packaged Python resources. The instance root is the filesystem visible to Python as /.
import com.hubspot.boomslang.PythonExecutorFactory;
import com.hubspot.boomslang.PythonInstance;
import com.hubspot.boomslang.PythonResult;
import java.nio.file.Files;
import java.nio.file.Path;
Path pythonRoot = Files.createTempDirectory("boomslang-python");
PythonExecutorFactory factory = PythonExecutorFactory
.builder()
.withStdlibPath(pythonRoot)
.build();
PythonResult result = factory.runOnWasmThread(() -> {
PythonInstance instance = factory.createInstance(pythonRoot);
return instance.execute("print('hello from Python')");
});
System.out.println(result.stdout());Run Python work through runOnWasmThread. It uses a larger JVM stack and supports timeouts:
PythonInstance instance = factory.createInstance(pythonRoot);
PythonResult result = factory.runOnWasmThread(
() -> instance.execute("print(sum(range(10)))"),
Duration.ofSeconds(5),
instance
);If execution times out, the instance is poisoned. Call reset() before reusing it, or discard it.
These imports are expected to work with the bundled runtime:
import ijson
import jinja2
import matplotlib
import numpy as np
import pandas as pd
from pydantic import BaseModelUse compile and loadCode when the same source runs many times:
PythonInstance instance = factory.createInstance(pythonRoot);
byte[] bytecode = instance.compile(sourceCode);
PythonResult first = instance.loadCode(bytecode);
instance.reset();
PythonResult second = instance.loadCode(bytecode);The stock host exposes boomslang_host.call(name, args) and boomslang_host.log(level, message).
PythonExecutorFactory factory = PythonExecutorFactory
.builder()
.withStdlibPath(pythonRoot)
.addExtension(
HostBridge
.builder()
.withFunction("lookup_user", userId -> userService.findById(userId).toJson())
.withLogHandler((level, message) -> LOG.info("[Python] {}", message))
.buildExtension()
)
.build();from boomslang_host import call, log
user_json = call("lookup_user", "12345")
log(2, "loaded user")Use a custom host if you need typed WASM imports or custom Python modules. See examples/custom-host/.
Install small in-memory packages at factory creation:
PythonExecutorFactory factory = PythonExecutorFactory
.builder()
.withStdlibPath(pythonRoot)
.withModule("my_package", "helpers", "def double(x): return x * 2")
.build();Build larger packages into the WASM/Python resource pipeline instead.
Most packaged Python runtime files under core/src/main/resources/python/bin/ and core/src/main/resources/python/usr/ are generated by the WASM/CPython resource pipeline and are intentionally ignored by Git. Small source-controlled Python additions that should ship with the stock runtime live under core/src/main/resources/python-overlay/ instead.
The overlay mirrors the final guest filesystem layout. During PythonExecutorFactory creation, boomslang extracts the generated python/ resources first, then copies python-overlay/ on top of that tree. For example:
core/src/main/resources/python-overlay/usr/local/lib/python3.14/boomslang_host/asyncio.py
is copied to:
<stdlibPath>/usr/local/lib/python3.14/boomslang_host/asyncio.py
Use the overlay for small tracked Python helper modules or patches that should not require rebuilding the generated CPython resource tree. Larger third-party packages should still go through the WASM/Python resource pipeline.
Use com.hubspot:boomslang for the stock runtime. It includes the Java API, bundled WASM, Python resources, and generated Chicory AOT classes.
Use the no-python-runtime classifier when your app or another artifact provides the Python runtime:
<dependency>
<groupId>com.hubspot</groupId>
<artifactId>boomslang</artifactId>
<version>${boomslang.version}</version>
<classifier>no-python-runtime</classifier>
</dependency>This classifier excludes python/** and com/hubspot/boomslang/compiled/**. It still includes the Java API.
Your app must provide:
- a WASM binary, usually at
python/bin/boomslang.wasm - Python resources under
python/usr/local/lib/python3.14 - an AOT machine factory if you want AOT instead of interpreter fallback
If your WASM is not at the default classpath location, set it with withWasmResource(...).
Build a custom host when the stock boomslang_host.call(...) bridge is not enough. Custom hosts let you change the Rust host, add extensions, prewarm modules, and statically link additional native libraries into the WASI binary.
WASI does not support dynamic linking in this runtime. Any native code needed by Python extensions must be statically linked into the host build.
Use a custom host for:
- typed WASM imports instead of string/JSON calls
- host functions exposed as custom Python modules
- extra Python modules prewarmed into the Wizer snapshot
- native libraries required by Python extensions
Start from examples/custom-host/. The flow is:
- Define an extension contract in
extension.toml. - Use
boomslang-hostgento generate Rust and Java bridge code. - Compose the extension with
python-host-corein a custom Rust host. - Add any required native libraries to the WASI build as static libraries.
- Build the host to
wasm32-wasip1. - Package the custom
boomslang.wasmand matching Python resources in your app or artifact. - Depend on
com.hubspot:boomslang:no-python-runtimefor the Java API.
Minimal build command from the example:
export CPYTHON_WASI_DIR=../../cpython/build/cpython-wasi
cargo build --target wasm32-wasip1 --releaseFor the stock repo build that produces the bundled runtime, use just wasm and just resources.
Requirements: Java 21, Maven, just, and Docker on Linux. Apple container is also supported on macOS.
With Nix, use the project dev shell:
nix developThe dev shell provides Java 21, Maven, just, Python 3, and the Maven JDK toolchain configuration required by basepom. Docker or Apple container still needs to be installed and running on the host for the full WASM pipeline.
./mill artifacts.installAll
./mill buildThat builds the native WASM artifacts, Rust host, Python resources, Java AOT classes, and Maven packages. First runs take about an hour because the CPython and library builds are container-heavy.
The selected container engine is stored in the ignored .boomslang-container-cli file so Mill daemon builds see a stable input. The ./mill wrapper also writes that file when BOOMSLANG_CONTAINER_CLI is set.
Docker is the default container engine. To be explicit on Linux:
./mill artifacts.setContainerCli --cli docker
./mill artifacts.showContainerCli
./mill artifacts.installAll
./mill buildDocker builds require BuildKit/buildx because the Dockerfiles use BuildKit syntax and automatic target architecture args.
Use Apple container instead of Docker:
container system start
./mill artifacts.setContainerCli --cli container
./mill artifacts.showContainerCli
./mill artifacts.installAll
./mill buildCommon local workflows:
just fetch-main-wasm # download latest main runtime resources from GitHub release assets
just build # package with AOT, skips tests
just test # tests module
mvn compile -pl core
mvn test -pl testsjust fetch-main-wasm installs the latest main runtime artifact published as a GitHub release asset into core/src/main/resources/python/bin/ and core/src/main/resources/python/usr/. Use it when you want a fast local Java build without rebuilding the full WASM/CPython pipeline. To fetch a specific artifact, pass through selectors:
just fetch-main-wasm -- --branch main
just fetch-main-wasm -- --sha <commit-sha>After Rust or host changes:
just wasm
just resources
just build
just testArtifact DAG and cache inspection:
./mill artifacts.dag
./mill artifacts.dagDot
./mill artifacts.cacheStatus
./mill path artifacts.installAll artifacts.wasm./mill plan artifacts.installAll prints execution order only. To check caching behavior, run ./mill artifacts.installAll twice; the second run should skip task bodies and finish much faster.
Full pipeline stages:
just build-pydantic-core-wasi
just build-numpy-wasi
just build-pandas-wasi
just build-matplotlib-wasi
just build-pillow-wasi
just build-ijson-wasi
just build-cpython-wasi
just pip-packages
just wasm
just resources
just build
just testcore/: Java runtime API and bundled Python resourcestests/: integration testsbenchmarks/: JMH benchmarkspython-host/: stock Rust WASM hostpython-host-core/: reusable Rust host coreextensions/: built-in host extensionsboomslang-hostgen/: extension code generatorexamples/custom-host/: custom host examplecpython/: CPython, native library, and container build pipeline
Apache 2.0