使用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函数中返回除了简单数字以外的其他方式