組み込み(Embedding)ガイド¶
このガイドでは、YanmaJS を Zig ホストアプリケーションに組み込むための API を一通り解説します。すべて Context(yanmajs.Context)というオブジェクトを起点に操作します。
Context のライフサイクル¶
Context は 1 つの JavaScript 実行環境(グローバルオブジェクト・ヒープ・GC 状態などをまとめたもの)を表します。
Context.init(allocator: std.mem.Allocator) !*Context— 指定したAllocatorを使うContextを生成します。返り値はヒープ確保された*Contextです。Context.deinit(ctx: *Context) void—Contextが保持するすべてのリソース(VM・登録済みホスト関数のラッパーなど)を解放します。
メモリ上限付きで初期化する¶
信頼できない JavaScript コードをサンドボックス実行する場合、メモリ使用量に上限を設けたいことがあります。initWithMemoryLimit を使うと、内部で確保量を追跡するアロケータでラップした Context を作成できます。
const ctx = try Context.initWithMemoryLimit(allocator, 1 * 1024 * 1024); // 1MiB
defer ctx.deinit();
// 上限を超えるアロケーションは error.OutOfMemory になる
_ = ctx.eval("...") catch |err| {
if (err == error.OutOfMemory) {
// メモリ上限に達した
}
};
ctx.getMemoryUsage() usize— 現在の使用量(バイト)。initで作成したContextでは常に0。ctx.getMemoryLimit() ?usize— 設定された上限。initで作成した場合はnull。
JavaScript コードを評価する¶
eval — その場で評価する¶
もっとも単純な方法は eval です。ソース文字列をパース・コンパイル・実行して結果の Value を返します。
戻り値の型は EvalError!Value です。EvalError には OutOfMemory・StackOverflow・ArityMismatch・UncaughtException・NotCallable・RuntimeError などが含まれます。JavaScript 側で例外が投げられた場合は error.UncaughtException になり、エラーハンドリング の節で説明する getPendingException で例外オブジェクトを取り出せます。
compileScript + runScript — 繰り返し実行する¶
同じスクリプトを何度も実行する場合は、パース・コンパイルを 1 回だけ行い CompiledScript として保持しておき、runScript で繰り返し実行できます。
const script = try ctx.compileScript("1 + 2 * 3");
const result1 = try ctx.runScript(script);
const result2 = try ctx.runScript(script); // 再コンパイルなしで再実行
serializeScript / deserializeScript — バイトコードキャッシュ¶
コンパイル済みスクリプトはバイトコードとしてシリアライズでき、次回起動時に再パースなしで読み込めます。
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);
ES モジュール¶
evalModule を使うと import/export を含む ES モジュールを評価できます。ホストからモジュールの中身を供給するには ModuleLoader を実装して setModuleLoader で登録します。
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; // 解決できない場合は 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;");
ModuleLoader.load_fn は(解決済みの)specifier を受け取り、モジュールのソースコードを返します。ファイルシステムやネットワークから読み込む実装をホスト側で用意してください。相対指定子や自己 import を読み込み元(referrer)基準で正規化したい場合は、任意の resolve_fn を併せて登録します(未設定なら指定子はそのまま使われます)。
グローバル変数の設定・取得¶
try ctx.setGlobal("answer", yanmajs.makeInt(ctx, 42));
const v = ctx.getGlobal("answer");
const result = try ctx.eval("answer * 2"); // 84
setGlobal(name: []const u8, val: Value) !voidgetGlobal(name: []const u8) Value— 未定義の場合はundefinedを返します。
ホスト関数を登録する¶
JavaScript 側から呼び出せる Zig 関数(ホスト関数)を登録する方法は 2 通りあります。
setFunction — シンプルなケース¶
this(レシーバ)を必要としない、グローバル関数として登録する場合はこちらを使います。
const addFn: yanmajs.HostFn = struct {
fn call(c: *Context, args: []const Value) !Value {
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
makeFunction / makeFunctionWithData — this を扱いたい・Value として使いたいケース¶
オブジェクトのメソッドとして登録したい場合や、アクセサ(getter/setter)として使いたい場合は makeFunction を使います。こちらは Value(native 関数オブジェクト)を返すので、setProperty/setGlobal/defineProperty に渡して初めて JavaScript 側から到達可能になります。
const getX: yanmajs.HostMethod = struct {
fn call(c: *Context, this: Value, args: []const Value) !Value {
_ = args;
return yanmajs.getProperty(this, "_x");
}
}.call;
const fn_val = try ctx.makeFunction("getX", getX);
try ctx.setProperty(obj, "getX", fn_val); // メソッドとしてアタッチ
注意:
makeFunction/makeFunctionWithDataが返すValueは GC のルートに繋がっていません。setProperty/setGlobal/definePropertyで即座にオブジェクトへアタッチするか、protect/HandleScope.pinでルート化してください。アタッチする前に GC を誘発する別のアロケーションを行わないよう注意してください。
個別の userdata を関数に紐付けたい場合は makeFunctionWithData を使います。コールバック内では getFunctionData(callee) でその userdata を取り出せます(callee は getCurrentCallee() で取得可能)。
var state: MyState = .{ ... };
const fn_val = try ctx.makeFunctionWithData("handler", myHandler, @ptrCast(&state));
HostFn と HostMethod のシグネチャ¶
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はグローバル関数として呼ばれることを想定しており、レシーバを受け取りません。HostMethodはメソッドとして呼ばれた際のレシーバをそのままthisとして受け取ります(プリミティブ値の場合もボクシングされません)。グローバル関数として呼ばれた場合のthisはundefinedになります。
JavaScript 関数を呼び出す¶
ホスト側から、JavaScript 側で定義された関数を呼び出すには 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
this(レシーバ)を明示して呼び出したい場合は callFunctionWithReceiver を使います(QuickJS の JS_Call / V8 の Function::Call 相当。メソッドやイベントリスナーの this をホストから指定するケース)。
// method_fn を this = receiver で呼ぶ
const result = try ctx.callFunctionWithReceiver(method_fn, receiver, &[_]Value{});
コンソール出力を差し替える¶
デフォルトでは console.log/info/warn/error/debug は標準出力に出力されますが、setConsoleHandler でホスト側のコールバックにリダイレクトできます。
const Handler = struct {
fn write(ptr: ?*anyopaque, level: yanmajs.ConsoleLevel, text: []const u8) void {
_ = ptr;
std.debug.print("[{s}] {s}\n", .{ @tagName(level), text });
}
};
ctx.setConsoleHandler(.{ .write_fn = Handler.write });
// null を渡すと標準出力へのデフォルト動作に戻る
text はコールバックの呼び出し中のみ有効です。後で使い回す場合はホスト側で複製(dupe)してください。
ユーザーデータ(user data)¶
ホスト側の任意の状態(バインディング全体など)を Context に 1 つだけ紐付けられます。エンジンはこの値を一切解釈せず、GC の対象にもなりません。
var app_state: AppState = .{ ... };
ctx.setUserData(@ptrCast(&app_state));
// HostMethod のコールバック内などから
const state: *AppState = @ptrCast(@alignCast(ctx.getUserData().?));
イベントループ¶
YanmaJS は setTimeout/setInterval などのタイマー登録を、ホストのイベントループに委譲する形で扱います。
const Hooks = struct {
fn setTimer(id: u32, delay_ms: u32, is_repeat: bool, userdata: ?*anyopaque) void {
// ホストのイベントループにタイマーを登録する
}
fn clearTimer(id: u32, userdata: ?*anyopaque) void {
// 対応するタイマーをキャンセルする
}
};
ctx.setEventLoopHook(.{
.set_timer = Hooks.setTimer,
.clear_timer = Hooks.clearTimer,
});
setEventLoopHook を設定しない場合、setTimeout/setInterval は登録と同時に即座にコールバックが実行されます(フォールバック動作)。
エンジンが保持するのは各タイマーの JS コールバックだけで、発火時刻(due)の管理はホストの責務です。set_timer で (id, 現在時刻 + delay_ms, is_repeat) を記録しておき、期限が来たら fireTimer(id) を呼びます。内部でコールバックの呼び出しに続けてマイクロタスクのドレイン(drainMicrotasks 相当)も行われます。clearTimeout/clearInterval 済みの id に対する fireTimer は安全な no-op です。
hasPendingTimers() bool— 未発火のタイマーが残っているかどうか。イベントループを終了してよいかの判定に使えます。drainMicrotasks() !void— Promise の.thenなどで積まれたマイクロタスクキューを処理します。eval/callFunctionの後、Promise の解決状態を反映させたい場合に明示的に呼び出してください。
完全な参照実装が 2 つ付属しています。
- Zig:
yanmajs_runtime(yanmajs_runtime/src/event_loop.zigのEventLoop)。due 昇順の挿入ソートリストで管理し、native ホスト(CLI/GUI)にも WASM ホストにも使える共通実装として設計されています。現在は CLI(yanmajs_cli/)がこれを使っており、CLI は既定でこのイベントループが有効で、--no-timersオプションを付けるとフックを設定せず従来の同期フォールバック動作になります。 - C:
example/c/event_loop.c。同じパターンの C API 版で、GUI ホストが自前のメインループに組み込む際のひな形です。
EventLoop(yanmajs_runtime)の 2 層構成¶
yanmajs_runtime.EventLoop は「仮想時計を持つ non-blocking なコア(pump)」と「実クロックで回すブロッキングの便利層(runBlocking)」の 2 層に分かれています。
const runtime = @import("yanmajs_runtime");
var el = runtime.EventLoop.init(allocator);
defer el.deinit();
ctx.setEventLoopHook(el.hook());
// eval より前に、必ず実時刻(または任意の仮想時刻)で seed する。
el.setNow(runtime.monotonicNowMs());
_ = try ctx.eval("setTimeout(() => console.log('hi'), 1000);");
el.runBlocking(ctx); // 次の due までスリープ → pump、を繰り返す(native 専用)
pump(ctx, now_ms) ?u64— non-blocking なコア。now_msまで仮想時計を進め、マイクロタスクをドレインし、due になったタイマーをすべて発火してから、次の due(なければnull)を返します。実クロックにも仮想クロック(WASM でホストが注入する時刻)にも使えます。runBlocking(ctx) void— native 専用の便利層。pumpを実クロック(monotonicNowMs())で繰り返し呼び、次の due までのあいだは rawnanosleepでスリープします。CLI はこれをそのまま使っています。setNow(now_ms) void— eval より前に呼ぶ必須の初期化です。EventLoopは自前の仮想時計(now_ms、初期値 0)を持っており、set_timerフックはこの仮想時計を基準に due(絶対時刻)を計算します。setNowで実時刻を seed する前にsetTimeoutが登録されると、due が0 + delay_msという過去寄りの値になり、最初のpump呼び出し(実際の "今" を渡す)の時点で「もう期限切れ」として即座に発火してしまいます。CLI(yanmajs_cli)は eval の直前に必ずel.setNow(runtime.monotonicNowMs())を呼ぶことでこれを避けています。- WASM サンドボックスのように実クロックを持たないホストは
runBlockingを使わず、自前のループ(例:requestAnimationFrame)からpumpを直接、外部から渡したnow_msで呼び出します。
実行時間の制限(watchdog)¶
信頼できないスクリプトの暴走(無限ループ等)を時間で打ち切るには setExecutionTimeLimitMs を使います。以後の eval/evalModule/runScript/callFunction/fireTimer の各呼び出しに、それぞれ独立した壁時計バジェットが課され、超過すると Error("Execution time limit exceeded")(error.RuntimeError)で中断されます。中断後も Context は引き続き利用可能です。
ctx.setExecutionTimeLimitMs(100); // 各 eval/call ごとに 100ms まで
const result = ctx.eval("while (true) {}");
// => error.RuntimeError
const ex = ctx.getPendingException().?; // "Execution time limit exceeded"
ctx.setExecutionTimeLimitMs(null); // 制限を解除
チェックは一定命令数ごと(4096 命令間隔)に行われるため検出には若干の遅れがあります。requestInterrupt とは独立に動作します。時刻源が Linux の CLOCK_MONOTONIC であるため、非 Linux ターゲットでは事実上無効(恒久 no-op)です。詳細は context を、C API での利用例は example/c/watchdog.c を参照してください。
実行の割り込み(interrupt)¶
長時間実行される、あるいは無限ループするスクリプトを途中で止めたい場合は requestInterrupt/clearInterrupt を使います。これらは atomic フラグ(release/acquire オーダリング)で実装されており、別スレッドから呼び出しても安全です。実行中の VM がインタプリタループの中でフラグを検知すると、実行を中断して例外を送出します。
// 実行中のスレッド側
const result = ctx.eval("while (true) {}");
// => error.RuntimeError (中断された)
const ex = ctx.getPendingException().?; // "interrupted" を含むメッセージ
clearInterrupt() で保留中の割り込み要求を取り消せます。中断後も Context は引き続き利用可能です。
エラーハンドリング¶
eval/callFunction/runScript などは JavaScript 例外を error.UncaughtException として返します。実際の例外オブジェクト(Error インスタンスなど)は getPendingException で取得します(取得すると同時にクリアされます)。
const result = ctx.eval("throw new TypeError('bad')");
if (result) |_| {} else |err| {
if (err == error.UncaughtException) {
const ex = ctx.getPendingException().?;
const msg = try yanmajs.toString(ctx, ex);
defer ctx.allocator.free(msg);
// msg == "TypeError: bad"
}
}
ホスト関数の中から JavaScript 側に例外を投げたい場合は throwValue/throwError/throwTypeError/throwRangeError/throwReferenceError/throwSyntaxError を使います。
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;
HostFn/HostMethod が Zig の error を返した場合も、自動的に Error オブジェクトとして JavaScript 側に例外が送出されます。
次のステップ¶
Value の作成・変換・型判定については Value の扱い方 を参照してください。