diff --git a/examples/hello_world/mod.test.ts b/examples/hello_world/mod.test.ts index f203f4e..491e99c 100644 --- a/examples/hello_world/mod.test.ts +++ b/examples/hello_world/mod.test.ts @@ -12,4 +12,34 @@ describe("example mod", () => { it("addon without parameters", () => { expect(example.surprise()).toEqual("Surprise!"); }); + + it("copies immutable input into independently writable buffers", () => { + const arrayBuffer = example.copyArrayBuffer(false); + const arrayBufferBytes = new Uint8Array(arrayBuffer); + expect(new TextDecoder().decode(arrayBufferBytes)).toEqual("copy me"); + arrayBufferBytes[0] = "C".charCodeAt(0); + expect(new TextDecoder().decode(arrayBufferBytes)).toEqual("Copy me"); + expect(new TextDecoder().decode(new Uint8Array(example.copyArrayBuffer(false)))).toEqual( + "copy me" + ); + + const buffer = example.copyBuffer(); + expect(Buffer.isBuffer(buffer)).toBe(true); + buffer[0] = "C".charCodeAt(0); + expect(buffer.toString()).toEqual("Copy me"); + expect(example.copyBuffer().toString()).toEqual("copy me"); + }); + + it("copies empty input into an empty ArrayBuffer", () => { + const arrayBuffer = example.copyArrayBuffer(true); + expect(arrayBuffer).toBeInstanceOf(ArrayBuffer); + expect(arrayBuffer.byteLength).toEqual(0); + }); + + it("shares mutable storage with an external Buffer", () => { + const buffer = example.externalBuffer(); + expect(Buffer.isBuffer(buffer)).toBe(true); + buffer[0] = 9; + expect(example.externalBufferFirstByte()).toEqual(9); + }); }); diff --git a/examples/hello_world/mod.zig b/examples/hello_world/mod.zig index fa4a4b3..0b4d745 100644 --- a/examples/hello_world/mod.zig +++ b/examples/hello_world/mod.zig @@ -62,6 +62,36 @@ fn exampleMod(env: zapi.Env, module: zapi.Value) anyerror!void { null, )); + try module.setNamedProperty("copyArrayBuffer", try env.createFunction( + "copyArrayBuffer", + 1, + copy_arraybuffer, + null, + )); + + try module.setNamedProperty("copyBuffer", try env.createFunction( + "copyBuffer", + 0, + copy_buffer, + null, + )); + + try module.setNamedProperty("externalBuffer", try env.createFunction( + "externalBuffer", + 0, + zapi.createCallback(0, external_buffer, .{ + .returns = .external_buffer, + }), + null, + )); + + try module.setNamedProperty("externalBufferFirstByte", try env.createFunction( + "externalBufferFirstByte", + 0, + zapi.createCallback(0, external_buffer_first_byte, .{}), + null, + )); + try module.setNamedProperty("update", try env.createFunction( "update", 1, @@ -139,6 +169,25 @@ fn surprise() []const u8 { return "Surprise!"; } +fn copy_arraybuffer(env: zapi.Env, cb: zapi.CallbackInfo(1)) !zapi.Value { + if (try cb.arg(0).getValueBool()) return try env.createArrayBufferCopy(""); + return try env.createArrayBufferCopy("copy me"); +} + +fn copy_buffer(env: zapi.Env, _: zapi.CallbackInfo(0)) !zapi.Value { + return try env.createBufferCopy("copy me", null); +} + +var external_buffer_data = [_]u8{ 1, 2, 3 }; + +fn external_buffer() []u8 { + return &external_buffer_data; +} + +fn external_buffer_first_byte() u8 { + return external_buffer_data[0]; +} + const S = struct { a: i32, b: i32, diff --git a/src/Env.zig b/src/Env.zig index 4735d80..999a898 100644 --- a/src/Env.zig +++ b/src/Env.zig @@ -253,6 +253,17 @@ pub fn createArrayBuffer(self: Env, size: usize, out: ?*[*]u8) NapiError!Value { }; } +/// Creates an ArrayBuffer containing a copy of `data`. +/// The returned ArrayBuffer owns its backing memory and may be mutated independently of `data`. +pub fn createArrayBufferCopy(self: Env, data: []const u8) NapiError!Value { + if (data.len == 0) return self.createArrayBuffer(0, null); + + var output: [*]u8 = undefined; + const value = try self.createArrayBuffer(data.len, &output); + @memcpy(output[0..data.len], data); + return value; +} + /// https://nodejs.org/api/n-api.html#napi_create_buffer pub fn createBuffer(self: Env, size: usize, out: ?*[*]u8) NapiError!Value { var value: c.napi_value = undefined; @@ -265,6 +276,8 @@ pub fn createBuffer(self: Env, size: usize, out: ?*[*]u8) NapiError!Value { }; } +/// Creates a Buffer containing a copy of `data`. +/// The returned Buffer owns its backing memory and may be mutated independently of `data`. /// https://nodejs.org/api/n-api.html#napi_create_buffer_copy pub fn createBufferCopy(self: Env, data: []const u8, out: ?*[*]u8) NapiError!Value { var value: c.napi_value = undefined; @@ -301,13 +314,21 @@ pub fn createExternal(self: Env, data: [*]const u8, finalize_cb: c.napi_finalize }; } +/// Creates an ArrayBuffer backed by caller-owned mutable memory. +/// JavaScript may mutate `data`. It must remain valid until `finalize_cb` runs, or until the +/// end of the ArrayBuffer's lifetime when no finalizer is provided. /// https://nodejs.org/api/n-api.html#napi_create_external_arraybuffer -pub fn createExternalArrayBuffer(self: Env, data: []const u8, finalize_cb: c.napi_finalize, finalize_hint: ?*anyopaque) NapiError!Value { +pub fn createExternalArrayBuffer( + self: Env, + data: []u8, + finalize_cb: c.napi_finalize, + finalize_hint: ?*anyopaque, +) NapiError!Value { var value: c.napi_value = undefined; try status.check( c.napi_create_external_arraybuffer( self.env, - @ptrCast(@constCast(data.ptr)), + @ptrCast(data.ptr), data.len, finalize_cb, finalize_hint, @@ -320,11 +341,26 @@ pub fn createExternalArrayBuffer(self: Env, data: []const u8, finalize_cb: c.nap }; } +/// Creates a Buffer backed by caller-owned mutable memory. +/// JavaScript may mutate `data`. It must remain valid until `finalize_cb` runs, or until the +/// end of the Buffer's lifetime when no finalizer is provided. /// https://nodejs.org/api/n-api.html#napi_create_external_buffer -pub fn createExternalBuffer(self: Env, data: []const u8, finalize_cb: c.napi_finalize, finalize_hint: ?*anyopaque) NapiError!Value { +pub fn createExternalBuffer( + self: Env, + data: []u8, + finalize_cb: c.napi_finalize, + finalize_hint: ?*anyopaque, +) NapiError!Value { var value: c.napi_value = undefined; try status.check( - c.napi_create_external_buffer(self.env, data.len, @ptrCast(@constCast(data.ptr)), finalize_cb, finalize_hint, &value), + c.napi_create_external_buffer( + self.env, + data.len, + @ptrCast(data.ptr), + finalize_cb, + finalize_hint, + &value, + ), ); return Value{ .env = self.env, diff --git a/src/to_from_value.zig b/src/to_from_value.zig index 57bc7f8..0c0d107 100644 --- a/src/to_from_value.zig +++ b/src/to_from_value.zig @@ -82,11 +82,17 @@ pub fn toValue( .pointer => |p| { const h = hint; if (p.child == u8 and p.size == .slice) { + if (h == .external_buffer) { + if (p.is_const) { + @compileError("external buffers require a mutable byte slice"); + } + const bytes: []u8 = @ptrCast(v); + return try env.createExternalBuffer(bytes, null, null); + } + const bytes: []const u8 = @ptrCast(v); if (h == .string) { return try env.createStringUtf8(bytes); - } else if (h == .external_buffer) { - return try env.createExternalBuffer(bytes, null, null); } else if (h == .buffer or h == .auto) { return try env.createBufferCopy(bytes, null); }