FFI:呼び出すことと、呼び込まれること

他の言語と話すことは二つの違う問題だ。C 関数を呼び出すのは一行 —— `extern fn time: unit -> int;` —— で、それは言語の保証が終わる正直な境界だ。ブラウザの JavaScript から呼び込まれることは、より難しい:host は Mere が表現できないオブジェクトを持ち、Mere のクロージャを起動できねばならない。両方向が、同じ一つのトリックに帰着することになる —— 相手側が所有するテーブルへのインデックスを交換すること。

mereffiwebassemblyinteroplanguage-design

四つの backend はどの Mere プログラムも走らせられ、同一の出力に保たれる。そのどれもまだできないのが、 別の言語で書かれたコードと話すこと —— C ライブラリ関数を呼ぶこと、あるいはブラウザが Mere の クロージャにイベントを手渡すこと —— だ。それが外部関数インターフェースの用途であり、それは実は 反対を向いた二つの別々の問題だ:外部コードへ呼び出すことと、それに呼び込まれること。Mere は それぞれを解き、二つの解は、注目に値するしかたで韻を踏む。

呼び出す:extern fn

この時点まで、Mere が外の世界にできることはすべて builtin だった —— 九十ほど、そのそれぞれが四つの backend すべてにわたってコンパイラにハードコードされていた。一つ足すことは、コンパイラを編集し新しい リリースを出すことを意味した。閉じた言語には結構だが、実用的な言語には成り立たない:time()getenv()sqrt() を欲しがる全員に、コンパイラにパッチを当てろとは頼めない。

修正は、普通のユーザコードから外部関数を宣言する道だ:

extern fn time:   unit -> int;
extern fn getenv: str -> str;

let now = time () in ...

一行が関数とその型を名づけ、それが呼べるようになる。選ばれた構文は素朴なもの —— Rust 風の extern "C" ブロックでも、エフェクト注釈された形でもない —— いまや馴染みの原則、最小の正直な版を 出し、儀式は実際の使用が要求してから足す、に立って。各 backend はすでに呼び出しを行う機構を持って いた:C は extern 宣言と直接呼び出しを吐き、Wasm は前回のまさに host-import 機構を再利用する —— Wasm の extern fn はただの名前付き (import "env" ...)print がすでに通った同じ門が、いまや host が提供する任意の関数へ開かれる。

extern fn について最も大事なのは、それが何を認めるかだ。言語全体が、コンパイラが検証できるものを 最大化するよう建てられている —— 明示的な型、明示的なエフェクト、byte 単位で同一の backend。外部 C 関数はそのすべての外にある:コンパイラは、それが何をするか、あなたがまだ持つポインタを free するか、 宣言した型に合うかを、検査できない。FFI は保証の本物の穴だ。言語の応答は、そうでないふりをすることでは なく、その穴に名をつけることだ:extern キーワードが、検証可能性がどこで終わるかをちょうど印づけ、 読む人がどの呼び出しが安全な世界を去るかを見られるようにする。これはプロジェクトがいたるところで 適用する同じ正直さだ —— feature を済みと呼ばないカバレッジ台帳、「十分近い」を同一と通さない拒否。 保証が終わるところで、言語は境界をぼかすのではなく、そうと声に出して言う。

呼び込まれる:ブラウザの問題

呼び出すのは易しい方向だ。Mere が制御しているからだ —— 引数をマーシャルし、結果を信じる。難しい 方向はその逆だ:ブラウザの JavaScript が、走る Mere プログラムへ呼び込み、Mere が持つよう作られ なかったものを持ち回る。Mere はすでに Wasm にコンパイルされページで走る;フロントエンドで役立つには、 DOM に触れイベントに応えねばならず、それがスカラーの extern fn では解けない二つの問題を浮かび 上がらせる。

一つ目は、Mere が表現できないオブジェクトだ。DOM 要素は JavaScript のオブジェクトだ;あらゆる Mere 値は 4 バイト語だ。<div>i32 に入れることはできない。答えは不透明ハンドルだ:新しい extern type JsRef、それはワイヤ上では i32 だが Mere の中では不透明 —— 持って渡し戻せるが、決して 覗けない。トリックは host 側に住む:JavaScript は実オブジェクトのテーブルを保ち、その中への インデックスを配る。dom_get_by_id は要素を格納してそのインデックスを返し、dom_set_text は インデックスを受け取り要素を引いて作用する。Mere は、Mere にとって無意味で host にとって有意味な 数を持ち回る。

二つ目は、JavaScript が Mere 関数を呼べるようにすることだ —— イベントハンドラは、クリックが 起きたとき Mere コードを走らせねばならない。そしてここで、二回前に建てられた何かが報われる。Wasm backend はすでにあらゆるクロージャを関数テーブルへのインデックスとして表現する。Wasm に関数ポインタが ないからだ;内部では、クロージャを呼ぶことはそのテーブル越しの call_indirect だ。Mere への コールバックは、ただその同じインデックスを、境界を越えて export したものだ:Mere は JavaScript に クロージャのテーブルインデックスを手渡し、イベントが発火すると、JavaScript はそれで関数テーブル越しに 呼び戻す。Mere 自身のクロージャのためだけに存在した機構が、外の世界が Mere を呼ぶ仕組みになる。

一つのトリック、二度

二つを並べれば、それらは同じアイデアだ。Mere のリニアメモリと JavaScript のオブジェクトヒープは、 ポインタを共有できない二つの世界だ —— どちらも他方への生アドレスを持てない。だから両方向が、同じ やり方で境界を越える:相手側が所有するテーブルへのインデックスを交換することで。DOM 要素は host の ハンドルテーブルへのインデックスだ。Mere のコールバックは Mere の関数テーブルへのインデックスだ。 それぞれの場合、越えていく値は、受け取る側にとって無意味な数で、発行した側にとって引き当てキーだ。 メモリを共有できない二つの世界も、整数は共有でき、両端の整数とテーブルが、何でも渡すのに足りる。

その対称性こそ、フロントエンドインターフェースがすでに手にある部品から組み上がった理由だ:JavaScript 側に鏡写しされたハンドルテーブル、Wasm backend の中にすでに建てられた関数テーブル、そしてそれらを 運ぶよう一般化された extern fn の import 機構。具体的な結果は、小さな DOM バインディングと、ブラウザ で走るカウンタ —— Mere のクロージャが生きたクリックハンドラを務める —— で、プロジェクト全体の動き方に 沿って、荒い縁を隠すのではなく名づけた最小 MVP として到達された(ハンドルはページの寿命の間リークする; 安全性の物語は、使用が要求するまで先送りされる)。

言語はいまや四つのやり方で走り、その両側のコードと話せる。自然な次の問いは、それが本物の重さを担え るか —— カウンタではなく、言語が実際に作りたい何かに使える、と証すほど実質的なプログラム —— だ。 プロジェクトの答えは、自分自身のドキュメントサイトを Mere で建てることだった。次回、Part IV の締め: ドキュメントサイトを言語自身で書く、そしてその規模での dogfood が何を露わにしたか。

← Back to Mere: 言語を作る