Rust和JVM一起使用 - itnext

banq發表於2021-07-21

我已經使用 JVM 二十年了,主要是在 Java 中。JVM 是一項了不起的技術。恕我直言,它最大的好處是它能夠使本機程式碼適應當前的工作負載;如果工作負載發生變化並且本機程式碼不是最佳的,它將相應地重新編譯位元組碼。
另一方面,當不再需要物件時,JVM 會自動從記憶體中釋放它們。這個過程被稱為垃圾收集。在沒有 GC 的語言中,開發人員必須負責釋放物件。對於遺留語言和大型程式碼庫,釋出應用不一致,並且在生產中發現了錯誤。
雖然 GC 演算法隨著時間的推移有所改進,但 GC 本身仍然是一個大型複雜機器。微調 GC 很複雜,並且在很大程度上取決於上下文。昨天有效的方法今天可能無效。總而言之,在您的上下文中配置 JVM 以最好地處理 GC 就像魔術一樣。
由於圍繞 JVM 的生態系統非常發達,因此使用 JVM 開發應用程式並將需要可預測性的部分委託給 Rust 是有意義的。
 

透過 JNI 整合 Java 和 Rust
整合Java和Rust需要以下幾步:

  1. 在 Java 中建立“skeleton”方法
  2. 從它們生成 C 標頭檔案
  3. 在 Rust 中實現它們
  4. 編譯 Rust 生成系統庫
  5. 從 Java 程式載入庫
  6. 呼叫第一步中定義的方法。此時,庫包含實現,並且整合已完成。

老手會意識到這些步驟與您需要與 C 或 C++ 整合時的步驟相同。因為他們也可以生成系統庫。讓我們詳細看看每個步驟。
先需要建立 Java skeleton方法。native方法將其實現委託給庫。

public native int doubleRust(int input);

接下來,我們需要生成相應的C標頭檔案。為了自動生成,我們可以利用 Maven 編譯器外掛:

<plugin> 
    <artifactId>maven-compiler-plugin</artifactId> 
    <version>3.8.1</version> 
    <configuration> 
        <compilerArgs> 
            <arg>-h</arg> <!--1--> 
            <arg >target/headers</arg> <!--2--> 
        </compilerArgs> 
    </configuration> 
</plugin>

上述 Java 片段的生成頭應如下所示:

include 
ifndef _Included_ch_frankel_blog_rust_Main
define _Included_ch_frankel_blog_rust_Main
ifdef __cplusplus
extern "C" {
endif
/*
 * Class:     ch_frankel_blog_rust_Main
 * Method:    doubleRust
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_ch_frankel_blog_rust_Main_doubleRust
  (JNIEnv *, jobject, jint);
ifdef __cplusplus
}
endif
endif
 

Rust 實現
現在,我們可以開始 Rust 實現。讓我們建立一個新專案:

cargo new lib-rust
<p class="indent">[package]
name = "dummymath"
version = "0.1.0"
authors = ["Nicolas Frankel "]
edition = "2018"
<p class="indent">[dependencies]
jni = "0.19.0"                                     
<p class="indent">[lib]
crate_type = ["cdylib"]                 


有幾種 crate 型別可用:cdylib用於動態系統庫,您可以從其他語言載入。您可以在文件中檢視所有其他可用型別。
API 一對一地對映到生成的 C 程式碼。我們可以相應地使用它:

#[no_mangle] 
pub extern "system" fn Java_ch_frankel_blog_rust_Main_doubleRust(_env: JNIEnv, _obj: JObject, x: jint) -> jint { 
    x * 2 
}


在上面的程式碼中發生了很多事情。讓我們詳細說明一下。
  • 該no_mangle宏告訴編譯器在編譯後的程式碼中保留相同的函式簽名。這很重要,因為 JVM 將使用此簽名。
  • 大多數時候,我們extern在 Rust 函式中使用將實現委託給其他語言:這稱為 FFI。這與我們在 Java 中使用native. 然而,Rust 也extern用於相反的情況,即,使函式可以從其他語言呼叫。
  • 簽名本身應該精確地模仿 C 標頭檔案中的程式碼,因此這個名字看起來很有趣
  • 最後,x是一個jint,是i32的別名。

現在構建:
cargo build


構建會生成一個系統相關的庫。例如,在 OSX 上,工件有一個dylib副檔名;在 Linux 上,它將有一個so。
 

使用Java端的庫
最後一部分是在Java端使用生成的庫。它需要首先載入它。有兩種方法可用於此目的,System.load(filename)以及System.loadLibrary(libname)。
load()需要庫的絕對路徑,包括其副檔名,例如, /path/to/lib.so. 對於需要跨系統工作的應用程式,這是不切實際的。loadLibrary()允許您只傳遞庫的名稱 - 沒有副檔名。請注意,庫是在java.library.pathSystem 屬性指示的位置載入的。

public class Main {
    static {
        System.loadLibrary("dummymath");
    }
}
請注意,在 Mac OS 上,lib字首不是庫名稱的一部分。
 

處理物件
上面的程式碼非常簡單:它涉及一個純函式,根據定義,它僅取決於其輸入引數。假設我們想要更多的東西。我們提出了一種新方法,將引數與物件狀態中的另一個引數相乘:

public class Main {
    private int state;
    public Main(int state) {
        this.state = state;
    }
    public static void main(String[] args) {
        try {
            var arg1 = Integer.parseInt(args[1]);
            var arg2 = Integer.parseInt(args[2]);
            var result = new Main(arg1).timesRust(arg2);                // 1
            System.out.println(arg1 + "x" + arg2 + " = " + result);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Arguments must be ints");
        }
    }
    public native int timesRust(int input);
}


native方法看起來與上面完全相同,但名稱不同。因此,生成的 C 標頭檔案看起來也一樣。魔法需要發生在 Rust 方面。
在純函式中,我們沒有使用JNIEnv和JObject引數:JObject表示 Java 物件,即,Main和JNIEnv允許訪問其資料(或行為)。

#[no_mangle]
pub extern "system" fn Java_ch_frankel_blog_rust_Main_timesRust(env: JNIEnv, obj: JObject, x: jint) -> jint {                    // 1
    let state = env.get_field(obj, "state", "I");            // 2
    state.unwrap().i().unwrap() * x                         // 3
}


第二行:傳遞物件的引用、Java 中的欄位名稱及其型別。型別是指正確的JVM 型別簽名,例如 "I"代表 int。
第三行:state是一個Result<JValue>。我們需要將它解包到一個 JValue,然後將它“投射”到一個Result<jint>viai()

這篇文章的完整原始碼可以在Github上找到。
 

相關文章