duchess-rs / duchess

Silky smooth Java-Rust interop

Home Page:https://duchess-rs.github.io/duchess/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Implementing java interfaces for Rust types

nikomatsakis opened this issue · comments

I observe that many Java APIs take "callbacks" of some kind. Often this has the form

Eg eg = new EgBuilder()
    .onEvent(new EventCallback() {
        int counter = 0;
        void eventArrived(Event e) {
            counter += 1;
            System.out.println("Event " + counter + ": " + e.toString());
        }
    })
    .build();

though of course for simple enough interfaces you can use the .onEvent(e -> ...) lambda syntax instead of an inner object.

I'd like Rust users to be able to call these APIs conveniently. I have a branch that is most of the way there. This is how e.g. I imagine it working. Given this Java code.

package eg;

interface EventCallback {
    void eventArrived(Event e);
}

record Event { ... }

class EgBuilder {
    EgBuilder onEvent(EventCallback ec) { .. }
    Eg build();
}

You could write Rust code like

duchess::package! {
    package eg;
    interface EventCallback {*}
    record Event {*}
    class EgBuilder {*}
}

struct CounterCallback {
    counter: AtomicUsize,
}

// Implement the Java interface for the Rust type
#[duchess::implement_java_interface(eg::EventCallback)]
impl CounterCallback {
    fn on_event(&self, event: &eg::Event) -> duchess::Result<()> {
        let v = self.counter.fetch_add(1, SeqCst);
        let s: String = event.to_string().to_rust().execute()?;
        println!("event {v} is {s}");
        Ok(())
    }
}

// Invoke builder and supply a `CounterCallback` Rust object
fn invoke_builder() {
    let eg = EgBuilder::new()
        .on_event(CounterCallback::default())
        .build()
        .global()
        .execute();
    ...
}

The intent is that

  • The CounterCallback will be "moved" into a Java object -- when that object is collected by the GC, a call will be made to the Rust code to drop CounterCallback
  • when the Java code calls onEvent it will be routed to the Rust code

I'm working on this in a branch.

Some interesting questions:

  • This has to generate some Java "glue code" class. I am planning to have a mode where, upon initialization, the Rust code "injects" the new Java class into the JVM, simplifying distribution.
  • But another option would be to generate a .class file and extend duchess with some kind of tool to create a JAR.

To interface with the GC, we leverage the JVM's Cleaner class. This approach internally creates a PhantomReference (and a Runnable...) for each Rust object that is embedded in a Java object. This will scale fine for a "reasonable" number of Rust-to-Java objects. It would be a problem if callbacks were used in very large quantities. The examples I'm most interested in tend to be small in number though, more like configuration settings.