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