0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

以前の誰得記事キャンペーンで、私は JVM on Ubuntu on QEMU for RISC-V on Ubuntu on WSL2 をやってみました 。今回のテーマはさらにニッチに、WebAssembly on JVM on Ubuntu on QEMU for RISC-V on Ubuntu on WSL2 をやってみる、です。

WebAssembly on JVM の可能性

WebAssembly はその名に反して、Web(ブラウザ)のためだけの技術ではありません。WASI を代表として、WebAssembly をブラウザの外で活かす場所は様々あります。

有望な試みの 1 つは、プラグイン(ユーザー拡張)機能を持たせる際の実行エンジンとしての役割です。WebAssembly が持つ以下の特長はプラグインにぴったりです。

  • マルチプラットフォーム対応
  • メモリ安全でサンドボックス化された実行環境を持つ
  • WebAssembly をサポートするあらゆる言語で記述可能

Java (JVM) のメリットとして「30 億のデバイスで走る」ポータビリティの高さがありますが、WebAssembly ならポータビリティを保ったまま安全にユーザのコードを実行できます。OpenJDK や Temurin は RISC-V をサポートしているので、うまくすれば WebAssembly 製のプラグインは JVM on RISC-V でも動くことになります。

さて、WebAssembly on JVM を実現する手段は大きく 2 種あります。Endive / Chicory と GraalWasm です。

Endive / Chicory で WebAssembly on JVM

2026 年 5 月 26 日、Bytecode Alliance に Endive プロジェクトが参加しました。6 月 26 日に 1.0.0 がリリースされたばかりですが、フォーク元の Chicory は 2024 年末には v1.0.0 に到達しており、最新は v1.7.5 です。

Java 関係のプロジェクト名はコーヒー関係を由来に持つものが多くありますが、チコリー(英: Chicory、学名: Cichorium intybus)は代用コーヒーの原料として知られるキク科の野菜です。チコリーを混ぜたカフェオレはニューオーリンズ・スタイルとして人気ですから、Java に WebAssembly を混ぜるプロジェクトを Chicory と名付けるのはお洒落な発想ですよね。アンディーヴ(仏: Endive)はチコリーの仏名ですが、エンダイブ(英: Endive、学名: Cichorium endivia)はチコリーではなく近縁種を指します。いずれにせよ、Endive は Chicory のフォークに相応しい名前と言えるでしょう。1

Endive / Chicory は JVM 上でネイティブ依存なしに WebAssembly を実行できます。実行モードにもよりますが、動的なモジュール差し替えも可能でプラグイン用途に向いています。

導入は簡単で、Endive なら run.endive:runtime を依存に追加するだけです。

pom.xml
<dependency>
    <groupId>run.endive</groupId>
    <artifactId>runtime</artifactId>
    <version>${endive.version}</version>
</dependency>

実際に以下の Rust コードからコンパイルした WebAssembly を呼び出してみましょう。なお、サンプルコードの全文はこちらのリポジトリに掲載しています。

lib.rs
/// 文字種別のカウント結果を格納する構造体
/// # Fields
/// * `hiragana` - ひらがなの数
/// * `katakana` - 全角カナの数
#[repr(C)]
pub struct CharTypeCount {
    pub hiragana: i32,
    pub katakana: i32,
}

/// メモリを割り当てる関数
/// # Arguments
/// * `len` - 割り当てるメモリの長さ
/// # Returns
/// 割り当てたメモリのポインタ
#[unsafe(no_mangle)]
pub extern "C" fn alloc(len: i32) -> *mut u8 {
    let mut buf = Vec::with_capacity(len as usize);
    let ptr = buf.as_mut_ptr();
    // Rust にクリーンアップしないように指示する
    mem::forget(buf);
    ptr
}

/// メモリを解放する関数
/// # Arguments
/// * `ptr` - 解放するメモリのポインタ
/// * `len` - 解放するメモリの長さ
#[unsafe(no_mangle)]
pub extern "C" fn dealloc(ptr: *mut u8, len: i32) {
    // メモリを解放する
    let _ = unsafe { Vec::from_raw_parts(ptr, 0, len as usize) };
}

/// count_kana が返した CharTypeCount を解放する関数
/// # Arguments
/// * `ptr` - count_kana が返した CharTypeCount ポインタ
#[unsafe(no_mangle)]
pub extern "C" fn free_result(ptr: i32) {
    if ptr == 0 {
        return;
    }
    let _ = unsafe { Box::from_raw(ptr as *mut CharTypeCount) };
}

