-
Notifications
You must be signed in to change notification settings - Fork 465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
objectwrap: gracefully handle constructor exceptions #600
Conversation
@gabrielschulhof If I well understood you delayed the creation of the object reference in a function |
@gabrielschulhof My initial solution to this was the changes you proposed; however, the solution removes some functionality that may break existing ObjectWrap users (maybe this is a reasonable tradeoff). With the proposed change, native subclasses of ObjectWrap can no longer reference the javascript via Value() and Unwrap() could not be used.
The above code feels contrived, but the rough equivalent in javascript seems more common.
An alternate solution would be to introduce an ObjectWrap overridable Construct() function. Subclasses would put constructor logic in the new function. Basically, separating the C++/napi_ref construction from the constructor logic. Then, ConstructCallWrapper could do:
This could also be a breaking change and I have not thought through all of the use cases. |
@dananderson good point! In that case, IINM the best we can do is, in T* instance;
napi_value wrapper = details::WrapCallback([&] {
CallbackInfo callbackInfo(env, info);
#ifdef NAPI_CPP_EXCEPTIONS
try {
instance = new T(callbackInfo);
} catch (const std::exception& e) {
napi_remove_wrap(env, info.This(), instance);
throw e;
}
#else
if (env.IsExceptionPending()) {
napi_remove_wrap(env, info.This(), instance, ...);
delete instance;
}
#endif Still, the scenario you present lends me to believe that the constructor chain which ultimately throws the exception may, in real addons, have lots of side effects which need to be cleaned up. At least with this modification it will fail with an exception rather than a crash, so that should spotlight the place where things need fixing, and should hopefully get people to look at all the side effects of their constructors. |
@gabrielschulhof I believe napi_remove_wrap() does not remove the finalizer (and C++this) from the napi_ref, so the GC will still try to delete the invalid pointer. I could be wrong, but this is my understanding from my quick look at the related V8 code (Reference). And, I did not see a way in V8 to disassociate the finalizer from the Reference. Another solution is to use finalizer hint. Though, I think this is a bit messy and would require a heap allocation. I don't see a great solution to this without breaking user code. |
bf8519d
to
9347e10
Compare
@dananderson AFAICT with the change in this PR the When C++ exceptions are used, it is deleted when the error is thrown, because the superclass of When C++ exceptions are not used, Either way, it looks like the failed object construction is now cleaned up correctly. |
... although it looks like Node.js v8.x does not have all our |
9347e10
to
f9a8178
Compare
I would prefer calling std::unique_ptr<T> instance(new T(cb_info));
napi_wrap(...);
instance.release();
return args.This(); |
@dananderson pointed out that moving the |
Just to make sure. Is it really needed to call |
Moreover this code in ObjectWrap constructor may be an undefined behavior and incorrect Lines 2971 to 2973 in 24d75dd
According to standard
Object of T is not constructed but static_cast is used to cast to T*. So that I'm not an expert in JS but this code looks broken and prints an unexpected value (however strictly defined): class Dependency {
constructor(my_class) {
console.log("Value: " + my_class.value)
}
}
class MyClass {
constructor() {
this.dependency = new Dependency(this)
this.value = 5
}
}
const c = new MyClass() This code is similar to unwrapping and using |
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600
f9a8178
to
683bfcc
Compare
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600
683bfcc
to
a1acd41
Compare
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600
a1acd41
to
0cc1985
Compare
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600
0cc1985
to
7ee9bfd
Compare
CI: That's right – v8.x doesn't have the patch in core that makes references work properly! Good thing it's going EOL post haste 🙂 |
@@ -3106,11 +3137,11 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) { | |||
napi_value wrapper = callbackInfo.This(); | |||
napi_status status; | |||
napi_ref ref; | |||
T* instance = static_cast<T*>(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dmitryash it's actually possible to avoid this cast, because this
works just fine in the code below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gabrielschulhof While I agree that this is nicer this way, it also requires updating the cast in FinalizeCallback
like #475 does
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gabrielschulhof It seems to me both #638 and #600 latest changes have some drawbacks in the implementation, but I guess we can't make it ideal so lets go with these #600 last changes here. I wish to make some naming suggestions to make the names more generic. Please consider them but if you think it does not improve or it's not worth it, then just ignore them and merge after CI works. I will make comments inline with the code tab. |
try { | ||
new ConstructorExceptionTest(); | ||
} catch (anException) { | ||
gotException = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use assert.throws(() =>
then the test will fail if there is no exception in the constructor. And currently the global.gc()
call will fail the process leaving npm to show a generic error message. I think it's slightly better if we fail the test if there is no exception from the constructor. Just a notch better. what do you think? Or just swap the assert before the gc
call maybe.
@gabrielschulhof I will post the naming suggestion here for readability
My way of thinking about this is the caller of Otherwise I think the implementation is great and fixes the issue. So 👍 for a merge. I am not sure about the CI. cheers |
@blagoev I went with |
This fix will not work on v8.x so we need to wait for #643 to land. |
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600
759800a
to
868b4bd
Compare
@KevinEady @mhdawson could you PTAL? I removed the "blocked" label, because the PR removing v8.x and v6.x support has landed. |
|
||
const test = (binding) => { | ||
const { ConstructorExceptionTest } = binding.objectwrapConstructorException; | ||
assert.throws(() => (new ConstructorExceptionTest())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be improved to validate that we get the same exception as we expect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
CI:
|
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: #599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: #600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Landed in 86384f9. |
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs/node-addon-api#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs/node-addon-api#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs/node-addon-api#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs/node-addon-api#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs/node-addon-api#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs/node-addon-api#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs/node-addon-api#599 Co-authored-by: blagoev <lubo@blagoev.com> PR-URL: nodejs/node-addon-api#600 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Move the
napi_wrap()
step out of theNapi::ObjectWrap<T>::ObjectWrap()
constructor to ensure that nonative instance pointer is associated with the JavaScript object under
construction if the native constructor should cause a JavaScript
exception to be thrown.
Fixes: #599