Skip to content
Open
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
69 changes: 47 additions & 22 deletions Core/Node-API-JSI/Include/napi/napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1321,20 +1321,37 @@ inline const T* TypedArrayOf<T>::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 <typename Call>
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<const jsi::Value&>(error.Value())}};
}
}

template <typename Callable, typename Return>
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)}; });
}
};

template <typename Callable>
struct Function<Callable, void> {
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 {}; });
}
};
}
Expand Down Expand Up @@ -2295,8 +2312,10 @@ ObjectWrap<T>::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<T>(callbackInfo)));
return {};
return details::CallHost(env, rt, [&]() -> jsi::Value {
thisVal.getObject(rt).setProperty(rt, env->native_name, jsi::Object::createFromHostObject(rt, std::make_shared<T>(callbackInfo)));
return {};
});
})).getObject(rt).getFunction(rt)};

*newTarget = {rt, static_cast<const jsi::Object&>(constructor)};
Expand All @@ -2316,28 +2335,26 @@ ObjectWrap<T>::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()) {
Expand All @@ -2346,30 +2363,38 @@ ObjectWrap<T>::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()) {
Expand Down
34 changes: 34 additions & 0 deletions Tests/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);

Expand Down
5 changes: 5 additions & 0 deletions Tests/UnitTests/Shared/Shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Napi::Function>().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));
});
Expand Down