ホスト関数とコールバック¶
このページでは、Zig で書いたホスト関数を JavaScript から呼び出せるようにする方法と、JavaScript の関数をホスト側から呼び出す方法を詳しく説明します。基本的な登録方法は 組み込み(Embedding)ガイド でも触れていますが、ここでは userdata の紐付けやエラーの扱いも含めて詳しく解説します。
2 つの関数型¶
YanmaJS には、ホスト関数のシグネチャが 2 種類あります。
pub const HostFn = *const fn (ctx: *Context, args: []const Value) anyerror!Value;
pub const HostMethod = *const fn (ctx: *Context, this: Value, args: []const Value) anyerror!Value;
HostFn—Contextと引数だけを受け取るシンプルな関数。レシーバ(this)を扱わないグローバル関数の登録に使います。HostMethod—this(呼び出し時のレシーバ)も受け取る関数。オブジェクトのメソッドやアクセサ(getter/setter)として使う場合はこちらです。グローバル関数として呼ばれた場合、thisはundefinedになります。プリミティブ値がレシーバの場合もボクシングされずそのまま渡されます。
どちらも戻り値は anyerror!Value です。Zig の error を返すとどう扱われるかは 下記「エラーの伝播」を参照してください。
グローバル関数として登録する: setFunction¶
this を必要としない場合、最も簡単な方法は ctx.setFunction です。内部でグローバルオブジェクトに直接バインドされます。
const addFn: yanmajs.HostFn = struct {
fn call(c: *Context, args: []const Value) !Value {
_ = c;
const a = yanmajs.toFloat(args[0]);
const b = yanmajs.toFloat(args[1]);
return yanmajs.makeFloat(a + b);
}
}.call;
try ctx.setFunction("add", addFn);
const result = try ctx.eval("add(3, 4)"); // 7
ctx.setFunction(name: []const u8, zig_fn: HostFn) !void— グローバルスコープにnameという名前の関数を登録する便利メソッドです。
Value として関数を作る: makeFunction¶
オブジェクトのメソッド、アクセサ、あるいは複数の場所にアタッチしたい関数には ctx.makeFunction を使います。こちらは HostMethod を受け取り、native 関数オブジェクトの Value を返します。
const getValue: yanmajs.HostMethod = struct {
fn call(c: *Context, this: Value, args: []const Value) !Value {
_ = args;
const ptr = yanmajs.getHostData(this) orelse return yanmajs.makeUndefined();
const val: *u32 = @ptrCast(@alignCast(ptr));
return yanmajs.makeInt(c, val.*);
}
}.call;
const method = try ctx.makeFunction("getValue", getValue);
try ctx.setProperty(obj, "getValue", method); // メソッドとしてアタッチ
ctx.makeFunction(name: []const u8, func: HostMethod) !Value
注意:
makeFunction/makeFunctionWithDataが返すValueは他のmake*系関数と同様、返却時点では GC のルートに繋がっていません。setProperty/setGlobal/definePropertyで即座にオブジェクトへアタッチするか、protect/HandleScope.pinで保護してください(詳細は メモリ管理)。
makeFunction で作った関数は、グローバル関数として呼ばれれば this === undefined、オブジェクトのメソッドとして呼ばれれば this === レシーバ になります。
userdata を紐付ける: makeFunctionWithData¶
複数の関数インスタンスにそれぞれ異なる状態(userdata)を持たせたい場合は makeFunctionWithData を使います。
var counter: u64 = 0;
const incFn: yanmajs.HostMethod = struct {
fn call(c: *Context, this: Value, args: []const Value) !Value {
_ = this;
_ = args;
const ptr = yanmajs.getFunctionData(c.getCurrentCallee()) orelse return yanmajs.makeUndefined();
const counter_ptr: *u64 = @ptrCast(@alignCast(ptr));
counter_ptr.* += 1;
return yanmajs.makeInt(c, @intCast(counter_ptr.*));
}
}.call;
const inc = try ctx.makeFunctionWithData("inc", incFn, @ptrCast(&counter));
try ctx.setGlobal("inc", inc);
ctx.makeFunctionWithData(name: []const u8, func: HostMethod, data: ?*anyopaque) !Value—dataはエンジンから一切解釈されず、GC 対象にもなりません(寿命の管理はホスト側の責任です)。yanmajs.getFunctionData(val: Value) ?*anyopaque—makeFunctionWithDataで紐付けた userdata を取り出します。makeFunction(userdata なし)やsetFunctionで作った関数に対しては常にnullを返します。ctx.getCurrentCallee() Value— native 関数の実行中に、現在実行されている関数自身のValueを返します。上記のようにgetFunctionData(ctx.getCurrentCallee())と組み合わせることで、HostMethodコールバックの内側から自分自身に紐付いた userdata を取得できます。
JavaScript 関数を呼び出す: callFunction¶
ホスト側から JavaScript の関数(あるいは呼び出し可能な任意の Value)を呼び出すには callFunction を使います。
_ = try ctx.eval("function double(x) { return x * 2; }");
const double_fn = ctx.getGlobal("double");
const result = try ctx.callFunction(double_fn, &[_]Value{yanmajs.makeInt(ctx, 21)}); // 42
ctx.callFunction(func: Value, args: []const Value) EvalError!Value
func が呼び出し可能でない場合は TypeError("value is not a function")を pending exception にセットした error.RuntimeError になります。JavaScript 側で例外が投げられた場合は error.UncaughtException になり、いずれも ctx.getPendingException() で例外オブジェクトを取得できます(詳しくは エラーハンドリング)。
エラーの伝播¶
HostFn/HostMethod からエラーを JavaScript 側に伝える方法は 2 通りあります。
1. Zig の error をそのまま返す¶
fn call(c: *Context, args: []const Value) !Value {
if (args.len == 0) return error.MissingArgument;
// ...
}
この場合、message が Zig のエラー名(例: "MissingArgument")になった汎用の Error オブジェクトが自動的に生成され、JavaScript 側の例外として送出されます。JavaScript 側からは通常の try/catch で捕捉できます。
2. throwXxxError を呼んでから通常の Value を返す¶
TypeError/RangeError など特定のエラー型で例外を投げたい場合は、ctx.throwTypeError(などの throw 系メソッド)を呼んだ 後に 通常どおり Value を return してください。Zig の error を返すのではなく、あくまで戻り値としては正常な Value(多くの場合 makeUndefined())を返す点に注意してください。
const validateFn: yanmajs.HostFn = struct {
fn call(c: *Context, args: []const Value) !Value {
if (args.len == 0 or !yanmajs.isNumber(args[0])) {
c.throwTypeError("argument must be a number");
return yanmajs.makeUndefined();
}
return args[0];
}
}.call;
try ctx.setFunction("validate", validateFn);
const result = ctx.eval("validate('not a number')");
// result は error.UncaughtException
// ctx.getPendingException() で TypeError オブジェクトを取得できる
詳細な throw 系 API 一覧は エラーハンドリング を参照してください。
例: native な fetch 風関数を実装する¶
ホストが非同期 I/O(HTTP リクエストなど)を行い、結果を Promise として JavaScript 側に返す典型的なパターンです。ホスト関数の中で makePromise を使い、実際の I/O が完了した時点(この例では簡略化のため即座に)で resolvePromise/rejectPromise します。Promise の詳細は Promise と非同期処理 を参照してください。
const fetchFn: yanmajs.HostFn = struct {
fn call(c: *Context, args: []const Value) !Value {
const url = try yanmajs.toString(c, args[0]);
defer c.allocator.free(url);
const result = try c.makePromise();
// 実際にはここで非同期にネットワーク I/O を開始し、
// 完了時のコールバックで resolvePromise/rejectPromise を呼ぶ。
// ここでは説明のため同期的にレスポンスを組み立てて即座に解決する。
const response = try c.makeObject();
try c.setProperty(response, "status", yanmajs.makeInt(c, 200));
try c.setProperty(response, "url", try yanmajs.makeString(c, url));
c.resolvePromise(result.handle, response);
return result.promise;
}
}.call;
try ctx.setFunction("fetch", fetchFn);
_ = try ctx.eval(
\\fetch('https://example.com').then(res => {
\\ console.log(res.status, res.url);
\\});
);
try ctx.drainMicrotasks(); // then コールバックを実行する
実際に非同期でリクエストを行う場合は、fetch 呼び出し時点では未解決の Promise だけを返し、ホストのイベントループでレスポンスを受信したタイミングで resolvePromise/rejectPromise を呼ぶ形になります。
次のステップ¶
- Promise・非同期処理の詳細については Promise と非同期処理 を参照してください。
- エラーハンドリングの詳細については エラーハンドリング を参照してください。