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_taklibプレフィックスと.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を試したいと思います。

参考: