Run Rust from Java with JNA (Java Native Access)

Introduction

Motivation

When listening to Rust, a system programming language (?), There are quite a few stories about FFI (Foreign function interface). I will. So I call it from Java that I usually write.

Target audience

Those who can read Java and Rust a little and know that there is a project management tool for Java called Maven. It's not difficult, it's about the level of creating a project template.

environment

The development environment uses VS Code. Rust and Java execution environment is built with Docker. DockerFiile The base is a sample of Java development environment (Debean 10) provided by Microsoft, forked and played with for myself. The only change is the Java version changed from 14 to 11. Fucking

Install the Rust compiler there. Add the following.

DockerFiile


ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:$PATH

RUN set -eux; \
    \
    url="https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init"; \
    wget "$url"; \
    chmod +x rustup-init; \
    ./rustup-init -y --no-modify-path --default-toolchain nightly; \
    rm rustup-init; \
    chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
    rustup --version; \
    cargo --version; \
    rustc --version;

RUN apt-get update &&  apt-get install -y lldb python3-minimal libpython3.7 python3-dev gcc \
    && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts

As the content --Adding environment variables --Install the required rust components --Installing the debugger, python, and gcc required by Rust Is doing

The reason why the Rust to be installed is ** nightly ** here will be described later. Rust The file created on the Rust side this time is as follows

tree


workspace
│  Cargo.toml
│
├─sample-jna
│  │  Cargo.toml
│  │
│  └─src
│          lib.rs
│
└─scripts
       cargo-build.sh

This is because I wanted to use the cargo command at the top level of the workspace.

Explanation below Cargo.toml

Cargo.toml


[workspace]
members = ["sample-jna"]

[profile.release]
lto = true

The top two lines recognize the sample-jna directory in the workspace as a project. ** lto = true ** is an option to reduce the file size during build.

sample-jna/Cargo.toml

Cargo.toml


