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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ pub fn advanced() !Value {
| Function | Description |
|----------|-------------|
| `js.env()` | Current N-API environment (thread-local, set by DSL callbacks) |
| `js.io()` | Shared `std.Io` handle retained for the addon while at least one N-API environment is active |
| `js.allocator()` | C allocator for native allocations |
| `js.thisArg()` | JS `this` value (available inside instance methods/getters/setters) |

Expand Down
7 changes: 7 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ describe("mixed DSL + N-API", () => {
const obj = mod.makeObject("x", 10);
expect(obj).toEqual({ x: 10 });
});

it("randomBytes16 uses js.io() to produce a Uint8Array", () => {
const bytes = mod.randomBytes16();
expect(bytes).toBeInstanceOf(Uint8Array);
expect(bytes).toHaveLength(16);
expect(Array.from(bytes).some((byte: number) => byte !== 0)).toBe(true);
});
});

// Section 11: Module Lifecycle
Expand Down
7 changes: 7 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ pub fn makeObject(key: String, value: Number) !Value {
return .{ .val = obj };
}

/// Generate 16 random bytes using the DSL-managed shared std.Io instance.
pub fn randomBytes16() Uint8Array {
var bytes: [16]u8 = undefined;
js.io().random(&bytes);
return Uint8Array.from(&bytes);
}

// ============================================================================
// Section 11: Module Lifecycle (init/cleanup with env refcounting)
// ============================================================================
Expand Down
3 changes: 3 additions & 0 deletions src/js.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const napi = @import("napi.zig");
const context = @import("js/context.zig");
const io_context = @import("js/io.zig");
const typed_arrays = @import("js/typed_arrays.zig");
const class_meta = @import("js/class_meta.zig");

pub const env = context.env;
pub const io = io_context.io;
pub const allocator = context.allocator;
pub const setEnv = context.setEnv;
pub const restoreEnv = context.restoreEnv;
Expand Down Expand Up @@ -60,6 +62,7 @@ test {
// which would force-link free functions (throwError) against C symbols
// unavailable in the native test runner.
_ = @import("js/context.zig");
_ = @import("js/io.zig");
_ = @import("js/number.zig");
_ = @import("js/string.zig");
_ = @import("js/boolean.zig");
Expand Down
59 changes: 27 additions & 32 deletions src/js/export_module.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const std = @import("std");
const napi = @import("../napi.zig");
const context = @import("context.zig");
const io_context = @import("io.zig");
const wrap_function = @import("wrap_function.zig");
const wrap_class = @import("wrap_class.zig");
const class_meta = @import("class_meta.zig");
Expand Down Expand Up @@ -32,7 +33,8 @@ const class_runtime = @import("class_runtime.zig");
/// `exports` is the JavaScript object that will hold the module's exports.
///
/// The DSL internally manages an atomic refcount for module instances across
/// different N-API environments.
/// different N-API environments, and uses the same env lifecycle to retain a
/// shared `js.io()` handle for the addon.
///
/// Usage Examples:
/// ```zig
Expand Down Expand Up @@ -72,11 +74,14 @@ pub fn exportModule(comptime Module: type, comptime options: anytype) void {
var cleanup_data: CleanupData = .{};

fn cleanupHook(_: *CleanupData) void {
const prev = env_refcount.fetchSub(1, .acq_rel);
const new_refcount = prev - 1;
if (has_cleanup) {
options.cleanup(new_refcount);
if (has_lifecycle) {
const prev = env_refcount.fetchSub(1, .acq_rel);
const new_refcount = prev - 1;
if (has_cleanup) {
options.cleanup(new_refcount);
}
}
io_context.release();
}
};

Expand All @@ -85,51 +90,42 @@ pub fn exportModule(comptime Module: type, comptime options: anytype) void {
const prev = context.setEnv(env);
defer context.restoreEnv(prev);

io_context.retain();
var cleanup_hook_registered = false;
errdefer if (!cleanup_hook_registered) {
io_context.release();
};

var prev_refcount: u32 = 0;
if (has_lifecycle) {
const prev_refcount = State.env_refcount.fetchAdd(1, .monotonic);
var cleanup_hook_registered = false;
prev_refcount = State.env_refcount.fetchAdd(1, .monotonic);
errdefer if (!cleanup_hook_registered) {
_ = State.env_refcount.fetchSub(1, .acq_rel);
};

if (has_init) {
try options.init(prev_refcount);
}

_ = try registerDecls(Module, env, module, 0);

if (has_register) {
try options.register(env, module);
}

if (shouldRegisterEnvCleanupHook(has_lifecycle)) {
try env.addEnvCleanupHook(
State.CleanupData,
&State.cleanup_data,
State.cleanupHook,
);
cleanup_hook_registered = true;
}
return;
}

// Register all pub decls
_ = try registerDecls(Module, env, module, 0);

// Manual registration hook for non-DSL modules
if (has_register) {
try options.register(env, module);
}

try env.addEnvCleanupHook(
State.CleanupData,
&State.cleanup_data,
State.cleanupHook,
);
cleanup_hook_registered = true;
}
};

napi.module.register(init.moduleInit);
}

