diff --git a/Core/Node-API-JSI/Include/napi/napi-inl.h b/Core/Node-API-JSI/Include/napi/napi-inl.h index 14fb745c..91cccede 100644 --- a/Core/Node-API-JSI/Include/napi/napi-inl.h +++ b/Core/Node-API-JSI/Include/napi/napi-inl.h @@ -1321,11 +1321,29 @@ inline const T* TypedArrayOf::Data() const { namespace details { + // Native callbacks report JavaScript errors either by throwing Napi::Error or by leaving an + // exception pending after ThrowAsJavaScriptException(). Convert both forms at the host-function + // boundary so JSI observes the original JavaScript value, matching the other Node-API backends. + template + inline jsi::Value CallHost(napi_env env, jsi::Runtime& rt, Call&& call) { + try { + auto result = call(); + if (!env->last_exception.isUndefined()) { + auto error = std::move(env->last_exception); + env->last_exception = jsi::Value::undefined(); + throw jsi::JSError{rt, jsi::Value{rt, error}}; + } + return result; + } catch (const Napi::Error& error) { + throw jsi::JSError{rt, jsi::Value{rt, static_cast(error.Value())}}; + } + } + template struct Function { static inline jsi::Value Callback(napi_env env, const jsi::Value& thisVal, const jsi::Value* args, size_t count, void* data, Callable cb) { CallbackInfo callbackInfo{env, thisVal, args, count, {}, data}; - return {env->rt, cb(callbackInfo)}; + return CallHost(env, env->rt, [&]() -> jsi::Value { return {env->rt, cb(callbackInfo)}; }); } }; @@ -1333,8 +1351,7 @@ namespace details struct Function { static inline jsi::Value Callback(napi_env env, const jsi::Value& thisVal, const jsi::Value* args, size_t count, void* data, Callable cb) { CallbackInfo callbackInfo{env, thisVal, args, count, {}, data}; - cb(callbackInfo); - return {}; + return CallHost(env, env->rt, [&]() -> jsi::Value { cb(callbackInfo); return {}; }); } }; } @@ -2295,8 +2312,10 @@ ObjectWrap::DefineClass(napi_env env, [env, newTarget, data](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { CallbackInfo callbackInfo{env, thisVal, args, count, *newTarget, data}; // TODO: use prototype to wrap object? - thisVal.getObject(rt).setProperty(rt, env->native_name, jsi::Object::createFromHostObject(rt, std::make_shared(callbackInfo))); - return {}; + return details::CallHost(env, rt, [&]() -> jsi::Value { + thisVal.getObject(rt).setProperty(rt, env->native_name, jsi::Object::createFromHostObject(rt, std::make_shared(callbackInfo))); + return {}; + }); })).getObject(rt).getFunction(rt)}; *newTarget = {rt, static_cast(constructor)}; @@ -2316,28 +2335,26 @@ ObjectWrap::DefineClass(napi_env env, if (p.staticVoidMethod != nullptr) { descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, - [env, method{p.staticVoidMethod}, data{p.data}](jsi::Runtime& /*rt*/, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - (*method)({env, thisVal, args, count, nullptr, data}); - return {}; + [env, method{p.staticVoidMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { + return details::CallHost(env, rt, [&]() -> jsi::Value { (*method)({env, thisVal, args, count, nullptr, data}); return {}; }); })); } else if (p.staticMethod != nullptr) { descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.staticMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - return {rt, (*method)({env, thisVal, args, count, nullptr, data})}; + return details::CallHost(env, rt, [&]() -> jsi::Value { return {rt, (*method)({env, thisVal, args, count, nullptr, data})}; }); })); } else if (p.staticGetter != nullptr || p.staticSetter != nullptr) { if (p.staticGetter != nullptr) { descriptor.setProperty(rt, "get", jsi::Function::createFromHostFunction(rt, name, 0, [env, getter{p.staticGetter}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count)-> jsi::Value { - return {rt, (*getter)({env, thisVal, args, count, nullptr, data})}; + return details::CallHost(env, rt, [&]() -> jsi::Value { return {rt, (*getter)({env, thisVal, args, count, nullptr, data})}; }); })); } if (p.staticSetter != nullptr) { descriptor.setProperty(rt, "set", jsi::Function::createFromHostFunction(rt, name, 0, [env, setter{p.staticSetter}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count)-> jsi::Value { - (*setter)({env, thisVal, args, count, nullptr, data}, {env, {rt, args[0]}}); - return {}; + return details::CallHost(env, rt, [&]() -> jsi::Value { (*setter)({env, thisVal, args, count, nullptr, data}, {env, {rt, args[0]}}); return {}; }); })); } } else if (!p.staticValue.IsEmpty()) { @@ -2346,30 +2363,38 @@ ObjectWrap::DefineClass(napi_env env, } else if (p.instanceVoidMethod != nullptr) { descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.instanceVoidMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; - (nativeObject->*method)({env, thisVal, args, count, nullptr, data}); - return {}; + return details::CallHost(env, rt, [&]() -> jsi::Value { + T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; + (nativeObject->*method)({env, thisVal, args, count, nullptr, data}); + return {}; + }); })); } else if (p.instanceMethod != nullptr) { descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.instanceMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; - return {rt, (nativeObject->*method)({env, thisVal, args, count, nullptr, data})}; + return details::CallHost(env, rt, [&]() -> jsi::Value { + T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; + return {rt, (nativeObject->*method)({env, thisVal, args, count, nullptr, data})}; + }); })); } else if (p.instanceGetter != nullptr || p.instanceSetter != nullptr) { if (p.instanceGetter != nullptr) { descriptor.setProperty(rt, "get", jsi::Function::createFromHostFunction(rt, name, 0, [env, getter{p.instanceGetter}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; - return {rt, (nativeObject->*getter)({env, thisVal, args, count, nullptr, data})}; + return details::CallHost(env, rt, [&]() -> jsi::Value { + T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; + return {rt, (nativeObject->*getter)({env, thisVal, args, count, nullptr, data})}; + }); })); } if (p.instanceSetter != nullptr) { descriptor.setProperty(rt, "set", jsi::Function::createFromHostFunction(rt, name, 0, [env, setter{p.instanceSetter}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { - T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; - (nativeObject->*setter)({env, thisVal, args, count, nullptr, data}, {env, {rt, args[0]}}); - return {}; + return details::CallHost(env, rt, [&]() -> jsi::Value { + T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; + (nativeObject->*setter)({env, thisVal, args, count, nullptr, data}, {env, {rt, args[0]}}); + return {}; + }); })); } } else if (!p.instanceValue.IsEmpty()) { diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 654c60b6..47dbaf45 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -8,6 +8,7 @@ Mocha.reporter('spec'); declare const hostPlatform: string; declare const hostEngine: string; declare const setExitCode: (code: number) => void; +declare const throwPendingAfterCallback: (callback: () => void) => void; describe("AbortController", function () { @@ -1291,6 +1292,39 @@ describe("URL", function () { }); // URL.createObjectURL / revokeObjectURL (blob: URL registry) +describe("native exceptions", function () { + it("reach scripts as the thrown error object, with its class and message, on every engine", function () { + // A polyfill throwing Napi::TypeError from native code must arrive as that TypeError. The JSI + // adapter used to deliver a generic Error("Exception in HostFunction: ...") instead. + let fromMethod: any; + try { + URL.createObjectURL({} as any); + } catch (e) { + fromMethod = e; + } + expect(fromMethod).to.be.an.instanceof(TypeError); + expect(fromMethod.message).to.match(/not a Blob/); + expect(fromMethod.message).to.not.match(/HostFunction/); + + let fromConstructor: any; + try { + new URL("not a url"); + } catch (e) { + fromConstructor = e; + } + expect(fromConstructor).to.be.an.instanceof(TypeError); + expect(fromConstructor.message).to.match(/Invalid URL/); + expect(fromConstructor.message).to.not.match(/HostFunction/); + }); + + it("propagate from the pending exception slot after a reentrant callback", function () { + let callbackRan = false; + expect(() => throwPendingAfterCallback(() => { callbackRan = true; })) + .to.throw(TypeError, "pending exception after callback"); + expect(callbackRan).to.equal(true); + }); +}); + describe("URL.createObjectURL", function () { this.timeout(0); diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 1c7e9ff7..13728e36 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -99,6 +99,11 @@ TEST(JavaScript, All) "setExitCode"); env.Global().Set("setExitCode", setExitCodeCallback); + env.Global().Set("throwPendingAfterCallback", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + info[0].As().Call({}); + Napi::TypeError::New(info.Env(), "pending exception after callback").ThrowAsJavaScriptException(); + })); + env.Global().Set("hostPlatform", Napi::Value::From(env, JSRUNTIMEHOST_PLATFORM)); env.Global().Set("hostEngine", Napi::Value::From(env, NAPI_JAVASCRIPT_ENGINE)); });