/// 文字列の文字種別をカウント
/// # Arguments
/// * `ptr` - 文字列データのメモリアドレス
/// * `len` - 文字列データのバイト数
/// # Returns
/// CharTypeCount 構造体へのポインタ(メモリ割り当て済み)
#[unsafe(no_mangle)]
pub extern "C" fn count_kana(ptr: i32, len: i32) -> i32 {
    let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) };
    let s = str::from_utf8(bytes).unwrap();

    let mut result = CharTypeCount {
        hiragana: 0,
        katakana: 0,
    };

    for ch in s.chars() {
        match ch as u32 {
            0x3040..=0x309F => result.hiragana += 1,
            0x30A0..=0x30FF => result.katakana += 1,
            _ => {}
        }
    }

    // 構造体をメモリに割り当て、ポインタを返す
    let boxed = Box::new(result);
    let ptr = Box::into_raw(boxed) as i32;
    ptr
}
Endive.java
public class Endive {
    public static void main(String[] args) {
        System.out.println("os.name\t" + System.getProperty("os.name"));
        System.out.println("os.arch\t" + System.getProperty("os.arch"));

        try (Scanner scanner = new Scanner(System.in)) {
            while (true) {
                executeWasm();
                System.out.println("\nPress Enter to continue (Ctrl-C to exit)...");
                scanner.nextLine();
            }
        }
    }

    private static void executeWasm() {
        Instance instance = Instance.builder(Parser.parse(new File("count_kana.wasm"))).build();

        ExportFunction alloc = instance.export("alloc");
        ExportFunction dealloc = instance.export("dealloc");
        ExportFunction freeResult = instance.export("free_result");
        ExportFunction countCharTypes = instance.export("count_kana");

        Memory memory = instance.memory();

        String message = "こんにちは、セカイ!";
        byte[] messageBytes = message.getBytes(java.nio.charset.StandardCharsets.UTF_8);
        int len = messageBytes.length;

        // 文字列データをメモリに書き込む
        int ptr = (int) alloc.apply(len)[0];
        memory.write(ptr, messageBytes);

        // count_kana を呼び出し、結果の構造体ポインタを取得
        int resultPtr = (int) countCharTypes.apply(ptr, len)[0];

        // メモリから CharTypeCount 構造体のデータを読み取る
        // 構造体レイアウト:i32 x 2 (各フィールド 4 バイト)
        int hiragana = memory.readInt(resultPtr);
        int katakana = memory.readInt(resultPtr + 4);

        // 結果を表示
        System.out.println("入力文字列:" + message);
        System.out.println("  ひらがな:" + hiragana);
        System.out.println("  全角カナ:" + katakana);

        // メモリを解放
        dealloc.apply(ptr, len);
        freeResult.apply(resultPtr);
    }
}

この通り、Windows 11 on x64 と Ubuntu on RISC-V で同じ JAR + WebAssembly が動作します。

endive.png

なるほど、30 億のデバイスで走ってくれそうですね。

なお、 Press Enter to continue (Ctrl-C to exit)... の表示中に WebAssembly を置換することで動的なモジュール差し替えを体験できます。

20260628-1401-55.1495739.gif

GraalWasm で WebAssembly on JVM

GraalWasm も JVM 上で WebAssembly を実行する手段です。言語実装フレームワークの Truffle で実装されており、Polyglot API 経由で Java コードと相互運用できます。2

こちらは org.graalvm.polyglot:polyglotorg.graalvm.polyglot:wasm を依存に追加することで導入できます。

pom.xml
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>${graalvm.version}</version>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>wasm</artifactId>
    <version>${graalvm.version}</version>
    <type>pom</type>
</dependency>

GraalWasm では前掲の count_kana.wasm を以下のコードで呼び出せます。

Graal.java
public class Graal {
    public static void main(String[] args) {
        System.out.println("os.name\t" + System.getProperty("os.name"));
        System.out.println("os.arch\t" + System.getProperty("os.arch"));
        try (Scanner scanner = new Scanner(System.in)) {
            while (true) {
                executeWasm();
                System.out.println("\nPress Enter to continue (Ctrl-C to exit)...");
                scanner.nextLine();
            }
        }
    }