[package]
name = "sample-jna"
version = "0.1.0"
authors = ["uesugi6111 <[email protected]>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

** [package] ** is created by ** cargo new **, so there is no problem. ** crate-type ** of ** [lib] ** is the compiled type. The assumed dynamic library to be called from another language is written in Reference to specify ** cdylib **. , Follow it.

lib.rs This is the main body of the library. This time, I prepared a program that enumerates the prime numbers up to the argument with an algorithm similar to the Eratosthenes sieve that I wrote before and returns the number.

lib.rs


#[no_mangle]
pub extern fn sieve_liner(n: usize) -> usize{
    let mut primes = vec![];
    let mut d = vec![0usize; n + 1];
    for i in 2..n + 1 {
        if d[i] == 0 {
            primes.push(i);
            d[i] = i;
        }
        for p in &primes {
            if p * i > n {
                break;
            } 
            d[*p * i] = *p;
        }
    }
    
    primes.len() 
}

In normal compilation, the function name is converted to another name, and when you call it from another program, you will not know the name. To prevent this, give the function ** # [no_mangle] **.

cargo-build.sh

cargo-build.sh


#!/bin/bash
cargo build --release -Z unstable-options --out-dir ./src/main/resources

It will be a library build script. --release Specifies the build with the release option. -Z unstable-options --out-dir ./src/main/resources It is an option to specify the directory to output after build. However, this option is only available for ** nightly **. Therefore, ** nightly ** is specified for installation in the environment built with Docker.

The destination of the directory is set to the location where it will be placed in the jar file when it is compiled on the Java side.

Java The file created on the Java side is as follows.

workspace
│  pom.xml
└─src
   └─main
      ├─java
      │  └─com
      │      └─mycompany
      │          └─app
      │                  App.java
      │
      └─resources

The directory is deep, but it doesn't mean anything.

pom.xml Add the following to ** \ **

pom.xml


    <dependency>
        <groupId>net.java.dev.jna</groupId>
        <artifactId>jna</artifactId>
        <version>5.6.0</version>
    </dependency>

App.java

App.java



package com.mycompany.app;

import java.util.ArrayList;
import java.util.List;

import com.sun.jna.Library;
import com.sun.jna.Native;

public class App {
    private static final int N = 100000000;

    public interface SampleJna extends Library {
        SampleJna INSTANCE = Native.load("/libsample_jna.so", SampleJna.class);

        int sieve_liner(int value);
    };

    public static void main(String[] args) {
        System.out.println("N = " + N);
        System.out.println("FFI  :" + executeFFI(N) + "ms");
        System.out.println("Java :" + executeJava(N) + "ms");

    }

    public static long executeFFI(int n) {
        long startTime = System.currentTimeMillis();
        SampleJna.INSTANCE.sieve_liner(n);
        return System.currentTimeMillis() - startTime;

    }

    public static long executeJava(int n) {
        long startTime = System.currentTimeMillis();
        sieveLiner(n);
        return System.currentTimeMillis() - startTime;

    }

    public static int sieveLiner(int n) {
        List<Integer> primes = new ArrayList<>();

        int d[] = new int[n + 1];
        for (int i = 2; i < n + 1; ++i) {

            if (d[i] == 0) {
                primes.add(i);
                d[i] = i;
            }
            for (int p : primes) {
                if (p * i > n) {
                    break;
                }
                d[p * i] = p;
            }
        }

        return primes.size();
    }

}

Implement the same logic as implemented on the Rust side and compare the execution times. Calling the library SampleJna INSTANCE = Native.load("/libsample_jna.so", SampleJna.class); I am doing it at. It seems to be described in the form of Native.load (Path of the library, interface that defines the library). Since the library will be placed directly under main / resources this time, it is written with an absolute path (?).

Maven Up to this point, you can check the operation, but I also set it when I thought about making it a jar. Flow to create jar --Compile Rust and place it in the resources directory on the Java side --Compiling on Java side

This is done in one action using Maven's functions.

maven-assembly-plugin Include dependent libraries in jar

maven-assembly-plugin


      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.3.0</version>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <classpathPrefix>/</classpathPrefix>
              <mainClass>com.mycompany.app.App</mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

exec-maven-plugin Required to execute shell scripts during maven processing.

exec-maven-plugin


      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.3.2</version>
        <executions>
          <execution>
            <id>dependencies</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <workingDirectory>${project.basedir}</workingDirectory>
              <executable>${project.basedir}/scripts/cargo-build.sh </executable>
            </configuration>
          </execution>
        </executions>
      </plugin>

A little commentary phase  Set the timing to execute the shell script. Maven has a concept of life cycle, so specify the one that matches the timing you want to execute. Reference executable Specify the target you want to execute here.

pom.xml Files that have been adapted so far

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>my-app</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.7.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna</artifactId>
      <version>5.6.0</version>
    </dependency>
  </dependencies>
  <properties>
    <jdk.version>11</jdk.version>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.3.0</version>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <classpathPrefix>/</classpathPrefix>
              <mainClass>com.mycompany.app.App</mainClass>
            </manifest>
          </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M3</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.3.2</version>
        <executions>
          <execution>
            <id>dependencies</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <workingDirectory>${project.basedir}</workingDirectory>
              <executable>${project.basedir}/scripts/cargo-build.sh </executable>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Run

Do the following at the root of the workspace

mvn package

Then

[INFO] --- maven-assembly-plugin:3.3.0:single (make-assembly) @ my-app ---
[INFO] Building jar: /workspace/target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

A log like this is output and compilation is complete.

After that, execute the jar output in the displayed path.

Example


java -jar ./target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar

output

N = 100000000
FFI  :1668ms
Java :3663ms

The time (ms) of Java and FFI (Rust), which was taken by counting the number of prime numbers up to 10 ^ 8, was output. I don't know if there is a lot of overhead because Java is faster with N smaller.

Last

For the time being, it has moved, so it will be completed once. It will be the source used https://github.com/uesugi6111/java-rust

There are many stories that I don't usually touch, and I still don't understand much, but I'll investigate them little by little.

Recommended Posts

Run Rust from Java with JNA (Java Native Access)
JNA (Java Native Access) pattern collection
Access API.AI from Java
Run batch with docker-compose with Java batch
Run Java VM with WebAssembly
Code Java from Emacs with Eclim
Run node.js from android java (processing)
Run a batch file from Java
Access Teradata from a Java application
Work with Google Sheets from Java
Easy database access with Java Sql2o
Run an application made with Java8 with Java6
Call Java library from C with JNI
API integration from Java with Jersey Client
Getting Started with Java Starting from 0 Part 1
Java development with Codenvy: Hello World! Run
Execute Java code from cpp with cocos2dx
Access protected fields from grandchildren (Java / PHP)
Access Forec.com from Java using Axis2 Enterprise WSDL
Run R from Java I want to run rJava
Use native libraries from Scala via Java CPP + Java
[Java] Set the time from the browser with jsoup
Text extraction in Java from PDF with pdfbox-2.0.8
Use Matplotlib from Java or Scala with Matplotlib4j
Using Gradle with VS Code, build Java → run
Refactor property access handling with Java Method Util
Get along with Java containers in Cloud Run
Access modifier [Java]
[Tutorial] Download Eclipse → Run the application with Java (Pleiades)
[Tutorial] Download Eclipse → Run Web application with Java (Pleiades)
Call a method with a Kotlin callback block from Java
I tried calling Java / Objective-C native code from Flutter
[Note] Create a java environment from scratch with docker
Read temperature / humidity with Java from Raspberry Pi 3 & DHT11
Try building Java into a native module with GraalVM
Serverless Java EE starting with Quarkus and Cloud Run