使用JNA(Java Native Access)通过Java运行Rust编程语言
首先
动机
听到有关Rust这种系统编程语言的讲述时,不可避免地会提到FFI(外部函数接口)的话题。
所以我从日常使用的Java中调用它。
目标读者
懂一些Java和Rust,并且知道有一个叫做Maven的Java项目管理工具的人。
这并不是很复杂的事情,只是关于创建项目模板的讨论。
环境
我将使用VSCode作为开发环境。
我将通过Docker来构建Rust和Java的执行环境。
DockerFiile
我使用了Microsoft提供的Java开发环境示例(Debian 10)作为基础,并根据自己的需要进行了修改。修改的部分仅仅是将Java的版本从14改为11。
我們要在那裡安裝 Rust 編譯器。請追加以下內容。
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
以下是内容:
– 添加环境变量
– 安装所需的Rust组件
– 安装Rust所需的调试器、Python、GCC
在这里将Rust安装为nightly版本的原因将在后文中解释。
生锈
以下是Rust方面创建的文件。
workspace
│ Cargo.toml
│
├─sample-jna
│ │ Cargo.toml
│ │
│ └─src
│ lib.rs
│
└─scripts
cargo-build.sh
出于希望在 workspace 的顶层可以使用 cargo 命令的考虑,因此构建了这样的配置。
以下解释
Cargo.toml 文件
[workspace]
members = ["sample-jna"]
[profile.release]
lto = true
我在前两行中将workspace内的sample-jna目录识别为项目。
lto = true 是用于在构建时减小文件大小的选项。
示例-jna/Cargo.toml
[package]
name = "sample-jna"
version = "0.1.0"
authors = ["uesugi6111 <59960488+aburaya6111@users.noreply.github.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
在使用 cargo new 创建项目时,[package] 是没有问题的。
[lib] 的 crate-type 将决定编译后的类型。如果是用于从其他语言调用的动态库,则应根据参考文档中的描述指定为 cdylib,我们将遵循该要求。
请将以下内容以中文本地化并转述,只需要提供一种选项:
lib.rs
这是图书馆的主要部分。这次我准备了一个类似于埃拉托斯特尼筛法的算法,用来列举参数范围内的素数,并返回其数量的程序,与之前所写的类似。
#[no_mangle]
pub extern fn sieve_liner(n: i32) -> i32{
let mut primes = vec![];
let mut d = vec![0i32; n as usize + 1];
for i in 2..n + 1 {
if d[i as usize] == 0 {
primes.push(i);
d[i as usize] = i;
}
for p in &primes {
if p * i > n {
break;
}
d[(*p * i) as usize] = *p;
}
}
primes.len() as i32
}
通常情况下,在编译过程中,函数名称会被转换为其他名称,导致在从其他程序中调用时无法知道其名称。为了防止这种情况发生,我们会给函数添加#[no_mangle](直译:无修改)的属性。
货物构建.sh
#!/bin/bash
cargo build --release -Z unstable-options --out-dir ./src/main/resources
这是一个用于库的构建脚本。
使用-release选项指定进行构建。
-Z不稳定选项–out-dir ./src/main/resources
指定构建后输出的目录选项。但是这个选项只能在nightly版本中使用。
因此,安装到通过Docker构建的环境中要求选择nightly版本。
目录的指定位置已设置为在Java编译时被编译为jar文件时放置到其中的地方。
Java – 爪哇
以下是用Java创建的文件。
workspace
│ pom.xml
└─src
└─main
├─java
│ └─com
│ └─mycompany
│ └─app
│ App.java
│
└─resources
这个目录看起来很深,但实际上没有特别的意义。
pom.xml -> pom文件
将以下内容添加到<依赖项>中
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.6.0</version>
</dependency>
App.java的本地化中文改写如下:“应用程序.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();
}
}
我們將實現與Rust中實現的邏輯相同的功能,並比較執行時間。
函數庫的調用為
SampleJna INSTANCE = Native.load(“/libsample_jna.so”, SampleJna.class);
以上是其調用方式的描述。
由於這次函數庫的位置預計在main/resources下,所以我們以絕對路徑(?)方式表示。
麦芬
在此之前已经确认了基本工作,但我也考虑了将其打包成JAR文件的配置。
打包成JAR文件的过程如下:
– 编译Rust代码并将其放置在Java端的resources目录中
– 编译Java端的代码
用Maven的功能来实现这一点,只需一个操作即可完成。
Maven集装箱插件
在JAR文件中包含依赖库。
<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>
执行maven插件
为了在Maven处理中执行Shell脚本所必需的。
<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>
设置执行Shell脚本的时间点。由于Maven存在生命周期的概念,因此需要指定与所需执行时间相匹配的内容。参考文件。在这里指定要执行的目标。
pom.xml -> 文件名为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>
执行
在workspace的根目录下执行以下命令。
mvn package
然后
[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] ------------------------------------------------------------------------
输出类似以下日志并完成编译。
请运行显示的路径下输出的jar文件。
java -jar ./target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
发挥能力
N = 100000000
FFI :1668ms
Java :3663ms
用Java和FFI(Rust)计算了素数数目直到10^8的数字,输出了它们所花费的时间(毫秒)。
如果将N变小,Java会更快,这是否意味着Java有很大的开销呢,我不确定。
最后
暂时先以能够运行为准,算是完成了。
这是我使用的源码。
https://github.com/uesugi6111/java-rust
我平时不接触的领域有很多,还有很多不了解的事情,但我会慢慢去研究。
– 从Java以外的方式调用JNA
– 在Rust函数中返回除了简单数字以外的其他方式