Skip to content

ホスト関数とコールバック

このページでは、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;
  • HostFnContext と引数だけを受け取るシンプルな関数。レシーバ(this)を扱わないグローバル関数の登録に使います。
  • HostMethodthis(呼び出し時のレシーバ)も受け取る関数。オブジェクトのメソッドやアクセサ(getter/setter)として使う場合はこちらです。グローバル関数として呼ばれた場合、thisundefined になります。プリミティブ値がレシーバの場合もボクシングされずそのまま渡されます。

どちらも戻り値は 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 === レシーバ になります。

checkThis();       // HostMethod 内で this は undefined
obj.getValue();     // HostMethod 内で this は obj

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) !Valuedata はエンジンから一切解釈されず、GC 対象にもなりません(寿命の管理はホスト側の責任です)。
  • yanmajs.getFunctionData(val: Value) ?*anyopaquemakeFunctionWithData で紐付けた 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 で捕捉できます。

try {
  missingArgument();
} catch (e) {
  console.log(e.message); // "MissingArgument"
}

2. throwXxxError を呼んでから通常の Value を返す

TypeError/RangeError など特定のエラー型で例外を投げたい場合は、ctx.throwTypeError(などの throw 系メソッド)を呼んだ 後に 通常どおり Valuereturn してください。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 を呼ぶ形になります。

次のステップ