Build Rust Cargo crates within a Java Maven Project.
$ mvn clean package
...
[INFO] --- rust-maven-plugin:1.1.1:build (str-reverse) @ rust-maven-jni-example ---
[INFO] Working directory: /home/adam/questdb/repos/rust-maven-plugin/rust-maven-jni-example/src/main/rust/str-reverse
[INFO] Environment variables:
[INFO] REVERSED_STR_PREFIX='Great Scott, A reversed string!'
[INFO] Running: cargo build --target-dir /home/adam/questdb/repos/rust-maven-plugin/rust-maven-jni-example/target/rust-maven-plugin/str-reverse --release
[INFO] Compiling proc-macro2 v1.0.49
[INFO] Compiling quote v1.0.23
[INFO] Compiling unicode-ident v1.0.6
[INFO] Compiling syn v1.0.107
...
- The plugin delegates the build to
cargo
and supports most ofcargo build
's features. - The primary use case is to simplify the build process of Rust JNI libs inside a Java Maven project.
- Additionally, the plugin can also compile binaries.
- The plugin can copy complied binaries to a custom location, so they can be bundled inside of
.jar
files.- We use the same naming scheme as JNA to support loading binaries from JARs, without having JNA as a dependency.
- Support for invoking
cargo test
duringmvn test
. - Points to https://www.rust-lang.org/tools/install if
cargo
isn't found.
For your convenience, we've also made jar-jni
available:
An optional Java library to load JNI dynamic libraries from JARs.
Both the plugin and the library support a common directory naming convention to organize and find compiled artifacts for a multitude of platforms.
When integrating Rust, we recommend using JNI due to better performance and features, but JNA is also supported.
See the rust-maven-jni-example
directory for a working
example using Java JNI via the Rust jni crate.
Thi example also uses the jar-jni
library to load the Rust binaries from the compiled
JAR file.
See the rust-maven-jna-example
directory for calling Rust from
Java via JNA.
Note that neither rust-maven-plugin
nor jar-jni
depend on JNA: Instead both the plugin and the library
support a common directory naming convention to organize and find compiled artifacts for a multitude of platforms.
Edit your pom.xml
to add the plugin:
<project ...>
...
<!-- Note: Don't add rust-maven-plugin to <dependencies>. -->
<build>
<plugins>
<plugin>
<groupId>org.questdb</groupId>
<artifactId>rust-maven-plugin</artifactId>
<version>1.1.1</version>
<executions>
<execution>
<id>rust-build-id</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<path>src/main/rust/your-rust-crate</path>
<copyTo>${project.build.directory}/classes/io/questdb/jni/example/rust/libs</copyTo>
<copyWithPlatformDir>true</copyWithPlatformDir>
</configuration>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
</project>
Here, <path>..</path>
is the path to the Rust crate to build, and it is relative to the pom.xml
file itself.
The plugin will invoke cargo build
on the crate.
The <id>
is an arbitrary string that you can use to identify the execution.
It does not need to match the crate name.
If you need to build multiple crates, you can add multiple executions.
The plugin can also invoke cargo test
during mvn test
.
To enable running tests:
- Duplicate the
<execution>
block above. - Change it's
<id>
to a new name. - Change the
<goal>
totest
.
<execution>
<id>str-reverse-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<path>src/main/rust/str-reverse</path>
</configuration>
</execution>
The settings below go in the <configuration>
section of the <execution>
block.
If cargo
isn't in your PATH
, you can specify the path to the cargo
command with the <cargoPath>
configuration option.
You can also specify this on the command line via mvn ... -DcargoPath=...
.
The plugin can be configured to forward various verbosity flags to cargo
by setting
<verbosity>-v</verbosity>
(or other value) in the <configuration>
block.
Accepted values are:
Value | Description |
---|---|
<verbosity></verbosity> (or no tag) |
Default - no additional flags passed to cargo |
<verbosity>-q</verbosity> |
Quiet |
<verbosity>-v</verbosity> |
Verbose |
<verbosity>-vv</verbosity> |
Very verbose |
The plugin can be configured to build in release mode by setting
<release>true</release>
in the <configuration>
block.
Building --release
will cut down binary size considerably and should be taken
into consideration when shipping binaries in .jar
files.
The equivalent of cargo build --features feat1,feat2,feat3
is
<features>
<feature>feat1</feature>
<feature>feat2</feature>
<feature>feat3</feature>
</features>
You can also specify --all-features
via <all-features>true</all-features>
and --no-default-features
via <no-default-features>true</no-default-features>
.
Note that you can drive features with maven profiles by introducing variables.
<!-- in the plugin configuration -->
<features>
<feature>$rustFeature1</feature>
<feature>$rustFeature2</feature>
</features>
<!-- in the profiles section -->
<profile>
<id>feat-ssl</id>
<properties>
<rustFeature1>ssl</rustFeature1>
<rustFeature1>use-rustls</rustFeature1>
</properties>
</profile>
Then building with mvn package -P feat-ssl
will call
cargo build --features ssl,use-rustls
.
Additional arguments to can go in the <extra-args>
configuration section.
<extra-args>
<extra-arg>--verbose</extra-arg>
<extra-arg>--color=always</extra-arg>
</extra-args>
If you need to skip an execution, you can set <skip>true</skip>
in the
<configuration>
block of the <execution>
of the plugin.
<skip>true</skip> <!-- or `false`, the default value. -->
This is probably most useful when driven from a property.
<skip>${skipRustBuild}</skip>
The property can be defaulted in the <properties>
section of the pom.xml
file.
<properties>
<skipRustBuild>false</skipRustBuild>
</properties>
And then overridden on the command line with -DskipRustBuild=true
.
$ mvn package -DskipRustBuild=true
The plugin can be configured to override environment variables during the build.
This might be useful for setting RUSTFLAGS
.
In the <configuration>
section, add:
<environmentVariables>
<RUSTFLAGS>-C target-cpu=native</RUSTFLAGS>
</environmentVariables>
Regular mvn clean
will also clean the Rust build without additional config.
This is because the plugin builds crates inside Maven's target
build
directory, via cargo build --target-dir ...
.
If you (or your IDE) end up invoking cargo build
on your Rust crate without
the plugin, you'll notice this creates a duplicate target
dir, inside the
crate's directory, that will not be cleaned at the next mvn clean
.
To avoid this duplicate target
directory problem, consider adding
.cargo/config.toml
files configured to match the --target-dir
argument
passed by this plugin.
See .cargo/config.toml
from the str-reverse
crate in the example.
The <copyTo>
configuration (as shown in the example) allows copying the
binaries any path. The example however choses to copy them to
${project.build.directory}/classes/...
. Anything placed there gets bundled
into the JAR file.
The classes
directory sits within the target
directory, outside the
source tree.
If you prefer to keep your binaries in the source tree, you can instead
configure the plugin to copy binaries to the resources
directory
instead:
<copyTo>src/main/resources/io/questdb/jni/example/rust/libs</copyTo>
<copyWithPlatformDir>true</copyWithPlatformDir>
In such case, you may opt to move the rust-maven-plugin
inside a
Maven Profile and only build the Rust
code when you need to.
<project ...>
...
<profiles>
<profile>
<id>rust</id>
<build>
<plugins>
<plugin>
<groupId>org.questdb</groupId>
<artifactId>rust-maven-plugin</artifactId>
<version>1.1.1</version>
...
You can then enable the profile in Maven via mvn clean package -P rust ...
.
During the binary copy step, the <copyWithPlatformDir>true</copyWithPlatformDir>
config setting (used in the examples
above) will further nest the binaries in a directory named after the platform.
target
classes
io/questdb/jni/example/rust/libs/
linux-x86-64/libstr_reverse.so
darwin-aarch64/libstr_reverse.dylib
win32-x86-64/str_reverse.dll
If you only intend to target one single platform (e.g. linux-amd64), then you
don't need <copyWithPlatformDir>true</copyWithPlatformDir>
and the plugin will not create a nested directory.
Whilst we don't depend on the JNA library, the plugin's naming convention matches that of JNA's Platform.RESOURCE_PREFIX.
The jar-jni
library is configured as so:
<!-- pom.xml -->
<project ...>
...
<dependencies>
<dependency>
<groupId>org.questdb</groupId>
<artifactId>jar-jni</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
...
</project>
It helps with bundling JNI native code in .jar
files by establishing a directory
naming convention for organising binaries for different operating systems and
architectures.
Assuming you've compiled with <copyWithPlatformDir>true</copyWithPlatformDir>
, load
the binary from the .jar
file with:
JarJniLoader.loadLib(
Main.class,
// A platform-specific path is automatically suffixed to path below.
"/io/questdb/jni/example/rust/libs",
// The "lib" prefix and ".so|.dynlib|.dll" suffix are added automatically as needed.
"str_reverse");
If instead you compiled with <copyWithPlatformDir>false</copyWithPlatformDir>
, then:
JarJniLoader.loadLib(
Main.class,
"/io/questdb/jni/example/rust/libs",
"str_reverse",
null);
If you're using IntelliJ, you'll notice that the rust-maven-plugin
is not invoked
when you run Build -> Build Project
or Build -> Rebuild Project
.
This is because IntelliJ uses its own build system, and does not invoke Maven.
To work around this, you can add a Maven Run Configuration that invokes an Ant task before each build. The Ant task then invokes the Maven build step.
Whilst it does sound a little scary to also involve Ant, the actual XML is (for once) very simple.
<your-proj-root>/rust-intellij.xml
:
<project name="str-reverse-intellij-integration" default="str-reverse-intellij-build" basedir=".">
<description>
IntelliJ integration to trigger maven steps to build the Rust code via the rust-maven-plugin.
</description>
<target name="str-reverse-intellij-build">
<exec executable="mvn">
<arg value="org.questdb:rust-maven-plugin:build@str-reverse"/>
</exec>
</target>
</project>
In the XML above, str-reverse
is the <id>
of the <execution>
block in the
rust-maven-plugin
configuration in the pom.xml
file.
You can add multiple executions here, for example to also build release binaries.
Check the file into your source control.
You can then go to the Ant window in IntelliJ (View -> Tool Windows -> Ant) and select the configured ant target, right-click, Execute On -> Before Build.
This will generate a new .idea/ant.xml
file that will be picked up by IntelliJ
and will trigger the Ant task before each build which you probably want to add to source control.
This file will look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AntConfiguration">
<buildFile url="file://$PROJECT_DIR$/rust-intellij.xml">
<executeOn event="beforeCompilation" target="str-reverse-intellij-build" />
</buildFile>
</component>
</project>
Here is the IntelliJ reference for this feature: https://www.jetbrains.com/help/idea/configuring-triggers-for-ant-build-target.html#db2565bc
- Test cases, features, docs, tutorials, etc are always welcome.
- Raise an issue if you find bugs.
- We've got a list of open issues.
- Raise a pull request if you need a new feature.
If you want to talk to us, we're on Slack.
Also read the Developer's Notes with instructions on building and running from source.