Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof mod.FactoryResource> | 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
Expand Down
40 changes: 31 additions & 9 deletions src/js/class_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/js/wrap_class.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading