Tutorial: Call Java from Rust

Setup

Be sure to follow the setup instructions.

The Java class we would like to use from Rust

Imagine we have a Java class Factory that we would like to use from Rust, defined like so:

package com.widgard;

public class Factory {
    public Factory() { /* ... */ }
    public Widget produceWidget() { /* ... */ }
    public void consumeWidget(widget w) { /* ... */ }
}

public class Widget { /* ... */ }

Using a package from Rust

Using duchess, we can declare a Rust version of this class with the java_package! macro:

duchess::java_package! {
    // First, identify the package you are mirroring,
    // and the visibility level that you want.
    package com.widgard;

    // Next, identify classes whose methods you would like to call. 
    // The `*` indicates "reflect all methods".
    // You can also name methods individually (see below).
    class Factory { * }

    // For Widget, we choose not to mirror any methods.
    class Widget { }
}

Generated code

This module will expand to a module hierarchy matching the Java package name:

pub mod com {
    pub mod widgard {
        // One struct per Java class:
        pub struct Factory { /* ... */ }
        
        // The inherent impl defines the constructor
        // and any static methods:
        impl Factory { /* ... */ }

        // The extension trait defines the methods
        // on the struct, like `produceWidget`
        // and `consumeWidget`.
        pub trait FactoryExt { /* ... */ }
        
        // There is also a struct for other classes
        // in the same package if they appear in
        // the signature of the reflected methods. 
        //
        // In this case, `Factory#produceWidget`
        // returns a `Widget`, so we get this struct here.
        //
        // Since we did not tell duchess to reflect any
        // methods, there is no `WidgetExt` trait,
        // nor an inherent impl.
        pub struct Widget  { /* ... */ }
    }
}

NB: The java_package macro relies on the javap tool to reflect Java signatures. You will need to have the Java Development Kit (JDK) installed for it to to work. You will also need to help us to find the java code by setting CLASSPATH appropriately. Note that you can configure the environment in your Cargo.toml if desired.

Using the generated code

Once you've created the Java package, you can create java objects and invoke their methods. This should mostly just work as you would expect, with one twist. Invoking a Java method doesn't immediately cause it to execute. Instead, like an iterator or an async function, it returns a JvmOp, which is like a suspended JVM operation that is ready to execute. To actually cause the method to execute, you call execute.

use duchess::prelude::*;
use com::widgard::Factory;

// Constructors are `Type::new`...
let f: Java<Factory> = Factory::new().execute();

// ...method names are converted to snake-case...    
let w: Java<Option<Widget>> = f.produce_widget().execute();

// ...use `assert_not_null` to assert that return values are not null...
let w: Java<Widget> = f.produce_widget().assert_not_null().execute();

// ...references to Java objects are passed with `&`.
f.consume_widget(&w).execute();

Passing null values

If you want to pass a null value as a parameter, you can use duchess::Null:

use com::widgard::Factory;
let f = Factory::new().execute();
f.consume_widget(duchess::Null).execute();
//               ^^^^^^^^^^^^^ like this!

Another option is to use Option types:

use com::widgard::{Factory, Widget};
let f = Factory::new().execute();
let j: Option<Java<Widget>> = None;
f.consume_widget(j).execute();
//               ^ like this!

Launching the JVM

Note that to call methods on the JVM, we first had to start it. You do that via duchess::Jvm::with. This method will launch a JVM if it hasn't already started and attach it to the current thread. OpenJDK only supports one JVM per process, so the JVM is global. You can learn more about launching a JVM (including how to set options like the classpath) in the JVM chapter of the reference.

Combining steps into one

Because jvm-ops are lazy, you can also chain them together:

use com::widgard::Factory;

let f: Java<Factory> = Factory::new().execute();

// Consume and produce the widget in one step:
f.consume_widget(f.produce_widget()).execute();

In terms of efficiency, combining steps is currently equivalent to invoking them individually. However, the plan is for it to become more efficient by reducing the number of times we invoke JNI methods.