    private static void executeWasm() {
        try (Context context = Context.create()) {
            File wasmFile = new File("count_kana.wasm");
            Value module = context.eval(Source.newBuilder("wasm", wasmFile).build());
            Value instance = module.newInstance();
            Value exports = instance.getMember("exports");

            Value alloc = exports.getMember("alloc");
            Value dealloc = exports.getMember("dealloc");
            Value countKana = exports.getMember("count_kana");
            Value freeResult = exports.getMember("free_result");
            Value memory = exports.getMember("memory");

            String message = "こんにちは、セカイ!";
            byte[] messageBytes = message.getBytes(java.nio.charset.StandardCharsets.UTF_8);
            int len = messageBytes.length;
            int ptr = alloc.execute(len).asInt();

            // メモリに文字列データを書き込む
            for (int i = 0; i < len; i++) {
                memory.setArrayElement((long) ptr + i, (int) messageBytes[i]);
            }

            int resultPtr = countKana.execute(ptr, len).asInt();

            // メモリから CharTypeCount 構造体のデータを読み取る
            // 構造体レイアウト:i32 x 2 (各フィールド 4 バイト)
            ByteBuffer buffer = ByteBuffer.allocate(8);
            buffer.order(ByteOrder.LITTLE_ENDIAN);

            for (int i = 0; i < 8; i++) {
                Value byteVal = memory.getArrayElement((long) resultPtr + i);
                buffer.put(i, (byte) byteVal.asInt());
            }

            int hiragana = buffer.getInt(0);
            int katakana = buffer.getInt(4);

            // 結果を表示
            System.out.println("入力文字列:" + message);
            System.out.println("  ひらがな:" + hiragana);
            System.out.println("  全角カナ:" + katakana);

            // メモリを解放
            dealloc.execute(ptr, len);
            freeResult.execute(resultPtr);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

GraalWasm はその名の通り Oracle GraalVM の関連プロジェクトですから、GraalVM 上で実行するのが素直です。

graalwasm-graalvm.png

こちらも動的なモジュール差し替えが可能です。

20260628-1405-20.4884690.gif

GraalWasm は OpenJDK でも動作しますが、警告が表示されます。

graalwasm-temurin-x64.png

JEP 243: Java-Level JVM Compiler Interface で Graal コンパイラを指定しないと遅くなるぞ、という内容ですね。-XX:+EnableJVMCI -XX:+UseJVMCICompiler を付与し、--upgrade-module-path で Graal コンパイラと依存関係を指定するとなくなります。

graalwasm-temurin-x64-jvmci.png

RISC-V では GraalVM がサポートされていないので、Temurin で試してみましょう。

graalwasm-temurin-risc.png

上記のオプションを指定した状態でも起動に失敗します。ここで、依存関係に含まれる org.graalvm.truffle:truffle-api を確認してみると、RISC-V 用のネイティブコードが含まれていないことがわかります。

graalwasm-temurin-risc2.png

比較とまとめ

ここまでに言及していない観点を含め、Endive / Chicory と GraalWasm を比較してみます。3

項目 Endive / Chicory GraalWasm
開発開始 2023 年 9 月(Chicoryとして) 2019 年 12 月(初公開)
ステータス v1.0.0 到達(2026 年 6 月) 安定版到達(2024 年 9 月、v24.1.0)4
ライセンス Apache License 2.0 Universal Permissive License 1.0 5
主導 Bytecode Alliance Oracle
マルチプラットフォーム 〇(ネイティブ依存なし、JVMのサポート範囲で動作) △(GraalVMのサポート範囲で動作)
動的なモジュール差し替え △(実行モードによる)
Java 関数の呼び出し △(GraalJS 経由6 7
Wasm GC 8 △(Experimental9
Exception Handling 8 △(Experimental9
Memory64 △(Experimental9
Multiple Memories 8 9
Threads 8 △(Experimental)
Fixed-width SIMD
Relaxed SIMD × 9
Tail Call ×

こうしてみると一長一短といったところでしょうか。ライセンスや対応プラットフォームの観点では Endive / Chicory の使い勝手が良さそうですが、GraalWasm の歴史の長さや Polyglot のエコシステムも魅力的です。

というわけで、on RISC-V こそニッチでしたが、2 系統の OSS が積極的に開発されるホットなジャンル、JVM で WebAssembly も走らせる をご紹介する記事でした。

参考リンク

  1. ちなみに私はえらんで参加!アウトプット大会ではシャキッとコーヒー派チームに参加しています。

  2. 他にも JavaScriptPythonRRuby、そして Java 自身 までもが Truffle 上で動きます。

  3. Feature Status - WebAssembly に基づきますが、新たにサポートされた機能を修正しています。

  4. https://www.graalvm.org/release-notes/JDK_23/#graalwasm

  5. Graal コンパイラは GPLv2+CE です。

  6. https://2025.wasm.io/slides/the-future-of-write-once-run-anywhere-from-java-to-webassembly-wasmio25.pdf

  7. https://github.com/oracle/graal/blob/master/wasm/docs/user/GraalWasmAPIMigration.md#importing-host-functions

  8. https://github.com/bytecodealliance/endive#completed 2 3 4

  9. https://github.com/oracle/graal/blob/master/wasm/CHANGELOG.md 2 3 4 5

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?