Duchess: Java-Rust Interop
repository·main·Indexed 42 days ago
https://github.com/duchess-rs/duchessA Rust crate providing ergonomic and efficient interoperability with Java. It allows developers to reflect Java classes into Rust using the `java_package!` macro, invoke Java methods with Rust-idiomatic snake_case syntax via lazy `JvmOp` execution, and implement Java native methods in Rust.
What's inside Duchess
- Duchess is a Rust crate designed for easy, ergonomic, and efficient interoperation with Java code. It allows you to reflect Java classes into Rust and invoke methods on Java objects directly from your Rust codebase.
Interoperate with Java code from Rust using Duchess
mainDuchess allows you to reflect Java classes into Rust and invoke methods on Java objects safely and efficiently. You can instantiate Java classes and call their methods using a syntax that maps Java's object-oriented patterns to Rust's ergonomic patterns.
Key workflow steps:
- Instantiate a Java class using
.new().execute()?to get aJava<T>handle. - Call methods on the handle using snake_case versions of the Java method names.
- Wrap method arguments in the appropriate Java types (e.g.,
java::util::Date::new()). - Chain
.execute()?at the end of method calls to perform the actual interop operation.
// Example: Calling Java methods from Rust let logger: Java<log::Logger> = log::Logger::new().execute()?; logger .add_event( log::Event::builder() .with_time(java::util::Date::new()) .with_name("foo") .build(), ) .execute()?;- Instantiate a Java class using
Understand the structure of Duchess end-to-end tests
mainThe
test-cratesdirectory contains crates designed to test the end-to-end behavior ofduchess. This setup is specifically used to validate differentCLASSPATHconfigurations and to test integration with real-world libraries that are too large to be bundled directly in the repository.Tests are organized into two main categories based on which language initiates the application:
ui(Rust-initiated): Rust tests that verify the ability of applications started in Rust to interoperate with Java.java_ui(Java-initiated): Java tests that verify the ability of applications started in Java to interoperate with Rust.
Execution Order:
java_uitests are run after the completion of the Rustuitests. This sequence is required because the Rust tests in theuidirectory are compiled into.sofiles, which the Java tests then consume.How `JavaObject` references are managed
mainThe
JavaObjecttrait is anunsafetrait used to represent Java types in Rust. When a structSimplementsJavaObject, it guarantees that every&Sreference corresponds to a valid JNI local or global reference.Construction Pattern: To prevent users from creating invalid Java object references, Duchess uses a pattern where:
- Structs representing Java types (e.g.,
duchess::java::lang::Object) have private fields that make them impossible to construct via safe Rust code. - Instances are created using constructors like
Object::new, which returns an implementation of theJavaConstructortrait. - Evaluating the constructor yields a
Localwrapper, which is the only safe way to obtain a reference to a JNI-managed object.
- Structs representing Java types (e.g.,
How `Jvm::with` manages thread attachment
mainDuchess uses
Jvm::withto provide access to the JVM. This method guarantees that:- The JVM has been started (using default settings if necessary).
- The current thread is attached to the JVM.
Thread Attachment Lifecycle:
- Duchess tracks the current thread's status via thread-local storage.
- If the thread is not already attached, Duchess invokes the JNI
AttachCurrentThreadmethod. - Once the
Jvm::withclosure returns, the thread is automatically detached.
Optimization: If you want to avoid the overhead of attaching and detaching for every call, you can use
Jvm::attach_thread_permanentlyto set the thread-local variable to a permanent state, which prevents automatic detaching.// Example concept: Jvm::with manages attachment automatically Jvm::with(|jvm| { // The thread is guaranteed to be attached here }); // The thread is detached here (unless attached permanently)How transitive upcasts are computed
mainTransitive upcasts are computed by iteratively growing a map of types and their supertypes.
- Seeding: A map is initialized where each known type
Cis mapped to its direct supertypes. - Iteration: The system iterates through the map. For each class
C, it fetches the supertypes of its direct supertypesDand performs a union of those supertypes into the set forC.
Substitution: When dealing with generics, the system accounts for substitution. If a class
Fooextends a generic classBaz<X>, the generic parameterXis substituted into the type definition ofBazduring the upcast process.- Seeding: A map is initialized where each known type
Understand the lack of nested local frame support
mainDuchess does not support nested JNI local frames. This is a deliberate design choice to simplify the API and avoid requiring users to pass a 'context' or 'environment' object into every single method call.
Implications:
- Lifetimes of local variables are tied directly to the input objects rather than a separate context object.
- For example, if a method on
MyObjectreturns aLocal<'jvm, ReturnType>, the lifetime'jvmis derived directly from the lifetime of&'jvm self. - While this makes the API cleaner for most operations, static methods and constructors still require an explicit environment/JDK pointer.
How Java objects are represented in Rust
mainIn Duchess, Java objects are represented by a dummy Rust struct that implements the
JavaObjecttrait.To maintain memory safety and the JNI invariant, users are never allowed to own these dummy structs directly. Instead, they interact with Java objects through specific pointer types (
LocalorGlobal) that ensure every&MyObjectreference is actually a validsys::jobjectpointer. The lifetime of these references is tied to the JVM's local frame length.// Example of the internal representation pattern pub struct MyObject { _dummy: () } unsafe impl JavaObject for MyObject { }How JVM operations work in Duchess
mainJVM operations in Duchess are lazy. Instead of executing immediately, you compose them using a series of method calls to build up a sequence of actions. This lazy composition allows Duchess to optimize the execution by reducing the number of JNI (Java Native Interface) invocations, which improves performance.
To run the composed operations, you must call the
executemethod and provide a mutable reference to aJvminstance.Understand Libjvm loading and linking
mainDuchess handles the linking of
libjvmin two ways:Dynamic Linking (Default)
By default, the
dylibjvmfeature is enabled. Duchess will dynamically load and linklibjvmat runtime. It searches forlibjvmin the following order:- The directory specified by
JAVA_HOME. - The JRE installation located via the
javaexecutable on yourPATH.
Static Linking
If you disable the
dylibjvmfeature,libjvmmust be statically linked.Custom Configuration
For non-standard installations, you can use
JvmBuilderto configure the JVM location.- The directory specified by
Understand the Upcast trait and type relations
mainThe
Upcasttrait is used to encodeextendsandimplementsrelationships between Java classes and interfaces within Rust. It supports both direct supertypes and indirect (transitive) supertypes.For example, if a Java hierarchy is defined as:
class Foo extends Bar { } class Bar implements Serializable { }Then the Rust representation of
Foowill implementUpcastfor:Bar(direct superclass)java::io::Serializable(transitive interface viaBar)java::lang::Object(transitive superclass viaBar)
Limitation: Transitive upcasts are only computed up to the "water's edge". This means upcasting stops once the traversal encounters classes that are outside the current package being reflected into Rust.
Modeling Method Resolution Order (MRO) with `ViewAs` structs
mainTo mimic Java's inheritance and method lookup, Duchess uses
ViewAsstructs that form aDerefchain based on the C3 linearization algorithm.For a class
X, aViewAsXObj<J, N>struct is created where:Jis the original subtype.Nis the next type in the MRO (the remainder of the chain).
The Deref Chain: If
Foo extends Bar, Baz, the typeFooderefs through a chain:ViewAsFooObj<Foo, ViewAsBarObj<Foo, ViewAsBazObj<Foo, ()>>>$\rightarrow$ViewAsBarObj<Foo, ...>$\rightarrow$ViewAsBazObj<Foo, ()>$\rightarrow$().This allows Rust's method dispatch to walk the hierarchy to find the most specific implementation of a method.
#[repr(transparent)] struct ViewAsXObj<J, N> { this: J, phantom: PhantomData<N>, } impl<J, N> Deref for ViewAsXObj<J, N> { type Target = N; fn deref(&self) -> &Self::Target { // ... } }