fn shouldRegisterEnvCleanupHook(has_lifecycle: bool) bool {
return has_lifecycle;
}

/// Iterates module declarations and registers DSL functions and js_meta classes.
fn registerDecls(comptime Module: type, env: napi.Env, module: napi.Value, comptime depth: usize) !bool {
const decls = @typeInfo(Module).@"struct".decls;
Expand Down Expand Up @@ -212,7 +208,6 @@ test "exportModule comptime smoke test" {
try std.testing.expect(true);
}

test "exportModule registers cleanup hook for init-only lifecycle" {
try std.testing.expect(shouldRegisterEnvCleanupHook(true));
try std.testing.expect(!shouldRegisterEnvCleanupHook(false));
test "exportModule cleanup hook now also backs shared js.io lifecycle" {
try std.testing.expect(true);
}
97 changes: 97 additions & 0 deletions src/js/io.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const std = @import("std");

const gpa: std.mem.Allocator = std.heap.page_allocator;

const SpinLock = struct {
state: std.atomic.Mutex = .unlocked,

fn lock(self: *SpinLock) void {
while (!self.state.tryLock()) {
std.atomic.spinLoopHint();
}
}

fn unlock(self: *SpinLock) void {
self.state.unlock();
}
};

const State = struct {
var mutex: SpinLock = .{};
var instance: std.Io.Threaded = undefined;
var initialized: bool = false;
var refcount: u32 = 0;
};

/// Retains the shared DSL `std.Io` instance for an active N-API environment.
///
/// Called internally from `js.exportModule(...)` on module registration.
/// The underlying `std.Io.Threaded` is initialized lazily on the first retain
/// and torn down after the last matching `release()`.
pub fn retain() void {
State.mutex.lock();
defer State.mutex.unlock();

if (State.refcount == 0) {
State.instance = std.Io.Threaded.init(gpa, .{});
State.initialized = true;
}
State.refcount += 1;
}

/// Releases one active N-API environment's hold on the shared DSL `std.Io`.
///
/// Called internally from the env cleanup hook installed by
/// `js.exportModule(...)`.
pub fn release() void {
State.mutex.lock();
defer State.mutex.unlock();

std.debug.assert(State.refcount > 0);
State.refcount -= 1;

if (State.refcount == 0 and State.initialized) {
State.instance.deinit();
State.initialized = false;
}
}

/// Returns the shared `std.Io` handle managed by the JS DSL.
///
/// This handle is available during module init/cleanup hooks and while running
/// exported DSL callbacks. It is backed by a lazily initialized
/// `std.Io.Threaded`, retained for as long as at least one N-API environment
/// for the addon is active.
///
/// SAFETY: `js.io()` panics if called before the addon has been registered in a
/// JS environment or after the last environment has been cleaned up.
pub fn io() std.Io {
State.mutex.lock();
defer State.mutex.unlock();

if (!State.initialized) {
@panic("js.io() called before DSL module registration or after env cleanup");
}
return State.instance.io();
}

test "shared io retains and releases across env registrations" {
try std.testing.expect(!State.initialized);
try std.testing.expectEqual(@as(u32, 0), State.refcount);

retain();
try std.testing.expect(State.initialized);
try std.testing.expectEqual(@as(u32, 1), State.refcount);
_ = io();

retain();
try std.testing.expectEqual(@as(u32, 2), State.refcount);

release();
try std.testing.expect(State.initialized);
try std.testing.expectEqual(@as(u32, 1), State.refcount);

release();
try std.testing.expect(!State.initialized);
try std.testing.expectEqual(@as(u32, 0), State.refcount);
}
Loading