Skip to content

高度な機能

このページでは、複数の独立した実行環境(Realm)、ES モジュール、バイトコードのシリアライズ、タイマー駆動のイベントループ、実行の中断、そして内部の文字列エンコーディングについて説明します。

Realm — 同一 Context 内の独立したグローバル環境

Context は 1 つの VM(ヒープ・GC 状態など)を持ちますが、その中に複数の「Realm」(グローバルオブジェクト・グローバル変数の集合)を作成できます。iframe や worker のように、グローバルスコープだけを分離したい場合に使えます。

const realm2 = try ctx.createRealm();
defer ctx.destroyRealm(realm2);

const old_realm = ctx.setRealm(realm2);
try ctx.setGlobal("x", yanmajs.makeInt(ctx, 20));
// この時点で realm2 の x は 20

_ = ctx.setRealm(old_realm); // 元の Realm に戻す(戻り値は "切り替え前" の Realm)
  • ctx.createRealm() !*Realm — 新しい Realm を作成します(現在アクティブな Realm には影響しません)。
  • ctx.setRealm(realm: *Realm) *Realm — アクティブな Realm を切り替えます。戻り値は切り替え前の Realm なので、後で元に戻す際に使えます。
  • ctx.getRealm() *Realm — 現在アクティブな Realm を返します。
  • ctx.destroyRealm(realm: *Realm) void — Realm を破棄します。アクティブな Realm を破棄する前には、他の Realm に setRealm で切り替えておいてください。

eval/callFunction などはすべて「現在アクティブな Realm」に対して働きます。Realm を切り替えるとグローバル変数(var/関数宣言など)が完全に分離されるため、同じ Context(= 同じヒープ・GC)を共有しつつ、複数の独立したスクリプト実行環境を扱えます。

_ = try ctx.eval("var a = 100");

const realm2 = try ctx.createRealm();
defer ctx.destroyRealm(realm2);
const old = ctx.setRealm(realm2);

_ = try ctx.eval("var a = 200");
const result = try ctx.eval("a"); // 200(realm2 の a)

_ = ctx.setRealm(old);
const result2 = try ctx.eval("a"); // 100(元の Realm の a)

ES モジュール

import/export を含む ES モジュールを評価するには evalModule を使います。モジュールの実体(ソースコード)はホストが ModuleLoader を実装して供給します。

_ = try ctx.evalModule(specifier: []const u8, source: []const u8) EvalError!Value
pub const ModuleLoader = struct {
    ptr: *anyopaque,
    // 省略可: specifier を referrer 基準で正規キーに解決する
    resolve_fn: ?*const fn (ptr: *anyopaque, specifier: []const u8, referrer: []const u8, allocator: std.mem.Allocator) anyerror!?[]u8 = null,
    // 解決済み specifier からソースコードを読み込む
    load_fn: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8,
};
const Loader = struct {
    fn load(ptr: *anyopaque, specifier: []const u8) !?[]const u8 {
        _ = ptr;
        if (std.mem.eql(u8, specifier, "constants")) {
            return "export const ANSWER = 42;";
        }
        return null; // 解決できない specifier の場合は null
    }
};

var loader_state: u8 = 0;
ctx.setModuleLoader(.{ .ptr = @ptrCast(&loader_state), .load_fn = Loader.load });

_ = try ctx.evalModule("main", "import { ANSWER } from 'constants'; ANSWER;");
  • ctx.setModuleLoader(loader: ModuleLoader) void
  • load_fn は解決済みの specifier を受け取り、ソースコードの文字列(またはモジュールが見つからない場合は null)を返します。実際のファイルシステムアクセスやネットワーク読み込みはホスト側の実装に委ねられています。
  • resolve_fn(省略可)は import に書かれた生の specifier を、referrer(import 元モジュールの解決済みキー。エントリモジュールは "")基準で正規化します。返した文字列がモジュールの同一性キーになり、そのまま load_fn に渡されます。相対 import(./util.js)や自己 import の同一性を成立させたい場合は実装してください。未設定の場合は生の specifier がそのままキーになります
  • モジュールは top-level await・循環 import・import.meta に対応しています。import.meta.url にはモジュールの解決済み specifier が入ります

バイトコードのシリアライズ

スクリプトの起動を高速化したい場合、パース・コンパイル済みのバイトコードをシリアライズして保存し、次回起動時にパースをスキップして読み込めます。

const script = try ctx.compileScript(source);        // パース + コンパイル
const bytes = try ctx.serializeScript(script);        // バイト列にシリアライズ
defer ctx.allocator.free(bytes);

// bytes をファイルなどに保存しておき、次回起動時に読み込む
const loaded = try ctx.deserializeScript(bytes);      // デシリアライズ
const result = try ctx.runScript(loaded);             // 実行
  • ctx.compileScript(source: []const u8) EvalError!CompiledScript — パース・コンパイルのみを行い、実行はしません。
  • ctx.serializeScript(script: CompiledScript) ![]u8 — バイトコードをバイト列にシリアライズします(呼び出し側が解放)。
  • ctx.deserializeScript(data: []const u8) !CompiledScript — シリアライズされたバイト列から CompiledScript を復元します。
  • ctx.runScript(script: CompiledScript) EvalError!Value — コンパイル済みスクリプトを実行します。同じ CompiledScript を何度も実行できます。

