Skip to content

dchenbecker/duchess

Repository files navigation

duchess

Experiments with Java-Rust interop

Instructions

You need the javap tool on your path.

On Ubuntu, I installed java-20-amazon-corretto-jdk/stable, but openjdk-17-jre/stable-security might also work. --nikomatsakis

How to use

This is a README from the future, in that it describes the intended plan for the crate.

What it does

Duchess makes it easy to call Java APIs from your Rust code. It may eventually help with bidirectional support, but right now that is not supported.

How duchess works

Let's suppose that you have a java class me.ferris.Logger:

class Logger {
    public static Logger globalLogger();

    public Logger();

    // Simple, convenient log method
    public void log(String data);

    public void logFull(LogMessage message);
}

class LogMessage {
    public LogMessage(String message);

    LogMessage level(int level);
}

which you can use in your java code to issue simple logs

Logger.globalLogger().log("Hello, world");

or to issue more complex ones

LogMessage m = new LogMessage("Hello, world").level(22);
Logger.globalLogger().log(m);

But now you would like to write some Rust code that invokes this same logging service. What do you do?

TL;DR

For the impatient among you, here is the kind of code you're going to be able to write when we're done. First you declare the java classes you want to work with:

duchess::duchess! {
    me.ferris.Logger,
    me.ferris.LogMessage,
}

and then instead of this java code

Logger.globalLogger().log("Hello, world");

you can write Rust like this

duchess::with_jvm(|jni| {
    use me::ferris::Logger;
    Logger::globalLogger().log("Hello, world").execute(jni);
});

and instead of this Java code

LogMessage m = new LogMessage("Hello, world").level(22);
Logger.globalLogger().log(m);

you can write Rust like this

duchess::with_jvm(|jni| {
    use me::ferris::{Logger, LogMessage};
    LogMessage::new("Hello, world").level(22).execute(jni);
    Logger::globalLogger().log(&m).execute(jni);
});

Huzzah!

What code does the macro generate?

Let's walk through this in more detail. To start, use the duchess! macro to create a Rust view onto the java code. The duchess! macro supports various bells and whistles, but in its most simple form, you just declare a module and list some java classes inside of it.

duchess::duchess! {
    me.ferris.Logger // Always list the java classes by their full dotted name!
}

The procedural macro will create a module named jlog and, for each class that you name, a struct and an impl containing all of its methods, but mirrored into Rust. The structs are named after the full Java name (including the package), but there are type aliases for more convenient access:

mod me {
    pub mod ferris {
        pub struct Logger<'jvm> { ... }    
    
        impl<'jvm> Logger<'jvm> {
            pub fn globalLogger(jvm: Jvm<'jvm>) -> Logger<'jvm> {
                ...
            }

            pub fn log(&self, jvm: Jvm<'jvm>, s: impl AsJavaString) {
                ...
            }

            pub fn log_full(&self, jvm: Jvm<'jvm>, s: &impl AsRef<LogMessage<'jvm>>) {
                ...
            }
        }
    }

    ... // more to come
}

Where possible, we translate the Java argument types into Rust-like forms. References to Java strings, for example, compile to impl AsJavaString, a trait which is implemented for &str and String (but also for a reflected Java string).

pub fn log(&self, jvm: Jvm<'jvm>, s: impl AsJavaString) {
    ...
}

In some cases, methods will reference Java classes besides the one that appeared in the proc macro, like me.ferris.LogMessage:

pub fn log_full(&self, jvm: Jvm<'jvm>, s: &impl AsRef<LogMessage<'jvm>>)

These extra types get translated to structs as well. But these structs don't have impl blocks or methods. They're just opaque values you can pass around:

mod me {
    pub mod ferris {
        // From before:
        pub struct Logger<'jvm> { ... }
        impl<'jvm> Logger<'jvm> { ... }

        // Also generated:
        pub struct LogMessage<'jvm> { ... }
    }

    ...
}

In fact, we'll also generate entries for other Java classes, like

mod me {...}
mod java {
    pub mod lang {
        pub struct Object<'jvm> { ... }
        pub struct String<'jvm> { ... }
    }
}

Finally, we generate various AsRef (and From) impls that allow for upcasting between Java types:

mod me { /* as before */  }
mod java { /* as before */ }

impl<'jvm> AsRef<java::lang::Object<'jvm>> for me::ferris::Logger<'jvm> { ... }
impl<'jvm> AsRef<java::lang::Object<'jvm>> for me::ferris::LogMessage<'jvm> { ... }

impl<'jvm> From<me::ferris::Logger<'jvm>> for java::lang::Object<'jvm>> { ... }
impl<'jvm> From<me::ferris::LogMessage<'jvm>> for java::lang::Object<'jvm>> { ... }

Implementation details

Our structs are actually thin wrappers around jni::JObject:

#[repr(transparent)]
pub struct Logger<'jvm> {
    object: jni::JObject<'jvm>
}

In some cases, the JNI crate only supplies &JObject values. These can be safely transmuted into (e.g.) &Logger values because of the repr(transprent) layout (c.f. rust reference, though I'd like to check on the details here --nikomatsakis). We provide a helper function cast for this purpose.

Creating a jni

The duchess::with_jvm code starts a JVM and invokes your closure. Clearly this needs to be cached, and we have to think about attaching threads and the like.

Deleting locals

It'd be nice to delete locals automatically. I wonder if we can do this via a Drop impl. It'll require a thread-local to get access to the JVM. Seems ok.

About

Experiments with Java-Rust interop

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Rust 98.6%
  • Java 1.4%