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