同一プロセス内で同じスクリプトを繰り返し実行するだけであれば compileScript + runScript で十分です。serializeScript/deserializeScript はプロセスを跨いでバイトコードキャッシュを再利用したい場合(例: CLI 起動の高速化)に使います。

タイマー(setTimeout/setInterval)とイベントループフック

JavaScript の setTimeout/setInterval は、実際のタイマー管理をホストのイベントループに委譲する形で実装されています。setEventLoopHook を設定していない場合、タイマーは登録と同時に(遅延なしで)即座に実行されるフォールバック動作になります。

const Hooks = struct {
    fn setTimer(id: u32, delay_ms: u32, is_repeat: bool, userdata: ?*anyopaque) void {
        // ホストのイベントループ(epoll/タイマーホイールなど)にタイマーを登録する
    }
    fn clearTimer(id: u32, userdata: ?*anyopaque) void {
        // 対応するタイマーをキャンセルする(clearTimeout/clearInterval から呼ばれる)
    }
};

ctx.setEventLoopHook(.{
    .set_timer = Hooks.setTimer,
    .clear_timer = Hooks.clearTimer,
});
pub const EventLoopHook = struct {
    set_timer: *const fn (id: u32, delay_ms: u32, is_repeat: bool, userdata: ?*anyopaque) void,
    clear_timer: *const fn (id: u32, userdata: ?*anyopaque) void,
    userdata: ?*anyopaque = null,
};

実際に時間が経過してタイマーを発火させる際は ctx.fireTimer(id) を呼びます。setInterval で登録されたタイマーは発火後も登録が残り続け、setTimeout は 1 回発火すると自動的に削除されます。イベントループの組み方や Promise との連携は Promise と非同期処理 の「タイマーと組み合わせたイベントループ」を参照してください。

実行の中断(interrupt)

無限ループや長時間実行されるスクリプトを外部から停止させたい場合は requestInterrupt/clearInterrupt を使います。

ctx.requestInterrupt(); // 別スレッドから呼び出しても安全(atomic)
const result = ctx.eval("while (true) {}");
// => error.RuntimeError(中断された)
const ex = ctx.getPendingException().?; // "interrupted" を含むメッセージ
  • ctx.requestInterrupt() void — 割り込みフラグを立てます。atomic 操作(release/acquire オーダリング)で実装されているため、スクリプトを実行しているスレッドとは別のスレッドから呼び出しても安全です。実行中の VM がインタプリタループの中でフラグを検知すると、実行を中断して例外を送出します。
  • ctx.clearInterrupt() void — 保留中の割り込み要求を取り消します。

中断後も Context は引き続き通常どおり使用できます(再度 eval を呼べます)。

setEventLoopHook と組み合わせて、ホストのメインループの中で定期的に割り込みの必要性(タイムアウト・ユーザーによるキャンセルなど)をチェックし、必要なら requestInterrupt を呼ぶ、という使い方もできます。

文字列エンコーディング

YanmaJS の内部文字列表現は CESU-8(サロゲートペアを持つ UTF-16 コードユニット列を、それぞれ独立した UTF-8 風の 3 バイトシーケンスとして符号化した形式)です。JavaScript の文字列メソッド(.lengthcharAt・スライスなど)は仕様どおり UTF-16 コード単位ベースのインデックスで動作します。

ホストから makeString で文字列を渡す際は、通常の UTF-8 をそのまま渡して問題ありません。U+10000 以上の astral 文字(4 バイトの UTF-8 シーケンス)が含まれる場合は、makeString が内部で自動的に UTF-8 → CESU-8 へ正規化します。

// "😀" (U+1F600) は astral 文字。内部的には CESU-8(サロゲートペア)として
// 格納され、JS 側から見た .length は 2 になる(UTF-16 の仕様どおり)。
const s = try yanmajs.makeString(ctx, "😀");
try ctx.setGlobal("s", s);
const len = try ctx.eval("s.length"); // 2

toString で Zig 側に文字列を取り出す際は、CESU-8 表現がそのまま UTF-8 として妥当な形式でもあるため、通常の UTF-8 文字列として扱えます。

詳しい Value の作成・変換については Value の扱い方 も参照してください。

JSON

JSON.parse/JSON.stringify 相当の機能を、グローバルの JSON オブジェクトを経由せずホスト側から直接呼び出せるヘルパーが用意されています。

const val = try yanmajs.parseJSON(ctx, "{\"x\": 42}");
const json = try yanmajs.stringifyJSON(ctx, val);
defer ctx.allocator.free(json);
  • yanmajs.parseJSON(ctx: *Context, json_str: []const u8) !Value
  • yanmajs.stringifyJSON(ctx: *Context, val: Value) ![]const u8

不正な JSON をパースしようとした場合、戻り値は error.UncaughtException になり、実際の SyntaxError は保留中の例外として残ります。ctx.getPendingException() で取得(同時にクリア)してください。

const result = yanmajs.parseJSON(ctx, "{invalid") catch |err| {
    if (err == error.UncaughtException) {
        const ex = ctx.getPendingException().?; // SyntaxError
        _ = ex;
    }
    return err;
};

エラーハンドリングの一般的なパターンについては エラーハンドリング を参照してください。

次のステップ