From 388d12d7ba435a12ebb33a1dbc67029a995791dc Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 24 Jun 2024 22:37:19 +0200 Subject: [PATCH 1/9] Refacto Env JS execution and result - return a JSValue instead of a JSResult, preserving the underlying value and avoiding automatic stringification (with allocation) - let the caller handle TryCatch if needed, including retreiving exception and stack if needed - refacto the TryCatch interface to hide v8 implementation from the public implementation - simplify the Env related methods (exec + execWait) - do not close shell on JS exec error Signed-off-by: Francis Bouvier --- src/api.zig | 2 +- src/engines/v8/callback.zig | 2 +- src/engines/v8/v8.zig | 222 ++++++++-------------------- src/interfaces.zig | 67 ++++----- src/loop.zig | 2 +- src/private_api.zig | 4 +- src/shell.zig | 63 ++++---- src/tests/cbk_test.zig | 4 +- src/tests/global_test.zig | 4 +- src/tests/proto_test.zig | 2 +- src/tests/test_utils.zig | 71 ++++----- src/tests/types_complex_test.zig | 4 +- src/tests/types_multiple_test.zig | 4 +- src/tests/types_native_test.zig | 4 +- src/tests/types_object.zig | 2 +- src/tests/types_primitives_test.zig | 4 +- src/tests/userctx_test.zig | 4 +- 17 files changed, 174 insertions(+), 291 deletions(-) diff --git a/src/api.zig b/src/api.zig index 69711d9..f02795c 100644 --- a/src/api.zig +++ b/src/api.zig @@ -59,7 +59,7 @@ pub const UserContext = @import("user_context.zig").UserContext; const Engine = @import("private_api.zig").Engine; -pub const JSResult = Engine.JSResult; +pub const JSValue = Engine.JSValue; pub const JSObject = Engine.JSObject; pub const JSObjectID = Engine.JSObjectID; diff --git a/src/engines/v8/callback.zig b/src/engines/v8/callback.zig index c1ef694..2207b2b 100644 --- a/src/engines/v8/callback.zig +++ b/src/engines/v8/callback.zig @@ -367,7 +367,7 @@ pub const Func = struct { // execute function const result = js_func.call(js_ctx, this, args); if (result == null) { - return error.JSCallback; + return error.JSExecCallback; } } }; diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 244b2c6..2ef04df 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -165,7 +165,7 @@ pub const Env = struct { } // start a Javascript context - pub fn start(self: *Env, alloc: std.mem.Allocator) anyerror!void { + pub fn start(self: *Env) anyerror!void { // context self.js_ctx = v8.Context.init(self.isolate, self.globals.getInstanceTemplate(), null); @@ -197,13 +197,9 @@ pub const Env = struct { // TODO: this is an horrible hack if (comptime T_refl.isException()) { const script = T_refl.name ++ ".prototype.__proto__ = Error.prototype"; - const res = try self.execTryCatch( - alloc, - script, - "errorSubclass", - ); - defer res.deinit(alloc); - if (!res.success) return error.errorSubClass; + _ = self.exec(script, "errorSubclass") catch { + return error.errorSubClass; + }; } } } @@ -293,119 +289,59 @@ pub const Env = struct { } } - // compile and run a Javascript script - // if no error you need to call deinit on the returned result + // compile and run a JS script + // This is a non-blocking operation, it will returned before any callback is executed pub fn exec( self: Env, - alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8, - try_catch: TryCatch, - ) !JSResult { + ) anyerror!JSValue { if (self.js_ctx == null) { return error.EnvNotStarted; } - var res = JSResult.init(); - try res.exec(alloc, script, name, self.isolate, self.js_ctx.?, try_catch.try_catch.*); + const res = jsExec(script, name, self.isolate, self.js_ctx.?) catch { + return error.JSExec; + }; return res; } - // compile and run a Javascript script with try/catch - // if no error you need to call deinit on the returned result - pub fn execTryCatch( - self: Env, - alloc: std.mem.Allocator, - script: []const u8, - name: ?[]const u8, - ) anyerror!JSResult { - - // JS try cache - var try_catch = TryCatch.init(self); - defer try_catch.deinit(); - - return self.exec(alloc, script, name, try_catch); - } - // wait I/O loop until all JS callbacks are executed - // This is a blocking operation. pub fn wait( self: Env, - alloc: std.mem.Allocator, - try_catch: v8.TryCatch, - cbk_res: ?*JSResult, - ) !void { + ignore_errors: bool, + ) anyerror!void { if (self.js_ctx == null) { return error.EnvNotStarted; } // run loop self.nat_ctx.loop.run() catch |err| { - if (try_catch.hasCaught()) { - if (cbk_res) |res| { - res.success = false; - return res.setError(alloc, self.isolate, self.js_ctx.?, try_catch); - } - // otherwise ignore JS errors - } else { + if (err != error.JSExecCallback) { // IO kernel error return err; + } else if (!ignore_errors) { + return err; } }; - - if (cbk_res) |res| res.success = true; } - pub fn waitTryCatch( - self: Env, - alloc: std.mem.Allocator, - ) anyerror!JSResult { - - // JS try cache - var try_catch: v8.TryCatch = undefined; - try_catch.init(self.isolate); - defer try_catch.deinit(); - - var res = JSResult{}; - try self.wait(alloc, try_catch, &res); - return res; - } - - // run a JS script and wait for all callbacks - // try_catch + exec + wait - pub fn run( + // compile and run a JS script and wait for all callbacks (exec + wait) + // This is a blocking operation. + pub fn execWait( self: Env, - alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8, - res: *JSResult, - cbk_res: ?*JSResult, - ) anyerror!void { - if (self.js_ctx == null) { - return error.EnvNotStarted; - } - - // JS try cache - var try_catch: v8.TryCatch = undefined; - try_catch.init(self.isolate); - defer try_catch.deinit(); + ignore_errors: bool, + ) anyerror!JSValue { // exec script - try res.exec(alloc, script, name, self.isolate, self.js_ctx.?, try_catch); + const res = try self.exec(script, name); - // run loop - self.nat_ctx.loop.run() catch |err| { - if (try_catch.hasCaught()) { - if (cbk_res) |r| { - r.success = false; - return r.setError(alloc, self.isolate, self.js_ctx.?, try_catch); - } - // otherwise ignore JS errors - } else { - // IO kernel error - return err; - } - }; + // wait + try self.wait(ignore_errors); + + return res; } }; @@ -493,91 +429,55 @@ pub const JSObject = struct { } }; +pub const JSValue = struct { + value: v8.Value, + + pub fn toString(self: JSValue, alloc: std.mem.Allocator, env: Env) anyerror![]const u8 { + return valueToUtf8(alloc, self.value, env.isolate, env.js_ctx.?); + } +}; + pub const TryCatch = struct { - try_catch: *v8.TryCatch, + inner: v8.TryCatch, + + pub fn init(self: *TryCatch, env: Env) void { + self.inner.init(env.isolate); + } - pub inline fn init(env: Env) TryCatch { - var try_catch: v8.TryCatch = undefined; - try_catch.init(env.isolate); - return .{ .try_catch = &try_catch }; + pub fn hasCaught(self: TryCatch) bool { + return self.inner.hasCaught(); } - pub inline fn exception(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { - if (self.try_catch.getException()) |msg| { + pub fn exception(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { + if (self.inner.getException()) |msg| { return try valueToUtf8(alloc, msg, env.isolate, env.js_ctx.?); } return null; } - pub inline fn deinit(self: *TryCatch) void { - self.try_catch.deinit(); - } -}; - -pub const JSResult = struct { - success: bool = false, - result: []const u8 = undefined, - stack: ?[]const u8 = null, - - pub fn init() JSResult { - return .{}; + pub fn stack(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { + const stck = self.inner.getStackTrace(env.js_ctx.?); + if (stck) |s| return try valueToUtf8(alloc, s, env.isolate, env.js_ctx.?); + return null; } - pub fn deinit(self: JSResult, alloc: std.mem.Allocator) void { - alloc.free(self.result); - if (self.stack) |stack| { - alloc.free(stack); - } + pub fn deinit(self: *TryCatch) void { + self.inner.deinit(); } +}; - pub fn exec( - self: *JSResult, - alloc: std.mem.Allocator, - script: []const u8, - name: ?[]const u8, - isolate: v8.Isolate, - js_ctx: v8.Context, - try_catch: v8.TryCatch, - ) !void { - - // compile - var origin: ?v8.ScriptOrigin = undefined; - if (name) |n| { - const scr_name = v8.String.initUtf8(isolate, n); - origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue()); - } - const scr_js = v8.String.initUtf8(isolate, script); - const scr = v8.Script.compile(js_ctx, scr_js, origin) catch { - return self.setError(alloc, isolate, js_ctx, try_catch); - }; +pub fn jsExec(script: []const u8, name: ?[]const u8, isolate: v8.Isolate, js_ctx: v8.Context) !JSValue { - // run - const res = scr.run(js_ctx) catch { - return self.setError(alloc, isolate, js_ctx, try_catch); - }; - self.success = true; - self.result = try valueToUtf8(alloc, res, isolate, js_ctx); + // compile + var origin: ?v8.ScriptOrigin = undefined; + if (name) |n| { + const scr_name = v8.String.initUtf8(isolate, n); + origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue()); } + const scr_js = v8.String.initUtf8(isolate, script); + const scr = try v8.Script.compile(js_ctx, scr_js, origin); - pub fn setError( - self: *JSResult, - alloc: std.mem.Allocator, - isolate: v8.Isolate, - js_ctx: v8.Context, - try_catch: v8.TryCatch, - ) !void { - - // exception - const except = try_catch.getException().?; - self.success = false; - self.result = try valueToUtf8(alloc, except, isolate, js_ctx); - - // stack - if (self.stack != null) { - return; - } - if (try_catch.getStackTrace(js_ctx)) |stack| { - self.stack = try valueToUtf8(alloc, stack, isolate, js_ctx); - } - } -}; + // run + const value = try scr.run(js_ctx); + return .{ .value = value }; +} diff --git a/src/interfaces.zig b/src/interfaces.zig index 5c53955..13a81c9 100644 --- a/src/interfaces.zig +++ b/src/interfaces.zig @@ -46,7 +46,7 @@ pub fn VM(comptime T: type) void { pub fn Env( comptime T: type, - comptime JSResult_T: type, + comptime JSValue_T: type, comptime Object_T: type, ) void { @@ -70,10 +70,7 @@ pub fn Env( ) anyerror!void); // start() - assertDecl(T, "start", fn ( - self: *T, - alloc: std.mem.Allocator, - ) anyerror!void); + assertDecl(T, "start", fn (self: *T) anyerror!void); // stop() assertDecl(T, "stop", fn (self: *T) void); @@ -97,42 +94,32 @@ pub fn Env( to_obj: ?Object_T, ) anyerror!void); - // TODO: check exec, wait who have v8 specific params - - // waitTryCatch - assertDecl(T, "waitTryCatch", fn ( - self: T, - alloc: std.mem.Allocator, - ) anyerror!JSResult_T); - - // execTryCatch() executes script in JS - assertDecl(T, "execTryCatch", fn ( + // exec() executes script in JS + assertDecl(T, "exec", fn ( self: T, - alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8, - ) anyerror!JSResult_T); + ) anyerror!JSValue_T); - // run() executes script in JS and waits all JS callbacks - assertDecl(T, "run", fn ( + // wait() all JS callbacks + assertDecl(T, "wait", fn ( + self: T, + ignore_errors: bool, + ) anyerror!void); + + // execWait() executes script in JS and waits all JS callbacks + assertDecl(T, "execWait", fn ( self: T, - alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8, - res: *JSResult_T, - cbk_res: ?*JSResult_T, - ) anyerror!void); + ignore_errors: bool, + ) anyerror!JSValue_T); } -pub fn JSResult(comptime T: type) void { +pub fn JSValue(comptime T: type, env: type) void { - // init() - assertDecl(T, "init", fn () T); - - // deinit() - assertDecl(T, "deinit", fn (self: T, alloc: std.mem.Allocator) void); - - // TODO: how to get the result? + // toString() + assertDecl(T, "toString", fn (self: T, alloc: std.mem.Allocator, env: env) anyerror![]const u8); } pub fn JSObjectID(comptime T: type) void { @@ -144,17 +131,27 @@ pub fn JSObjectID(comptime T: type) void { pub fn TryCatch(comptime T: type, comptime env: type) void { // init() - assertDecl(T, "init", fn (env: env) callconv(.Inline) T); + assertDecl(T, "init", fn (self: *T, env: env) void); // deinit() - assertDecl(T, "deinit", fn (self: *T) callconv(.Inline) void); + assertDecl(T, "deinit", fn (self: *T) void); + + // hasCaught() + assertDecl(T, "hasCaught", fn (self: T) bool); - // exception + // exception() assertDecl(T, "exception", fn ( self: T, alloc: std.mem.Allocator, env: env, - ) callconv(.Inline) anyerror!?[]const u8); + ) anyerror!?[]const u8); + + // stack() + assertDecl(T, "stack", fn ( + self: T, + alloc: std.mem.Allocator, + env: env, + ) anyerror!?[]const u8); } pub fn Callback(comptime T: type, comptime Res_T: type) void { diff --git a/src/loop.zig b/src/loop.zig index 4887d96..c510c0f 100644 --- a/src/loop.zig +++ b/src/loop.zig @@ -68,7 +68,7 @@ pub const SingleThreaded = struct { // at each iteration we might have new events registred by previous callbacks } if (self.cbk_error) { - return error.JSCallback; + return error.JSExecCallback; } } diff --git a/src/private_api.zig b/src/private_api.zig index 0f18fd6..57703f4 100644 --- a/src/private_api.zig +++ b/src/private_api.zig @@ -26,7 +26,7 @@ fn checkInterfaces(engine: anytype) void { interfaces.CallbackSync(engine.CallbackSync, engine.CallbackResult); interfaces.CallbackArg(engine.CallbackArg); - interfaces.JSResult(engine.JSResult); + interfaces.JSValue(engine.JSValue, engine.Env); interfaces.JSObjectID(engine.JSObjectID); interfaces.TryCatch(engine.TryCatch, engine.Env); @@ -34,7 +34,7 @@ fn checkInterfaces(engine: anytype) void { interfaces.VM(engine.VM); interfaces.Env( engine.Env, - engine.JSResult, + engine.JSValue, engine.Object, ); diff --git a/src/shell.zig b/src/shell.zig index a38bca8..0bd118a 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -105,7 +105,7 @@ const CmdContext = struct { buf: []u8, close: bool = false, - try_catch: public.TryCatch, + try_catch: *public.TryCatch, }; // I/O input command callback @@ -128,42 +128,40 @@ fn cmdCallback( return; } + defer { + + // acknowledge to repl result has been printed + _ = std.posix.write(ctx.socket, "ok") catch unreachable; + + // continue receving messages asynchronously + ctx.js_env.nat_ctx.loop.io.recv( + *CmdContext, + ctx, + cmdCallback, + completion, + ctx.socket, + ctx.buf, + ); + } + // JS execute const res = ctx.js_env.exec( - ctx.alloc, input, "shell.js", - ctx.try_catch, - ) catch |err| { - ctx.close = true; - std.debug.print("JS exec error: {s}\n", .{@errorName(err)}); + ) catch { + const except = ctx.try_catch.exception(ctx.alloc, ctx.js_env.*) catch unreachable; + defer ctx.alloc.free(except.?); + printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{except.?}); return; }; - defer res.deinit(ctx.alloc); // JS print result - if (res.success) { - if (std.mem.eql(u8, res.result, "undefined")) { - printStdout("<- \x1b[38;5;242m{s}\x1b[0m\n", .{res.result}); - } else { - printStdout("<- \x1b[33m{s}\x1b[0m\n", .{res.result}); - } + const s = res.toString(ctx.alloc, ctx.js_env.*) catch unreachable; + if (std.mem.eql(u8, s, "undefined")) { + printStdout("<- \x1b[38;5;242m{s}\x1b[0m\n", .{s}); } else { - printStdout("{s}\n", .{res.result}); + printStdout("<- \x1b[33m{s}\x1b[0m\n", .{s}); } - - // acknowledge to repl result has been printed - _ = std.posix.write(ctx.socket, "ok") catch unreachable; - - // continue receving messages asynchronously - ctx.js_env.nat_ctx.loop.io.recv( - *CmdContext, - ctx, - cmdCallback, - completion, - ctx.socket, - ctx.buf, - ); } fn exec( @@ -172,7 +170,7 @@ fn exec( ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); try shellExec(alloc, js_env); @@ -191,7 +189,8 @@ pub fn shellExec( try js_env.addObject(console, "console"); // JS try cache - var try_catch = public.TryCatch.init(js_env.*); + var try_catch: public.TryCatch = undefined; + try_catch.init(js_env.*); defer try_catch.deinit(); // create I/O contexts and callbacks @@ -202,7 +201,7 @@ pub fn shellExec( .js_env = js_env, .socket = undefined, .buf = &input, - .try_catch = try_catch, + .try_catch = &try_catch, }; var conn_ctx = ConnContext{ .socket = socket_fd, @@ -227,8 +226,8 @@ pub fn shellExec( try loop.io.tick(); if (loop.cbk_error) { if (try try_catch.exception(alloc, js_env.*)) |msg| { - printStdout("\n\rUncaught {s}\n\r", .{msg}); - alloc.free(msg); + defer alloc.free(msg); + printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{msg}); } loop.cbk_error = false; } diff --git a/src/tests/cbk_test.zig b/src/tests/cbk_test.zig index 79c1f3e..8a55acb 100644 --- a/src/tests/cbk_test.zig +++ b/src/tests/cbk_test.zig @@ -89,12 +89,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *jsruntime.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); // constructor diff --git a/src/tests/global_test.zig b/src/tests/global_test.zig index 09bf928..9d3f925 100644 --- a/src/tests/global_test.zig +++ b/src/tests/global_test.zig @@ -41,12 +41,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); // global diff --git a/src/tests/proto_test.zig b/src/tests/proto_test.zig index e20eead..35c71d5 100644 --- a/src/tests/proto_test.zig +++ b/src/tests/proto_test.zig @@ -261,7 +261,7 @@ pub fn exec( ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); const ownBase = tests.engineOwnPropertiesDefault(); diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index 3904769..a98b4ce 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -86,65 +86,52 @@ pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: var has_error = false; + var try_catch: public.TryCatch = undefined; + try_catch.init(js_env.*); + defer try_catch.deinit(); + // cases for (cases, 0..) |case, i| { + defer _ = arena.reset(.retain_capacity); test_case += 1; // prepare script execution var buf: [99]u8 = undefined; const name = try std.fmt.bufPrint(buf[0..], "test_{d}.js", .{test_case}); - var res = public.JSResult{}; - var cbk_res = public.JSResult{ - .success = true, - // assume that the return value of the successfull callback is "undefined" - .result = "undefined", - }; - // no need to deinit on a FixBufferAllocator - try js_env.run(alloc, case.src, name, &res, &cbk_res); + // run script error + const res = js_env.execWait(case.src, name, false) catch |err| { - // check script error - var case_error = false; - if (res.success) { - const equal = std.mem.eql(u8, case.ex, res.result); - if (!equal) { - case_error = true; - } - } else { - if (!isTypeError(case.ex, res.result)) { - case_error = true; - } - } + // is it an intended error? + const except = try try_catch.exception(alloc, js_env.*); + if (isTypeError(case.ex, except.?)) continue; - // check callback error - var cbk_error = false; - if (cbk_res.success) { - const equal = std.mem.eql(u8, case.cbk_ex, cbk_res.result); - if (!equal) { - cbk_error = true; - } - } else { - if (!isTypeError(case.cbk_ex, cbk_res.result)) { - cbk_error = true; + has_error = true; + if (i == 0) { + std.debug.print("\n", .{}); } - } - // log error - if (case_error or cbk_error) { + const expected = switch (err) { + error.JSExec => case.ex, + error.JSExecCallback => case.cbk_ex, + else => return err, + }; + const stack = try try_catch.stack(alloc, js_env.*); + caseError(case.src, expected, except.?, stack.?); + continue; + }; + + // check if result is expected + const res_string = try res.toString(alloc, js_env.*); + const equal = std.mem.eql(u8, case.ex, res_string); + if (!equal) { has_error = true; if (i == 0) { std.debug.print("\n", .{}); } + caseError(case.src, case.ex, res_string, null); } - if (case_error) { - caseError(case.src, case.ex, res.result, res.stack); - } else if (cbk_error) { - caseError(case.src, case.cbk_ex, cbk_res.result, cbk_res.stack); - } - - _ = arena.reset(.retain_capacity); } - if (has_error) { std.debug.print("\n", .{}); return error.NotEqual; @@ -172,7 +159,7 @@ pub fn runScript( var res = public.JSResult{}; defer res.deinit(alloc); - try js_env.run(alloc, script, name, &res, null); + try js_env.execWait(alloc, script, name, &res, null); // check result if (!res.success) { diff --git a/src/tests/types_complex_test.zig b/src/tests/types_complex_test.zig index 0d31fad..94b3d60 100644 --- a/src/tests/types_complex_test.zig +++ b/src/tests/types_complex_test.zig @@ -186,12 +186,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); var iter = [_]tests.Case{ diff --git a/src/tests/types_multiple_test.zig b/src/tests/types_multiple_test.zig index 4974f86..72319a8 100644 --- a/src/tests/types_multiple_test.zig +++ b/src/tests/types_multiple_test.zig @@ -82,12 +82,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); var cases = [_]tests.Case{ diff --git a/src/tests/types_native_test.zig b/src/tests/types_native_test.zig index 040571b..4f7ffe5 100644 --- a/src/tests/types_native_test.zig +++ b/src/tests/types_native_test.zig @@ -241,12 +241,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); var nested_arg = [_]tests.Case{ diff --git a/src/tests/types_object.zig b/src/tests/types_object.zig index f7095e8..1684227 100644 --- a/src/tests/types_object.zig +++ b/src/tests/types_object.zig @@ -90,7 +90,7 @@ pub fn exec( ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); // const o = Other{ .val = 4 }; diff --git a/src/tests/types_primitives_test.zig b/src/tests/types_primitives_test.zig index 9d45ef4..93c748a 100644 --- a/src/tests/types_primitives_test.zig +++ b/src/tests/types_primitives_test.zig @@ -115,12 +115,12 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); // constructor diff --git a/src/tests/userctx_test.zig b/src/tests/userctx_test.zig index b4b2999..26ff981 100644 --- a/src/tests/userctx_test.zig +++ b/src/tests/userctx_test.zig @@ -33,7 +33,7 @@ pub const Types = .{ // exec tests pub fn exec( - alloc: std.mem.Allocator, + _: std.mem.Allocator, js_env: *public.Env, ) anyerror!void { try js_env.setUserContext(Config{ @@ -41,7 +41,7 @@ pub fn exec( }); // start JS env - try js_env.start(alloc); + try js_env.start(); defer js_env.stop(); var tc = [_]tests.Case{ From fd737280870aad3c5c723f1f700a238d3ed2c505 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Tue, 25 Jun 2024 11:56:47 +0200 Subject: [PATCH 2/9] Change jsExec errors Signed-off-by: Francis Bouvier --- src/engines/v8/v8.zig | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 2ef04df..f802a37 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -198,6 +198,7 @@ pub const Env = struct { if (comptime T_refl.isException()) { const script = T_refl.name ++ ".prototype.__proto__ = Error.prototype"; _ = self.exec(script, "errorSubclass") catch { + // TODO: is there a reason to override the error? return error.errorSubClass; }; } @@ -290,7 +291,7 @@ pub const Env = struct { } // compile and run a JS script - // This is a non-blocking operation, it will returned before any callback is executed + // It doesn't wait for callbacks execution pub fn exec( self: Env, script: []const u8, @@ -299,11 +300,7 @@ pub const Env = struct { if (self.js_ctx == null) { return error.EnvNotStarted; } - - const res = jsExec(script, name, self.isolate, self.js_ctx.?) catch { - return error.JSExec; - }; - return res; + return try jsExec(script, name, self.isolate, self.js_ctx.?); } // wait I/O loop until all JS callbacks are executed @@ -475,9 +472,9 @@ pub fn jsExec(script: []const u8, name: ?[]const u8, isolate: v8.Isolate, js_ctx origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue()); } const scr_js = v8.String.initUtf8(isolate, script); - const scr = try v8.Script.compile(js_ctx, scr_js, origin); + const scr = v8.Script.compile(js_ctx, scr_js, origin) catch return error.JSCompile; // run - const value = try scr.run(js_ctx); + const value = scr.run(js_ctx) catch return error.JSExec; return .{ .value = value }; } From 7b3b403d444da62f260c266025fc7a0f8e002ffd Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Wed, 26 Jun 2024 12:27:55 +0200 Subject: [PATCH 3/9] Remove ignore_errors on Env.wait method Signed-off-by: Francis Bouvier --- src/engines/v8/v8.zig | 33 +++++++++++++-------------------- src/interfaces.zig | 6 +----- src/tests/test_utils.zig | 2 +- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index f802a37..a0b46d5 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -303,40 +303,33 @@ pub const Env = struct { return try jsExec(script, name, self.isolate, self.js_ctx.?); } - // wait I/O loop until all JS callbacks are executed - pub fn wait( - self: Env, - ignore_errors: bool, - ) anyerror!void { + // wait I/O Loop until all JS callbacks are executed + // This is a blocking operation. + // Errors can be either: + // - an error of the Loop (eg. IO kernel) + // - an error of one of the JS callbacks + // NOTE: the Loop does not stop when a JS callback throw an error + // ie. all JS callbacks are executed + // TODO: return at first error on a JS callback and let the caller + // decide whether going forward or not + pub fn wait(self: Env) anyerror!void { if (self.js_ctx == null) { return error.EnvNotStarted; } // run loop - self.nat_ctx.loop.run() catch |err| { - if (err != error.JSExecCallback) { - // IO kernel error - return err; - } else if (!ignore_errors) { - return err; - } - }; + return self.nat_ctx.loop.run(); } // compile and run a JS script and wait for all callbacks (exec + wait) // This is a blocking operation. - pub fn execWait( - self: Env, - script: []const u8, - name: ?[]const u8, - ignore_errors: bool, - ) anyerror!JSValue { + pub fn execWait(self: Env, script: []const u8, name: ?[]const u8) anyerror!JSValue { // exec script const res = try self.exec(script, name); // wait - try self.wait(ignore_errors); + try self.wait(); return res; } diff --git a/src/interfaces.zig b/src/interfaces.zig index 13a81c9..f8361e6 100644 --- a/src/interfaces.zig +++ b/src/interfaces.zig @@ -102,17 +102,13 @@ pub fn Env( ) anyerror!JSValue_T); // wait() all JS callbacks - assertDecl(T, "wait", fn ( - self: T, - ignore_errors: bool, - ) anyerror!void); + assertDecl(T, "wait", fn (self: T) anyerror!void); // execWait() executes script in JS and waits all JS callbacks assertDecl(T, "execWait", fn ( self: T, script: []const u8, name: ?[]const u8, - ignore_errors: bool, ) anyerror!JSValue_T); } diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index a98b4ce..05bd740 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -100,7 +100,7 @@ pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: const name = try std.fmt.bufPrint(buf[0..], "test_{d}.js", .{test_case}); // run script error - const res = js_env.execWait(case.src, name, false) catch |err| { + const res = js_env.execWait(case.src, name) catch |err| { // is it an intended error? const except = try try_catch.exception(alloc, js_env.*); From 094e67748a80f8b075c173ac2264cd3ad0877f72 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Wed, 26 Jun 2024 12:36:48 +0200 Subject: [PATCH 4/9] Add TODO on loop.run Signed-off-by: Francis Bouvier --- src/loop.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/loop.zig b/src/loop.zig index c510c0f..35da963 100644 --- a/src/loop.zig +++ b/src/loop.zig @@ -67,6 +67,10 @@ pub const SingleThreaded = struct { try self.io.tick(); // at each iteration we might have new events registred by previous callbacks } + // TODO: return instead immediatly on the first JS callback error + // and let the caller decide what to do next + // (typically retrieve the exception through the TryCatch and + // continue the execution of callbacks with a new call to loop.run) if (self.cbk_error) { return error.JSExecCallback; } From 5b91b2b48af73393c34891e4adb86ac3dbb65f29 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Thu, 27 Jun 2024 16:26:32 +0200 Subject: [PATCH 5/9] free string after calls to TryCatch exception and stack Signed-off-by: Francis Bouvier --- src/engines/v8/v8.zig | 2 ++ src/shell.zig | 6 ++++-- src/tests/test_utils.zig | 11 ++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index a0b46d5..26a0f10 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -438,6 +438,7 @@ pub const TryCatch = struct { return self.inner.hasCaught(); } + // the caller needs to deinit the string returned pub fn exception(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { if (self.inner.getException()) |msg| { return try valueToUtf8(alloc, msg, env.isolate, env.js_ctx.?); @@ -445,6 +446,7 @@ pub const TryCatch = struct { return null; } + // the caller needs to deinit the string returned pub fn stack(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { const stck = self.inner.getStackTrace(env.js_ctx.?); if (stck) |s| return try valueToUtf8(alloc, s, env.isolate, env.js_ctx.?); diff --git a/src/shell.zig b/src/shell.zig index 0bd118a..0dadc7a 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -150,8 +150,10 @@ fn cmdCallback( "shell.js", ) catch { const except = ctx.try_catch.exception(ctx.alloc, ctx.js_env.*) catch unreachable; - defer ctx.alloc.free(except.?); - printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{except.?}); + if (except) |msg| { + defer ctx.alloc.free(msg); + printStdout("\x1b[38;5;242mUncaught {s}\x1b[0m\n", .{msg}); + } return; }; diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index 05bd740..1b23226 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -104,7 +104,10 @@ pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: // is it an intended error? const except = try try_catch.exception(alloc, js_env.*); - if (isTypeError(case.ex, except.?)) continue; + if (except) |msg| { + defer alloc.free(msg); + if (isTypeError(case.ex, msg)) continue; + } has_error = true; if (i == 0) { @@ -116,8 +119,10 @@ pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: error.JSExecCallback => case.cbk_ex, else => return err, }; - const stack = try try_catch.stack(alloc, js_env.*); - caseError(case.src, expected, except.?, stack.?); + if (try try_catch.stack(alloc, js_env.*)) |stack| { + defer alloc.free(stack); + caseError(case.src, expected, except.?, stack); + } continue; }; From 13260c4659a224706aacc7f1a456eb1ad02d1e5f Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Thu, 27 Jun 2024 16:33:29 +0200 Subject: [PATCH 6/9] free string after JSValue.toString Signed-off-by: Francis Bouvier --- src/engines/v8/v8.zig | 1 + src/shell.zig | 1 + src/tests/test_utils.zig | 1 + 3 files changed, 3 insertions(+) diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 26a0f10..3a254e1 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -422,6 +422,7 @@ pub const JSObject = struct { pub const JSValue = struct { value: v8.Value, + // the caller needs to deinit the string returned pub fn toString(self: JSValue, alloc: std.mem.Allocator, env: Env) anyerror![]const u8 { return valueToUtf8(alloc, self.value, env.isolate, env.js_ctx.?); } diff --git a/src/shell.zig b/src/shell.zig index 0dadc7a..d9d7539 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -159,6 +159,7 @@ fn cmdCallback( // JS print result const s = res.toString(ctx.alloc, ctx.js_env.*) catch unreachable; + defer ctx.alloc.free(s); if (std.mem.eql(u8, s, "undefined")) { printStdout("<- \x1b[38;5;242m{s}\x1b[0m\n", .{s}); } else { diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index 1b23226..c8de3e4 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -128,6 +128,7 @@ pub fn checkCasesAlloc(allocator: std.mem.Allocator, js_env: *public.Env, cases: // check if result is expected const res_string = try res.toString(alloc, js_env.*); + defer alloc.free(res_string); const equal = std.mem.eql(u8, case.ex, res_string); if (!equal) { has_error = true; From ab7f6915a7f1b7cd3c7ec24b426e7c2c896b010d Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Thu, 27 Jun 2024 17:46:36 +0200 Subject: [PATCH 7/9] Add JSValue.typeOf Signed-off-by: Francis Bouvier --- src/api.zig | 11 +++++++++++ src/engines/v8/v8.zig | 12 ++++++++++++ src/interfaces.zig | 3 +++ vendor/zig-v8 | 2 +- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/api.zig b/src/api.zig index f02795c..478acdd 100644 --- a/src/api.zig +++ b/src/api.zig @@ -41,6 +41,17 @@ pub const test_utils = @import("tests/test_utils.zig"); // JS types // -------- +pub const JSTypes = enum { + object, + function, + string, + number, + boolean, + bigint, + null, + undefined, +}; + const types = @import("types.zig"); pub const i64Num = types.i64Num; pub const u64Num = types.u64Num; diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 3a254e1..7fbd21b 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -426,6 +426,18 @@ pub const JSValue = struct { pub fn toString(self: JSValue, alloc: std.mem.Allocator, env: Env) anyerror![]const u8 { return valueToUtf8(alloc, self.value, env.isolate, env.js_ctx.?); } + + pub fn typeOf(self: JSValue, env: Env) anyerror!public.JSTypes { + var buf: [20]u8 = undefined; + const str = try self.value.typeOf(env.isolate); + const len = str.lenUtf8(env.isolate); + const s = buf[0..len]; + _ = str.writeUtf8(env.isolate, s); + return std.meta.stringToEnum(public.JSTypes, s) orelse { + std.log.err("JSValueTypeNotHandled: {s}", .{s}); + return error.JSValueTypeNotHandled; + }; + } }; pub const TryCatch = struct { diff --git a/src/interfaces.zig b/src/interfaces.zig index f8361e6..73e34d9 100644 --- a/src/interfaces.zig +++ b/src/interfaces.zig @@ -116,6 +116,9 @@ pub fn JSValue(comptime T: type, env: type) void { // toString() assertDecl(T, "toString", fn (self: T, alloc: std.mem.Allocator, env: env) anyerror![]const u8); + + // typeOf() + assertDecl(T, "typeOf", fn (self: T, env: env) anyerror!public.JSTypes); } pub fn JSObjectID(comptime T: type) void { diff --git a/vendor/zig-v8 b/vendor/zig-v8 index 62f9da3..d34d83e 160000 --- a/vendor/zig-v8 +++ b/vendor/zig-v8 @@ -1 +1 @@ -Subproject commit 62f9da359d98554254022ecab07aef2ccf184338 +Subproject commit d34d83e8bee631dea9848010c825a8e164491747 From 0f6d5b8fc1091e178e8faed65f5b5d5718fa0dbb Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 8 Jul 2024 17:39:29 +0200 Subject: [PATCH 8/9] Update test utils runScript helper func Signed-off-by: Francis Bouvier --- src/tests/test_utils.zig | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index c8de3e4..eb3601c 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -151,7 +151,7 @@ pub const Case = struct { }; // a shorthand function to run a script within a JS env -// without providing a JS result +// with local TryCatch // - on success, do nothing // - on error, log error the JS result and JS stack if available pub fn runScript( @@ -161,18 +161,21 @@ pub fn runScript( name: []const u8, ) !void { - // init result - var res = public.JSResult{}; - defer res.deinit(alloc); - - try js_env.execWait(alloc, script, name, &res, null); + // local try catch + var try_catch: public.TryCatch = undefined; + try_catch.init(js_env.*); + defer try_catch.deinit(); // check result - if (!res.success) { - std.log.err("script {s} error: {s}\n", .{ name, res.result }); - if (res.stack) |stack| { - std.log.err("script {s} stack: {s}\n", .{ name, stack }); + _ = js_env.execWait(script, name) catch |err| { + if (try try_catch.exception(alloc, js_env.*)) |msg| { + defer alloc.free(msg); + std.log.err("script {s} error: {s}\n", .{ name, msg }); } - return error.Script; - } + if (try try_catch.stack(alloc, js_env.*)) |msg| { + defer alloc.free(msg); + std.log.err("script {s} stack: {s}\n", .{ name, msg }); + } + return err; + }; } From 049460a1f99dbf2e66915793003936240a59bfe0 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 8 Jul 2024 17:41:23 +0200 Subject: [PATCH 9/9] Add a TryCatch err method helper Signed-off-by: Francis Bouvier --- src/engines/v8/v8.zig | 12 ++++++++++++ src/interfaces.zig | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 7fbd21b..171d162 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -466,6 +466,18 @@ pub const TryCatch = struct { return null; } + // a shorthand method to return either the entire stack message + // or just the exception message + // - in Debug mode return the stack if available + // - otherwhise return the exception if available + // the caller needs to deinit the string returned + pub fn err(self: TryCatch, alloc: std.mem.Allocator, env: Env) anyerror!?[]const u8 { + if (builtin.mode == .Debug) { + if (try self.stack(alloc, env)) |msg| return msg; + } + return try self.exception(alloc, env); + } + pub fn deinit(self: *TryCatch) void { self.inner.deinit(); } diff --git a/src/interfaces.zig b/src/interfaces.zig index 73e34d9..1d7db3c 100644 --- a/src/interfaces.zig +++ b/src/interfaces.zig @@ -145,6 +145,13 @@ pub fn TryCatch(comptime T: type, comptime env: type) void { env: env, ) anyerror!?[]const u8); + // err() + assertDecl(T, "err", fn ( + self: T, + alloc: std.mem.Allocator, + env: env, + ) anyerror!?[]const u8); + // stack() assertDecl(T, "stack", fn ( self: T,