diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index 2971ce1..8a93704 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -273,3 +273,28 @@ export declare function get_buffer(buffer: ArrayBuffer): number; * @returns The buffer as a string */ export declare function get_buffer_as_string(buffer: ArrayBuffer): string; + +// ============== ArrayBuffer Functions ============== + +/** + * Creates a new array buffer + * @param size - The size of the array buffer + * @returns The new array buffer + */ +export declare function create_arraybuffer(): ArrayBuffer; + +/** + * Gets the array buffer length + * @param arraybuffer - The array buffer + * @returns The array buffer length + */ +export declare function get_arraybuffer(arraybuffer: ArrayBuffer): number; + +/** + * Gets the array buffer as a string + * @param arraybuffer - The array buffer + * @returns The array buffer as a string + */ +export declare function get_arraybuffer_as_string( + arraybuffer: ArrayBuffer +): string; diff --git a/examples/basic/src/arraybuffer.zig b/examples/basic/src/arraybuffer.zig new file mode 100644 index 0000000..3fcbc37 --- /dev/null +++ b/examples/basic/src/arraybuffer.zig @@ -0,0 +1,13 @@ +const napi = @import("napi"); + +pub fn create_arraybuffer(env: napi.Env) !napi.ArrayBuffer { + return napi.ArrayBuffer.New(env, 1024); +} + +pub fn get_arraybuffer(buf: napi.ArrayBuffer) !usize { + return buf.length(); +} + +pub fn get_arraybuffer_as_string(buf: napi.ArrayBuffer) ![]u8 { + return buf.asSlice(); +} diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index 4894801..4f6f3e5 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -11,6 +11,7 @@ const thread_safe_function = @import("thread_safe_function.zig"); const class = @import("class.zig"); const log = @import("log/log.zig"); const buffer = @import("buffer.zig"); +const arraybuffer = @import("arraybuffer.zig"); pub const test_i32 = number.test_i32; pub const test_f32 = number.test_f32; @@ -51,6 +52,10 @@ pub const create_buffer = buffer.create_buffer; pub const get_buffer = buffer.get_buffer; pub const get_buffer_as_string = buffer.get_buffer_as_string; +pub const create_arraybuffer = arraybuffer.create_arraybuffer; +pub const get_arraybuffer = arraybuffer.get_arraybuffer; +pub const get_arraybuffer_as_string = arraybuffer.get_arraybuffer_as_string; + comptime { napi.NODE_API_MODULE("hello", @This()); } diff --git a/src/napi.zig b/src/napi.zig index f90fad8..f7514fc 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -8,6 +8,7 @@ const err = @import("./napi/wrapper/error.zig"); const thread_safe_function = @import("./napi/wrapper/thread_safe_function.zig"); const class = @import("./napi/wrapper/class.zig"); const buffer = @import("./napi/wrapper/buffer.zig"); +const arraybuffer = @import("./napi/wrapper/arraybuffer.zig"); pub const napi_sys = @import("napi-sys"); pub const Env = env.Env; @@ -34,6 +35,7 @@ pub const ThreadSafeFunction = thread_safe_function.ThreadSafeFunction; pub const Class = class.Class; pub const ClassWithoutInit = class.ClassWithoutInit; pub const Buffer = buffer.Buffer; +pub const ArrayBuffer = arraybuffer.ArrayBuffer; pub const NODE_API_MODULE = module.NODE_API_MODULE; pub const NODE_API_MODULE_WITH_INIT = module.NODE_API_MODULE_WITH_INIT; diff --git a/src/napi/util/napi.zig b/src/napi/util/napi.zig index 9df35c5..2d4a6db 100644 --- a/src/napi/util/napi.zig +++ b/src/napi/util/napi.zig @@ -8,12 +8,13 @@ const Function = @import("../value/function.zig").Function; const ThreadSafeFunction = @import("../wrapper/thread_safe_function.zig").ThreadSafeFunction; const class = @import("../wrapper/class.zig"); const Buffer = @import("../wrapper/buffer.zig").Buffer; +const ArrayBuffer = @import("../wrapper/arraybuffer.zig").ArrayBuffer; pub const Napi = struct { pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T { const infos = @typeInfo(T); switch (T) { - NapiValue.BigInt, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer => { + NapiValue.BigInt, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer, ArrayBuffer => { return T.from_raw(env, raw); }, else => { @@ -135,7 +136,7 @@ pub const Napi = struct { const infos = @typeInfo(value_type); switch (value_type) { - NapiValue.BigInt, NapiValue.Bool, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer => { + NapiValue.BigInt, NapiValue.Bool, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer, ArrayBuffer => { return value.raw; }, // If value is already a napi_value, return it directly diff --git a/src/napi/wrapper/arraybuffer.zig b/src/napi/wrapper/arraybuffer.zig new file mode 100644 index 0000000..52d7b87 --- /dev/null +++ b/src/napi/wrapper/arraybuffer.zig @@ -0,0 +1,246 @@ +const std = @import("std"); +const napi = @import("napi-sys").napi_sys; +const Env = @import("../env.zig").Env; +const NapiError = @import("error.zig"); +const GlobalAllocator = @import("../util/allocator.zig"); + +pub const ArrayBuffer = struct { + env: napi.napi_env, + raw: napi.napi_value, + data: [*]u8, + len: usize, + + /// Create an ArrayBuffer from a raw napi_value + pub fn from_raw(env: napi.napi_env, raw: napi.napi_value) ArrayBuffer { + var data: ?*anyopaque = null; + var len: usize = 0; + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); + if (len == 0) { + return ArrayBuffer{ + .env = env, + .raw = raw, + .data = &[_]u8{}, + .len = 0, + }; + } + return ArrayBuffer{ + .env = env, + .raw = raw, + .data = @ptrCast(data), + .len = len, + }; + } + + /// Convert from napi_value to the specified type ([]u8 or [N]u8) + pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T { + const infos = @typeInfo(T); + + switch (infos) { + // Handle fixed-size array: [N]u8 + .array => |arr| { + if (arr.child != u8) { + @compileError("ArrayBuffer only supports u8 arrays, got: " ++ @typeName(arr.child)); + } + + var data: ?*anyopaque = null; + var len: usize = 0; + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); + + var result: T = undefined; + const copy_len = @min(len, arr.len); + const src: [*]const u8 = @ptrCast(data); + @memcpy(result[0..copy_len], src[0..copy_len]); + + // Zero-fill remaining bytes if buffer is smaller than array + if (copy_len < arr.len) { + @memset(result[copy_len..], 0); + } + + return result; + }, + // Handle slice: []u8 or []const u8 + .pointer => |ptr| { + if (ptr.size != .slice) { + @compileError("ArrayBuffer only supports slices, got pointer type: " ++ @typeName(T)); + } + if (ptr.child != u8) { + @compileError("ArrayBuffer only supports u8 slices, got: " ++ @typeName(ptr.child)); + } + + var data: ?*anyopaque = null; + var len: usize = 0; + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); + + const allocator = GlobalAllocator.globalAllocator(); + const buf = allocator.alloc(u8, len) catch @panic("OOM"); + const src: [*]const u8 = @ptrCast(data); + @memcpy(buf, src[0..len]); + + return buf; + }, + else => { + @compileError("ArrayBuffer.from_napi_value only supports []u8 or [N]u8, got: " ++ @typeName(T)); + }, + } + } + + /// Create a new ArrayBuffer from data using external buffer (zero-copy, transfers ownership) + /// Similar to napi-rs `ArrayBuffer::from(Vec)` which uses napi_create_external_arraybuffer + /// + /// The data ownership is transferred to JavaScript. When the JS ArrayBuffer is garbage collected, + /// the finalize callback will free the memory using the global allocator. + /// + /// Example: + /// ```zig + /// const allocator = GlobalAllocator.globalAllocator(); + /// const owned_data = try allocator.alloc(u8, 1024); + /// // ... fill data ... + /// const buf = try ArrayBuffer.from(env, owned_data); // ownership transferred + /// // Don't free owned_data, it's now managed by JS + /// ``` + pub fn from(env: Env, data: []u8) !ArrayBuffer { + var result: napi.napi_value = undefined; + + // Store the slice info for the finalizer + const hint = ArrayBufferHint.create(data) catch { + return NapiError.Error.fromStatus(NapiError.Status.GenericFailure); + }; + + const status = napi.napi_create_external_arraybuffer( + env.raw, + @ptrCast(data.ptr), + data.len, + externalArrayBufferFinalizer, + hint, + &result, + ); + + if (status != napi.napi_ok) { + // Clean up hint if buffer creation failed + hint.destroy(); + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + + return ArrayBuffer{ + .env = env.raw, + .raw = result, + .data = data.ptr, + .len = data.len, + }; + } + + /// Create a new ArrayBuffer by copying data (no ownership transfer) + /// Similar to napi-rs `ArrayBuffer::copy_from` + /// + /// Use this when you want to keep ownership of the original data, + /// or when the data is on the stack/temporary. + /// + /// Example: + /// ```zig + /// const stack_data = [_]u8{ 1, 2, 3, 4 }; + /// const buf = try ArrayBuffer.copy(env, &stack_data); + /// ``` + pub fn copy(env: Env, data: []const u8) !ArrayBuffer { + var result: napi.napi_value = undefined; + var result_data: ?*anyopaque = null; + + const status = napi.napi_create_arraybuffer( + env.raw, + data.len, + &result_data, + &result, + ); + + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + + // Copy the data into the newly created ArrayBuffer + const dest: [*]u8 = @ptrCast(result_data); + @memcpy(dest[0..data.len], data); + + return ArrayBuffer{ + .env = env.raw, + .raw = result, + .data = dest, + .len = data.len, + }; + } + + /// Create a new uninitialized ArrayBuffer with the specified length + /// Similar to napi-rs `env.create_arraybuffer(length)` + /// + /// Example: + /// ```zig + /// var buf = try ArrayBuffer.New(env, 1024); + /// @memset(buf.asSlice(), 0); // initialize + /// ``` + pub fn New(env: Env, len: usize) !ArrayBuffer { + var result: napi.napi_value = undefined; + var data: ?*anyopaque = null; + + const status = napi.napi_create_arraybuffer(env.raw, len, &data, &result); + + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + + return ArrayBuffer{ + .env = env.raw, + .raw = result, + .data = @ptrCast(data), + .len = len, + }; + } + + /// Get the ArrayBuffer data as a mutable slice + pub fn asSlice(self: ArrayBuffer) []u8 { + return self.data[0..self.len]; + } + + /// Get the ArrayBuffer data as a const slice + pub fn asConstSlice(self: ArrayBuffer) []const u8 { + return self.data[0..self.len]; + } + + /// Get the length of the ArrayBuffer + pub fn length(self: ArrayBuffer) usize { + return self.len; + } +}; + +/// Helper struct to store ArrayBuffer info for the finalizer +const ArrayBufferHint = struct { + ptr: [*]u8, + len: usize, + + fn create(data: []u8) !*ArrayBufferHint { + const allocator = GlobalAllocator.globalAllocator(); + const hint = try allocator.create(ArrayBufferHint); + hint.* = .{ + .ptr = data.ptr, + .len = data.len, + }; + return hint; + } + + fn destroy(self: *ArrayBufferHint) void { + const allocator = GlobalAllocator.globalAllocator(); + // Free the original buffer data + allocator.free(self.ptr[0..self.len]); + // Free the hint struct itself + allocator.destroy(self); + } +}; + +/// Callback invoked when the external ArrayBuffer is garbage collected +fn externalArrayBufferFinalizer( + _: napi.napi_env, + _: ?*anyopaque, + hint: ?*anyopaque, +) callconv(.C) void { + if (hint) |h| { + const arraybuffer_hint: *ArrayBufferHint = @ptrCast(@alignCast(h)); + arraybuffer_hint.destroy(); + } +}