Offline Protocol Java binding
The Offline Protocol Java binding is a typed Java API for the JVM on desktop and server, added in version 0.30.0. It is generated by uniffi-bindgen-java from the same interface definition as the Swift, Kotlin, Python and Ruby bindings, so the engine, records and errors match the other languages.
It calls the native library directly through the Java foreign function API. There is no JNA, no JNI and no hand-written native glue, and it does not go through the C API. The package is com.offlineprotocol.sdk:
OfflineProtocolis the engine, andMeshServicesandDataStorehang off it.- Every record and enum of the definition is a class.
- What a host implements are interfaces:
EventCallback, the transport callbacks and the two storage providers. - An engine refusal is a subclass of
ProtocolExceptionnamed for the variant, such asProtocolException.EncryptFailed. OfflineProtocolLibholds the functions that need no engine, such asderiveAddress,parseInviteandrunStorageConformance.
It is not the Kotlin binding. On Android, use the Android library instead; this jar is for any other JVM.
Maven Central coordinates and JDK 22
The jar is on Maven Central as com.offlineprotocol:offline-protocol-java, published at each release’s version beside the Android library:
// build.gradle.kts
dependencies {
implementation("com.offlineprotocol:offline-protocol-java:0.31.0")
}
The minimum is JDK 22, because the foreign function API is final in Java 22 and the code is compiled with --release 22. The JVM must also be allowed to call native code: pass --enable-native-access=ALL-UNNAMED, or your module’s name. Without it the JVM warns on the first native call, and a future JDK will refuse it.
Native library for each host
The jar holds the generated Java and no native code, so each host downloads the native library from the GitHub release’s desktop archives:
| Host | Library |
|---|---|
| Linux x86_64 and aarch64 | liboffline_protocol_uniffi.so |
| macOS arm64 | liboffline_protocol_uniffi.dylib |
| Windows x86_64 | offline_protocol_uniffi.dll |
Use a jar and a library from the same release. The JVM finds the library through java.library.path:
java --enable-native-access=ALL-UNNAMED \
-Djava.library.path=/path/to/library/directory \
-jar app.jar
To name the file by absolute path instead, pass -Duniffi.component.offline_protocol.libraryOverride= with the path. The generated Java checks the library’s FFI contract version and every function’s checksum when it loads, so a mismatched library fails at the first call rather than misbehaving later. On a platform without a published library, cargo build --release -p offline-protocol-uniffi builds it from source.
Java quick start
A Java program creates the engine from a ProtocolConfig, sets up storage, then starts it. This is the engine part of the binding’s own quick start:
try (OfflineProtocol engine = new OfflineProtocol(config)) {
// Storage before start(). The store key seals key material at rest;
// keep it out of the source.
engine.initializeMlsWithFileStores("/var/lib/app/mls", "/var/lib/app/state", storeKey32Bytes);
engine.setEventCallback(json -> queue.add(json)); // copy and return; never call the engine from here
engine.start();
System.out.println("this device is " + engine.localAddress());
try {
engine.sendMessage("off1...", "hi", MessagePriority.MEDIUM, null);
} catch (ProtocolException.EncryptFailed e) {
// each engine refusal is its own exception type
}
engine.stop();
}
Java has no default arguments, so new ProtocolConfig(...) takes all 35 fields in order, and optional fields take null. The binding’s ConsumerCheckTest.config() carries the definition’s defaults field for field, which makes it the reference to copy from.
Events arrive as the engine’s JSON text, through setEventCallback or by calling pollEvent; every event is queued either way. A callback runs on whichever thread is inside the engine, must be safe from any thread and must not call back into the engine, closing it included. Copy the event, queue it and return.
Host responsibilities in Java
In Java your process is the platform, so it decides how frames move and where state lives. The engine handles routing, encryption and delivery, and the host connects it to the world:
- Transports. Implement
BleTransportCallback,WifiDirectTransportCallback,ReticulumTransportCallbackandNostrTransportCallbackto learn when a queue has frames, then drain them with theble*,wifiDirect*,internet*,reticulum*andnostr*methods. - Storage. Implement
MlsStorageProviderfor key material, backed by a keystore, andProtocolStateStorageProvider, and pass both toinitializeMls. Prove an adapter withOfflineProtocolLib.runStorageConformance. A host with a filesystem can use the built-in file stores instead, as in the quick start. - Telemetry platform. If you enable telemetry, pass
TelemetryConfig.osandosMajoras the platform the process actually runs on, read from the JVM’sos.nameandos.version. - Lifecycle. Call
start,stop, andprocesson a timer.try-with-resourcesfrees the engine once; it does not wait for calls in flight on other threads.
This gives a JVM service full control over its carriers. For transports ready-made, the Python package ships Bluetooth LE, internet, LAN peer-stream and gateway bridges on desktop and server hosts, and on phones React Native is the most complete integration.
Runnable Java example
The SDK repository has a runnable Java program in examples/java-app, and since 0.31.0 CI runs it. Two devices, Alice and Bob, live in one JVM and:
- pair from events, answering the connection request with a key package exchange;
- form an end-to-end encrypted session and exchange presence, typing, a message, a read receipt and a threaded reply;
- send a file that is checked byte for byte on arrival;
- create an MLS group, invite the paired peer and send a group message.
The carrier between them is a dozen lines: take what one engine wants to send (internetGetNextMessage), hand it to the other (internetMessageReceived) and confirm the write (internetConfirmSent). A real app does the same over a radio or a socket, and the pairing and MLS encryption stay the engine’s.
To run it from a checkout, with JDK 22 or later and Gradle 9:
cargo build -p offline-protocol-uniffi # from the repository root
cd examples/java-app
gradle run # Gradle 9; -PnativeLibrary=<file> for another library
The binding’s own consumer check, ConsumerCheckTest, runs against the real library in CI on Linux, macOS and Windows. The SDK is AGPL-3.0-only, with a commercial licence for proprietary use.