diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index 95e282c..77ca454 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -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; export declare function fib_async_progress( diff --git a/examples/basic/src/err.zig b/examples/basic/src/err.zig index 744c89a..5cc115d 100644 --- a/examples/basic/src/err.zig +++ b/examples/basic/src/err.zig @@ -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; +} diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index 3e4b93a..748358f 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -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; diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index 6187954..3dfb301 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -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) => { diff --git a/node-test/napi/src/lib.zig b/node-test/napi/src/lib.zig index 1a2b452..b4d8ad5 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -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; diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index ae929a0..fbb0fb7 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -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..]); } diff --git a/src/build/napi-tsgen.zig b/src/build/napi-tsgen.zig index 99d5207..a6e7d9b 100644 --- a/src/build/napi-tsgen.zig +++ b/src/build/napi-tsgen.zig @@ -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; } @@ -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", @@ -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| @@ -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); @@ -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)}); diff --git a/src/napi.zig b/src/napi.zig index b690a06..bb1b5dd 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -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; @@ -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; diff --git a/src/napi/async.zig b/src/napi/async.zig index eb8fdbb..b82e6db 100644 --- a/src/napi/async.zig +++ b/src/napi/async.zig @@ -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 { diff --git a/src/napi/util/napi.zig b/src/napi/util/napi.zig index fd0351e..ff78ab0 100644 --- a/src/napi/util/napi.zig +++ b/src/napi/util/napi.zig @@ -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; diff --git a/src/napi/value/function.zig b/src/napi/value/function.zig index 80d4594..e41ce2d 100644 --- a/src/napi/value/function.zig +++ b/src/napi/value/function.zig @@ -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; @@ -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); } } }; diff --git a/src/napi/wrapper/class.zig b/src/napi/wrapper/class.zig index 5d61cbb..e8bfa46 100644 --- a/src/napi/wrapper/class.zig +++ b/src/napi/wrapper/class.zig @@ -79,6 +79,46 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { return argc; } + fn returnPayloadType(comptime ReturnType: type) type { + const payload = switch (@typeInfo(ReturnType)) { + .error_union => |eu| eu.payload, + else => ReturnType, + }; + return if (comptime NapiError.isResult(payload)) NapiError.resultPayload(payload) else payload; + } + + fn throwAnyAndNull(env: napi.napi_env, err: anyerror) napi.napi_value { + NapiError.mapAnyError(err).throwInto(napi_env.Env.from_raw(env)); + return null; + } + + fn toNapiReturn(env: napi.napi_env, value: anytype, comptime name: []const u8) napi.napi_value { + return Napi.to_napi_value_auto(env, value, name) catch |err| { + return throwAnyAndNull(env, err); + }; + } + + fn factoryValueFromPayload(env: napi.napi_env, payload: anytype) ?T { + const Payload = @TypeOf(payload); + if (Payload == T) return payload; + if (Payload == *T) return payload.*; + _ = env; + @compileError("Factory method must return " ++ @typeName(T) ++ " or *" ++ @typeName(T) ++ ", got: " ++ @typeName(Payload)); + } + + fn factoryValueFromResult(env: napi.napi_env, result: anytype) ?T { + if (comptime NapiError.isResult(@TypeOf(result))) { + return switch (result) { + .ok => |payload| factoryValueFromPayload(env, payload), + .err => |err| { + err.throwInto(napi_env.Env.from_raw(env)); + return null; + }, + }; + } + return factoryValueFromPayload(env, result); + } + fn constructor_callback(env: napi.napi_env, callback_info: napi.napi_callback_info) callconv(.c) napi.napi_value { const constructor_arg_count = comptime blk: { if (HasInit and @hasDecl(T, "init")) { @@ -110,17 +150,18 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { tuple_args[i] = converted; } - if (@typeInfo(init_fn_info.@"fn".return_type.?) == .error_union) { - data.* = @call(.auto, init_fn, tuple_args) catch { - if (NapiError.last_error) |last_err| { - last_err.throwInto(napi_env.Env.from_raw(env)); - } + const init_result = if (@typeInfo(init_fn_info.@"fn".return_type.?) == .error_union) + @call(.auto, init_fn, tuple_args) catch |err| { + NapiError.mapAnyError(err).throwInto(napi_env.Env.from_raw(env)); instance.destroyUninitialized(); return null; - }; - } else { - data.* = @call(.auto, init_fn, tuple_args); - } + } + else + @call(.auto, init_fn, tuple_args); + data.* = factoryValueFromResult(env, init_result) orelse { + instance.destroyUninitialized(); + return null; + }; } else { data.* = std.mem.zeroes(T); if (comptime HasInit) { @@ -161,7 +202,13 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { var instance_data: T = undefined; if (params.len == 0) { - instance_data = factory_fn() catch return null; + const factory_result = if (@typeInfo(factory_fn_info.@"fn".return_type.?) == .error_union) + factory_fn() catch |err| { + return throwAnyAndNull(env, err); + } + else + factory_fn(); + instance_data = factoryValueFromResult(env, factory_result) orelse return null; } else { var tuple_args: std.meta.ArgsTuple(factory_fn_type) = undefined; inline for (params, 0..) |param, i| { @@ -174,9 +221,13 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { tuple_args[i] = converted; } if (@typeInfo(factory_fn_info.@"fn".return_type.?) == .error_union) { - instance_data = @call(.auto, factory_fn, tuple_args) catch return null; + const factory_result = @call(.auto, factory_fn, tuple_args) catch |err| { + return throwAnyAndNull(env, err); + }; + instance_data = factoryValueFromResult(env, factory_result) orelse return null; } else { - instance_data = @call(.auto, factory_fn, tuple_args); + const factory_result = @call(.auto, factory_fn, tuple_args); + instance_data = factoryValueFromResult(env, factory_result) orelse return null; } } @@ -368,11 +419,9 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { const method_args_offset = if (is_instance_method) 1 else 0; const return_type = method_info.@"fn".return_type.?; + const return_payload = returnPayloadType(return_type); const is_factory_method = blk: { - if ((return_type == T or return_type == *T)) break :blk true; - if (@typeInfo(return_type) == .error_union) { - if (@typeInfo(return_type).error_union.payload == T or @typeInfo(return_type).error_union.payload == *T) break :blk true; - } + if (return_payload == T or return_payload == *T) break :blk true; break :blk false; }; @@ -433,8 +482,14 @@ pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type { tuple_args[i] = converted; initialized_args = i + 1; } + if (@typeInfo(return_type) == .error_union) { + const result = @call(.auto, method, tuple_args) catch |err| { + return throwAnyAndNull(method_env, err); + }; + return toNapiReturn(method_env, result, fn_name); + } const result = @call(.auto, method, tuple_args); - return Napi.to_napi_value_auto(method_env, result, fn_name) catch null; + return toNapiReturn(method_env, result, fn_name); } }; diff --git a/src/napi/wrapper/error.zig b/src/napi/wrapper/error.zig index e36aa1d..53837f6 100644 --- a/src/napi/wrapper/error.zig +++ b/src/napi/wrapper/error.zig @@ -39,10 +39,12 @@ pub const ErrorStatus = error{ DetachableArraybufferExpected, WouldDeadlock, NoExternalBuffersAllowed, + CannotRunJs, + RuntimeSpecific24, Unknown, }; -fn toError(status: Status) anyerror { +pub fn toError(status: Status) anyerror { return switch (status) { .InvalidArg => error.InvalidArg, .ObjectExpected => error.ObjectExpected, @@ -66,6 +68,8 @@ fn toError(status: Status) anyerror { .DetachableArraybufferExpected => error.DetachableArraybufferExpected, .WouldDeadlock => error.WouldDeadlock, .NoExternalBuffersAllowed => error.NoExternalBuffersAllowed, + .CannotRunJs => error.CannotRunJs, + .RuntimeSpecific24 => error.RuntimeSpecific24, else => error.Unknown, }; } @@ -199,6 +203,21 @@ pub const Error = union(enum) { return Error{ .JsError = JsError.fromStatus(status) }; } + pub fn withCodeAndMessage(code: []const u8, message: []const u8) Error { + return Error{ + .JsError = JsError{ + .status = null, + .message = message, + .mode = JsErrorType{}, + .custom_status = code, + }, + }; + } + + pub fn fromAnyError(err: anyerror) Error { + return mapAnyError(err); + } + pub fn withTypeError(reason: []const u8) Error { return Error{ .JsTypeError = JsTypeError.fromMessage(reason) }; } @@ -245,3 +264,58 @@ pub const Error = union(enum) { } } }; + +pub fn mapAnyError(err: anyerror) Error { + if (last_error) |last_err| { + clearLastError(); + return last_err; + } + + return switch (err) { + error.Canceled, error.Cancelled => Error.withReason("AbortError"), + error.Closing => Error.withStatus(@as([]const u8, "Closing")), + else => |actual_err| blk: { + const name = @errorName(actual_err); + break :blk Error.withCodeAndMessage(name[0..name.len], name[0..name.len]); + }, + }; +} + +pub fn throwAnyErrorInto(err: anyerror, env: Env) void { + mapAnyError(err).throwInto(env); +} + +pub fn Result(comptime T: type) type { + return union(enum) { + pub const is_napi_result = true; + pub const payload_type = T; + + ok: T, + err: Error, + + const Self = @This(); + + pub fn Ok(value: T) Self { + return .{ .ok = value }; + } + + pub fn Err(err: Error) Self { + return .{ .err = err }; + } + }; +} + +pub fn isResult(comptime T: type) bool { + switch (@typeInfo(T)) { + .@"union" => {}, + else => return false, + } + return @hasDecl(T, "is_napi_result") and T.is_napi_result; +} + +pub fn resultPayload(comptime T: type) type { + if (!isResult(T)) { + @compileError("Type is not napi.Result: " ++ @typeName(T)); + } + return T.payload_type; +} diff --git a/src/napi/wrapper/status.zig b/src/napi/wrapper/status.zig index 675c0aa..bf5cfc5 100644 --- a/src/napi/wrapper/status.zig +++ b/src/napi/wrapper/status.zig @@ -33,11 +33,51 @@ pub const Status = enum(u32) { Unknown = 1024, // unknown status. for example, using napi3 module in napi7 Node.js, and generate an invalid napi3 status pub fn from_raw(raw: napi.napi_status) Status { - return @as(Status, @enumFromInt(@as(u32, @intCast(raw)))); + const status_code: u32 = @intCast(raw); + return switch (status_code) { + 0 => .Ok, + 1 => .InvalidArg, + 2 => .ObjectExpected, + 3 => .StringExpected, + 4 => .NameExpected, + 5 => .FunctionExpected, + 6 => .NumberExpected, + 7 => .BooleanExpected, + 8 => .ArrayExpected, + 9 => .GenericFailure, + 10 => .PendingException, + 11 => .Cancelled, + 12 => .EscapeCalledTwice, + 13 => .HandleScopeMismatch, + 14 => .CallbackScopeMismatch, + 15 => .QueueFull, + 16 => .Closing, + 17 => .BigintExpected, + 18 => .DateExpected, + 19 => .ArrayBufferExpected, + 20 => .DetachableArraybufferExpected, + 21 => .WouldDeadlock, + 22 => .NoExternalBuffersAllowed, + 23 => .CannotRunJs, + 24 => .RuntimeSpecific24, + else => .Unknown, + }; } pub fn New(status: anytype) Status { - return @as(Status, @enumFromInt(@as(u32, @intCast(status)))); + return Status.from_raw(status); + } + + pub fn isOk(self: Status) bool { + return self == .Ok; + } + + pub fn code(self: Status) u32 { + return @intFromEnum(self); + } + + pub fn toString(self: Status) []const u8 { + return self.ToString(); } pub fn ToString(self: Status) []const u8 { diff --git a/src/napi/wrapper/worker.zig b/src/napi/wrapper/worker.zig index bcf7a73..879bd9d 100644 --- a/src/napi/wrapper/worker.zig +++ b/src/napi/wrapper/worker.zig @@ -36,10 +36,14 @@ pub fn WorkerContext(comptime T: type) type { } const ExecuteReturn = ExecuteInfo.@"fn".return_type.?; - const ExecutePayload = switch (@typeInfo(ExecuteReturn)) { + const ExecuteReturnPayload = switch (@typeInfo(ExecuteReturn)) { .error_union => |eu| eu.payload, else => ExecuteReturn, }; + const ExecutePayload = if (comptime NapiError.isResult(ExecuteReturnPayload)) + NapiError.resultPayload(ExecuteReturnPayload) + else + ExecuteReturnPayload; comptime validateExecuteSignature(DataType, ExecuteFn); @@ -126,9 +130,9 @@ pub fn WorkerContext(comptime T: type) type { fn execute(inner_env: napi.napi_env, data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(data)); NapiError.clearLastError(); - self.run(inner_env) catch { + self.run(inner_env) catch |err| { self.status = .Rejected; - self.err = NapiError.last_error orelse NapiError.Error.withStatus("GenericFailure"); + self.err = NapiError.mapAnyError(err); return; }; self.status = .Resolved; @@ -179,32 +183,41 @@ pub fn WorkerContext(comptime T: type) type { fn run(self: *Self, inner_env: napi.napi_env) !void { const execute_fn = self.data.Execute; - if (@typeInfo(ExecuteReturn) == .error_union) { - if (ExecutePayload == void) { - if (ExecuteInfo.@"fn".params.len == 1) { - try execute_fn(self.data.data); - } else { - try execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); + const Runner = struct { + fn storeResult(this: *Self, result: anytype) !void { + if (comptime NapiError.isResult(@TypeOf(result))) { + switch (result) { + .ok => |payload| { + if (ExecutePayload != void) { + this.result = payload; + } + }, + .err => |err| { + NapiError.last_error = err; + return error.GenericFailure; + }, + } + return; } - } else { - self.result = if (ExecuteInfo.@"fn".params.len == 1) - try execute_fn(self.data.data) - else - try execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); - } - } else { - if (ExecutePayload == void) { - if (ExecuteInfo.@"fn".params.len == 1) { - _ = execute_fn(self.data.data); - } else { - _ = execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); + + if (ExecutePayload != void) { + this.result = result; } - } else { - self.result = if (ExecuteInfo.@"fn".params.len == 1) - execute_fn(self.data.data) - else - execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); } + }; + + if (@typeInfo(ExecuteReturn) == .error_union) { + const result = if (ExecuteInfo.@"fn".params.len == 1) + try execute_fn(self.data.data) + else + try execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); + try Runner.storeResult(self, result); + } else { + const result = if (ExecuteInfo.@"fn".params.len == 1) + execute_fn(self.data.data) + else + execute_fn(napi_env.Env.from_raw(inner_env), self.data.data); + try Runner.storeResult(self, result); } } }; diff --git a/test/errors-tsfn.spec.ts b/test/errors-tsfn.spec.ts index ca05433..1812e63 100644 --- a/test/errors-tsfn.spec.ts +++ b/test/errors-tsfn.spec.ts @@ -42,5 +42,12 @@ async function waitForThreadSafeFunction(native: NativeAddon) { export async function testErrorsAndThreadSafeFunction(native: NativeAddon) { assertThrows(() => native.throw_error(), "test", "throw_error repeat"); + assertThrows(() => native.throw_zig_error(), "ZigNativeFailure", "throw_zig_error"); + assertEqual(native.result_ok(), 42, "result_ok"); + assertThrows(() => native.result_error(), "result error", "result_error"); + assertEqual(native.result_void_ok(), undefined, "result_void_ok"); + assertEqual(native.result_after_try(true), 100, "result_after_try ok"); + assertThrows(() => native.result_after_try(false), "result type error", "result_after_try error"); + assertThrows(() => native.throw_zig_error_value(), "ZigValueFailure", "throw_zig_error_value"); await waitForThreadSafeFunction(native); }