Skip to content
Closed
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,31 @@ napi.createCallback(2, myFunc, .{
| `.buffer` | Borrow Buffer arguments as byte slices; copy byte-slice returns |
| `.external_buffer` | Transfer an `OwnedBuffer` to JavaScript without copying |

Composite types are converted recursively, and the hint applies to their nested byte slices too:

| Zig type | JavaScript |
|----------|------------|
| `?T` | `null`/`undefined` ↔ `null`; an omitted trailing argument is `null` |
| `enum(Int)` | integer tag value; unknown tags are rejected |
| `struct { ... }` | object with the same field names |
| `struct { A, B }` (tuple), `[N]T` | array of exactly that length |
| `[]T`, `[]const T` | array |

A `.auto` byte-slice argument accepts either a string or a Buffer. Strings and slices read from
arguments are allocated per call and freed when the function returns, so copy them if you need them
afterwards.

```zig
const Point = struct { x: f64, y: f64, label: ?[]const u8 };

fn scale(p: Point, factor: f64) Point {
return .{ .x = p.x * factor, .y = p.y * factor, .label = p.label };
}

// scale({ x: 1, y: 2, label: "p" }, 2) → { x: 2, y: 4, label: "p" }
napi.createCallback(2, scale, .{ .returns = .string });
```

External buffers require explicit ownership transfer:

```zig
Expand Down
29 changes: 29 additions & 0 deletions examples/hello_world/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ describe("example mod", () => {
expect(example.surprise()).toEqual("Surprise!");
});

it("converts optional string arguments", () => {
expect(example.greet("Alice", "Dr.")).toEqual("Hello, Dr. Alice!");
expect(example.greet("Bob")).toEqual("Hey Bob!");
expect(example.greet("Bob", null)).toEqual("Hey Bob!");
expect(() => example.greet(1)).toThrow();
});

it("converts structs to and from objects", () => {
expect(example.scalePoint({ x: 1, y: 2.5, label: "p" }, 2)).toEqual({ x: 2, y: 5, label: "p" });
expect(example.scalePoint({ x: 1, y: 1 }, 3)).toEqual({ x: 3, y: 3, label: null });
expect(() => example.scalePoint(1, 2)).toThrow();
});

it("converts slices, tuples and fixed arrays to and from arrays", () => {
expect(example.sum([1, 2, 3, 4])).toEqual(10);
expect(example.sum([])).toEqual(0);
expect(example.swap([7, true])).toEqual([true, 7]);
expect(example.reverse3([1, 2, 3])).toEqual([3, 2, 1]);
expect(() => example.swap([7])).toThrow();
expect(() => example.reverse3([1, 2])).toThrow();
expect(() => example.sum("nope")).toThrow();
});

it("converts enums to and from integers", () => {
expect(example.nextColor(0)).toEqual(1);
expect(example.nextColor(2)).toEqual(0);
expect(() => example.nextColor(7)).toThrow();
});

it("copies immutable input into independently writable buffers", () => {
const arrayBuffer = example.copyArrayBuffer(false);
const arrayBufferBytes = new Uint8Array(arrayBuffer);
Expand Down
90 changes: 90 additions & 0 deletions examples/hello_world/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,55 @@ fn exampleMod(env: zapi.Env, module: zapi.Value) anyerror!void {
null,
));

// Examples of composite types converted by createCallback: optionals map to
// null/undefined, structs to objects, tuples and slices to arrays, enums to integers.
try module.setNamedProperty("greet", try env.createFunction(
"greet",
2,
zapi.createCallback(2, greet, .{
.args = .{ .string, .string },
.returns = .string,
}),
null,
));

try module.setNamedProperty("scalePoint", try env.createFunction(
"scalePoint",
2,
zapi.createCallback(2, scale_point, .{
.returns = .string,
}),
null,
));

try module.setNamedProperty("sum", try env.createFunction(
"sum",
1,
zapi.createCallback(1, sum, .{}),
null,
));

try module.setNamedProperty("swap", try env.createFunction(
"swap",
1,
zapi.createCallback(1, swap, .{}),
null,
));

try module.setNamedProperty("reverse3", try env.createFunction(
"reverse3",
1,
zapi.createCallback(1, reverse3, .{}),
null,
));

try module.setNamedProperty("nextColor", try env.createFunction(
"nextColor",
1,
zapi.createCallback(1, next_color, .{}),
null,
));

try module.setNamedProperty("copyArrayBuffer", try env.createFunction(
"copyArrayBuffer",
1,
Expand Down Expand Up @@ -176,6 +225,47 @@ fn surprise() []const u8 {
return "Surprise!";
}

var greet_buf: [256]u8 = undefined;

fn greet(name: []const u8, title: ?[]const u8) ![]const u8 {
if (title) |t| return std.fmt.bufPrint(&greet_buf, "Hello, {s} {s}!", .{ t, name });
return std.fmt.bufPrint(&greet_buf, "Hey {s}!", .{name});
}

const Point = struct {
x: f64,
y: f64,
label: ?[]const u8,
};

fn scale_point(p: Point, factor: f64) Point {
return .{ .x = p.x * factor, .y = p.y * factor, .label = p.label };
}

fn sum(values: []const i32) i32 {
var total: i32 = 0;
for (values) |v| total += v;
return total;
}

fn swap(pair: struct { i32, bool }) struct { bool, i32 } {
return .{ pair[1], pair[0] };
}

fn reverse3(a: [3]u8) [3]u8 {
return .{ a[2], a[1], a[0] };
}

const Color = enum(u8) { red, green, blue };

fn next_color(color: Color) Color {
return switch (color) {
.red => .green,
.green => .blue,
.blue => .red,
};
}

fn copy_arraybuffer(env: zapi.Env, cb: zapi.CallbackInfo(1)) !zapi.Value {
if (try cb.arg(0).getValueBool()) return try env.createArrayBufferCopy("", null);
return try env.createArrayBufferCopy("copy me", null);
Expand Down
14 changes: 10 additions & 4 deletions src/create_callback.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub fn createCallback(
info: CallbackInfo(argc_cap),
) anyerror!Value {
var args: Args = undefined;
// Backs strings and slices read from arguments; freed after `func` returns.
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer arena.deinit();

var cb_arg_i: usize = 0;
inline for (0..fn_info.params.len) |i| {
Expand All @@ -38,7 +41,7 @@ pub fn createCallback(
const is_value_arg = arg_hint == .value or (arg_hint == .auto and param_type == Value);

if (arg_hint == .data) {
args[i] = @alignCast(@ptrCast(info.data));
args[i] = @ptrCast(@alignCast(info.data));
} else if (is_env_arg) {
args[i] = env;
} else if (is_value_arg) {
Expand All @@ -49,11 +52,14 @@ pub fn createCallback(
args[i] = value_arg;
cb_arg_i += 1;
} else {
const value_arg = info.getArg(cb_arg_i) orelse {
if (info.getArg(cb_arg_i)) |value_arg| {
args[i] = try fromValue(param_type, value_arg, arg_hint, arena.allocator());
} else if (@typeInfo(param_type) == .optional) {
args[i] = null;
} else {
try env.throwError("ERR_INVALID_ARG_COUNT", "Invalid callback argument count");
return error.PendingException;
};
args[i] = try fromValue(param_type, value_arg, arg_hint);
}
cb_arg_i += 1;
}
}
Expand Down
110 changes: 105 additions & 5 deletions src/to_from_value.zig
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
const std = @import("std");
const c = @import("c.zig").c;
const status = @import("status.zig");
const Env = @import("Env.zig");
const OwnedBuffer = @import("OwnedBuffer.zig");
const Value = @import("Value.zig");

/// Converts a JS value into `T`. Composite types (optionals, enums, structs,
/// tuples, arrays and slices) are converted recursively and pass `hint` down
/// to their elements. Strings and slices are allocated from `allocator`.
pub fn fromValue(
comptime T: type,
value: Value,
comptime hint: anytype,
allocator: std.mem.Allocator,
) !T {
const type_info = @typeInfo(T);
switch (type_info) {
Expand Down Expand Up @@ -35,20 +41,79 @@ pub fn fromValue(
return @floatCast(try value.getValueDouble());
}
},
.pointer => |p| {
if (p.child == u8 and p.size == .slice) {
if (hint == .buffer) {
return try value.getBufferInfo();
.optional => |o| {
return switch (try value.typeof()) {
.null, .undefined => null,
else => try fromValue(o.child, value, hint, allocator),
};
},
.@"enum" => |e| {
const n = try fromValue(e.tag_type, value, .auto, allocator);
return std.enums.fromInt(T, n) orelse error.InvalidArg;
},
.array => |a| {
try expectArrayLength(value, a.len);
var result: T = undefined;
for (&result, 0..) |*item, i| {
item.* = try fromValue(a.child, try value.getElement(@intCast(i)), hint, allocator);
}
return result;
},
.@"struct" => |s| {
var result: T = undefined;
if (s.is_tuple) {
try expectArrayLength(value, s.fields.len);
inline for (s.fields, 0..) |field, i| {
@field(result, field.name) = try fromValue(field.type, try value.getElement(i), hint, allocator);
}
} else {
if ((try value.typeof()) != .object) return error.ObjectExpected;
inline for (s.fields) |field| {
const prop = try value.getNamedProperty(field.name ++ "");
@field(result, field.name) = try fromValue(field.type, prop, hint, allocator);
}
}
return error.GenericFailure; // Unsupported pointer type
return result;
},
.pointer => |p| {
if (p.size != .slice or p.sentinel_ptr != null) {
return error.GenericFailure; // Unsupported pointer type
}
if (p.child == u8) {
return try bytesFromValue(value, hint, allocator);
}
const len = try value.getArrayLength();
const items = try allocator.alloc(p.child, len);
for (items, 0..) |*item, i| {
item.* = try fromValue(p.child, try value.getElement(@intCast(i)), hint, allocator);
}
return items;
},
else => {
return error.GenericFailure; // Unsupported type
},
}
}

/// `.buffer` borrows the Buffer's memory, `.string` copies a JS string into
/// `allocator`, and `.auto` accepts either.
fn bytesFromValue(value: Value, comptime hint: anytype, allocator: std.mem.Allocator) ![]u8 {
if (hint == .buffer) return try value.getBufferInfo();
if (hint != .string and (try value.typeof()) != .string) return try value.getBufferInfo();

var len: usize = 0;
try status.check(c.napi_get_value_string_utf8(value.env, value.value, null, 0, &len));
// N-API always writes a null terminator, so the buffer needs one extra byte.
const buf = try allocator.alloc(u8, len + 1);
const str = try value.getValueStringUtf8(buf);
return buf[0..str.len];
}

fn expectArrayLength(value: Value, len: usize) !void {
if (!try value.isArray()) return error.ArrayExpected;
if ((try value.getArrayLength()) != len) return error.InvalidArg;
}

pub fn toValue(
comptime T: type,
v: T,
Expand Down Expand Up @@ -113,8 +178,35 @@ pub fn toValue(
if (p.size == .one and child_type_info == .array and child_type_info.array.child == u8) {
return try env.createBufferCopy(@ptrCast(v), null);
}
if (p.size == .slice) {
return try arrayToValue(p.child, v, env, hint);
}
return error.GenericFailure; // Unsupported pointer type
},
.optional => |o| {
if (v) |inner| return try toValue(o.child, inner, env, hint);
return try env.getNull();
},
.@"enum" => |e| {
return try toValue(e.tag_type, @intFromEnum(v), env, .auto);
},
.array => |a| {
return try arrayToValue(a.child, &v, env, hint);
},
.@"struct" => |s| {
if (s.is_tuple) {
const array = try env.createArrayWithLength(s.fields.len);
inline for (s.fields, 0..) |field, i| {
try array.setElement(i, try toValue(field.type, @field(v, field.name), env, hint));
}
return array;
}
const object = try env.createObject();
inline for (s.fields) |field| {
try object.setNamedProperty(field.name ++ "", try toValue(field.type, @field(v, field.name), env, hint));
}
return object;
},
.error_union => |eu| {
return try toValue(eu.payload, try v, env, hint);
},
Expand All @@ -126,3 +218,11 @@ pub fn toValue(
},
}
}

fn arrayToValue(comptime Child: type, items: []const Child, env: Env, comptime hint: anytype) !Value {
const array = try env.createArrayWithLength(items.len);
for (items, 0..) |item, i| {
try array.setElement(@intCast(i), try toValue(Child, item, env, hint));
}
return array;
}
Loading