Foreign Function & Memory (FFM) APIでMoonBitの関数をJavaで呼び出すメモ
こちらの記事でJava 22で正式版となったForeign Function & Memory(FFM)APIを使用して、Rustで実装した関数をJavaから呼び出すサンプルを試しましたが、 今回はMoonBitで実装した関数をNativeビルドでエクスポートし、Javaから呼び出してみました。
Note
MoonBit自体は、WASM、JavaScript、Native、LLVMと様々なターゲットにビルド可能です。
題材は前回と同じく、竹内関数 を使いました。
MoonBit自体は初めて触るので、この記事ではインストールからメモします。
目次
検証環境
以下の環境で動作確認を行いました:
$ java -version
openjdk version "25.0.1" 2025-10-21
OpenJDK Runtime Environment GraalVM CE 25.0.1+8.1 (build 25.0.1+8-jvmci-b01)
OpenJDK 64-Bit Server VM GraalVM CE 25.0.1+8.1 (build 25.0.1+8-jvmci-b01, mixed mode, sharing)
$ moon version
moon 0.1.20251205 (073bdea 2025-12-05)
MoonBitのインストール
MoonBitは公式のインストールスクリプトを使用して簡単にインストールできます。
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
インストールが完了すると、~/.moon/binにMoonBitのツールチェーンが配置されます。インストールスクリプトは自動的に~/.zshrcにPATHを追加してくれます。
PATHを読み込んで、バージョンを確認します。
source ~/.zshrc
moon version
以下のような出力が表示されればインストール成功です:
moon 0.1.20251205 (073bdea 2025-12-05)
プロジェクト構成
今回のサンプルプロジェクトの構成は以下の通りです:
.
├── moonbit_tak/
│ ├── moon.mod.json
│ ├── moon.pkg.json
│ ├── moonbit_tak.mbt
│ ├── moonbit_tak_test.mbt
│ └── target/native/release/build/
│ └── libmoonbit_tak.dylib
└── java-moonbit-ffm/
├── pom.xml
└── src/
├── main/java/com/example/ffm/
│ ├── Main.java
│ ├── TakeuchiFunction.java
│ └── TakeuchiFunctionJ.java
└── test/java/com/example/ffm/
└── TakeuchiFunctionTest.java
MoonBitライブラリの実装
まず、MoonBitで竹内関数を実装します。
MoonBitへのログイン
MoonBitでプロジェクトを作成する前に、GitHubアカウントでログインします。これにより、プロジェクト作成時に自動的にユーザー名が設定されます:
moon login
ブラウザが開き、GitHubでの認証が求められます。認証が完了すると、GitHubのユーザー名(例: making)がMoonBitのユーザー名として使用されます。
このユーザー名は、後述する関数のシンボル名に含まれます。
MoonBitプロジェクトの作成
moon new moonbit_tak
cd moonbit_tak
moon newでプロジェクトを作成すると、moon.mod.jsonに自動的にログインしたユーザー名が設定されます:
{
"name": "making/moonbit_tak",
...
}
竹内関数の実装
moonbit_tak.mbt:
///| Takeuchi function (Tak function)
pub fn tak(x : Int, y : Int, z : Int) -> Int {
if y < x {
tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
} else {
y
}
}
mainコード
cmd/main/main.mbt
///|
fn main {
println(@lib.tak(12, 6, 0))
}
mainコードの実行
moon run cmd/main
以下のように出力されれば成功です:
12
テストコード
moonbit_tak_test.mbt:
///|
test "tak base cases" {
// When y >= x, should return y
inspect(tak(5, 10, 0), content="10")
inspect(tak(5, 5, 3), content="5")
inspect(tak(0, 10, 20), content="10")
}
///|
test "tak recursive cases" {
// Classic test cases for Takeuchi function
inspect(tak(6, 2, 1), content="6")
inspect(tak(10, 5, 0), content="10")
inspect(tak(12, 6, 0), content="12")
}
テストを実行
moon test
次のようにテストが成功すればOKです。
Total tests: 2, passed: 2, failed: 0.
パッケージ設定
moon.pkg.json:
{
"link": {
"native": {
"exports": [
"tak"
],
"cc-flags": "-fPIC",
"cc-link-flags": "-shared"
}
}
}
exports: MoonBitのtak関数をエクスポートcc-flags: Position Independent Code(PIC)フラグで共有ライブラリ用にコンパイルcc-link-flags: 共有ライブラリとしてリンク
ビルド
moon build --target native
moon build --target nativeを実行すると、moon.pkg.jsonの設定に基づいて
target/native/release/build以下にビルド結果が生成されます。
$ ls -la target/native/release/build
total 512
drwxr-xr-x@ 10 toshiaki wheel 320B 12 11 12:23 .
drwxr-xr-x@ 3 toshiaki wheel 96B 12 11 12:23 ..
-rw-r--r--@ 1 toshiaki wheel 340B 12 11 12:23 all_pkgs.json
-rw-r--r--@ 1 toshiaki wheel 747B 12 11 12:23 build.moon_db
drwxr-xr-x@ 3 toshiaki wheel 96B 12 11 12:23 cmd
-rw-r--r--@ 1 toshiaki wheel 16K 12 11 12:23 moonbit_tak.c
-rw-r--r--@ 1 toshiaki wheel 796B 12 11 12:23 moonbit_tak.core
-rwxr-xr-x@ 1 toshiaki wheel 176K 12 11 12:23 moonbit_tak.exe
-rw-r--r--@ 1 toshiaki wheel 199B 12 11 12:23 moonbit_tak.mi
-rw-r--r--@ 1 toshiaki wheel 47K 12 11 12:23 runtime.o
生成されたmoonbit_tak.exeが共有ライブラリのようです。Mac環境なのに、なぜか.exeが生成されますが、
fileコマンドで確認するとちゃんとMac用の共有ライブラリでした。
$ file target/native/release/build/moonbit_tak.exe
target/native/release/build/moonbit_tak.exe: Mach-O 64-bit dynamically linked shared library arm64
シンボルの確認:
nm -g target/native/release/build/moonbit_tak.exe | grep tak
出力例:
00000000000158b4 T _$making$moonbit_tak$tak
_$making$moonbit_tak$takシンボルがエクスポートされていることを確認できます(macOSでは関数名の前に
_が付きます)。
この名前は、以下の要素から構成されます:
making: プロジェクトのユーザー名moonbit_tak: パッケージ名tak: 関数名
Javaではlib<name>.dynlib形式が期待されているので、シンボリックリンクを作成します。これでいいのか?
ln -sf $PWD/target/native/release/build/moonbit_tak.exe $PWD/target/native/release/build/libmoonbit_tak.dylib
JavaでのFFM API実装
pom.xml
<?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>com.example</groupId>
<artifactId>java-moonbit-ffm</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<argLine>
-Djava.library.path=${project.basedir}/../moonbit_tak/target/native/release/build
</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
FFM APIを使用してMoonBitライブラリを呼び出すJavaクラス
src/main/java/com/example/ffm/TakeuchiFunction.java:
package com.example.ffm;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class TakeuchiFunction {
private static final SymbolLookup LIBRARY_LOOKUP;
private static final MethodHandle TAK_HANDLE;
static {
try {
// Load the MoonBit library
System.loadLibrary("moonbit_tak");
LIBRARY_LOOKUP = SymbolLookup.loaderLookup();
// Create function descriptor for tak(int, int, int) -> int
FunctionDescriptor takDescriptor = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return type
ValueLayout.JAVA_INT, // x parameter
ValueLayout.JAVA_INT, // y parameter
ValueLayout.JAVA_INT // z parameter
);
// Find the MoonBit's mangled tak function directly
TAK_HANDLE = LIBRARY_LOOKUP.find("$making$moonbit_tak$tak")
.map(symbol -> Linker.nativeLinker().downcallHandle(symbol, takDescriptor))
.orElseThrow(() -> new RuntimeException("Failed to find tak function"));
} catch (Exception e) {
throw new RuntimeException("Failed to load native library", e);
}
}
public static int tak(int x, int y, int z) {
try {
return (int) TAK_HANDLE.invokeExact(x, y, z);
} catch (Throwable t) {
throw new RuntimeException("Failed to invoke tak function", t);
}
}
}
System.loadLibrary("moonbit_tak"): ライブラリ名はmoonbit_tak(libプレフィックスと.dylib拡張子は自動的に付加されます)LIBRARY_LOOKUP.find("$making$moonbit_tak$tak"): エクスポートされた関数名をそのまま使用
FFM APIの主要な概念:
System.loadLibrary(): ネイティブライブラリをロードSymbolLookup: ライブラリ内のシンボルを検索FunctionDescriptor: 関数のシグネチャを定義Linker.nativeLinker().downcallHandle(): ネイティブ関数呼び出し用のメソッドハンドルを作成
Java実装(比較用)
src/main/java/com/example/ffm/TakeuchiFunctionJ.java:
package com.example.ffm;
public class TakeuchiFunctionJ {
public static int tak(int x, int y, int z) {
if (y < x) {
return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y));
} else {
return y;
}
}
}
動作確認用のメインクラス
src/main/java/com/example/ffm/Main.java
package com.example.ffm;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// Check for options
boolean useJavaImpl = false;
boolean doWarmup = false;
for (String arg : args) {
if ("--java".equals(arg)) {
useJavaImpl = true;
}
if ("--warmup".equals(arg)) {
doWarmup = true;
}
}
// Perform warmup if requested
if (doWarmup) {
performWarmup(useJavaImpl);
}
Scanner scanner = new Scanner(System.in);
System.out.println("Takeuchi Function Calculator");
System.out.println("Implementation: " + (useJavaImpl ? "Java" : "MoonBit (FFM)"));
System.out.println("Enter 'quit' or 'q' to exit");
while (true) {
System.out.print("\nEnter x y z (space separated): ");
String input = scanner.nextLine().trim();
if (input.equalsIgnoreCase("quit") || input.equalsIgnoreCase("q")) {
System.out.println("Goodbye!");
break;
}
String[] parts = input.split("\\s+");
if (parts.length != 3) {
System.out.println("Error: Please enter exactly 3 integers");
continue;
}
try {
int x = Integer.parseInt(parts[0]);
int y = Integer.parseInt(parts[1]);
int z = Integer.parseInt(parts[2]);
long startTime = System.currentTimeMillis();
int result = useJavaImpl ? TakeuchiFunctionJ.tak(x, y, z) : TakeuchiFunction.tak(x, y, z);
long endTime = System.currentTimeMillis();
System.out.println("tak(" + x + ", " + y + ", " + z + ") = " + result);
System.out.println("Time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.out.println("Error: Please enter valid integers");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
scanner.close();
}
private static void performWarmup(boolean useJavaImpl) {
System.out.println("Warming up implementation...");
if (useJavaImpl) {
System.out.print("Java warmup: ");
for (int i = 0; i < 50; i++) {
TakeuchiFunctionJ.tak(12, 6, 0);
if ((i + 1) % 10 == 0)
System.out.print(".");
}
System.out.println(" done");
} else {
System.out.print("Rust warmup: ");
for (int i = 0; i < 50; i++) {
TakeuchiFunction.tak(12, 6, 0);
if ((i + 1) % 10 == 0)
System.out.print(".");
}
System.out.println(" done");
}
System.out.println("Warmup completed!\n");
}
}
テストクラス
src/test/java/com/example/ffm/TakeuchiFunctionTest.java:
package com.example.ffm;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
class TakeuchiFunctionTest {
@FunctionalInterface
interface TakFunction {
int tak(int x, int y, int z);
}
static Stream<TakFunction> takFunctionProvider() {
return Stream.of(TakeuchiFunction::tak, TakeuchiFunctionJ::tak);
}
@ParameterizedTest
@MethodSource("takFunctionProvider")
void testBaseCases(TakFunction takFunction) {
// When y >= x, should return y
assertThat(takFunction.tak(5, 10, 0)).isEqualTo(10);
assertThat(takFunction.tak(5, 5, 3)).isEqualTo(5);
assertThat(takFunction.tak(0, 10, 20)).isEqualTo(10);
}
@ParameterizedTest
@MethodSource("takFunctionProvider")
void testRecursiveCases(TakFunction takFunction) {
// Classic test cases for Takeuchi function
assertThat(takFunction.tak(6, 2, 1)).isEqualTo(6);
assertThat(takFunction.tak(10, 5, 0)).isEqualTo(10);
assertThat(takFunction.tak(12, 6, 0)).isEqualTo(12);
}
}
テスト実行
mvn test
結果:
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
MoonBit実装とJava実装の両方で全テストが成功しました!
アプリケーション実行
Rust FFM実装の実行:
java -cp target/classes --enable-native-access=ALL-UNNAMED -Djava.library.path=../moonbit_tak/target/native/release/build com.example.ffm.Main
Java実装の実行(比較用):
java -cp target/classes com.example.ffm.Main --java
実行結果例
Takeuchi Function Calculator
Implementation: MoonBit (FFM)
Enter 'quit' or 'q' to exit
Enter x y z (space separated): 12 6 0
tak(12, 6, 0) = 12
Time: 68 ms
Enter x y z (space separated): 10 5 0
tak(10, 5, 0) = 10
Time: 2 ms
パフォーマンス比較
tak(12, 6, 0)、tak(14, 7, 0)、tak(15, 5, 0)、tak(15, 7, 0)の実行をRust FFM実装とJava実装で比較してみました。
Rust FFM実装:
echo -e "12 6 0\n14 7 0\n15 5 0\n15 7 0\nq" | java -cp target/classes --enable-native-access=ALL-UNNAMED -Djava.library.path=../moonbit_tak/target/native/release/build com.example.ffm.Main --warmup
Java実装:
echo -e "12 6 0\n14 7 0\n15 5 0\n15 7 0\nq" | java -cp target/classes com.example.ffm.Main --java --warmup
結果は次の通りでした。Rustで試した場合はRust FFM実装の方がJava実装に比べて約1.4倍高速でしたが、MoonBit FFM実装の場合は、Java実装の約0.5倍の速度になりました。 竹内関数が再帰が多いので、その辺りのパフォーマンス的にはまだ発展途上でしょうか。
| Test Case | MoonBit FFM (ms) | Java (ms) | MoonBit優位率 | 差分 (ms) |
|---|---|---|---|---|
tak(12, 6, 0) |
21 | 10 | 0.47x | 11 |
tak(14, 7, 0) |
974 | 497 | 0.51x | 477 |
tak(15, 5, 0) |
4,852 | 2,456 | 0.50x | 2396 |
tak(15, 7, 0) |
6,670 | 3,375 | 0.50x | 3295 |
fibonacci関数のサンプルコードは次のように、loop構文で末尾再帰最適化をしているように見えます。
pub fn fib(n : Int) -> Int64 {
loop (n, 0L, 1L) {
(0, _, b) => b
(i, a, b) => continue (i - 1, b, a + b)
}
}
竹内関数は非末尾再帰なのでloop構文では書けず、最適化ができていないのかもしれません。
fib関数のloop構文有無とRust FFM実装、Java実装のパフォーマンス比較も試してみたいところです。
この記事では、MoonBitで実装した竹内関数をJavaのFFM APIから呼び出す方法を紹介しました。
MoonBitは現在ベータ版で、2026年前半に正式版1.0のリリースが予定されています。今後、より多くの言語との連携が期待されます。
今回の記事はMoonBitの例としてはあまり適切ではないかもしれません。もっと別のユースケースでMoonBitを試したいと思います。
参考: