【Rustを一緒に学ぶ | 上級者向け記事 | jniライブラリ】JavaとRustの相互作用を実現するJNI



序文

Rust言語の中国語コミュニティで、boss metawormさんの「Rust and Java Interaction - JNI Module Writing - Practice Summary」という投稿を見て、RustがJNIを使​​ってJavaとやり取りする方法を詳しく解説していて、私の勉強にもなっていますプロセス. いくつかのマイナーなバグが見つかり、微調整の後、記事の例が機能しました. この記事の目的は、実際の戦闘体験を促進し、読者の読書に影響を与えるいくつかの小さな問題を修正し、Rust 開発エコロジーの普及を促進することです。

JNIは、C言語を中心にJavaと他の言語が相互に呼び出すための標準のセットであり、Cに基づく公式のC++インターフェースも提供されています。理論的には、C API をサポートする言語は Java 言語で相互に呼び出すことができ、Rust もその 1 つです。

Rust と Java はオリジナルの JNI インターフェースを使用して相互に呼び出すことができますが、操作プロセスが面倒です: Rust コミュニティの誰かが、オリジナルの JNI インターフェースに基づいて一連の安全なインターフェースをカプセル化し、そのクレートの名前が jni であり、開発に便利です。

元の記事のコメント欄でこの質問を見ました

理由はわかりません。 -Djava.library.path=target/debug はこれを使用して dll パスを指定し、パスが見つからないという Java の実行プロンプトを表示します。

筆者が提供したケースを個人的に実行しようとしたところ、この問題が発生し、修正されました.その経験を要約するだけでなく、エラーの修正もあります.

この記事では、Maven および Cargo プロジェクトの構成を理解する必要があります。


1.エンジニアリング構成

Maven と Cargo に詳しい場合は、作者の倉庫に直接行くことをお勧めします

1. Rust プロジェクトの設定

  1. 新しいプロジェクトを作成する
    ターミナルを開いてコマンドを実行します
cargo new java-rust-demo
  1. java-rust-demo フォルダーに移動し、Cargo.toml ファイルを編集します。
[package]
name = "rust-java-demo"
version = "0.1.0"
edition = "2021"

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

[dependencies]
jni = {version = '0.19'}

lib セクションは、Rust プロジェクトが動的ライブラリ タイプであることを意味します. コンパイル後、Windows システムの場合は target/debug に dll ファイルが生成され、Linux システムの場合は so ファイルが生成されます。生成されます。

実際、作成者は後でマクロを作成して、グローバル参照とオブジェクト キャッシュを処理しました. とにかく、ここに依存関係として追加する必要があります. したがって、最終的な構成ファイルは次のようになります。

[package]
name = "java-rust-demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
jni = {version = '0.19'}
anyhow = "1.0.65"
  1. src の main.rs を lib.rs に変更し (Rust ライブラリ タイプのプロジェクト コンパイル エントリは lib.rs)、コードを追加します。
use jni::objects::*;
use jni::JNIEnv;

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_init(env: JNIEnv, _class: JClass) {
    
    
    println!("rust-java-demo inited");
}

以下は C 言語関数の init をエクスポートしたもので、関数名は Java_pers_metaworm_RustJNI_init
Java_<类完整路径>_<方法名>. で、ここではデバッグ情報が 1 文だけ出力されています。

  1. 動的ライブラリをコンパイルして生成する
    ターミナルを開き、次のコマンドを実行して動的ライブラリを生成します
cargo build

正常に実行された場合、target/debug に動的ライブラリが生成されます.Windows の場合はrust_java_demo.dll.Linux の場合はrust_java_demo.so.この時点で、Rust プロジェクトは正常に構成されたと見なされます。

2.Java プロジェクトの構成

  1. pom.xml ファイルの作成
    Maven を使用しているため、まず Maven 構成ファイルを使用します. Java で依存関係を使用する場合は、ここで構成できます。
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pers.metaworm</groupId>
    <artifactId>RustJNI</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <exec.mainClass>pers.metaworm.RustJNI</exec.mainClass>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>

    <dependencies>
    </dependencies>

    <build>
        <sourceDirectory>java</sourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 対応するJavaクラスを作成します.
    まずjavaディレクトリを作成し,次にpers/metaworm/RustJNI.javaファイルを順番に作成します.作成後はこんな感じです.

    作成したjavaファイルに以下の内容を記述してください.
package pers.metaworm;

public class RustJNI {
    
    
    static {
    
    
        System.loadLibrary("rust_java_demo");
    }

    public static void main(String[] args) {
    
    
        init();
    }

    static native void init();
}

ここで、メイン メソッドは Java によって呼び出されるエントリ関数であり、クラスがロードされるとすぐに静的コード ブロックが呼び出されます。

System.loadLibrary("java_rust_demo");

ライブラリをロードすることを意味し、Windows システムの場合は java_rust_demo.dll、Linux システムの場合は java_rust_demo.so です。

