mcpField
M
moonbit-js-binding

moonbit-js-binding

github.com/mizchi開発41655 回インストール1 週間前に更新
リポジトリ

moonbit-js-binding について

MoonBitのJSバックエンドを使ってJavaScript APIを接続する手順を示すスキルです。FFI宣言、型の対応付け、Promiseの非同期処理、パッケージ出力設定を扱います。

主な機能

  1. 01JavaScriptとMoonBitの型対応
  2. 02JS FFI関数の宣言
  3. 03名前付きモジュールのエクスポートをインポート
  4. 04Promiseと非同期呼び出しの橋渡し
  5. 05MoonBitのJSパッケージ出力設定
  6. 06nullとundefinedの扱い

活用シーン

  1. 01ブラウザーDOMやNode.js APIのバインド
  2. 02npmパッケージ関数のMoonBitラッパー作成
  3. 03ESM、CJS、IIFE形式の出力設定
  4. 04TypeScript宣言ファイルの生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mizchi/skills --skill moonbit-js-binding

インストール後、Claude Code で/moonbit-js-bindingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
assets/js_binding_proj/
moon.mod
assets/js_binding_proj/src/
async_test.mbt
async.mbt
cross_target_test.mbt
cross_target.mbt
ffi.mbt
lib_test.mbt
lib.mbt
modules_test.mbt
modules.mbt
moon.pkg
references/
cfg-and-target-gating.md
error-handling.md
interop-patterns.md
promise-bridging.md
typescript-integration.md

詳細

MoonBit JS バインディングガイド

js バックエンドを通じて、JavaScript API(ブラウザー DOM、Node の組み込み機能、npm パッケージ)を MoonBit にバインドする手順です。

MoonBit v0.10.12 および moonbitlang/async 0.21.0 で動作確認済みです。

使用する場面

