diff --git a/src/api.zig b/src/api.zig index 354f0c5..4ee9f45 100644 --- a/src/api.zig +++ b/src/api.zig @@ -44,6 +44,7 @@ pub const API = Engine.API; pub const TPL = Engine.TPL; pub const JSResult = Engine.JSResult; +pub const JSObject = Engine.JSObject; pub const Callback = Engine.Callback; pub const CallbackSync = Engine.CallbackSync; pub const CallbackArg = Engine.CallbackArg; diff --git a/src/engines/v8/generate.zig b/src/engines/v8/generate.zig index 26d38a4..7f21fda 100644 --- a/src/engines/v8/generate.zig +++ b/src/engines/v8/generate.zig @@ -10,6 +10,7 @@ const utils = internal.utils; const public = @import("../../api.zig"); const Loop = public.Loop; +const JSObject = public.JSObject; const cbk = @import("callback.zig"); const nativeToJS = @import("types_primitives.zig").nativeToJS; @@ -167,7 +168,6 @@ fn getArg( isolate: v8.Isolate, ctx: v8.Context, ) arg.T { - _ = this; var value: arg.T = undefined; if (arg.isNative()) { @@ -192,6 +192,7 @@ fn getArg( std.mem.Allocator => alloc, *Loop => utils.loop, cbk.Func, cbk.FuncSync, cbk.Arg => unreachable, + JSObject => JSObject{ .ctx = ctx, .js_obj = this }, else => jsToNative( alloc, arg.T, @@ -395,7 +396,7 @@ pub fn setNativeObject( obj: anytype, js_obj: v8.Object, isolate: v8.Isolate, -) !void { +) !*T { // assign and bind native obj to JS obj var obj_ptr: *T = undefined; @@ -420,7 +421,7 @@ pub fn setNativeObject( // if the object is an empty struct (ie. a kind of container) // no need to keep it's reference if (T_refl.isEmpty()) { - return; + return obj_ptr; } // bind the native object pointer to JS obj @@ -436,13 +437,16 @@ pub fn setNativeObject( try refs.addObject(alloc, int_ptr.*, T_refl.index); } js_obj.setInternalField(0, ext); + return obj_ptr; } fn setReturnType( alloc: std.mem.Allocator, comptime all_T: []refl.Struct, comptime ret: refl.Type, + comptime func: refl.Func, res: anytype, + js_res: v8.ReturnValue, ctx: v8.Context, isolate: v8.Isolate, ) !v8.Value { @@ -454,7 +458,7 @@ fn setReturnType( // if null just return JS null return isolate.initNull().toValue(); } - return setReturnType(alloc, all_T, ret, res.?, ctx, isolate); + return setReturnType(alloc, all_T, ret, func, res.?, js_res, ctx, isolate); } // Union type @@ -468,7 +472,9 @@ fn setReturnType( alloc, all_T, tt, + func, @field(res, tt.name.?), + js_res, ctx, isolate, ); @@ -490,7 +496,9 @@ fn setReturnType( alloc, all_T, field, + func, @field(res, name), + js_res, ctx, isolate, ); @@ -509,14 +517,32 @@ fn setReturnType( // instantiate a JS object from template // and bind it to the native object const js_obj = gen.getTpl(index).tpl.getInstanceTemplate().initInstance(ctx); - _ = try setNativeObject( + const obj_ptr = setNativeObject( alloc, all_T[index], ret.underT(), res, js_obj, isolate, - ); + ) catch unreachable; + + // call postAttach func + const T_refl = all_T[index]; + if (comptime try refl.postAttachFunc(T_refl.T)) |piArgsT| { + postAttach( + utils.allocator, + T_refl, + all_T, + func, + piArgsT, + obj_ptr, + js_obj, + js_res, + ctx, + isolate, + ); + } + return js_obj.toValue(); } @@ -530,6 +556,44 @@ fn setReturnType( return js_val; } +fn postAttach( + alloc: std.mem.Allocator, + comptime T_refl: refl.Struct, + comptime all_T: []refl.Struct, + comptime func: refl.Func, + comptime argsT: type, + obj_ptr: anytype, + js_obj: v8.Object, + js_res: v8.ReturnValue, + ctx: v8.Context, + isolate: v8.Isolate, +) void { + var args: argsT = undefined; + @field(args, "0") = obj_ptr; + @field(args, "1") = JSObject{ .ctx = ctx, .js_obj = js_obj }; + const f = @field(T_refl.T, "postAttach"); + const ret = comptime try refl.funcReturnType(@TypeOf(f)); + if (comptime refl.isErrorUnion(ret)) { + _ = @call(.auto, f, args) catch |err| { + return throwError( + alloc, + T_refl, + all_T, + func, + err, + js_res, + isolate, + ); + }; + } else { + _ = @call( + .auto, + f, + args, + ); + } +} + fn getNativeObject( comptime T_refl: refl.Struct, comptime all_T: []refl.Struct, @@ -639,7 +703,7 @@ fn callFunc( if (comptime func_kind == .constructor) { // bind native object to JS object this - setNativeObject( + _ = setNativeObject( utils.allocator, T_refl, func.return_type.underT(), @@ -647,6 +711,22 @@ fn callFunc( cbk_info.getThis(), isolate, ) catch unreachable; // TODO: internal errors + + // call postAttach func + if (comptime try refl.postAttachFunc(T_refl.T)) |piArgsT| { + postAttach( + utils.allocator, + T_refl, + all_T, + func, + piArgsT, + &res, + cbk_info.getThis(), + cbk_info.getReturnValue(), + ctx, + isolate, + ); + } } else { // return to javascript the result @@ -654,7 +734,9 @@ fn callFunc( utils.allocator, all_T, func.return_type, + func, res, + cbk_info.getReturnValue(), ctx, isolate, ) catch unreachable; // TODO: internal errors diff --git a/src/engines/v8/v8.zig b/src/engines/v8/v8.zig index 2234bbb..5b0d6e3 100644 --- a/src/engines/v8/v8.zig +++ b/src/engines/v8/v8.zig @@ -18,6 +18,7 @@ pub const CallbackArg = @import("callback.zig").Arg; pub const LoadFnType = @import("generate.zig").LoadFnType; pub const loadFn = @import("generate.zig").loadFn; const setNativeObject = @import("generate.zig").setNativeObject; +const nativeToJS = @import("types_primitives.zig").nativeToJS; const valueToUtf8 = @import("types_primitives.zig").valueToUtf8; pub const API = struct { @@ -387,6 +388,20 @@ fn createJSObject( ); } +pub const JSObject = struct { + ctx: v8.Context, + js_obj: v8.Object, + + pub fn set(self: JSObject, key: []const u8, value: anytype) !void { + const isolate = self.ctx.getIsolate(); + const js_value = try nativeToJS(@TypeOf(value), value, isolate); + const js_key = v8.String.initUtf8(isolate, key); + if (!self.js_obj.setValue(self.ctx, js_key, js_value)) { + return error.SetV8Object; + } + } +}; + pub const TryCatch = struct { try_catch: *v8.TryCatch, diff --git a/src/reflect.zig b/src/reflect.zig index 6c63c97..0fb55c6 100644 --- a/src/reflect.zig +++ b/src/reflect.zig @@ -9,6 +9,8 @@ const Callback = public.Callback; const CallbackSync = public.CallbackSync; const CallbackArg = public.CallbackArg; +const JSObject = public.JSObject; + const i64Num = public.i64Num; const u64Num = public.u64Num; @@ -36,6 +38,7 @@ const builtin_types = [_]type{ const internal_types = [_]type{ std.mem.Allocator, Loop, + JSObject, Callback, CallbackSync, CallbackArg, @@ -520,6 +523,14 @@ pub const Func = struct { index_offset += 1; } + // JSObject + if (args_types[i].T == JSObject) { + // JSObject arg is not allowed in a constructor function + // as the corresponding JS object has not been yet created + if (kind == .constructor) return error.FuncCstrWithJSObject; + index_offset += 1; + } + // callback // ensure function has only 1 callback as argument // TODO: is this necessary? @@ -1213,6 +1224,15 @@ pub const Struct = struct { } } + // postAttach + if (@hasDecl(T, "postAttach")) { + _ = postAttachFunc(T) catch { + const msg = "function 'postAttach' not well formed"; + fmtErr(msg.len, msg, T); + return error.FuncPostAttach; + }; + } + // check deinit // only if at least one function has an allocator argument var check_deinit = false; @@ -1407,6 +1427,179 @@ pub fn do(comptime types: anytype) Error![]Struct { } } +// New style reflect +// ----------------- + +// EqlOptions to handle how check equality is done +// if ptr, check is also done with *T +// if err, T can be wrapped in an ErrorUnion +// if opt, T can be wrapped in an Optional +// by default all those options are not allowed +const EqlOptions = struct { + ptr: bool = false, + err: bool = false, + opt: bool = false, +}; + +// assert T is equal to X +// see EqlOptions for behavior details +fn assertT(comptime T: type, comptime X: type, comptime opts: EqlOptions) !void { + if (T == X) return; + if (opts.ptr and T == *X) return; + const err = error.AssertT; + const info = @typeInfo(X); + switch (info) { + .ErrorUnion => { + if (opts.err) return try assertT(T, info.ErrorUnion.payload, opts); + return err; + }, + .Optional => { + if (opts.opt) return try assertT(T, info.Optional.child, opts); + return err; + }, + else => return err, + } +} + +// assert T is a supported container type +// currently only Struct and Union +fn assertApi(comptime T: type) !void { + const info = @typeInfo(T); + return switch (info) { + .Struct, .Union => {}, + else => error.AssertAPI, + }; +} + +// assert func is a function +fn assertFunc(comptime func: type) !void { + if (@typeInfo(func) != .Fn) return error.AssertFunc; +} + +// assert func is a method of T +// if not strict, T and *T are allowed +fn assertFuncIsMethod(comptime T: type, comptime func: type, comptime strict: bool) !void { + try assertFunc(func); + const err = error.AssertFuncIsMethod; + const info = @typeInfo(func).Fn; + if (info.params.len == 0) return err; + + const first = info.params[0].type.?; + if (first == T) return; + // only non strict assertion allows *T + if (!strict and first == *T) return; + return err; +} + +// assert func has the correct number of parameters +fn assertFuncParamsNb(comptime func: type, comptime nb: u8) !void { + try assertFunc(func); + const info = @typeInfo(func).Fn; + if (info.params.len != nb) return error.AssertFuncParamsNb; +} + +// assert function parameter at index is of type T +fn assertFuncParamIsT(comptime func: type, comptime T: type, comptime index: u8) !void { + try assertFunc(func); + const err = error.AssertFuncHasParam; + const info = @typeInfo(func).Fn; + + if (info.params.len < index + 1) return err; + if (info.params[index].type.? != T) { + return err; + } +} + +// assert function has at least 1 parameter of type T +fn assertFuncHasParamT(comptime func: type, comptime T: type) !void { + try assertFunc(func); + const info = @typeInfo(func).Fn; + + for (info.params) |param| { + if (param.type.? == T) { + return; + } + } + return error.AssertFuncHasParam; +} + +// assert func returns T +// see EqlOptions for behavior details +fn assertFuncReturnT(comptime func: type, comptime T: type, comptime opts: EqlOptions) !void { + try assertFunc(func); + const ret = @typeInfo(func).Fn.return_type.?; + assertT(T, ret, opts) catch return error.AssertFuncReturnT; +} + +// createTupleT generate a tuple type +// with the members passed as fields +fn createTupleT(comptime members: []type) type { + var fields: [members.len]std.builtin.Type.StructField = undefined; + for (members, 0..) |member, i| { + fields[i] = std.builtin.Type.StructField{ + .name = try itoa(i), + .type = member, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(member), + }; + } + const s = std.builtin.Type.Struct{ + .layout = std.builtin.Type.ContainerLayout.Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = true, + }; + const t = std.builtin.Type{ .Struct = s }; + return @Type(t); +} + +// argsT generate from the func parameters +// a tuple type suitable for the @call builtin func +fn argsT(comptime func: type) type { + try assertFunc(func); + const info = @typeInfo(func).Fn; + var params: [info.params.len]type = undefined; + for (info.params, 0..) |param, i| { + params[i] = param.type.?; + } + return createTupleT(¶ms); +} + +// public functions + +// funcReturnType retrieve the return type of a func +pub fn funcReturnType(comptime func: type) !type { + std.debug.assert(@inComptime()); + try assertFunc(func); + const info = @typeInfo(func).Fn; + return info.return_type.?; +} + +// isErrorUnion check if a type is an ErrorUnion +pub fn isErrorUnion(comptime T: type) bool { + std.debug.assert(@inComptime()); + const info = @typeInfo(T); + return info == .ErrorUnion; +} + +// postAttachFunc check if T has `postAttach` function +// and returns the arguments tuple type expected as parameters +pub fn postAttachFunc(comptime T: type) !?type { + std.debug.assert(@inComptime()); + try assertApi(T); + + const name = "postAttach"; + if (!@hasDecl(T, name)) return null; + + const func = @TypeOf(@field(T, name)); + try assertFuncIsMethod(*T, func, true); + try assertFuncParamsNb(func, 2); + try assertFuncParamIsT(func, JSObject, 1); + try assertFuncReturnT(func, void, .{ .err = true }); + return argsT(func); +} + // Utils funcs // ----------- @@ -1524,6 +1717,8 @@ const Error = error{ FuncVariadicNotLastOne, FuncReturnTypeVariadic, FuncErrorUnionArg, + FuncCstrWithJSObject, + FuncPostAttach, // type errors TypeTaggedUnion, @@ -1699,6 +1894,15 @@ const TestFuncReturnTypeVariadic = struct { const TestFuncErrorUnionArg = struct { pub fn _example(_: TestFuncErrorUnionArg, _: anyerror!void) void {} }; +const TestFuncCstrWithJSObject = struct { + pub fn constructor(_: JSObject) TestFuncCstrWithJSObject { + return .{}; + } +}; +const TestFuncPostAttach = struct { + // missing JSObject arg + pub fn postAttach(_: *TestFuncPostAttach) void {} +}; // types tests const TestTaggedUnion = union { @@ -1845,6 +2049,14 @@ pub fn tests() !void { .{TestFuncErrorUnionArg}, error.FuncErrorUnionArg, ); + try ensureErr( + .{TestFuncCstrWithJSObject}, + error.FuncCstrWithJSObject, + ); + try ensureErr( + .{TestFuncPostAttach}, + error.FuncPostAttach, + ); // types checks try ensureErr( diff --git a/src/run_tests.zig b/src/run_tests.zig index 39a8884..d3c7db9 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -15,6 +15,7 @@ const primitive_types = @import("tests/types_primitives_test.zig"); const native_types = @import("tests/types_native_test.zig"); const complex_types = @import("tests/types_complex_test.zig"); const multiple_types = @import("tests/types_multiple_test.zig"); +const object_types = @import("tests/types_object.zig"); const callback = @import("tests/cbk_test.zig"); test { @@ -31,8 +32,9 @@ test { const do_nat = true; // TODO: if enable alone we have "exceeded 1000 backwards branches" error const do_complex = true; const do_multi = true; + const do_obj = true; const do_cbk = true; - if (!do_proto and !do_prim and !do_nat and !do_complex and !do_multi and !do_cbk) { + if (!do_proto and !do_prim and !do_nat and !do_complex and !do_multi and !do_obj and !do_cbk) { std.debug.print("\nWARNING: No end to end tests.\n", .{}); return; } @@ -96,6 +98,17 @@ test { _ = try eng.loadEnv(&multi_arena, multiple_types.exec, multi_apis); } + // object types tests + var obj_alloc: bench.Allocator = undefined; + if (do_obj) { + tests_nb += 1; + const obj_apis = comptime object_types.generate(); // stage1: we need to comptime + obj_alloc = bench.allocator(std.testing.allocator); + var obj_arena = std.heap.ArenaAllocator.init(obj_alloc.allocator()); + defer obj_arena.deinit(); + _ = try eng.loadEnv(&obj_arena, object_types.exec, obj_apis); + } + // callback tests var cbk_alloc: bench.Allocator = undefined; if (do_cbk) { @@ -170,6 +183,15 @@ test { try t.addRow(.{ "Multiples", multi_alloc.alloc_nb, multi_alloc_size }); } + if (do_obj) { + const obj_alloc_stats = obj_alloc.stats(); + const obj_alloc_size = pretty.Measure{ + .unit = "b", + .value = obj_alloc_stats.alloc_size, + }; + try t.addRow(.{ "Objects", obj_alloc.alloc_nb, obj_alloc_size }); + } + if (do_cbk) { const cbk_alloc_stats = cbk_alloc.stats(); const cbk_alloc_size = pretty.Measure{ diff --git a/src/tests/proto_test.zig b/src/tests/proto_test.zig index fda84f8..4588839 100644 --- a/src/tests/proto_test.zig +++ b/src/tests/proto_test.zig @@ -254,11 +254,9 @@ pub fn exec( try js_env.start(alloc, apis); defer js_env.stop(); - const ownBase = switch (public.Env.engine()) { - .v8 => 5, - }; - const ownBaseLen = intToStr(alloc, ownBase); - defer alloc.free(ownBaseLen); + const ownBase = tests.engineOwnPropertiesDefault(); + const ownBaseStr = tests.intToStr(alloc, ownBase); + defer alloc.free(ownBaseStr); // global try js_env.attachObject(try js_env.getGlobal(), "self", null); @@ -315,15 +313,15 @@ pub fn exec( try tests.checkCases(js_env, &cases4); // static attr - const ownPersonLen = intToStr(alloc, ownBase + 2); - defer alloc.free(ownPersonLen); + const ownPersonStr = intToStr(alloc, ownBase + 2); + defer alloc.free(ownPersonStr); var cases_static = [_]tests.Case{ // basic static case .{ .src = "Person.AGE_MIN === 18", .ex = "true" }, .{ .src = "Person.NATIONALITY === 'French'", .ex = "true" }, // static attributes are own properties .{ .src = "let ownPerson = Object.getOwnPropertyNames(Person)", .ex = "undefined" }, - .{ .src = "ownPerson.length", .ex = ownPersonLen }, + .{ .src = "ownPerson.length", .ex = ownPersonStr }, // static attributes are also available on instances .{ .src = "p.AGE_MIN === 18", .ex = "true" }, .{ .src = "p.NATIONALITY === 'French'", .ex = "true" }, @@ -341,7 +339,7 @@ pub fn exec( .{ .src = "User.NATIONALITY === 'French'", .ex = "true" }, // static attributes inherited are NOT own properties .{ .src = "let ownUser = Object.getOwnPropertyNames(User)", .ex = "undefined" }, - .{ .src = "ownUser.length", .ex = ownBaseLen }, + .{ .src = "ownUser.length", .ex = ownBaseStr }, }; try tests.checkCases(js_env, &cases_proto_constructor); diff --git a/src/tests/test_utils.zig b/src/tests/test_utils.zig index bf2af01..ecf3dfc 100644 --- a/src/tests/test_utils.zig +++ b/src/tests/test_utils.zig @@ -24,6 +24,24 @@ pub fn sleep(nanoseconds: u64) void { std.os.nanosleep(s, ns); } +// result memory is owned by the caller +pub fn intToStr(alloc: std.mem.Allocator, nb: u8) []const u8 { + return std.fmt.allocPrint( + alloc, + "{d}", + .{nb}, + ) catch unreachable; +} + +// engineOwnPropertiesDefault returns the number of own properties +// by default for a current Type +// result memory is owned by the caller +pub fn engineOwnPropertiesDefault() u8 { + return switch (public.Env.engine()) { + .v8 => 5, + }; +} + var test_case: usize = 0; fn caseError(src: []const u8, exp: []const u8, res: []const u8, stack: ?[]const u8) void { diff --git a/src/tests/types_object.zig b/src/tests/types_object.zig new file mode 100644 index 0000000..9ed3dbc --- /dev/null +++ b/src/tests/types_object.zig @@ -0,0 +1,84 @@ +const std = @import("std"); + +const public = @import("../api.zig"); +const tests = public.test_utils; + +pub const MyObject = struct { + val: bool, + + pub fn constructor(do_set: bool) MyObject { + return .{ .val = do_set }; + } + + pub fn postAttach(self: *MyObject, js_obj: public.JSObject) !void { + if (self.val) try js_obj.set("a", @as(u8, 1)); + } + + pub fn get_val(self: MyObject) bool { + return self.val; + } + + pub fn set_val(self: *MyObject, val: bool) void { + self.val = val; + } +}; + +pub const MyAPI = struct { + pub fn constructor() MyAPI { + return .{}; + } + + pub fn _obj(_: MyAPI, _: public.JSObject) !MyObject { + return MyObject.constructor(true); + } +}; + +// generate API, comptime +pub fn generate() []public.API { + return public.compile(.{ + MyObject, + MyAPI, + }); +} + +// exec tests +pub fn exec( + alloc: std.mem.Allocator, + js_env: *public.Env, + comptime apis: []public.API, +) !void { + + // start JS env + try js_env.start(alloc, apis); + defer js_env.stop(); + + const ownBase = tests.engineOwnPropertiesDefault(); + const ownBaseStr = tests.intToStr(alloc, ownBase); + defer alloc.free(ownBaseStr); + + var direct = [_]tests.Case{ + .{ .src = "Object.getOwnPropertyNames(MyObject).length;", .ex = ownBaseStr }, + .{ .src = "let myObj = new MyObject(true);", .ex = "undefined" }, + // check object property + .{ .src = "myObj.a", .ex = "1" }, + .{ .src = "Object.getOwnPropertyNames(myObj).length;", .ex = "1" }, + // check if setter (pointer) still works + .{ .src = "myObj.val", .ex = "true" }, + .{ .src = "myObj.val = false", .ex = "false" }, + .{ .src = "myObj.val", .ex = "false" }, + // check other object, same type, has no property + .{ .src = "let myObj2 = new MyObject(false);", .ex = "undefined" }, + .{ .src = "myObj2.a", .ex = "undefined" }, + .{ .src = "Object.getOwnPropertyNames(myObj2).length;", .ex = "0" }, + }; + try tests.checkCases(js_env, &direct); + + var indirect = [_]tests.Case{ + .{ .src = "let myAPI = new MyAPI();", .ex = "undefined" }, + .{ .src = "let myObjIndirect = myAPI.obj();", .ex = "undefined" }, + // check object property + .{ .src = "myObjIndirect.a", .ex = "1" }, + .{ .src = "Object.getOwnPropertyNames(myObjIndirect).length;", .ex = "1" }, + }; + try tests.checkCases(js_env, &indirect); +}