学習中にライブラリがロードされない原因となった原文のここに小さな問題があることに注意してください. これはパスの問題ではありません. file. はうまくいきません。

  1. コンパイルして実行するコマンドラインを書く

ここで著者は与える

java -Djava.library.path=target/debug -classpath target/classes pers.metaworm.RustJNI

コマンドラインで直接実行することも確かに可能ですが、より面倒なので、ここに 2 つのスクリプトを記述しました。

  1. build.bat を作成したら
    、ファイルに次の内容を記述します。
@echo off

echo rust compiling
cargo build
echo java compiling
mvn compile
  1. run.bat を作成したら
    、ファイルに次の内容を記述します。
@echo off

set cpath=%~dp0
set library_path=%cpath%target\debug
set class_path=%cpath%target\classes

echo output
java -Djava.library.path=%library_path% -classpath %class_path% pers.metaworm.RustJNI

作成後のプロジェクトディレクトリはこのようになっているはずです

4. コンパイルと実行
このとき、build.bat を実行してコンパイルし、次に run.bat を実行して実行すると出力されます。

rust-java-demo inited

実際に init 関数が呼び出されたので、この時点で動的ライブラリがロードされたことを意味します。ここまでで、Rust と Java が対話するための環境がセットアップされました。

2. Java が Rust を呼び出す

Rust がネイティブ メソッドを Java に公開する方法については既に紹介しました。つまり、Java_<类完整路径>_<方法名>という名前の関数をエクスポートし、対応する Java クラスで対応するネイティブ メソッドを宣言します。

拡大

この記事で使用した方法に加えて、JNI_Onload 関数をエクスポートし、JNIEnv::register_native_methods を呼び出して動的に登録する動的登録の別の方法があります。

動的に登録する場合は、以下を参照することをお勧めします

#[no_mangle]
pub fn test_func(_env: JNIEnv, _class: JClass){
    
    
    println!("register_native_methods test_func")
}

#[no_mangle]
pub unsafe extern "C" fn JNI_Onload(_env: JNIEnv, _class: JObject){
    
    
    let fn_ptr = test_func;
    let nmd: jni::NativeMethod = jni::NativeMethod{
    
    
        name: JNIString::from("test_func"),
        sig: JNIString::from("Ljava/lang/Void;"),
        fn_ptr: fn_ptr as *mut c_void
    };
    JNIEnv::register_native_methods(&_env, _class, &[nmd]).expect("register_native_methods");
}

このうち、jvm が JNI をロードするときに JNI_Onload が実行され、ネイティブ メソッドが自動的に登録されます。

ネイティブ メソッドが Java で初めて呼び出されると、JVM は対応する名前のエクスポートまたは動的に登録されたネイティブ関数を探し、Java ネイティブ メソッドを Rust 関数に関連付けます; JVM が対応するネイティブ関数を見つけられない場合関数、それは java.lang.UnsatisfiedLinkError 例外を報告します。

これは著者によって提供された例です

use jni::objects::*;
use jni::sys::{
    
    jint, jobject, jstring};
use jni::JNIEnv;

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_addInt(
    env: JNIEnv,
    _class: JClass,
    a: jint,
    b: jint,
) -> jint {
    
    
    a + b
}

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisField(
    env: JNIEnv,
    this: JObject,
    name: JString,
    sig: JString,
) -> jobject {
    
    
    let result = env
        .get_field(
            this,
            &env.get_string(name).unwrap().to_string_lossy(),
            &env.get_string(sig).unwrap().to_string_lossy(),
        )
        .unwrap();
    result.l().unwrap().into_inner()
}

上記のコードは 2 つの関数 addInt と getThisField をエクスポートします。addInt は最も一般的な 2 つの整数の合計です。getThisField はクラス内のフィールドの値を取得するために使用されます。この時点で RustJNI.java を変更します。

package pers.metaworm;

public class RustJNI {
    
    
    static {
    
    
        System.loadLibrary("rust_java_demo");
    }

    public static void main(String[] args) {
    
    
        init();

        System.out.println("test addInt: " + (addInt(1, 2) == 3));

        RustJNI jni = new RustJNI();
        System.out.println("test getThisField: " + (jni.getThisField("stringField", "Ljava/lang/String;") == jni.stringField));

        System.out.println("test success");
    }

    String stringField = "abc";

    static native void init();
    static native int addInt(int a, int b);
    native Object getThisField(String name, String sig);
}

ここは上と同じで、ネイティブメソッドをクラスに宣言し、戻り値が複合型ならObjectを使う

この時点で、コンパイルして実行すると出力されます

rust-java-demo inited
test addInt: true
test getThisField: true

Java が Rust を正常に呼び出していることを証明します。

パラメーターの受け渡し

