Skip to content

Commit 84c245d

Browse files
authored
Add ArrayBuffer (#27)
1 parent 7197b90 commit 84c245d

6 files changed

Lines changed: 294 additions & 2 deletions

File tree

examples/basic/index.d.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,28 @@ export declare function get_buffer(buffer: ArrayBuffer): number;
273273
* @returns The buffer as a string
274274
*/
275275
export declare function get_buffer_as_string(buffer: ArrayBuffer): string;
276+
277+
// ============== ArrayBuffer Functions ==============
278+
279+
/**
280+
* Creates a new array buffer
281+
* @param size - The size of the array buffer
282+
* @returns The new array buffer
283+
*/
284+
export declare function create_arraybuffer(): ArrayBuffer;
285+
286+
/**
287+
* Gets the array buffer length
288+
* @param arraybuffer - The array buffer
289+
* @returns The array buffer length
290+
*/
291+
export declare function get_arraybuffer(arraybuffer: ArrayBuffer): number;
292+
293+
/**
294+
* Gets the array buffer as a string
295+
* @param arraybuffer - The array buffer
296+
* @returns The array buffer as a string
297+
*/
298+
export declare function get_arraybuffer_as_string(
299+
arraybuffer: ArrayBuffer
300+
): string;

examples/basic/src/arraybuffer.zig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const napi = @import("napi");
2+
3+
pub fn create_arraybuffer(env: napi.Env) !napi.ArrayBuffer {
4+
return napi.ArrayBuffer.New(env, 1024);
5+
}
6+
7+
pub fn get_arraybuffer(buf: napi.ArrayBuffer) !usize {
8+
return buf.length();
9+
}
10+
11+
pub fn get_arraybuffer_as_string(buf: napi.ArrayBuffer) ![]u8 {
12+
return buf.asSlice();
13+
}

examples/basic/src/hello.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const thread_safe_function = @import("thread_safe_function.zig");
1111
const class = @import("class.zig");
1212
const log = @import("log/log.zig");
1313
const buffer = @import("buffer.zig");
14+
const arraybuffer = @import("arraybuffer.zig");
1415

1516
pub const test_i32 = number.test_i32;
1617
pub const test_f32 = number.test_f32;
@@ -51,6 +52,10 @@ pub const create_buffer = buffer.create_buffer;
5152
pub const get_buffer = buffer.get_buffer;
5253
pub const get_buffer_as_string = buffer.get_buffer_as_string;
5354

55+
pub const create_arraybuffer = arraybuffer.create_arraybuffer;
56+
pub const get_arraybuffer = arraybuffer.get_arraybuffer;
57+
pub const get_arraybuffer_as_string = arraybuffer.get_arraybuffer_as_string;
58+
5459
comptime {
5560
napi.NODE_API_MODULE("hello", @This());
5661
}

src/napi.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const err = @import("./napi/wrapper/error.zig");
88
const thread_safe_function = @import("./napi/wrapper/thread_safe_function.zig");
99
const class = @import("./napi/wrapper/class.zig");
1010
const buffer = @import("./napi/wrapper/buffer.zig");
11+
const arraybuffer = @import("./napi/wrapper/arraybuffer.zig");
1112

1213
pub const napi_sys = @import("napi-sys");
1314
pub const Env = env.Env;
@@ -34,6 +35,7 @@ pub const ThreadSafeFunction = thread_safe_function.ThreadSafeFunction;
3435
pub const Class = class.Class;
3536
pub const ClassWithoutInit = class.ClassWithoutInit;
3637
pub const Buffer = buffer.Buffer;
38+
pub const ArrayBuffer = arraybuffer.ArrayBuffer;
3739

3840
pub const NODE_API_MODULE = module.NODE_API_MODULE;
3941
pub const NODE_API_MODULE_WITH_INIT = module.NODE_API_MODULE_WITH_INIT;

src/napi/util/napi.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ const Function = @import("../value/function.zig").Function;
88
const ThreadSafeFunction = @import("../wrapper/thread_safe_function.zig").ThreadSafeFunction;
99
const class = @import("../wrapper/class.zig");
1010
const Buffer = @import("../wrapper/buffer.zig").Buffer;
11+
const ArrayBuffer = @import("../wrapper/arraybuffer.zig").ArrayBuffer;
1112

1213
pub const Napi = struct {
1314
pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T {
1415
const infos = @typeInfo(T);
1516
switch (T) {
16-
NapiValue.BigInt, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer => {
17+
NapiValue.BigInt, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer, ArrayBuffer => {
1718
return T.from_raw(env, raw);
1819
},
1920
else => {
@@ -135,7 +136,7 @@ pub const Napi = struct {
135136
const infos = @typeInfo(value_type);
136137

137138
switch (value_type) {
138-
NapiValue.BigInt, NapiValue.Bool, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer => {
139+
NapiValue.BigInt, NapiValue.Bool, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer, ArrayBuffer => {
139140
return value.raw;
140141
},
141142
// If value is already a napi_value, return it directly

src/napi/wrapper/arraybuffer.zig

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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

Comments
 (0)