Duchess: Java-Rust Interop

repository·main·Indexed 42 days ago

https://github.com/duchess-rs/duchess

A 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.

Tokens
26.6K
Snippets
76
Records
129
Agent score
52%

What's inside Duchess

  1. Overview of Duchess Java-Rust Interoperability

    main
    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.
  2. Interoperate with Java code from Rust using Duchess

    main

    Duchess 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:

    1. Instantiate a Java class using .new().execute()? to get a Java<T> handle.
    2. Call methods on the handle using snake_case versions of the Java method names.
    3. Wrap method arguments in the appropriate Java types (e.g., java::util::Date::new()).
    4. 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()?;
  3. Understand the structure of Duchess end-to-end tests

    main

    The test-crates directory contains crates designed to test the end-to-end behavior of duchess. This setup is specifically used to validate different CLASSPATH configurations 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:

    1. ui (Rust-initiated): Rust tests that verify the ability of applications started in Rust to interoperate with Java.
    2. java_ui (Java-initiated): Java tests that verify the ability of applications started in Java to interoperate with Rust.

    Execution Order: java_ui tests are run after the completion of the Rust ui tests. This sequence is required because the Rust tests in the ui directory are compiled into .so files, which the Java tests then consume.

  4. How `JavaObject` references are managed

    main

    The JavaObject trait is an unsafe trait used to represent Java types in Rust. When a struct S implements JavaObject, it guarantees that every &S reference 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:

    1. Structs representing Java types (e.g., duchess::java::lang::Object) have private fields that make them impossible to construct via safe Rust code.
    2. Instances are created using constructors like Object::new, which returns an implementation of the JavaConstructor trait.
    3. Evaluating the constructor yields a Local wrapper, which is the only safe way to obtain a reference to a JNI-managed object.
  5. How `Jvm::with` manages thread attachment

    main

    Duchess uses Jvm::with to 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 AttachCurrentThread method.
    • Once the Jvm::with closure 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_permanently to 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)
  6. How transitive upcasts are computed

    main

    Transitive upcasts are computed by iteratively growing a map of types and their supertypes.

    1. Seeding: A map is initialized where each known type C is mapped to its direct supertypes.
    2. Iteration: The system iterates through the map. For each class C, it fetches the supertypes of its direct supertypes D and performs a union of those supertypes into the set for C.

    Substitution: When dealing with generics, the system accounts for substitution. If a class Foo extends a generic class Baz<X>, the generic parameter X is substituted into the type definition of Baz during the upcast process.

  7. Understand the lack of nested local frame support

    main

    Duchess 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 MyObject returns a Local<'jvm, ReturnType>, the lifetime 'jvm is 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.
  8. How Java objects are represented in Rust

    main

    In Duchess, Java objects are represented by a dummy Rust struct that implements the JavaObject trait.

    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 (Local or Global) that ensure every &MyObject reference is actually a valid sys::jobject pointer. 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 { }
  9. How JVM operations work in Duchess

    main

    JVM 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 execute method and provide a mutable reference to a Jvm instance.

  10. Understand Libjvm loading and linking

    main

    Duchess handles the linking of libjvm in two ways:

    Dynamic Linking (Default)

    By default, the dylibjvm feature is enabled. Duchess will dynamically load and link libjvm at runtime. It searches for libjvm in the following order:

    1. The directory specified by JAVA_HOME.
    2. The JRE installation located via the java executable on your PATH.

    Static Linking

    If you disable the dylibjvm feature, libjvm must be statically linked.

    Custom Configuration

    For non-standard installations, you can use JvmBuilder to configure the JVM location.

  11. Understand the Upcast trait and type relations

    main

    The Upcast trait is used to encode extends and implements relationships 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 Foo will implement Upcast for:

    • Bar (direct superclass)
    • java::io::Serializable (transitive interface via Bar)
    • java::lang::Object (transitive superclass via Bar)

    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.

  12. Modeling Method Resolution Order (MRO) with `ViewAs` structs

    main

    To mimic Java's inheritance and method lookup, Duchess uses ViewAs structs that form a Deref chain based on the C3 linearization algorithm.

    For a class X, a ViewAsXObj<J, N> struct is created where:

    • J is the original subtype.
    • N is the next type in the MRO (the remainder of the chain).

    The Deref Chain: If Foo extends Bar, Baz, the type Foo derefs 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 {
            // ...
        }
    }