通常、JNI関数の最初の2つのパラメータはJNIEnvとクラスオブジェクトです.JNIEnvはインタラクティブな操作を提供します.クラスオブジェクトはメソッドによって意味が異なります.静的ネイティブメソッドの場合,ここでクラスオブジェクトを取得します.インスタンス ネイティブ メソッドの場合、取得されるのは this インスタンスです。3 番目のパラメーターから始まるのは、宣言されたメソッドによって提供されるパラメーターです。

基本型は use jni::sys::* で提供されている j から始まる一連の型を直接使用して宣言することができます. 以下に著者提供の比較表を示します. 複合型 (参照型) の場合は宣言します.

jni::objects::JObject

例外をスローする

一般的に言えば、例外は次のようにスローされます

env.exception_clear().expect("clear");
env.throw_new("Ljava/lang/Exception;", format!("{err:?}"))
                .expect("throw");
            std::ptr::null_mut()

例外がスローされる前に、例外をクリアするために env.exception_clear() が呼び出されることがわかります. これは、前の get_field が既に例外をスローしているためです. env に既に例外がある場合、env 関数への後続の呼び出しは失敗します. . , この例外は引き続き上位レベルの Java 呼び出し元に渡されるため、実際にはこの 2 つの文はありません. null を直接返すと、Java は例外をキャッチすることもできますが、例外の種類をカスタマイズして、 throw_new による例外メッセージ。

これは、作成者が提供する例外をスローする典型的な例です

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_divInt(
    env: JNIEnv,
    _class: JClass,
    a: jint,
    b: jint,
) -> jint {
    
    
    if b == 0 {
    
    
        env.throw_new("Ljava/lang/Exception;", "divide zero")
            .expect("throw");
        0
    } else {
    
    
        a / b
    }
}

3. Rust が Java を呼び出す

RustでJavaオブジェクト、クラス、アクセスフィールド、およびその他の操作を呼び出す方法について、著者は次のコードを提供します

#[allow(non_snake_case)]
fn call_java(env: &JNIEnv) {
    
    
    match (|| {
    
    
        let File = env.find_class("java/io/File")?;
        // 获取静态字段
        let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?;
        let separator = env
            .get_string(separator.l()?.into())?
            .to_string_lossy()
            .to_string();
        println!("File.separator: {}", separator);
        assert_eq!(separator, format!("{}", std::path::MAIN_SEPARATOR));
        // env.get_static_field_unchecked(class, field, ty)

        // 创建实例对象
        let file = env.new_object(
            "java/io/File",
            "(Ljava/lang/String;)V",
            &[JValue::Object(env.new_string("")?.into())],
        )?;

        // 调用实例方法
        let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?;
        let abs_path = env
            .get_string(abs.l()?.into())?
            .to_string_lossy()
            .to_string();
        println!("abs_path: {}", abs_path);

        jni::errors::Result::Ok(())
    })() {
    
    
        Ok(_) => {
    
    }
        // 捕获异常
        Err(jni::errors::Error::JavaException) => {
    
    
            let except = env.exception_occurred().expect("exception_occurred");
            let err = env
                .call_method(except, "toString", "()Ljava/lang/String;", &[])
                .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string()))
                .unwrap_or_default();
            env.exception_clear().expect("clear exception");
            println!("call java exception occurred: {err}");
        }
        Err(err) => {
    
    
            println!("call java error: {err:?}");
        }
    }
}

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJava(env: JNIEnv) {
    
    
    println!("call java");
    call_java(&env)
}

ご覧のとおり、クラスを取得するには、フィールドは次のように機能します

let File = env.find_class("java/io/File")?;
let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?;

インスタンスメソッドを呼び出したい場合は、

let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?;
let abs_path = env
            .get_string(abs.l()?.into())?
            .to_string_lossy()
            .to_string();

ここで、著者は一般的に使用される方法を要約します

  • オブジェクト new_object を作成
  • 文字列オブジェクト new_string を作成します
  • call メソッド call_method call_static_method
  • フィールドを取得 get_field get_static_field
  • フィールドを変更する set_field set_static_field

例外をキャッチすることもできます

let except = env.exception_occurred().expect("exception_occurred");
let err = env
          .call_method(except, "toString", "()Ljava/lang/String;", &[])
          .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string()))
          .unwrap_or_default();
env.exception_clear().expect("clear exception");
println!("call java exception occurred: {err}");

要約する

さて、この問題は以上です。この記事は主に Rust が JNI を使って Java との相互呼び出しを実装していることを書いています. 内容の多くは big guy metaworm の記事から引用しています. この記事はすべての内容を書いているわけではありません. グローバルオブジェクト参照, 例外処理と他の関連コンテンツ. 興味がある場合は、原文を読むことをお勧めします. ここでは主に、読者が理解するのが難しいいくつかの大物によって書かれた記事のいくつかの小さな問題を修正します.

参考

  1. 参考記事
  2. コード

おすすめ

転載: blog.csdn.net/weixin_47754149/article/details/127086458