Dosu LogoDosu Logo
Ask
Join our Discord
PersonalPublic
Apache OpenDAL
DocumentsPersonal
Java-Rust JNI Bindings
Java-Rust JNI Bindings
Type
Topic
Status
Published
Created
Jul 12, 2026
Updated
Jul 12, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Java-Rust JNI Bindings in OpenDAL#

OpenDAL's Java bindings expose the core Rust blocking::Operator to Java via the Java Native Interface (JNI), implemented using the jni crate. The binding layer lives in bindings/java/src/ and bridges two type systems, two memory models, and two error-handling conventions.

Key source files:

LayerFile
Rust JNI entry pointbindings/java/src/lib.rs
Operator JNI functionsbindings/java/src/operator.rs
Error conversionbindings/java/src/error.rs
Input stream JNIbindings/java/src/operator_input_stream.rs
Output stream JNIbindings/java/src/operator_output_stream.rs
Java Operator classOperator.java
Java OperatorInputStreamOperatorInputStream.java
Java OperatorOutputStreamOperatorOutputStream.java

Function Naming Convention#

JNI resolves native methods by symbol name. Every Rust function exported to Java follows the pattern:

Java_{package_underscored}_{ClassName}_{methodName}

For example, Java_org_apache_opendal_Operator_read maps to the Java declaration private static native byte[] read(long op, String path, ReadOptions options) in Operator.java . All exported functions are #[no_mangle] pub unsafe extern "system".


Return Type Consistency#

Java native declarations and their Rust counterparts must agree on types. The mapping follows standard JNI conventions:

Java typeRust JNI type
void() (no return)
longjlong
byte[]jbyteArray
Object (any)jobject
Object[]jobjectArray

Examples from the codebase:

  • write → void: Java declares native void write(...) ; Rust function returns () .
  • read → byte[]: Java declares native byte[] read(...) ; Rust returns jbyteArray .
  • stat → Metadata (object): Java declares native Metadata stat(...) ; Rust returns jobject .
  • list → Entry[] (array): Java declares native Entry[] list(...) ; Rust returns jobjectArray .
  • constructReader → long (pointer): Java declares native long constructReader(long op, String path) ; Rust stores the StdBytesIterator pointer as a jlong .

⚠️ Mismatch pitfall: createDir, copy, and rename are declared as native long in Java but their Rust implementations return () (void). These return values are unused on the Java side and exist only as a declaration artifact. When adding new methods, ensure the Java return type matches the Rust return type to avoid silent UB.


Error Propagation Pattern#

JNI does not support Rust's Result type natively. OpenDAL's binding layer uses a two-function pattern to propagate errors as Java exceptions:

  1. Inner function (intern_*): Returns crate::Result<T> and uses ? for early exit on errors. This is ordinary Rust error handling.
  2. Outer exported function: Calls the inner function, then handles errors with .unwrap_or_else(|e| { e.throw(&mut env); <default_value> }).

When an error occurs, e.throw(&mut env) calls env.throw() via JNI to set a pending exception on the Java thread. The outer function returns a safe default value (e.g., JByteArray::default().into_raw() for byte arrays, JObject::default().into_raw() for objects, or 0 for longs), which Java ignores once the exception is pending.

See this pattern across: operator.rs read, operator.rs write, operator_input_stream.rs constructReader, and operator_output_stream.rs writeBytes.

Exception Type: OpenDALException#

All Rust errors are converted to org.apache.opendal.OpenDALException . The to_exception method maps opendal::ErrorKind variants to string error codes:

ErrorKindString code
NotFound"NotFound"
PermissionDenied"PermissionDenied"
AlreadyExists"AlreadyExists"
Unsupported"Unsupported"
ConfigInvalid"ConfigInvalid"
IsADirectory / NotADirectory"IsADirectory" / "NotADirectory"
RateLimited"RateLimited"
IsSameFile"IsSameFile"
ConditionNotMatch"ConditionNotMatch"
RangeNotSatisfied"RangeNotSatisfied"
all others"Unexpected"

JNI errors (e.g., from type conversions) are also funneled through this path: jni::errors::Error converts to an opendal::Error with kind Unexpected , so all errors surface uniformly as OpenDALException.


Native Object Lifetime and Memory#

Rust objects (operators, readers, writers) are heap-allocated and passed to Java as jlong pointer values stored in a NativeObject base class. Java owns the lifecycle: when close() or disposeInternal() is called, the corresponding Rust drop or close runs. For example:

  • Operator.disposeInternal calls Rust drop(Box::from_raw(op)) .
  • OperatorOutputStream.disposeWriter calls writer.close() to flush before dropping .

Stream I/O#

OperatorInputStream wraps a StdBytesIterator that yields byte chunks. The Java side buffers one chunk and drains it byte-by-byte via read() . A null return from Rust readNextBytes signals EOF .

OperatorOutputStream buffers writes up to a configurable maxBytes (default 16 384) then flushes by calling native writeBytes to a blocking::Writer . The writer is closed (and the stream committed) in disposeWriter, which calls writer.close() .

Documents
Cloud Service Authentication
GCS Path Encoding
Java-Rust JNI Bindings
OpenDAL Read Validation