外部関数インターフェース (FFI)#
ここまでで紹介してきたのは、純粋な計算を記述する方法でした。実際には、現実世界とやり取りする必要があります。ただし、その「世界」はバックエンドごとに異なり(C、JS、Wasm、WasmGC)、ランタイム(Wasmtime、Deno、Browser など)に基づくこともあります。
バックエンド#
MoonBit には現在 5 つのバックエンドがあります:
Wasm
Wasm GC
JavaScript
C
LLVM(実験的)
ここでいう Wasm は、MVP 以降のいくつかの提案を含む WebAssembly を指します。
bulk-memory-operations
multi-value
reference-types
互換性を高めるため、init 関数は start 関数 としてコンパイルされ、main 関数は _start としてエクスポートされます。
注釈
Wasm バックエンドでは、外部世界とやり取りする関数はすべてホストに依存します。例えば、Wasm と Wasm GC バックエンドの println は、呼び出しごとに UTF-16 コードユニットを 1 つ出力する spectest.print_char という関数のインポートに依存します。標準ライブラリの env パッケージや moonbitlang/x の一部のパッケージは、MoonBit ランタイム用に定義された特定のホスト関数に依存します。生成した Wasm の移植性を高めたいなら、これらは使わないでください。
ここでいう Wasm GC は、Garbage Collection 提案を備えた WebAssembly を指し、struct や array などの参照型でデータ構造を表現し、線形メモリは既定では使われないことを意味します。さらに、次のような MVP 以降の提案もサポートします:
multi-value
JS string builtins
互換性を高めるため、init 関数は start 関数 としてコンパイルされ、main 関数は _start としてエクスポートされます。
注釈
Wasm バックエンドでは、外部世界とやり取りする関数はすべてホストに依存します。例えば、Wasm と Wasm GC バックエンドの println は、呼び出しごとに UTF-16 コードユニットを 1 つ出力する spectest.print_char という関数のインポートに依存します。標準ライブラリの env パッケージや moonbitlang/x の一部のパッケージは、MoonBit ランタイム用に定義された特定のホスト関数に依存します。生成した Wasm の移植性を高めたいなら、これらは使わないでください。
JavaScript バックエンドは JavaScript ファイルを生成します。これは configuration に応じて CommonJS モジュール、ES モジュール、IIFE のいずれかになります。
C バックエンドは C ファイルを生成します。MoonBit ツールチェーンは、configuration に基づいてプロジェクトをコンパイルし、実行ファイルも生成します。
LLVM バックエンドはオブジェクトファイルを生成します。このバックエンドは実験段階であり、FFI をサポートしません。
外部型の宣言#
#external 属性を使うと、次のように外部型を宣言できます:
#external
type ExternalRef
外部関数の宣言#
外部の世界とやり取りするには、外部関数を宣言できます。
注釈
MoonBit は多相的な外部関数をサポートしていません。
重要
関数を宣言するときは、シグネチャが実際の外部関数と対応していることを確認する必要があります。外部関数が値を返さない場合は -> Unit を使ってください。これは C では void に、Wasm では結果を持たない関数に対応します。
外部関数を宣言する方法は 2 つあります。関数をインポートするか、インライン関数を書くかです。
ランタイムホストから、モジュール名と関数名を指定して関数をインポートできます:
fn cos(d : Double) -> Double = "math" "cos"
あるいは、Wasm 構文を使ってインライン関数を書くこともできます:
extern "wasm" fn identity(d : Double) -> Double =
#|(func (param f64) (result f64))
注釈
インライン関数を書くときは、関数名を指定しないでください。
外部関数を宣言する方法は 2 つあります。関数をインポートするか、インライン関数を書くかです。
モジュール名と関数名を指定して関数をインポートできます。これは module.function として解釈されます。例えば:
fn cos(d : Double) -> Double = "Math" "cos"
これは const cos = (d) => Math.cos(d) という関数を指します。
あるいは、JavaScript のラムダを定義するインライン関数を書くこともできます:
extern "js" fn cos(d : Double) -> Double =
#|(d) => Math.cos(d)
関数名を指定して関数をインポートすることで、外部関数を宣言できます:
extern "C" fn put_char(ch : UInt) -> Unit = "function_name"
パッケージが外部 C ライブラリと動的リンクする必要がある場合は、moon.pkg に cc-link-flags を追加してください。これは C コンパイラに直接渡されます。
options(
"link": {
"native": {
"cc-link-flags": "-l<c library>"
}
},
)
ラッパー関数を定義するには、パッケージに C スタブファイルを追加し、そのパッケージの moon.pkg に次を追加します:
options(
"native-stub": [
// list of stub file names
],
)
おそらく #include "moonbit.h" を使いたくなるはずです。これには MoonBit の C インターフェース向けの型定義と便利なユーティリティが含まれています。ヘッダは ~/.moon/include にあり、詳細はその中身を確認してください。
型#
下の表は、いくつかの MoonBit 型の内部表現を示しています:
MoonBit type |
ABI |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
MoonBit type |
ABI |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
|
|
MoonBit type |
ABI |
|---|---|
|
|
|
|
|
|
|
|
|
|
constant |
|
external type ( |
|
|
|
|
|
|
|
|
|
注釈
数値用の FixedArray[T] は、将来的に TypedArray に移行する可能性があります。
MoonBit type |
ABI |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constant |
|
abstract type ( |
pointer (must be valid MoonBit object) |
external type ( |
|
|
|
|
|
|
Function pointer |
注釈
Unit を返す外部関数は、void を返す C 関数に対応します。FuncRef[T] の T の戻り値型が Unit の場合、それは void を返す関数を指します。
上で触れていない型には安定した ABI がないため、その内部表現に依存したコードを書くべきではありません。
コールバック#
ときには、MoonBit の関数をコールバックとして外部インターフェースに渡したくなります。MoonBit ではクロージャを扱えます。MDN glossary によると:
クロージャとは、関数と、その周囲の状態(レキシカル環境)への参照をひとまとめにしたものです。言い換えると、クロージャによって関数は外側のスコープにアクセスできます。JavaScript では、関数が作られるたびにクロージャが作成されます。
場合によっては、ローカルな自由変数を一切捕捉しないコールバック関数を渡したいことがあります。そのために MoonBit では、型 T の閉じた関数を表す特別な型 FuncRef[T] を用意しています。FuncRef[T] 型の値は、型 T の閉じた関数でなければならず、そうでない場合は 型エラー が発生します。
それ以外の場合、MoonBit の関数引数は、関数と周囲の状態を保持するオブジェクトとして表現されます。
Wasm バックエンドでは、コールバックはホストの関数を表す externref として渡されます。ただし、関数と捕捉したデータをまとめてホスト側の関数に変換することが重要です。
そのために、Wasm モジュールは moonbit:ffi モジュール、関数名 make_closure の関数をインポートします。この関数は関数とオブジェクトを受け取り、関数の最初の引数がそのオブジェクトであるものを、ホストの関数として返す必要があります。つまり、部分適用はホスト側の責任です。実装例は次のとおりです:
{
"moonbit:ffi": {
"make_closure": (funcref, closure) => funcref.bind(null, closure)
}
}
JavaScript はクロージャをサポートしているので、ここで特別なことは必要ありません。
C ライブラリの関数の中には、コールバック関数に加えて追加データを渡せるものがあります。次の C ライブラリ関数があるとします:
void register_callback(void (*callback)(void*), void *data);
この C 関数をバインドし、次の工夫でクロージャを渡すことができます:
extern "C" fn register_callback_ffi(
call_closure : FuncRef[(() -> Unit) -> Unit],
closure : () -> Unit
) -> Unit = "register_callback"
fn register_callback(callback : () -> Unit) -> Unit {
register_callback_ffi(
fn (f) { f() },
callback
)
}
FuncRef[_] 型の値は、MoonBit から直接呼び出すこともできます。これは、シンボル名で関数を動的に読み込む場合や、ネイティブバックエンドで JIT を実装する場合に便利です。
定数 enum の整数値をカスタマイズする#
MoonBit のすべてのバックエンドで、定数 enum(すべてのコンストラクタがペイロードを持たない enum)は整数に変換されます。コンストラクタ宣言の後に = <integer literal> を追加すると、各コンストラクタの実際の整数表現をカスタマイズできます:
enum SpecialNumbers {
Zero = 0
One
Two
Three
Ten = 10
FourtyTwo = 42
}
コンストラクタの整数値を指定しない場合は、直前のコンストラクタの値に 1 を加えた値が既定になります(最初のコンストラクタは 0 です)。この機能は、C ライブラリのフラグをバインドするときに特に便利です。
関数のエクスポート#
メソッドでも多相関数でもない public 関数は、link configuration の exports フィールドを設定することでエクスポートできます。
options(
"link": {
"<backend>": {
"exports": [ "add", "fib:test" ]
}
}
)
前の例では add と fib をエクスポートしますが、fib は test としてエクスポートされます。
注釈
これは設定したパッケージにのみ有効で、下流のパッケージには影響しません。
注釈
これは設定したパッケージにのみ有効で、下流のパッケージには影響しません。
CommonJS モジュール(cjs)、ES Module(esm)、iife としてエクスポートするための別の format オプションもあります。
注釈
これは設定したパッケージにのみ有効で、下流のパッケージには影響しません。
エクスポートした関数の名前変更は、現時点ではサポートしていません
ライフタイム管理#
MoonBit はガベージコレクションを持つプログラミング言語です。そのため、外部オブジェクトを扱うときや MoonBit オブジェクトをホストに渡すときは、ライフタイム管理を意識することが重要です。現在、MoonBit は Wasm バックエンドと C バックエンドでは参照カウントを使います。Wasm GC バックエンドと JavaScript バックエンドでは、ランタイムの GC を再利用します。
外部オブジェクトのライフタイム管理#
MoonBit で外部オブジェクトやリソースを扱うときは、メモリやリソースのリークを防ぐために、適切なタイミングでオブジェクトを破棄したりリソースを解放したりすることが重要です。
注釈
C バックエンドのみ
moonbit.h には、MoonBit 独自の自動メモリ管理システムを使って外部オブジェクトやリソースのライフタイムを扱うための API moonbit_make_external_object があります:
void *moonbit_make_external_object(
void (*finalize)(void *self),
uint32_t payload_size
);
moonbit_make_external_object は、サイズが payload_size + sizeof(finalize) の新しい MoonBit オブジェクトを作成します。オブジェクトのレイアウトは次のとおりです:
| MoonBit object header | ... payload | finalize function |
^
|
|_
pointer returned by `moonbit_make_external_object`
そのため、オブジェクトをそのペイロードへのポインタとして直接扱えます。MoonBit の自動メモリ管理システムが、moonbit_make_external_object で作成されたオブジェクトがもう生きていないと判断すると、オブジェクト自身を引数として finalize 関数を呼び出します。これにより finalize は、オブジェクトのペイロードが保持していた外部リソースやメモリを解放できます。
注釈
finalize はオブジェクト自身を解放してはいけません。これは MoonBit ランタイムが処理します。
MoonBit 側では、moonbit_make_external_object が返すオブジェクトは、type T で宣言した abstract 型に束縛しておくべきです。そうすることで、MoonBit のメモリ管理システムがそのオブジェクトを無視しなくなります。
MoonBit オブジェクトのライフタイム管理#
関数を通して MoonBit オブジェクトをホストに渡すときは、MoonBit 自体のライフタイム管理に注意することが重要です。前述のとおり、MoonBit の Wasm バックエンドと C バックエンドは、コンパイラ最適化された参照カウントでオブジェクトのライフタイムを管理します。メモリエラーやリークを避けるため、FFI 関数は MoonBit オブジェクトの参照カウントを適切に維持しなければなりません。
注釈
C バックエンドと Wasm バックエンドのみ
参照カウントの呼び出し規約#
既定では、MoonBit は参照カウントに owned 呼び出し規約を使います。つまり、被呼び出し側(呼び出される関数)が、moonbit_decref / $moonbit.decref 関数を使って引数を解放する責任を持ちます。引数を複数回使う場合は、被呼び出し側が moonbit_incref / $moonbit.incref 関数を使って参照カウントを増やす必要があります。状況ごとに必要な操作は次のとおりです:
イベント |
操作 |
|---|---|
フィールド / 要素を読む |
なし |
データ構造に格納する |
|
MoonBit 関数に渡す |
|
他の外部関数に渡す |
なし |
返す |
なし |
スコープ末尾(返さない) |
|
例えば、ファイルを開く標準の open 関数に対する、ライフタイムが正しいバインディングは次のようになります:
extern "C" fn open(filename : Bytes, flags : Int) -> Int = "open_ffi"
int open_ffi(moonbit_bytes_t filename, int flags) {
int fd = open(filename, flags);
moonbit_decref(filename);
return fd;
}
管理対象の型#
次の型は常に unboxed であり、ライフタイムを管理する必要はありません:
IntやDoubleなどの組み込み数値型定数
enum(すべてのコンストラクタがペイロードを持たないenum)
次の型は常に boxed で、参照カウントされます:
FixedArray[T]、Bytes、Stringabstract 型(
type T)
外部型(#external type T)も boxed ですが、これは外部ポインタを表すため、MoonBit はそれらに対して参照カウント操作を行いません。
ペイロード付きの struct / enum のレイアウトは、現時点では安定していません。
borrow と owned 属性#
FFI を通して引数を渡すとき、その所有権が維持される場合とされない場合があります。#borrow と #owned 属性を使うと、この 2 つの条件を指定できます。
警告
既定の意味論は #owned から #borrow へ移行中です
#borrow と #owned の構文は次のとおりです:
#borrow(params..)
extern "C" fn c_ffi(..) -> .. = ..
ここで params は c_ffi の引数の部分集合です。
#borrow の引数は borrow ベースの呼び出し規約で渡されます。つまり、呼び出される関数はこれらの引数を decref する必要がありません。FFI 関数が引数をローカルに読むだけなら(つまり引数を返さず、データ構造にも保存しないなら)、#borrow 属性を直接使えます。例えば、前述の open 関数は #borrow を使うと次のように書き換えられます:
#borrow(filename)
extern "C" fn open(filename : Bytes, flags : Int) -> Int = "open"
もはやスタブ関数は不要です。ここでは open の元の版に直接バインドしています。#borrow 属性を使っても、この版はライフタイム的に正しいままです。
他の理由でスタブ関数が必要な場合でも、#borrow によってライフタイム管理を簡単にできることがよくあります。borrow 引数 に対して必要な操作は、状況ごとに次のとおりです:
イベント |
操作 |
|---|---|
フィールド / 要素を読む |
なし |
データ構造に格納する |
|
MoonBit 関数に渡す |
|
他の C 関数 / |
なし |
返す |
|
スコープ末尾(返さない) |
なし |
反対に #owned は、引数が FFI 関数側で保持され、後で decref を手動で実行する必要がある意味論です。利用例の 1 つは、クロージャが owned になるコールバックの登録です。