Skip to content
Merged
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
6 changes: 6 additions & 0 deletions examples/basic/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ export declare function raw_string_len(value: string): number;
export declare function copied_string_len(value: string): number;
export declare const text: string;
export declare function throw_error(): void;
export declare function result_ok(): number;
export declare function result_error(): number;
export declare function result_void_ok(): void;
export declare function result_after_try(input: boolean): number;
export declare function throw_zig_error(): void;
export declare function throw_zig_error_value(): number;
export declare function fib(n: number): void;
export declare function fib_async(n: number): Promise<number>;
export declare function fib_async_progress(
Expand Down
25 changes: 25 additions & 0 deletions examples/basic/src/err.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,28 @@ const napi = @import("napi");
pub fn throw_error() !void {
return napi.Error.fromReason("test");
}

pub fn result_ok() napi.Result(i32) {
return napi.Result(i32).Ok(42);
}

pub fn result_error() napi.Result(i32) {
return napi.Result(i32).Err(napi.Error.withReason("result error"));
}

pub fn result_void_ok() napi.Result(void) {
return napi.Result(void).Ok({});
}

pub fn result_after_try(input: bool) !napi.Result(i32) {
if (input) return napi.Result(i32).Ok(100);
return napi.Result(i32).Err(napi.Error.withTypeError("result type error"));
}

pub fn throw_zig_error() !void {
return error.ZigNativeFailure;
}

pub fn throw_zig_error_value() !i32 {
return error.ZigValueFailure;
}
6 changes: 6 additions & 0 deletions examples/basic/src/hello.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pub const copied_string_len = string.copied_string_len;
pub const text = string.text;

pub const throw_error = err.throw_error;
pub const result_ok = err.result_ok;
pub const result_error = err.result_error;
pub const result_void_ok = err.result_void_ok;
pub const result_after_try = err.result_after_try;
pub const throw_zig_error = err.throw_zig_error;
pub const throw_zig_error_value = err.throw_zig_error_value;

pub const fib = worker.fib;
pub const fib_async = async_examples.fib_async;
Expand Down
12 changes: 11 additions & 1 deletion node-test/napi/__tests__/values.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,19 @@ test("global, undefined, null and symbol", (t) => {
});

test("Result", (t) => {
t.throws(bindings.throwError);
t.throws(bindings.throwError, { message: /native error/ });
t.throws(bindings.throwZigError, { message: /ZigNativeFailure/ });
t.throws(bindings.throwTypeError, { instanceOf: TypeError });
t.throws(bindings.throwRangeError, { instanceOf: RangeError });
t.is(bindings.resultOk(), 42);
t.throws(bindings.resultErr, { message: /result error/ });
t.is(bindings.resultVoidOk(), undefined);
t.is(bindings.resultAfterTry(true), 100);
t.throws(() => bindings.resultAfterTry(false), {
instanceOf: TypeError,
message: /result type error/,
});
t.throws(bindings.throwZigErrorValue, { message: /ZigValueFailure/ });
});

test("buffer", (t) => {
Expand Down
6 changes: 6 additions & 0 deletions node-test/napi/src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ pub const setSymbolInObj = values.setSymbolInObj;
pub const throwError = values.throwError;
pub const throwTypeError = values.throwTypeError;
pub const throwRangeError = values.throwRangeError;
pub const resultOk = values.resultOk;
pub const resultErr = values.resultErr;
pub const resultVoidOk = values.resultVoidOk;
pub const resultAfterTry = values.resultAfterTry;
pub const throwZigError = values.throwZigError;
pub const throwZigErrorValue = values.throwZigErrorValue;
pub const getBuffer = values.getBuffer;
pub const getEmptyBuffer = values.getEmptyBuffer;
pub const getEmptyBufferFromNew = values.getEmptyBufferFromNew;
Expand Down
25 changes: 25 additions & 0 deletions node-test/napi/src/values.zig
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,31 @@ pub fn throwRangeError() !void {
return napi.Error.rangeError("range error from native");
}

pub fn resultOk() napi.Result(i32) {
return napi.Result(i32).Ok(42);
}

pub fn resultErr() napi.Result(i32) {
return napi.Result(i32).Err(napi.Error.withReason("result error"));
}

pub fn resultVoidOk() napi.Result(void) {
return napi.Result(void).Ok({});
}

pub fn resultAfterTry(input: bool) !napi.Result(i32) {
if (input) return napi.Result(i32).Ok(100);
return napi.Result(i32).Err(napi.Error.withTypeError("result type error"));
}

pub fn throwZigError() !void {
return error.ZigNativeFailure;
}

pub fn throwZigErrorValue() !i32 {
return error.ZigValueFailure;
}

pub fn getBuffer(env: napi.Env) !napi.Buffer {
return try napi.Buffer.copy(env, "hello world"[0..]);
}
Expand Down
42 changes: 26 additions & 16 deletions src/build/napi-tsgen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ fn isAbortSignalType(comptime T: type) bool {
return @hasDecl(T, "is_napi_abort_signal");
}

fn isResultType(comptime T: type) bool {
switch (@typeInfo(T)) {
.@"union" => {},
else => return false,
}
return @hasDecl(T, "is_napi_result") and T.is_napi_result;
}

fn resultPayloadType(comptime T: type) type {
return T.payload_type;
}

fn functionReturnPayloadType(comptime T: type) type {
const payload = switch (@typeInfo(T)) {
.error_union => |eu| eu.payload,
else => T,
};
return if (comptime isResultType(payload)) resultPayloadType(payload) else payload;
}

fn asyncResultType(comptime T: type) type {
return T.async_result_type;
}
Expand Down Expand Up @@ -974,6 +994,8 @@ fn isIdentifierChar(ch: u8) bool {
}

fn emitType(state: *State, comptime T: type) ![]const u8 {
if (comptime isResultType(T)) return emitType(state, resultPayloadType(T));

switch (T) {
void => return "void",
bool => return "boolean",
Expand Down Expand Up @@ -1212,20 +1234,14 @@ fn emitMethodSignature(state: *State, writer: *StringBuilder, comptime fn_type:
try emitMethodParams(state, writer, fn_type, skip_first, param_names);

const ret = info.return_type.?;
const ret_payload = switch (@typeInfo(ret)) {
.error_union => |eu| eu.payload,
else => ret,
};
const ret_payload = functionReturnPayloadType(ret);
try appendFmt(writer, ": {s}", .{try emitType(state, ret_payload)});
}

fn emitMethodParams(state: *State, writer: *StringBuilder, comptime fn_type: type, comptime skip_first: bool, param_names: ?[]const []const u8) !void {
const info = @typeInfo(fn_type).@"fn";
const ret = info.return_type.?;
const ret_payload = switch (@typeInfo(ret)) {
.error_union => |eu| eu.payload,
else => ret,
};
const ret_payload = functionReturnPayloadType(ret);
var first = true;
const total = if (skip_first) info.params.len - 1 else info.params.len;
const source_offset: usize = if (param_names) |names|
Expand Down Expand Up @@ -1288,10 +1304,7 @@ fn emitClassDecl(state: *State, comptime ExportName: []const u8, comptime T: typ
const fn_info = @typeInfo(decl_type).@"fn";
const is_instance = fn_info.params.len > 0 and (fn_info.params[0].type.? == *Wrapped or fn_info.params[0].type.? == Wrapped);
const ret = fn_info.return_type.?;
const ret_payload = switch (@typeInfo(ret)) {
.error_union => |eu| eu.payload,
else => ret,
};
const ret_payload = functionReturnPayloadType(ret);
const is_factory = ret_payload == Wrapped or ret_payload == *Wrapped;
if (!is_instance and is_factory) {
const method_param_names = try state.source.getClassMethodParamNames(ExportName, decl.name);
Expand Down Expand Up @@ -1344,10 +1357,7 @@ fn emitExportFunction(state: *State, comptime name: []const u8, comptime fn_valu
}

const ret = info.return_type.?;
const ret_payload = switch (@typeInfo(ret)) {
.error_union => |eu| eu.payload,
else => ret,
};
const ret_payload = functionReturnPayloadType(ret);
if (comptime isAsyncDescriptorType(ret_payload) and asyncHasEvents(ret_payload)) {
if (!first) try append(&state.exports, ", ");
try appendFmt(&state.exports, "onEvent?: {s}", .{try emitAsyncEventCallbackType(state, ret_payload)});
Expand Down
9 changes: 5 additions & 4 deletions src/napi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub const Array = value.Array;

pub const Error = err.Error;
pub const Status = err.Status;
pub const Result = err.Result;
pub const JsError = err.JsError;
pub const JsTypeError = err.JsTypeError;
pub const JsRangeError = err.JsRangeError;
Expand Down Expand Up @@ -90,11 +91,11 @@ pub fn resetOperationAllocator() void {
pub fn AsyncContext(comptime Event: type) type {
return async.AsyncContext(Event);
}
pub fn Async(comptime Result: type, comptime runtime: async.RuntimeModel) type {
return async.Async(Result, runtime);
pub fn Async(comptime AsyncResult: type, comptime runtime: async.RuntimeModel) type {
return async.Async(AsyncResult, runtime);
}
pub fn AsyncWithEvents(comptime Result: type, comptime Event: type, comptime runtime: async.RuntimeModel) type {
return async.AsyncWithEvents(Result, Event, runtime);
pub fn AsyncWithEvents(comptime AsyncResult: type, comptime Event: type, comptime runtime: async.RuntimeModel) type {
return async.AsyncWithEvents(AsyncResult, Event, runtime);
}

pub const NODE_API_MODULE = module.NODE_API_MODULE;
Expand Down
11 changes: 1 addition & 10 deletions src/napi/async.zig
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,7 @@ pub fn AsyncContext(comptime Event: type) type {
}

pub fn mapAnyError(err: anyerror) NapiError.Error {
if (NapiError.last_error) |last_error| return last_error;

return switch (err) {
error.Canceled, error.Cancelled => NapiError.Error.withReason(@as([]const u8, "AbortError")),
error.Closing => NapiError.Error.withStatus(@as([]const u8, "Closing")),
else => |actual_err| blk: {
const name = @errorName(actual_err);
break :blk NapiError.Error.withStatus(name[0..name.len]);
},
};
return NapiError.mapAnyError(err);
}

fn createOptionalCallbackRef(env: napi.napi_env, raw: ?napi.napi_value) !?napi.napi_ref {
Expand Down
10 changes: 10 additions & 0 deletions src/napi/util/napi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,16 @@ pub const Napi = struct {
const value_type = @TypeOf(value);
const infos = @typeInfo(value_type);

if (comptime NapiError.isResult(value_type)) {
return switch (value) {
.ok => |payload| try Napi.to_napi_value_auto(env, payload, name),
.err => |err| {
NapiError.last_error = err;
return error.GenericFailure;
},
};
}

switch (value_type) {
NapiValue.BigInt, NapiValue.Bool, NapiValue.Number, NapiValue.String, NapiValue.Object, NapiValue.Promise, NapiValue.Array, NapiValue.Undefined, NapiValue.Null, Buffer, ArrayBuffer, DataView => {
return value.raw;
Expand Down
114 changes: 64 additions & 50 deletions src/napi/value/function.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,68 @@ pub fn Function(comptime Args: type, comptime Return: type) type {
}
}

fn inner_fn(inner_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const return_info = infos.@"fn".return_type.?;
const return_payload = switch (@typeInfo(return_info)) {
fn returnPayloadType(comptime T: type) type {
const payload = switch (@typeInfo(T)) {
.error_union => |eu| eu.payload,
else => return_info,
else => T,
};
return if (comptime NapiError.isResult(payload)) NapiError.resultPayload(payload) else payload;
}

fn undefinedValue(inner_env: napi.napi_env) napi.napi_value {
return Undefined.New(Env.from_raw(inner_env)).raw;
}

fn throwAndUndefined(inner_env: napi.napi_env, err: NapiError.Error) napi.napi_value {
err.throwInto(Env.from_raw(inner_env));
return undefinedValue(inner_env);
}

fn throwAnyAndUndefined(inner_env: napi.napi_env, err: anyerror) napi.napi_value {
return throwAndUndefined(inner_env, NapiError.mapAnyError(err));
}

fn completePayload(
inner_env: napi.napi_env,
payload: anytype,
event_listener: napi.napi_value,
abort_signal: ?AbortSignal,
cleanup_params: *bool,
) napi.napi_value {
if (comptime helper.isAsyncDescriptor(@TypeOf(payload))) {
cleanup_params.* = false;
var task = payload;
const promise = task.scheduleWithListenerAndSignal(Env.from_raw(inner_env), event_listener, abort_signal) catch |err| {
return throwAnyAndUndefined(inner_env, err);
};
return promise.raw;
}

return Napi.to_napi_value_auto(inner_env, payload, null) catch |err| {
return throwAnyAndUndefined(inner_env, err);
};
}

fn completeReturn(
inner_env: napi.napi_env,
ret: anytype,
event_listener: napi.napi_value,
abort_signal: ?AbortSignal,
cleanup_params: *bool,
) napi.napi_value {
if (comptime NapiError.isResult(@TypeOf(ret))) {
return switch (ret) {
.ok => |payload| completePayload(inner_env, payload, event_listener, abort_signal, cleanup_params),
.err => |err| throwAndUndefined(inner_env, err),
};
}

return completePayload(inner_env, ret, event_listener, abort_signal, cleanup_params);
}

fn inner_fn(inner_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const return_info = infos.@"fn".return_type.?;
const return_payload = returnPayloadType(return_info);
const async_returns_descriptor = comptime helper.isAsyncDescriptor(return_payload);
const has_async_events = comptime async_returns_descriptor and return_payload.async_has_events;
const expected_argc = params.len - env_index + if (has_async_events) 1 else 0;
Expand Down Expand Up @@ -108,55 +164,13 @@ pub fn Function(comptime Args: type, comptime Return: type) type {
null;

if (@typeInfo(return_info) == .error_union) {
const ret = @call(.auto, value, napi_params) catch {
if (NapiError.last_error) |last_err| {
last_err.throwInto(Env.from_raw(inner_env));
}
const undefined_value = Undefined.New(Env.from_raw(inner_env));
return undefined_value.raw;
const ret = @call(.auto, value, napi_params) catch |err| {
return throwAnyAndUndefined(inner_env, err);
};
if (comptime async_returns_descriptor) {
cleanup_params = false;
var task = ret;
const promise = task.scheduleWithListenerAndSignal(Env.from_raw(inner_env), event_listener, abort_signal) catch {
if (NapiError.last_error) |last_err| {
last_err.throwInto(Env.from_raw(inner_env));
}
const undefined_value = Undefined.New(Env.from_raw(inner_env));
return undefined_value.raw;
};
return promise.raw;
}
const n_value = Napi.to_napi_value_auto(inner_env, ret, null) catch {
if (NapiError.last_error) |last_err| {
last_err.throwInto(Env.from_raw(inner_env));
}
const undefined_value = Undefined.New(Env.from_raw(inner_env));
return undefined_value.raw;
};
return n_value;
return completeReturn(inner_env, ret, event_listener, abort_signal, &cleanup_params);
} else {
const ret = @call(.auto, value, napi_params);
if (comptime async_returns_descriptor) {
cleanup_params = false;
var task = ret;
const promise = task.scheduleWithListenerAndSignal(Env.from_raw(inner_env), event_listener, abort_signal) catch {
if (NapiError.last_error) |last_err| {
last_err.throwInto(Env.from_raw(inner_env));
}
const undefined_value = Undefined.New(Env.from_raw(inner_env));
return undefined_value.raw;
};
return promise.raw;
}
const n_value = Napi.to_napi_value_auto(inner_env, ret, null) catch {
if (NapiError.last_error) |last_err| {
last_err.throwInto(Env.from_raw(inner_env));
}
const undefined_value = Undefined.New(Env.from_raw(inner_env));
return undefined_value.raw;
};
return n_value;
return completeReturn(inner_env, ret, event_listener, abort_signal, &cleanup_params);
}
}
};
Expand Down
Loading
Loading