Skip to content

feat(bindings): Java binding generated from the UDL - #582

Merged
bahdotsh merged 12 commits into
mainfrom
feat/java-bindings-uniffi
Oct 20, 2026
Merged

bahdotsh merged 12 commits into
mainfrom
feat/java-bindings-uniffi

Conversation

@aditipolkam

@aditipolkam aditipolkam commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a Java binding, generated directly from the UDL by uniffi-bindgen-java 0.5.2 (the community generator for UniFFI 0.32). It is not a wrapper over the C API: it sits beside the Swift, Kotlin and Python bindings and is regenerated with them by scripts/generate-bindings.sh.

  • Generated sources under bindings/java/src/main/java (package com.offlineprotocol.sdk), using the JDK foreign function API (JDK 22 or later).
  • scripts/generate-bindings.sh generates Java with the other three, and its guard tests cover it (61 guards pass).
  • A Gradle project (bindings/java) and a consumer check that loads the real library, with an in-memory MLS and protocol-state storage (7 tests).
  • A Java Binding CI job on Linux and macOS, and the install of uniffi-bindgen-java in the Python-bindings drift gate and in release.yml (all installs use --force, as in chore(uniffi): bump UniFFI from 0.30 to 0.32 #578).
  • docs/bridges/java.md records the contract, and the bridge docs, README and CONTRIBUTING name it.

Related issues

Builds on #578 (the UniFFI 0.30 to 0.32 bump, which the Java generator needs).

Type of change

  • feat: new feature

Checks run locally

  • ./scripts/generate-bindings.sh: no drift on the rebased branch.
  • scripts/tests/test-generate-bindings.sh: 61 guards pass.
  • gradle build in bindings/java (JDK 26, Gradle 9.3.1): the 7 consumer-check tests pass against the freshly built library.

Not run locally: the macOS CI job, release.yml changes, and the React Native and Swift builds (this PR does not change their sources, only the shared generate script).

…ndgen-java

267 files in com.offlineprotocol.sdk: the engine (OfflineProtocol), every
record and enum of the interface definition, the callback interfaces a host
implements (EventCallback, the transport callbacks, the storage providers),
MeshServices and DataStore. It is the same API surface Swift, Kotlin and
Python expose, typed, and talks to the library through the Java 22 foreign
function API, with no JNA and no native glue. uniffi.toml sets the package
and the library name.

Written by scripts/generate-bindings.sh, which the next commit teaches to
run the Java generator; the output here is what it produces.
scripts/generate-bindings.sh runs uniffi-bindgen-java after Swift, Kotlin
and Python, so the four are one artifact set (C1) and no caller can produce
a partial one. The Java generator is versioned apart from uniffi and tracks
it by minor, so the script refuses any series but 0.5, before writing
anything, for the reason it refuses a mismatched uniffi-bindgen: the Java
checks the library's FFI contract and fails at the first call.

The guard script runs the generator against stub bindgens as before and now
asserts four calls, the Java output path, the Java series refusal, and that
the scan for direct bindgen invocations also catches uniffi-bindgen-java.
CI deletes and diffs the Java output with the rest, and every job that runs
the generator, the release workflow's three included, installs the Java
generator. The scripts that check for uniffi-bindgen up front check for it
too.
Compiles bindings/java/src/main/java with release 22, the first Java in
which the foreign function API the generated code calls is final, and
builds the binary, sources and Javadoc jars. There is no dependency: no
JNA, no native glue. The tests take the native library from
-PnativeLibrary (default: this checkout's Cargo debug output) through the
generated loader's libraryOverride property, and run with
--enable-native-access.
The counterpart of bindings/c/consumer-check/main.c, against the generated
API: an instance-less function, an engine whose events reach the callback
on the calling thread and the poll queue, a refusal as a typed
ProtocolException subclass, a Java provider passing the storage
conformance suite and giving the engine an address, the built-in file
stores, and a provider that cannot store failing the suite. The generated
code checks the library's FFI checksums before the first call, so a binding
generated from a different definition than the library fails all of it.
The generated ProtocolConfig has no defaults, so the test spells out the
definition's.
…macOS

Builds the library, then compiles the committed generated sources and runs
the consumer check against it on JDK 25 with Gradle 9.3.1. The generated
code checks the library's FFI checksums at load, so the job also says the
two were generated from the same definition.
…in the docs

bindings/java/README.md is the guide: what to install and how the JVM finds
the library, a quick start, events, transports and storage, and the build.
docs/bridges/java.md is the contract: which shared rules reach a binding
with no hand-written code, why the generator is pinned to a series, that the
generated records have no defaults and so a Java caller can disagree with
the other bindings about one, that the host fills the telemetry platform,
and what the JVM requires (JDK 22, native access).

The binding gets its row wherever the others have one, and every place that
said the artifact set was three bindings, or listed what to install to
regenerate it, now says four.
The Gradle project and the docs claim desktop JVMs, and the C binding
already has a Windows job. The generated loader's Windows path was the
one unexercised.
… version

Only the series was checked at run time, so a moved pin that missed the
CI, release, or documentation lines still passed. The exact release is
now one variable in the generator, and a guard compares every written
install, and the release workflow's variable, against it.
Asserting only that the report was not all-pass would also pass if the
exception had crashed the engine. The report must carry the storage
error it was converted to.
The bundle and upload scripts named the Android library's coordinate and
its aar. MAVEN_ARTIFACT_ID and MAVEN_PACKAGING select the Java binding's
jar, defaulting to the Android library so its release is unchanged. The
tests bundle a Java repository, refuse the Android one as Java, and pin
that the re-run probe asks for the Java POM, not the Android one.
com.offlineprotocol:offline-protocol-java, with the POM the Android
library carries and the license notices under META-INF. CI writes the
repository on every pull request, so a publication error shows there and
not on a tag.
Built and tested against the release profile's library in a credential
free job, then signed and uploaded as its own deployment under the
Android library's switch, so one refusal does not hold back the other.
The release waits on its tests. Documented as J6, with install steps.
@bahdotsh
bahdotsh merged commit e524bfa into main Oct 20, 2026
30 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants