|
| 1 | +const std = @import("std"); |
| 2 | +const napi = @import("napi-sys").napi_sys; |
| 3 | +const Env = @import("../env.zig").Env; |
| 4 | +const NapiError = @import("error.zig"); |
| 5 | +const GlobalAllocator = @import("../util/allocator.zig"); |
| 6 | + |
| 7 | +pub const ArrayBuffer = struct { |
| 8 | + env: napi.napi_env, |
| 9 | + raw: napi.napi_value, |
| 10 | + data: [*]u8, |
| 11 | + len: usize, |
| 12 | + |
| 13 | + /// Create an ArrayBuffer from a raw napi_value |
| 14 | + pub fn from_raw(env: napi.napi_env, raw: napi.napi_value) ArrayBuffer { |
| 15 | + var data: ?*anyopaque = null; |
| 16 | + var len: usize = 0; |
| 17 | + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); |
| 18 | + if (len == 0) { |
| 19 | + return ArrayBuffer{ |
| 20 | + .env = env, |
| 21 | + .raw = raw, |
| 22 | + .data = &[_]u8{}, |
| 23 | + .len = 0, |
| 24 | + }; |
| 25 | + } |
| 26 | + return ArrayBuffer{ |
| 27 | + .env = env, |
| 28 | + .raw = raw, |
| 29 | + .data = @ptrCast(data), |
| 30 | + .len = len, |
| 31 | + }; |
| 32 | + } |
| 33 | + |
| 34 | + /// Convert from napi_value to the specified type ([]u8 or [N]u8) |
| 35 | + pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T { |
| 36 | + const infos = @typeInfo(T); |
| 37 | + |
| 38 | + switch (infos) { |
| 39 | + // Handle fixed-size array: [N]u8 |
| 40 | + .array => |arr| { |
| 41 | + if (arr.child != u8) { |
| 42 | + @compileError("ArrayBuffer only supports u8 arrays, got: " ++ @typeName(arr.child)); |
| 43 | + } |
| 44 | + |
| 45 | + var data: ?*anyopaque = null; |
| 46 | + var len: usize = 0; |
| 47 | + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); |
| 48 | + |
| 49 | + var result: T = undefined; |
| 50 | + const copy_len = @min(len, arr.len); |
| 51 | + const src: [*]const u8 = @ptrCast(data); |
| 52 | + @memcpy(result[0..copy_len], src[0..copy_len]); |
| 53 | + |
| 54 | + // Zero-fill remaining bytes if buffer is smaller than array |
| 55 | + if (copy_len < arr.len) { |
| 56 | + @memset(result[copy_len..], 0); |
| 57 | + } |
| 58 | + |
| 59 | + return result; |
| 60 | + }, |
| 61 | + // Handle slice: []u8 or []const u8 |
| 62 | + .pointer => |ptr| { |
| 63 | + if (ptr.size != .slice) { |
| 64 | + @compileError("ArrayBuffer only supports slices, got pointer type: " ++ @typeName(T)); |
| 65 | + } |
| 66 | + if (ptr.child != u8) { |
| 67 | + @compileError("ArrayBuffer only supports u8 slices, got: " ++ @typeName(ptr.child)); |
| 68 | + } |
| 69 | + |
| 70 | + var data: ?*anyopaque = null; |
| 71 | + var len: usize = 0; |
| 72 | + _ = napi.napi_get_arraybuffer_info(env, raw, &data, &len); |
| 73 | + |
| 74 | + const allocator = GlobalAllocator.globalAllocator(); |
| 75 | + const buf = allocator.alloc(u8, len) catch @panic("OOM"); |
| 76 | + const src: [*]const u8 = @ptrCast(data); |
| 77 | + @memcpy(buf, src[0..len]); |
| 78 | + |
| 79 | + return buf; |
| 80 | + }, |
| 81 | + else => { |
| 82 | + @compileError("ArrayBuffer.from_napi_value only supports []u8 or [N]u8, got: " ++ @typeName(T)); |
| 83 | + }, |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /// Create a new ArrayBuffer from data using external buffer (zero-copy, transfers ownership) |
| 88 | + /// Similar to napi-rs `ArrayBuffer::from(Vec<u8>)` which uses napi_create_external_arraybuffer |
| 89 | + /// |
| 90 | + /// The data ownership is transferred to JavaScript. When the JS ArrayBuffer is garbage collected, |
| 91 | + /// the finalize callback will free the memory using the global allocator. |
| 92 | + /// |
| 93 | + /// Example: |
| 94 | + /// ```zig |
| 95 | + /// const allocator = GlobalAllocator.globalAllocator(); |
| 96 | + /// const owned_data = try allocator.alloc(u8, 1024); |
| 97 | + /// // ... fill data ... |
| 98 | + /// const buf = try ArrayBuffer.from(env, owned_data); // ownership transferred |
| 99 | + /// // Don't free owned_data, it's now managed by JS |
| 100 | + /// ``` |
| 101 | + pub fn from(env: Env, data: []u8) !ArrayBuffer { |
| 102 | + var result: napi.napi_value = undefined; |
| 103 | + |
| 104 | + // Store the slice info for the finalizer |
| 105 | + const hint = ArrayBufferHint.create(data) catch { |
| 106 | + return NapiError.Error.fromStatus(NapiError.Status.GenericFailure); |
| 107 | + }; |
| 108 | + |
| 109 | + const status = napi.napi_create_external_arraybuffer( |
| 110 | + env.raw, |
| 111 | + @ptrCast(data.ptr), |
| 112 | + data.len, |
| 113 | + externalArrayBufferFinalizer, |
| 114 | + hint, |
| 115 | + &result, |
| 116 | + ); |
| 117 | + |
| 118 | + if (status != napi.napi_ok) { |
| 119 | + // Clean up hint if buffer creation failed |
| 120 | + hint.destroy(); |
| 121 | + return NapiError.Error.fromStatus(NapiError.Status.New(status)); |
| 122 | + } |
| 123 | + |
| 124 | + return ArrayBuffer{ |
| 125 | + .env = env.raw, |
| 126 | + .raw = result, |
| 127 | + .data = data.ptr, |
| 128 | + .len = data.len, |
| 129 | + }; |
| 130 | + } |
| 131 | + |
| 132 | + /// Create a new ArrayBuffer by copying data (no ownership transfer) |
| 133 | + /// Similar to napi-rs `ArrayBuffer::copy_from` |
| 134 | + /// |
| 135 | + /// Use this when you want to keep ownership of the original data, |
| 136 | + /// or when the data is on the stack/temporary. |
| 137 | + /// |
| 138 | + /// Example: |
| 139 | + /// ```zig |
| 140 | + /// const stack_data = [_]u8{ 1, 2, 3, 4 }; |
| 141 | + /// const buf = try ArrayBuffer.copy(env, &stack_data); |
| 142 | + /// ``` |
| 143 | + pub fn copy(env: Env, data: []const u8) !ArrayBuffer { |
| 144 | + var result: napi.napi_value = undefined; |
| 145 | + var result_data: ?*anyopaque = null; |
| 146 | + |
| 147 | + const status = napi.napi_create_arraybuffer( |
| 148 | + env.raw, |
| 149 | + data.len, |
| 150 | + &result_data, |
| 151 | + &result, |
| 152 | + ); |
| 153 | + |
| 154 | + if (status != napi.napi_ok) { |
| 155 | + return NapiError.Error.fromStatus(NapiError.Status.New(status)); |
| 156 | + } |
| 157 | + |
| 158 | + // Copy the data into the newly created ArrayBuffer |
| 159 | + const dest: [*]u8 = @ptrCast(result_data); |
| 160 | + @memcpy(dest[0..data.len], data); |
| 161 | + |
| 162 | + return ArrayBuffer{ |
| 163 | + .env = env.raw, |
| 164 | + .raw = result, |
| 165 | + .data = dest, |
| 166 | + .len = data.len, |
| 167 | + }; |
| 168 | + } |
| 169 | + |
| 170 | + /// Create a new uninitialized ArrayBuffer with the specified length |
| 171 | + /// Similar to napi-rs `env.create_arraybuffer(length)` |
| 172 | + /// |
| 173 | + /// Example: |
| 174 | + /// ```zig |
| 175 | + /// var buf = try ArrayBuffer.New(env, 1024); |
| 176 | + /// @memset(buf.asSlice(), 0); // initialize |
| 177 | + /// ``` |
| 178 | + pub fn New(env: Env, len: usize) !ArrayBuffer { |
| 179 | + var result: napi.napi_value = undefined; |
| 180 | + var data: ?*anyopaque = null; |
| 181 | + |
| 182 | + const status = napi.napi_create_arraybuffer(env.raw, len, &data, &result); |
| 183 | + |
| 184 | + if (status != napi.napi_ok) { |
| 185 | + return NapiError.Error.fromStatus(NapiError.Status.New(status)); |
| 186 | + } |
| 187 | + |
| 188 | + return ArrayBuffer{ |
| 189 | + .env = env.raw, |
| 190 | + .raw = result, |
| 191 | + .data = @ptrCast(data), |
| 192 | + .len = len, |
| 193 | + }; |
| 194 | + } |
| 195 | + |
| 196 | + /// Get the ArrayBuffer data as a mutable slice |
| 197 | + pub fn asSlice(self: ArrayBuffer) []u8 { |
| 198 | + return self.data[0..self.len]; |
| 199 | + } |
| 200 | + |
| 201 | + /// Get the ArrayBuffer data as a const slice |
| 202 | + pub fn asConstSlice(self: ArrayBuffer) []const u8 { |
| 203 | + return self.data[0..self.len]; |
| 204 | + } |
| 205 | + |
| 206 | + /// Get the length of the ArrayBuffer |
| 207 | + pub fn length(self: ArrayBuffer) usize { |
| 208 | + return self.len; |
| 209 | + } |
| 210 | +}; |
| 211 | + |
| 212 | +/// Helper struct to store ArrayBuffer info for the finalizer |
| 213 | +const ArrayBufferHint = struct { |
| 214 | + ptr: [*]u8, |
| 215 | + len: usize, |
| 216 | + |
| 217 | + fn create(data: []u8) !*ArrayBufferHint { |
| 218 | + const allocator = GlobalAllocator.globalAllocator(); |
| 219 | + const hint = try allocator.create(ArrayBufferHint); |
| 220 | + hint.* = .{ |
| 221 | + .ptr = data.ptr, |
| 222 | + .len = data.len, |
| 223 | + }; |
| 224 | + return hint; |
| 225 | + } |
| 226 | + |
| 227 | + fn destroy(self: *ArrayBufferHint) void { |
| 228 | + const allocator = GlobalAllocator.globalAllocator(); |
| 229 | + // Free the original buffer data |
| 230 | + allocator.free(self.ptr[0..self.len]); |
| 231 | + // Free the hint struct itself |
| 232 | + allocator.destroy(self); |
| 233 | + } |
| 234 | +}; |
| 235 | + |
| 236 | +/// Callback invoked when the external ArrayBuffer is garbage collected |
| 237 | +fn externalArrayBufferFinalizer( |
| 238 | + _: napi.napi_env, |
| 239 | + _: ?*anyopaque, |
| 240 | + hint: ?*anyopaque, |
| 241 | +) callconv(.C) void { |
| 242 | + if (hint) |h| { |
| 243 | + const arraybuffer_hint: *ArrayBufferHint = @ptrCast(@alignCast(h)); |
| 244 | + arraybuffer_hint.destroy(); |
| 245 | + } |
| 246 | +} |
0 commit comments