Skip to content

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) void
  • ctx.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();
_ = try ctx.eval(main_source);
try ctx.drainMicrotasks(); // await の継続を実行するために必要

例: ホストと 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 する必要はありません。

次のステップ