次の場合に、このスキルを使用してください。

  • JS API の extern "js" fn 宣言を追加する場合
  • インライン JS(#| (x) => ...)を埋め込む場合、またはモジュール名と名前でインポートする場合(= "Math" "max")
  • link.js エクスポートおよび format: esm/cjs/iife を使って moon.pkg を設定する場合
  • #external pub type と %identity キャストで JS オブジェクトをラップする場合
  • async fn + %async.suspend + %async.run で Promise を橋渡しする場合
  • undefined と null を MoonBit の Option に区別してマッピングする場合
  • require(...) または ESM インポートで npm パッケージを利用する場合
  • TypeScript の利用者向けに .d.ts を生成する場合

計画モードの指示

計画モードで moonbit-js-binding に関する計画を作成する場合:

  1. 最終的な計画ファイルの冒頭に、実装を始める前に moonbit-js-binding スキルを読むよう、実行エージェントに明示的に指示してください。
  2. 指示は次のように記述してください。
    **Before starting implementation:** Use the Skill tool to load the moonbit-js-binding skill, which provides comprehensive guidance on `extern "js"` declarations, JS value wrapping, Promise bridging, and moon.pkg export configuration.
    

型のマッピング

宣言を書く前に、JS の型を MoonBit の型にマッピングしてください。

JavaScriptMoonBit備考
booleanBool直接対応
number(整数)Int、UIntJS は IEEE-754 を使用します。大きな Int64 は精度が失われるため、代わりに BigInt を使用してください
number(浮動小数点数)Float、Double直接対応
bigintBigInt直接対応
stringString直接対応(どちらも UTF-16)
Uint8ArrayBytes、FixedArray[Byte]直接対応。コピーは不要です
Array<T>Array[T]、FixedArray[T]直接対応
FunctionFuncRef[(...) -> R] またはクロージャ直接対応
any(不透明型)#external pub type JsValue公称型でラップします
T | undefinedis_undefined のチェックによる T?Null/Undefined のセクションを参照してください
T | nullNullable[T] ラッパーnull は undefined ではありません
Promise<T>#external pub type Promise[T]async fn/wait() で橋渡しします
定数 enumnumberpub(all) enum E { A; B }

警告 — Number の精度: JavaScript には整数型がありません。FFI 境界を越える値は IEEE-754 の倍精度浮動小数点数に丸められます。\|x\| < 2^53 であれば Int で問題ありません。それを超える場合は BigInt を使用してください。UInt64/Int64 は現在、JS の number を介してマッピングされるため、2^53 を超えると下位ビットが失われます。

警告 — トレイトオブジェクトと MoonBit の列挙型: Map[K, V]、Result[_]、Json、または #external ではないトレイトオブジェクトを FFI 経由で渡すと、MoonBit の内部ランタイム形状が露出します。先にプレーンな構造体、JsValue、またはプリミティブ型に変換してください。

ワークフロー

以下の 4 つのフェーズを順番に進めてください。

フェーズ 1:プロジェクトのセットアップ

モジュールとパッケージを JS 出力向けに設定します。

モジュール(moon.mod) — preferred_target を設定し、moon check、moon build、moon test のデフォルトターゲットを js にします。v0.10.9 以降、コンパイラー自身のデフォルトターゲットは wasm です。そのため、これを設定しないと、すべてのコマンドに --target js が必要になります。

name = "user/pkg"

version = "0.1.0"

source = "src"

preferred_target = "js"

import {
  "moonbitlang/[email protected]",
}

JSON 形式の moon.mod.json(ハイフン区切りの preferred-target、source、 deps キーを使用)は非推奨で、削除が予定されています。moon fmt は 既存のプロジェクトを DSL に移行します。

パッケージ(src/moon.pkg) — .mbt ファイルの対象を js バックエンドに限定し、リンク出力を設定します。

ファイル形式 — moon.pkg(DSL)と moon.pkg.json(JSON): MoonBit はどちらのファイル名も引き続き読み取りますが、JSON のサポートは非推奨で、削除が予定されています。このスキルでは全体を通して DSL 形式の moon.pkg(拡張子なし)を使用します。moon.pkg.json は書かないでください。これは別の構文であり、純粋な JSON で、options(...) ラッパーも末尾のカンマも使いません。プロジェクトの途中で両者を混在させると、「Unable to read moon.pkg」という解析エラーが発生します。.pkg.json を使う古いテンプレートをクローンした場合は、moon fmt を実行して変換してください。 targets: — バックエンド固有のファイルにのみ必要です: targets: ブロックを使うと、個々の .mbt ファイルを一部のバックエンドに限定できます。extern "js"、extern "c"、またはバックエンド固有の #cfg ブロックを含むファイルは、必ず一覧に記載してください。純粋な MoonBit ファイル(FFI なし)には targets: エントリーは不要です。パッケージ内のすべてのファイルが純粋な MoonBit であれば、targets: ブロック自体を省略してください。上の例では ffi.mbt と async.mbt に extern "js" が含まれているため一覧に記載しています。FFI のないシンプルなライブラリでは、link: { ... } だけが必要です。

import {
  "moonbitlang/async",
} for "test"

options(
  targets: {
    "ffi.mbt": ["js"],
    "async.mbt": ["js"],
    "async_test.mbt": ["js"],
  },
  link: {
    "js": {
      "exports": ["add", "greet"],
      "format": "esm",
    },
  },
)

主なフィールド:

フィールド目的
targets.mbt ファイルを ["js"] に限定します。JS 専用の FFI ファイルは、他のバックエンドで問題が起きないよう、必ず限定してください。
link.js.exports出力モジュールから再エクスポートする公開関数の一覧です。名前を変更するには "name:alias" を使用します。
link.js.format"esm"(ES モジュール。最新のバンドラーでのデフォルト)、"cjs"(Node の require)、または "iife"(ブラウザーのスクリプトタグ)です。
import ... for "test"moonbitlang/async はテスト(async test)でのみ必要です。本番ビルドには含めないでください。

警告 — supported-targets と targets の違い: パッケージレベルで supported-targets: ["js"] を使用しないでください。下流の利用者に制限がかかります。代わりに targets で個々のファイルを限定してください。

警告 — preferred_target = "js": これはデフォルトであり、固定ではありません。moon test --target wasm-gc は、対応するファイルで引き続き動作します。これはクロスバックエンドライブラリに便利です。

バックエンドごとにコードを限定する方法は 2 つあります — targets:(ファイルレベル)と #cfg()(宣言レベル):

方法適用範囲使用する場面
moon.pkg の targets: { "foo.mbt": ["js"] }ファイル全体ファイル全体が JS 固有の場合(FFI 専用、バックエンド限定のロジック)。JS コードが他のバックエンドと状態を共有する必要がない場合に、最もすっきりした方法です。
宣言に付ける #cfg(target="js")単一の fn、const、またはブロック共有部分が大半のモジュールで、1 つのシンボルだけにバックエンド固有の実装が必要な場合。関連するコードを 1 つのファイルにまとめられます。

#cfg(...) の中で使われる条件は次のとおりです。

#cfg(target="js")                          // JS only
#cfg(target="native")                      // native only
#cfg(not(target="js"))                     // everything except JS
#cfg(any(target="wasm", target="wasm-gc")) // either wasm backend
#cfg(any(target="native", target="llvm"))  // either non-GC backend
#cfg(false)                                // always exclude (quick disable)

moonbitlang/core では、このパターンが広く使われています。たとえば Int16::from_int64 は、ネイティブ/wasm では %i64_to_i16 組み込み関数を使いますが、64 ビット整数にネイティブ表現がない JS では代替実装を使います。

///|
#cfg(not(target="js"))
pub fn Int16::from_int64(self : Int64) -> Int16 = "%i64_to_i16"

///|
#cfg(target="js")
pub fn Int16::from_int64(self : Int64) -> Int16 {
  Int16::from_int(self.to_int())
}

#cfg にも同じファイルレベルのルールが適用されます。関数を呼び出せるすべてのターゲットに、対応する定義がちょうど 1 つ存在しなければなりません。 #cfg ガードが重複するとコンパイルエラーになります。#cfg(target="js") と #cfg(not(target="js")) を組み合わせればすべてのターゲットをカバーできます。組み込み関数ではない宣言に #cfg(target="js") だけを指定すると、その関数は他のバックエンドには存在しなくなります。その関数を呼び出すコード自体も #cfg で限定するか、targets: で限定したファイルに置く必要があります。

どちらを選ぶか: ファイル全体が extern "js" ラッパーであれば、targets: でファイルを限定してください。よりシンプルで、moon check --target wasm-gc ではファイル全体がスキップされます。ロジックの 90% が共有され、一部の関数だけが異なるクロスバックエンドライブラリを作成する場合は、#cfg を使ってください。

フェーズ 2:FFI レイヤー

extern "js" 宣言を記述します。非公開にするか、ffi_ を接頭辞として付け、フェーズ 3 で安全なラッパーを公開してください。

インライン JS(#| リテラル):

///|
extern "js" fn ffi_console_log(msg : String) -> Unit =
  #| (msg) => console.log(msg)

///|
extern "js" fn ffi_sqrt(x : Double) -> Double =
  #| (x) => Math.sqrt(x)

#| リテラルは、生の JS ソースとしての関数本体です。MoonBit コンパイラーは呼び出し箇所にインライン展開するため、実行時にクロージャーは割り当てられません。

モジュールと名前の形式(既存の JS 関数を直接インポートします):

///|
extern "js" fn math_max(a : Double, b : Double) -> Double = "Math" "max"

クロージャーでラップせずに、グローバルにアクセス可能な関数を参照する場合に使用します。わずかに高速で、サイズも小さくなります。

#module("pkg") — コンパイル時の ESM/CJS インポート:

JS モジュール(Node の組み込みモジュール、npm パッケージ、ローカルの .js ファイル)から名前付きエクスポートを使う場合は、extern "js" に #module("module-id") を付けます。右辺はそのモジュール内のエクスポート名です(インライン JS 本体ではありません)。

///|
#module("node:path")
extern "js" fn path_basename(p : String) -> String = "basename"

///|
#module("node:path")
extern "js" fn path_extname(p : String) -> String = "extname"

コンパイラーが出力する内容:

link.js.format生成されるコード
"esm"import { basename as basename$7 } from "node:path";
"cjs"const { basename: basename$1548 } = require("node:path");
"iife"同じ CJS 形式の分割代入(IIFE 内でラップ)

手書きの require("...") ラッパーより #module() が優れている理由:

  • 静的に解析できます。 Vite / esbuild / Rollup は依存関係を追跡し、ツリーシェイクを行い、パスを正しく解決できます。
  • ソースがフォーマットに依存しません。 同じ .mbt ファイルが、require が定義されていない ESM と CJS の両方で動作し、別のコードパスも不要です。
  • 生成コードを小さくできます。 呼び出しごとのクロージャーは不要で、分割代入したローカル変数が再利用されます。
  • バンドラーと互換性のあるパスに対応します。 npm パッケージ指定子("lodash")、Node 組み込みモジュール("node:path")、相対ファイル("./helpers.js")、およびランタイムがサポートする場合は URL 形式の指定子を使用できます。

制約:

  1. 名前付きエクスポートのみです。 RHS はモジュールからエクスポートされたキーでなければなりません。デフォルトエクスポート、またはトップレベルの値自体が関数であるモジュールの場合は、JS 側で再エクスポートするブリッジが必要です。または、FFI 本体内に import / require をインラインで記述してください。
  2. 宣言ごとにエクスポートは 1 つです。 複数の #module FFI 間で分割代入を共有することはできません。各アノテーションは独自のインポート行を生成します。コンパイラーはモジュール指定子を重複排除するため、関数ごとではなく、一意のモジュールごとに 1 回の require で済みます。
  3. モジュール名のリテラルはコンパイル時定数でなければなりません。 動的なモジュールパス(#module(some_variable))はサポートされていません。モジュールを動的に選択する場合は、インライン JS の require(name) を使用してください。
  4. インライン本体は指定できません。 #module("pkg") + extern "js" fn f() = "name" の形式のみ使用できます。#| (...) => ... と組み合わせるとコンパイルエラーになります。
  5. ブラウザーのグローバルには不要です。 window、document、fetch、Math などには、インライン JS またはモジュール+名前の形式(= "Math" "max")を使用してください。これらはインポートされるのではなく、アクセスされるだけです。
  6. 副作用のあるインポートには、引き続き副作用があります。 コンパイル済みモジュールの読み込み時に import / require が実行されます。pkg にトップレベルの副作用(CLI バナー、グローバルのパッチ適用)がある場合、FFI 関数を一度も呼び出さなくても、インポート時に実行されます。
  7. テストとリリースでフォーマットが異なります。 moon test は link.js.format の設定にかかわらず、CJS ハーネスを使用します。モジュールが ESM 専用(package.json の exports に CJS エントリーポイントがない)の場合、moon build --release が動作してもテストが失敗する可能性があります。該当するテストを条件付きにするか、両方の形式に対応したパッケージを使用してください。

形式の使い分け:

形式使用する場面
#|(x) => ... インライン小さな式、複雑なロジック、または複数の JS 呼び出しを 1 つのラッパー内で組み合わせる必要がある場合
= "Math" "max" モジュール+名前定義済みのグローバル関数を呼び出す場合(モジュールのインポートは不要)
#module("pkg") + = "name"インポート可能なモジュール(Node 組み込みモジュール、npm パッケージ、ローカルファイル)の名前付きエクスポートを呼び出す場合
require("pkg").get("name") ヘルパーモジュール名が動的な場合、または実行時にしかモジュール名がわからない場合

複数行のインライン JS:

///|
extern "js" fn ffi_do_work(x : Int) -> Int =
  #| (x) => {
  #|   const y = x * 2;
  #|   return y + 1;
  #| }

不透明な JS 値のパターン:

MoonBit に相当する型がない値(DOM ノード、npm オブジェクト、undefined、null)は、#external pub type でラップしてください。

///|
#external
pub type JsValue

///|
/// Zero-cost bitcast — compiles to nothing.
pub fn[A, B] identity(a : A) -> B = "%identity"

%identity はコンパイラー組み込み機能です。型チェッカーのレベルで値を別の型に移し、JS コードは出力しません。プリミティブ値を JsValue に変換し、また戻すために使用してください。実行時には検証されません。誤った使い方をすると、型エラーが発生しても気づけません。

JsValue に対する汎用 FFI 操作:

///|
pub extern "js" fn JsValue::get(self : JsValue, key : String) -> JsValue =
  #| (obj, key) => obj[key]

///|
pub extern "js" fn JsValue::set(
  self : JsValue,
  key : String,
  value : JsValue,
) -> Unit =
  #| (obj, key, value) => { obj[key] = value }

///|
pub extern "js" fn JsValue::call_method(
  self : JsValue,
  name : String,
  args : Array[JsValue],
) -> JsValue =
  #| (obj, name, args) => obj[name](...args)

この 3 つのプリミティブ(get / set / call_method)を使うと、事実上あらゆる JS オブジェクトとのやり取りを表現できます。

null / undefined の区別:

MoonBit の T? では T | null | undefined を安全に表現できません。Some(null) になってしまいます。is_undefined / is_null を使って明示的に確認してください。

///|
pub extern "js" fn is_undefined(v : JsValue) -> Bool =
  #| (v) => v === undefined

///|
pub fn[T] js_get_opt(obj : JsValue, key : String) -> T? {
  let v = obj.get(key)
  if is_undefined(v) {
    None
  } else {
    Some(identity(v))
  }
}

null と undefined を区別するライブラリ(例:FileReader.error)では、Nullable[T] と Nullish[T] のラッパー型を導入し、明示的なチェックで絞り込んでください。

フェーズ 3:MoonBit API

生の ffi_* externs をラップする、安全で型付きの pub ラッパーを構築します。

///|
pub fn console_log(msg : String) -> Unit {
  ffi_console_log(msg)
}

///|
pub fn sqrt(x : Double) -> Double {
  if x < 0 { panic() }
  ffi_sqrt(x)
}

JS コンシューマー向けのエクスポート:

moon.pkg の link.js.exports に記載されているものはすべて pub である必要があります。コンパイラーは、それぞれに名前付き ESM エクスポート(または CJS プロパティ、IIFE グローバル)を生成します。公開シグネチャから .d.ts が自動生成されます。

#export_name(v0.10.4) は、単一の pub 非ジェネリック関数に対して、生成されるシンボル名を固定します。新しいエクスポートでは、バックエンド固有の exports リンク設定よりもこちらが推奨されます。ジェネリック関数や省略可能な引数を持つ関数では動作せず、名前はパッケージ内で一意な、有効な C 識別子である必要があります。

#export_name("add_ints")
pub fn add(a : Int, b : Int) -> Int { a + b }

エクスポート設定は、アーティファクトを生成するパッケージに適用されます。依存関係内の属性によって、出力にシンボルが追加されることはありません。

フェーズ 4:Promise/非同期ブリッジ

MoonBit の非同期処理では、JS の Promise 内部処理に変換されるコンパイラー組み込み機能を 2 つ使用します。

組み込み機能役割
%async.suspend再開/拒否コールバックのペアを await ポイントに変換します
%async.run同期コンテキストから非同期計算を開始します

標準的なセットアップ(詳細は references/promise-bridging.md を参照)。各項目は前の項目に依存するため、この順番で宣言します。

///|
#external
pub type Promise[T]

///|
/// Cast Promise[T] to JsValue so JsValue methods (get/call_method) apply.
/// Required by `Promise::wait` below.
pub fn[T] Promise::to_any(self : Promise[T]) -> JsValue = "%identity"

///|
pub async fn[T, E : Error] suspend(
  f : ((T) -> Unit, (E) -> Unit) -> Unit,
) -> T raise E = "%async.suspend"

///|
pub fn run_async(f : async () -> Unit noraise) -> Unit = "%async.run"

///|
/// The bridge. `await promise` in JS = `promise.wait()` in MoonBit.
pub async fn[T] Promise::wait(self : Promise[T]) -> T {
  suspend(fn(ok, err) {
    self.to_any().call_method("then", [identity(fn(v : JsValue) { ok(identity(v)) })])
        .call_method("catch", [identity(fn(e : JsValue) { err(identity(e)) })])
    |> ignore
  })
}

%async.run と %async.suspend のどちらが必要ですか?

状況run_async が必要ですか?理由
async test "..." { ... } ブロックいいえテストハーネスがすでにブロックを非同期コンテキストで実行します
.wait() を呼び出す pub async fnいいえ呼び出し元はすでに非同期であり、await チェーンが自然に機能します
Promise を開始して JS に返す、エクスポート済みの MoonBit 同期関数はい同期から非同期へ切り替えるため、run_async が本体をスケジュールします
async fn エグゼキューターから Promise[T] を構築するはいnew Promise(...) は同期処理ですが、エグゼキューター本体を実行する必要があります

要するに、async test を使う場合、run_async を記述する必要はありません。同期コンテキストから非同期計算を %identity で JS の Promise にキャストする場合にのみ必要です。

標準パターン — Promise を返す FFI + 非同期ラッパー:

///|
extern "js" fn ffi_fetch_text(url : String) -> Promise[String] =
  #| (url) => fetch(url).then(r => r.text())

///|
pub async fn fetch_text(url : String) -> String {
  ffi_fetch_text(url).wait()
}

呼び出し側は、async test または async fn 内で let body = fetch_text(url) と記述します。呼び出し箇所で .wait() を呼び出す必要はありません。テスト:

async test "fetch_text resolves" {
  let body = fetch_text("https://example.com")
  inspect(body.length() > 0, content="true")
}

注 — moonbitlang/async: async test ブロックを使用するには、moon.pkg に import { "moonbitlang/async" } for "test" が必要です。この依存関係が必要なのはテスト時のみです。moon add moonbitlang/async で追加します。バージョン 0.17.0 以降が必要です(古い 0.1.x リリースは、現在の %async.suspend ABI をサポートしていません)。0.21.0 で動作確認済みです。

0.20 より後にアップグレードする際は、次の破壊的変更に注意してください。@http.Headers は Map[String, String] ではなく、大文字と小文字を区別しないマップになりました。@fs から create/truncate パラメーターが削除され、@fs.write_file のデフォルトは CreateOrTruncate になりました。@stdio.{Input,Output}::fd は値を返す代わりに例外を発生させます。@async.protect_from_cancel の resume_on_cancel=true がデフォルトになりました。ランタイムは、デッドロックを検出すると処理を中断します(設定には @async.set_deadlock_handler を使用してください)。

フェーズ 5:テスト
moon check                           # type-check, fast
moon test                            # run all tests (js is default)
moon test src --filter "greet"       # filter by name
moon build                           # emit .js + .d.ts to _build/js/release/build/

ビルド出力パス — デバッグとリリース:

コマンド出力ディレクトリ
moon build(デフォルト)_build/js/debug/build/<pkg>.js + .d.ts
moon build --release_build/js/release/build/<pkg>.js + .d.ts
moon test(自動)_build/js/debug/test/<pkg>.{internal,blackbox}_test.js

.d.ts はどちらのモードでも生成されます。npm に公開する場合は、--release を使用してください。ローカルで試す場合は、デフォルトの debug 出力で十分であり、リンクもより高速です。

生成されたモジュールを Node から実行する:

# debug build
node --input-type=module -e 'import("./_build/js/debug/build/<pkg>.js").then(m => console.log(m.add(2,3)))'

# release build (after `moon build --release`)
node --input-type=module -e 'import("./_build/js/release/build/<pkg>.js").then(m => console.log(m.add(2,3)))'

実行可能なエンドツーエンドのサンプルは、このスキルの assets/js_binding_proj/ にあります。最小限のプロジェクトで同期・非同期のテストが 8 件成功する例です。ディレクトリ内の内容をコピーして調整し、moon test で実行してください。

判断表

状況パターン主な操作
JS のグローバルを呼び出す(1 行で記述)アロー関数を使ったインラインの #|extern "js" fn ... = #| (x) => ...
既存のグローバルを参照する(ラップなし)モジュール+名前の形式= "Math" "max"
npm/Node 組み込みモジュールから名前付きエクスポートを参照する#module("pkg") + = "exportName"コンパイラーが import { exportName } または require を出力するため、バンドラーと相性がよい
不透明な JS オブジェクト#external pub type T%identity でラップしてキャストする
JS の any コンテナーget/set/call_method を備えた単一の JsValue 型オブジェクトプロトコルのヘルパーを一度作成して再利用する
T | undefinedis_undefined(v) で確認し、T? を返すSome(null) に意味があると決して仮定しない
T | null(undefined とは区別される)Nullable[T] ラッパー型is_null で明示的に分岐する
JS Promise#external type Promise[T] + wait()%async.suspend でブリッジする
JS の利用側にエクスポートするmoon.pkg の link.js.exportspub 関数を列挙する。.d.ts は自動生成される
ファイル全体を 1 つのバックエンドに限定するmoon.pkg の targets: { "f.mbt": ["js"] }ファイル全体がバックエンド固有の場合に最もすっきりする
単一の関数を 1 つのバックエンドに限定するfn の上に #cfg(target="js") を記述するファイルの 90% が共有される場合に使う。ほかのバックエンドには #cfg(not(target="js")) を併用する
大きな整数(|x| > 2^53)Int64 ではなく BigIntJS の number は 2^53 を超えるとビット精度が失われる
npm パッケージを呼び出すrequire("pkg") をラップし、JsValue.get("fn") を使うreferences/interop-patterns.md を参照
JS 例外を捕捉するJS 側で呼び出しを try { ... } catch { ... } でラップするreferences/error-handling.md を参照

よくある落とし穴

  1. ファイルを targets で限定し忘れる。 extern "js" を含むファイルが ["js"] に限定されていないと、moon check --target wasm-gc と native が失敗します。FFI ファイルは必ず限定してください。

  2. T | null | undefined の表現に T? を使う。 MoonBit の None は JS の null ではなく、Some(null) は意味のない値です。値を変換する前に is_undefined と is_null で分けてください。

  3. FFI をまたいで Int64 に依存する。 JS の number は IEEE-754 倍精度浮動小数点数です。2^53 を超える 64 ビット整数は、警告なしに丸められます。BigInt を使うか、2 つの Int に分割してください。

  4. Map/Result/トレイトオブジェクトをそのまま渡して MoonBit の内部表現を公開する。 これらは MoonBit 固有のランタイムレイアウトを持ちます。境界でプレーンな構造体、Array[(K, V)]、または JsValue に変換してください。

  5. async test に moonbitlang/async が必要なことを忘れる。 テスト専用のインポートは忘れやすいものです。最初の async test を書く前に、moon.pkg に import { "moonbitlang/async" } for "test" を追加してください。

  6. 同期 JS から非同期関数を呼び出す際に %async.run を忘れる。 エクスポートされた同期関数では await を使えません。同期エクスポートから非同期処理を開始する必要がある場合は、run_async(async fn() noraise { ... }) でラップし、Promise を明示的に返してください。

  7. async コンテキスト外で .wait() を呼び出す。 .wait() は async です。コンパイルできるのは async fn または async test の内部だけです。非同期ではない呼び出し側は、Promise をそのまま受け取る必要があります。

  8. 実際に変換が必要な箇所で %identity を使う。 %identity は型チェッカーを回避するためのもので、ランタイムへの影響はありません。Int と String のキャストに使うと、警告なしにランタイムエラーになります。JS のランタイム表現が実際に一致する場合にのみ使用してください。

  9. link.js.exports に含まれない pub 関数をエクスポートする。ほかの MoonBit パッケージからは呼び出せますが、コンパイル済み JS モジュールからは再エクスポートされません。関数名を exports に追加してください。

サンプルプロジェクト

上記のすべてのパターンを示す、最小限の実行可能なプロジェクトです。

assets/js_binding_proj/
├── moon.mod                # preferred_target = "js", moonbitlang/async dep
└── src/
    ├── moon.pkg            # targets + link.js.exports
    ├── ffi.mbt             # extern "js" + JsValue + inline JS
    ├── lib.mbt             # Safe public wrappers (add, greet, js_get_opt)
    ├── lib_test.mbt        # Sync tests
    ├── async.mbt           # Promise[T] + suspend + wait
    ├── async_test.mbt      # async test blocks
    ├── modules.mbt         # #module("node:path") ESM/CJS import demo
    ├── modules_test.mbt
    ├── cross_target.mbt    # #cfg(target="...") per-declaration gating demo
    └── cross_target_test.mbt

実行方法:

cd assets/js_binding_proj
moon test              # 12 tests pass (sync + async + #cfg + #module)
moon build             # emits ESM + .d.ts
node --input-type=module -e 'import("./_build/js/debug/build/js_binding_proj.js").then(m => console.log(m.add(2,3), m.greet("world"), m.basename("/a/b/c.html")))'
# → 5 Hello, world! c.html

参考資料

@references/promise-bridging.md @references/interop-patterns.md @references/error-handling.md @references/typescript-integration.md @references/cfg-and-target-gating.md

コメント