Android wheels¶
PyJNIus can be published as a prebuilt PEP 738
Android wheel (android_* tags), so an Android packager can pip install
it instead of compiling from source per app. The wheel is SDL-agnostic and
Java-free: it resolves the JVM JNIEnv at runtime and ships no Java. In
return, the host application must satisfy a small runtime contract (below).
Note
This describes the redistributable wheel. On desktop, PyJNIus still starts
its own JVM and bundles the compiled NativeInvocationHandler.class as
before; nothing here changes desktop behavior.
The runtime contract¶
The wheel does not create a JVM on Android; it attaches to the one the host process already has. A conforming host must provide:
An in-process JVM discoverable at import. The
JNIEnvis resolved at the firstimport jniusin this order:SDL_GetAndroidJNIEnvfromlibSDL3.so(SDL3), elseSDL_AndroidGetJNIEnvfromlibSDL2.so(SDL2), elseJNI_GetCreatedJavaVMsfromlibnativehelper.so(SDL-independent, API 31+ only) +AttachCurrentThread.
For an SDL host, its SDL library must be loaded before the first
import jnius(a Kivy/SDL app loads SDL at startup, which captures theJavaVMvia SDL’s ownJNI_OnLoad). Each tier triesdlsym(RTLD_DEFAULT, ...)first, thendlopens the library by soname and resolves the symbol from that handle – a CPython extension imported viadlopendoes not have the host’sSystem.loadLibrary‘d SDL in its default lookup scope. NoDT_NEEDEDon anylibSDL/libnativehelperis created, so the wheel loads regardless of which (if any) SDL is present.The Java glue on the app’s dex classpath. PyJNIus’s Python-implements-a-Java-interface feature (
PythonJavaClass/@java_method) needs the classorg.jnius.NativeInvocationHandler. A.whlcarries no.dexand a class insite-packagesis not on ART’s classpath, so the wheel does not and cannot deliver it. The packager (bootstrap/build system) must compile and dexorg.jnius.NativeInvocationHandlerinto the APK. Plainautoclassand method calls need no glue; only proxies do.Warning
Matched-pair coupling. The wheel’s native
invoke0contract (bound at runtime viaRegisterNatives) and the packager’s copy ofNativeInvocationHandler.javamust be updated together. Nothing enforces this once the glue is out of the wheel – a mismatch surfaces as aClassNotFound/NoSuchMethodat first proxy creation. The canonical source lives in the PyJNIus repository atjnius/src/org/jnius/NativeInvocationHandler.java.
If no JVM/glue is found, the first import jnius raises a clear
RuntimeError naming the missing getter, rather than a cryptic dlopen/symbol
failure.
Building the wheel¶
Build on a Linux x86_64 (WSL2 counts) or macOS host with an Android SDK;
cibuildwheel
drives the NDK via sdkmanager. CI pins the cibuildwheel action in
.github/workflows/android-wheels.yml; the selected Android Python bundles
specify the NDK version. Android build settings live in
[tool.cibuildwheel.android] in pyproject.toml; its before-build
check rejects a compiler from an unexpected NDK version.
# 1. Android compiles a *pre-generated* jnius.c (Cython is build-only and not
# needed on the target). config.pxi selects the android include branch.
printf "DEF JNIUS_PLATFORM = 'android'\n" > jnius/config.pxi
cython -3 jnius/jnius.pyx -o jnius/jnius.c
# 2. Build from a CLEAN tree. `python -m build --no-isolation` reuses
# build/lib.android-*; a stale one from an earlier build re-injects the
# Java payload and breaks the Java-free guarantee. A fresh checkout (CI) is
# already clean.
rm -rf build
# 3. arm64_v8a ships to devices; x86_64 is the testability ABI (cibuildwheel's
# emulator testbed only runs the build-host arch).
cibuildwheel --only cp314-android_arm64_v8a --output-dir wheelhouse
cibuildwheel --only cp314-android_x86_64 --output-dir wheelhouse
The frontend must be build/uv (Android does not support the pip
frontend); this is set in pyproject.toml. CPython 3.15 (pre-release until
Oct 2026) additionally needs --enable cpython-prerelease.
16 KB page alignment. Android 15/16 requires native .sos to have
16 KB-aligned LOAD segments. The build pins
LDFLAGS = "-Wl,-z,max-page-size=16384" in [tool.cibuildwheel.android] so
the wheel is 16 KB-aligned independent of the NDK’s default. This aligns only
PyJNIus’s .so – the packager is responsible for aligning the bootstrap libs
(SDL, libpython, …) and for 16 KB zip-aligning the final APK.
Verifying a built wheel¶
# unzip the wheel, then, with the NDK's llvm-readelf:
llvm-readelf -d jnius/jnius*.so | grep NEEDED # no libSDL / libnativehelper / libart
llvm-readelf -l jnius/jnius*.so | grep LOAD # Align column == 0x4000 (16 KB)
llvm-readelf --dyn-syms jnius/jnius*.so | grep -E 'dlopen|dlsym' # runtime-resolved
The wheel should contain only the .so and the Python modules – no
src/org, .java or .class.