diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 505bd95..128d389 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -407,6 +407,51 @@ describe("class materialization", () => { it("rejects cross-class static factory binding during materialization", () => { expect(() => mod.Point.create.call(mod.Buffer, 1, 2)).toThrow(); }); + + it("preserves normal nested construction inside subclass constructors", () => { + // Same-class materialization may call a JS subclass constructor via the preferred + // receiver constructor. A nested normal `new` inside that constructor must not + // inherit the internal materialization marker, otherwise it skips native wrapping. + let nested: InstanceType | undefined; + class DerivedFactoryResource extends mod.FactoryResource { + constructor() { + super(); + nested = new mod.FactoryResource(); + } + } + + const resource = DerivedFactoryResource.withByte(5); + + expect(resource).toBeInstanceOf(DerivedFactoryResource); + expect(resource.getByte()).toEqual(5); + expect(nested?.getByte()).toEqual(0); + }); + + it("does not run cross-class constructors during failed materialization", () => { + const initBefore = mod.getFactoryResourceInitCount(); + const deinitBefore = mod.getFactoryResourceDeinitCount(); + + expect(() => mod.Point.create.call(mod.FactoryResource, 1, 2)).toThrow(); + + expect(mod.getFactoryResourceInitCount()).toEqual(initBefore); + expect(mod.getFactoryResourceDeinitCount()).toEqual(deinitBefore); + }); + + it("rejects non-zapi constructors during materialization", () => { + function FakePoint() {} + + expect(() => mod.Point.create.call(FakePoint, 1, 2)).toThrow(); + }); + + it("deinitializes returned native resources when materialization fails", () => { + const initBefore = mod.getFactoryResourceInitCount(); + const deinitBefore = mod.getFactoryResourceDeinitCount(); + + expect(() => mod.FactoryResource.withByte.call(mod.Point, 7)).toThrow(); + + expect(mod.getFactoryResourceInitCount()).toEqual(initBefore + 1); + expect(mod.getFactoryResourceDeinitCount()).toEqual(deinitBefore + 1); + }); }); // Section 15: Getters and Setters diff --git a/src/js/class_runtime.zig b/src/js/class_runtime.zig index 7af0b03..7e1373f 100644 --- a/src/js/class_runtime.zig +++ b/src/js/class_runtime.zig @@ -85,26 +85,48 @@ pub fn registerClass(comptime T: type, env: napi.Env, ctor: napi.Value) !void { try env.addEnvCleanupHook(State.Entry, entry, State.cleanupHook); } +/// Per-thread marker set by `materializeClassInstance` to tell the generated +/// constructor "this `new` call comes from the DSL — don't allocate a placeholder, +/// I'll wrap with the real object after `napi_new_instance` returns." +/// Compared by identity against `internalCtorMarkerPtr(T)`. +threadlocal var materialize_target: ?*const anyopaque = null; + +pub fn isMaterializing(comptime T: type) bool { + return materialize_target == @as(?*const anyopaque, @ptrCast(internalCtorMarkerPtr(T))); +} + +pub fn hasPendingMaterialization() bool { + return materialize_target != null; +} + +pub fn consumeMaterialization(comptime T: type) bool { + if (!isMaterializing(T)) return false; + materialize_target = null; + return true; +} + pub fn materializeClassInstance(comptime T: type, env: napi.Env, instance: T, preferred_ctor: ?napi.Value) !napi.Value { const ctor = preferred_ctor orelse try getConstructor(T, env); - const internal_arg = try env.createExternal(@ptrCast(internalCtorMarkerPtr(T)), null, null); - var raw_args = [_]napi.c.napi_value{internal_arg.value}; + + const obj_ptr = try std.heap.c_allocator.create(T); + errdefer destroyNativeObject(T, obj_ptr); + obj_ptr.* = instance; + + const prev = materialize_target; + materialize_target = @ptrCast(internalCtorMarkerPtr(T)); + defer materialize_target = prev; var js_instance_raw: napi.c.napi_value = null; try napi.status.check(napi.c.napi_new_instance( env.env, ctor.value, - 1, - &raw_args, + 0, + null, &js_instance_raw, )); const js_instance = napi.Value{ .env = env.env, .value = js_instance_raw }; - const placeholder = try env.removeWrapChecked(T, js_instance, typeTag(T)); - destroyInternalPlaceholder(T, placeholder); - - const obj_ptr = try std.heap.c_allocator.create(T); - obj_ptr.* = instance; + if (materialize_target != null) return error.InvalidMaterializationConstructor; try wrapTaggedObject(T, env, js_instance, obj_ptr, null); return js_instance; diff --git a/src/js/wrap_class.zig b/src/js/wrap_class.zig index 7b10cef..b694f84 100644 --- a/src/js/wrap_class.zig +++ b/src/js/wrap_class.zig @@ -366,6 +366,17 @@ pub fn wrapClass(comptime T: type) type { return null; }; + // Fast path: materializeClassInstance is creating this instance. + // Skip the placeholder alloc/wrap — materialize will wrap directly + // with the real native pointer after napi_new_instance returns. + if (class_runtime.consumeMaterialization(T)) { + return this_arg; + } + if (class_runtime.hasPendingMaterialization()) { + e.throwTypeError("", "Invalid materialization constructor") catch {}; + return null; + } + if (actual_argc == 1) { const internal_arg = napi.Value{ .env = raw_env, .value = raw_args[0] }; if ((internal_arg.typeof() catch null) == .external) {