Hello Project Panama, on Java17
   4 min read

はじめに

で、 Project Panama (リンク1, リンク2) の機能を利用して、 Java から Rust を呼び出してみました。

当時(Java14)は Project Panama 用にビルドされた JDK を利用する必要がありましたが、 Java17 では incubator ではあるものの JEP 412: Foreign Function & Memory API が標準 JDK に導入された

ので、標準の JDK でも冒頭にリンクしたコード相当のものをビルド、実行できるようになりました(※ 後述の通り jextract コマンド は標準 JDK に含まれていないので別途取得する必要があります)。

API, jextract コマンド引数など、 Java14 当時と結構変わっていましたので、改めて project Panama を使って Hello, world してみたいと思います。

今回の成果物は次のリンク先にあります:

環境

  • Ubuntu 20.04

    • 後で Windows10 上でも試してみましたが、こちらも上手く動作しました

  • Java Corretto-17.0.0.35.1

  • jextract Build 17-panama+3-167 (2021/5/18)

  • rustc 1.55.0

作成手順

Rust でダイナミックリンクライブラリ作成

greeter という名前でプロジェクトを作成します。 プロジェクトルートディレクトリで次のコマンドを実行します。

cargo new --lib greeter
cd greeter

libc クレートを依存関係に追加します。 また、ダイナミックリンクライブラリを生成するように crate-typecdylib を設定します。

Cargo.toml
[dependencies]
libc = "0.2.103"
[lib]
crate-type = ["cdylib"]

Rust 側で行う処理を実装します。

src/lib.rs
use libc::size_t;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub unsafe extern "C" fn greet(name: *const c_char, message: *mut c_char, count: size_t) {
    let name = CStr::from_ptr(name);
    let name = name.to_str().unwrap();
    let text = format!("こんにちは、{}!", name);
    let text = CString::new(text).unwrap();

    message.copy_from(text.as_ptr(), count);
}

ビルドします。

cargo build --release

ダイナミックリンクライブラリの C ヘッダ自動生成

上で作成したライブラリのヘッダファイルを自動生成します。

cbindgen コマンドをインストールします。

cargo install --force cbindgen

プロジェクトルートディレクトリで次のコマンドを実行します。

cbindgen -l c -o bridges/greeter.h greeter

上記コマンド実行により bridges/greeter.h ヘッダファイルが生成されます。

bridges/greeter.h
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

void greet(const char *name, char *message, size_t count);

C ヘッダから Java API 自動生成

jextract コマンドを利用してヘッダファイルから Java API を自動生成します。

jextract コマンドは Project Panama の Early Access Build をダウンロードし展開すると bin ディレクトリ下にあります。

プロジェクトルートディレクトリで次のコマンドを実行します。

/path/to/jextract \
-l greeter \
-d classes \
-t com.example \
./bridges/greeter.h

上記コマンドを実行すると classes ディレクトリ下にクラスファイルが生成されます。

補足/参考

  • 上で自動生成した bridges/greeter.h は、実際には不必要なインクルードを含んでいます。 これを削減し、 #include <stddef.h> (size_t を定義しているヘッダファイル) だけにすると、ここで自動生成されるファイルも減ります。

  • jextract コマンドに --source オプションを付与すると、 .class ファイルでなく .java ファイルが生成されます。

  • jextract コマンドの詳細は次のリンク先を参照:

呼び出し側を Java で実装

jdk.incubator.foreign 機能を用いてメモリ領域を確保し、自動生成した API com.example.greeter_h.greeter() を呼ぶコードを実装します。

src/Main.java
import static com.example.greeter_h.*;
import static jdk.incubator.foreign.CLinker.*;

import jdk.incubator.foreign.*;
import java.awt.BorderLayout;
import java.io.Serial;
import java.nio.charset.StandardCharsets;
import javax.swing.*;

public class Main extends JFrame {

    @Serial
    private static final long serialVersionUID = 4648172894076113183L;

    public Main() {
        super("Rust GUI Frontend by Java Swing");
        setLayout(new BorderLayout());

        final JTextField nameField = new JTextField(20);

        final JTextField outputField = new JTextField(30);
        outputField.setEditable(false);

        final JButton greetButton = new JButton("greet");
        greetButton.addActionListener((e) -> {
            try (ResourceScope scope = ResourceScope.newConfinedScope()) {
                final SegmentAllocator allocator = SegmentAllocator.ofScope(scope);
                final MemorySegment name = toCString(nameField.getText(), scope);
                final long size = 256;
                final MemorySegment message = allocator.allocateArray(C_CHAR, size);

                greet(name, message, size);

                // Project PanamaのJDKには存在するが、通常のJDK17には無い
                // final String retval = toJavaString(message, StandardCharsets.UTF_8);
                final String retval = toJavaString(message);
                outputField.setText(retval);
            }
        });

        add(nameField, BorderLayout.WEST);
        add(greetButton, BorderLayout.EAST);
        add(outputField, BorderLayout.SOUTH);
        pack();
    }

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {
            final Main app = new Main();
            app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            app.setVisible(true);
        });
    }
}

補足

Java コードビルド

上記のコードを JDK17 でビルドするには --add-modules jdk.incubator.foreign オプションを付与する必要があります。

javac \
-encoding utf-8 \
-d ./classes \
-cp ./classes \
--add-modules jdk.incubator.foreign \
./src/Main.java

実行

--enable-native-access=ALL-UNNAMED, --add-modules jdk.incubator.foreign オプションが必要です。

LD_LIBRARY_PATH=./greeter/target/release \
java \
-Dfile.encoding=utf-8 \
--enable-native-access=ALL-UNNAMED \
--add-modules jdk.incubator.foreign \
-cp ./classes Main

参考/補足