Promise と非同期処理¶
このページでは、ホスト(Zig)側から Promise を作成・解決する方法、マイクロタスクキューの処理、そしてタイマーと組み合わせたイベントループの実装パターンについて説明します。
ホストから Promise を作る: makePromise¶
const result = try ctx.makePromise();
// result.promise: Value (JS 側に渡す Promise オブジェクト)
// result.handle: PromiseHandle (resolve/reject 用のハンドル)
ctx.makePromise() !struct { promise: Value, handle: PromiseHandle }
返される promise はまだ pending 状態です。JavaScript 側にそのまま渡して .then/.catch/await で扱えます。
try ctx.setGlobal("testPromise", result.promise);
_ = try ctx.eval("testPromise.then(v => console.log('resolved:', v));");
解決・棄却する: resolvePromise / rejectPromise¶
ctx.resolvePromise(result.handle, yanmajs.makeInt(ctx, 42));
// または
ctx.rejectPromise(result.handle, try yanmajs.makeString(ctx, "something went wrong"));
try ctx.drainMicrotasks(); // .then/.catch コールバックを実際に走らせる
ctx.resolvePromise(handle: PromiseHandle, val: Value) voidctx.rejectPromise(handle: PromiseHandle, val: Value) void
PromiseHandleは 1 回限り(one-shot)です。resolvePromise/rejectPromiseのどちらか一方を 1 回呼ぶと、そのハンドルは無効になります。同じハンドルに対して 2 回目の resolve/reject を呼び出さないでください(JavaScript の Promise 仕様上、2 回目以降の解決は無視されるのが正しい挙動であり、PromiseHandleの再利用を想定した設計にはなっていません)。
GC からの保護¶
makePromise が返す promise は、内部的に Context.protect によって GC から保護された状態になっています。ホスト側で Value をどこにも保持していなくても、resolvePromise/rejectPromise が呼ばれるまで(=PromiseHandle が消費されるまで)GC に回収される心配はありません。resolvePromise/rejectPromise を呼ぶと内部で自動的に保護が解除されます(unprotect 相当)。
マイクロタスクキューを処理する: drainMicrotasks¶
.then/.catch/.finally に登録されたコールバックや async/await の継続は、すぐには実行されず「マイクロタスクキュー」に積まれます。これを実際に実行するには drainMicrotasks を明示的に呼び出す必要があります。
_ = try ctx.eval(
\\let resolved = 0;
\\Promise.resolve(99).then(v => { resolved = v; });
);
// この時点ではまだ then コールバックは実行されていない
try ctx.drainMicrotasks();
// ここで resolved === 99
ctx.drainMicrotasks() !void— キューが空になるまでマイクロタスクを処理します。実行後、未処理の reject のチェックも行われます。
eval/callFunction/fireTimer を呼んだ直後は、Promise の状態を JavaScript 側に反映させるために drainMicrotasks を呼ぶ習慣をつけてください。
Promise の状態を調べる¶
const state = yanmajs.promiseState(val); // ?PromiseState
const result = yanmajs.promiseResult(val); // Value
yanmajs.promiseState(val: Value) ?PromiseState—.pending・.fulfilled・.rejectedのいずれか。valが Promise でなければnull。yanmajs.promiseResult(val: Value) Value— 解決済み(fulfilled/rejected)の場合の結果値。pendingの間や非 Promise 値に対してはundefined。
_ = try ctx.eval("var p = Promise.resolve(42);");
try ctx.drainMicrotasks();
const p = ctx.getGlobal("p");
// yanmajs.promiseState(p) == .fulfilled
// yanmajs.promiseResult(p) == 42 相当の Value
未処理の reject を検知する¶
Promise が reject されたにもかかわらず、drainMicrotasks が完了する時点でも .then/.catch/.finally のいずれのハンドラも付いていない場合(いわゆる unhandled rejection)、setUnhandledRejectionHandler で登録したコールバックに通知されます。
const Handler = struct {
fn onUnhandled(ptr: ?*anyopaque, reason: yanmajs.Value) void {
_ = ptr;
// reason を使ってログ出力するなど
std.debug.print("unhandled rejection\n", .{});
}
};
ctx.setUnhandledRejectionHandler(.{ .handler_fn = Handler.onUnhandled });
// null を渡すと通知を止める
ctx.setUnhandledRejectionHandler(handler: ?UnhandledRejectionHandler) void
後から .catch が付いた場合は通知対象から外れます(HostPromiseRejectionTracker 相当の仕様どおりの挙動です)。
タイマーと組み合わせたイベントループ¶
setTimeout/setInterval で登録されたコールバックは、ホストのイベントループが実際の時間経過を管理し、fireTimer を呼び出すことで初めて実行されます(登録・発火の仕組みの詳細は 高度な機能 の「タイマー」を参照)。典型的なイベントループの骨格は次のようになります。
while (ctx.hasPendingTimers()) {
const id = /* ホスト側のスケジューラから次に発火すべきタイマー id を取得する */;
// 実時間になるまで待つ(あるいはホストのイベントループに制御を戻す)
ctx.fireTimer(id);
// fireTimer は内部でコールバック実行 → drainMicrotasks → 未処理 reject チェック
// まで一括して行うため、明示的に drainMicrotasks を呼び直す必要はない
}
ctx.hasPendingTimers() bool— 未発火のタイマーが残っているか。イベントループを終了してよいかの判定に使えます。ctx.fireTimer(id: u32) void— 指定した id のタイマーコールバックを実行します。setIntervalで登録されたタイマーは実行後も登録が残り続けます(setTimeoutは 1 回実行後に自動的に削除されます)。
async/await との関係¶
JavaScript 側の async function/await は通常の Promise の上に構築されているため、ホストから作成・解決した Promise ともそのまま連携できます。
async function main() {
const res = await fetch('https://example.com'); // ホストが実装した Promise を返す関数
console.log(res.status);
}
main();
例: ホストと JavaScript を繋ぐ非同期ブリッジ¶
タイマー完了後に非同期でホスト側の処理を行い、結果を Promise として JavaScript に返す例です。
const State = struct {
ctx: *Context,
handle: yanmajs.PromiseHandle,
};
const delayedValueFn: yanmajs.HostFn = struct {
fn call(c: *Context, args: []const Value) !Value {
_ = args;
const result = try c.makePromise();
// 実際にはホストのタイマー/ワーカースレッド等から
// 後で resolvePromise を呼ぶ形になる。ここでは概念のみ示す。
const state = try c.allocator.create(State);
state.* = .{ .ctx = c, .handle = result.handle };
scheduleHostCallback(state); // ホスト側のスケジューラに登録(擬似コード)
return result.promise;
}
}.call;
// スケジューラのコールバック側(擬似コード): 完了時にこれを呼ぶ
fn onHostWorkDone(state: *State) void {
state.ctx.resolvePromise(state.handle, yanmajs.makeInt(state.ctx, 42));
state.ctx.allocator.destroy(state);
}
PromiseHandle はホスト側の任意のタイミング(別スレッド・タイマーコールバックなど)まで保持しておき、準備が整った時点で resolvePromise/rejectPromise を呼び出せます。Promise 自体は GC から保護されているため、ホスト側で Value を明示的に protect する必要はありません。