-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
n-api: add napi_define_subclass #36148
n-api: add napi_define_subclass #36148
Conversation
Review requested:
|
Recommend reviewing while ignoring whitespace, because at least one large blocks has ended up inside an if-statement. |
9ea6e84
to
bbd1bef
Compare
assert.strictEqual(superItem.chainableMethod('something'), 'something-1'); | ||
assert.strictEqual(subItem.chainableMethod('something'), '1-something-1'); | ||
assert.strictEqual(superItem.value, 5); | ||
assert.strictEqual(subItem.value, 5); |
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.
Do any other subclass/superclass interactions come to mind that I should be testing here?
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.
It’s worth verifying that the prototype chains are set up correctly, I think
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.
@addaleax I added some tests 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.
This adds a really large amount of complexity for something that is not a hugely common pattern in native code, with lots of implementation details (e.g. the brand checks in the JS code generation) that different use cases may or may not want to have.
This is a really good example of something that should go into userland, imo.
The idea of having this in N-API sounds good to me, but I'll leave it to others in the N-API team to decide whether the complexity of this particular implementation is worth it in core (or whether we should wait for the engine to implement this instead of working around it ourselves in the N-API) |
Add the ability to define a subclass of a JS class by generating a JS snippet that uses the `extends` syntax and calls the native bindings which are passed in as parameters to the generated function responsible for defining the subclass. An example of a generated JS snippet might be: ```js (function(parent, ctor, getSuperParams, prop0, prop1) { 'use strict'; class NativeSubclass extends parent { constructor() { super(...getSuperParams.apply(null, arguments)); ctor.apply(this, arguments); } subMethod() { if (!(this instanceof NativeSubclass)) throw new Error('Illegal invocation'); return prop0.apply(this, arguments); } chainableMethod() { if (!(this instanceof NativeSubclass)) throw new Error('Illegal invocation'); return prop1.apply(this, arguments); } } return NativeSubclass; }) ``` where `ctor`, `getSuperParams`, `prop0`, and `prop1` are native functions supplied to `napi_define_subclass`. Signed-off-by: Gabriel Schulhof <[email protected]> Refs: nodejs/node-addon-api#229
bbd1bef
to
e3db130
Compare
@addaleax now with things like We also have a precedent for doing things without the engine's assistance at first. In the case of There's also the aspect of doing inheritance correctly, and that, for the sake of that, it should be done once and (hopefully) well. Additionally, we've had requests for inheritance before: nodejs/node-addon-api#229. Don't get me wrong though. I'm also ambivalent about landing this because it doesn't use any internals, and discussion is exactly what I'm hoping for. |
Right, I agree with that – and I would argue that for answering specific questions like this, userland modules don’t generally do a worse job than Node.js core. I’m not saying this shouldn’t exist, I’m just wondering why we wouldn’t want to implement this as a userland library instead. I’ll still try to give this a good review, regardless of where it ends up. :) |
That's a good point. Could this be added as utility in https://github.com/nodejs/node-addon-api instead? I'm all for having this functionality, especially given that I have several scenarios right now that I'm working through that could definitely use this, but it might not need to go into core -- at least not yet. |
* `[in] env`: The environment that the API is invoked under. | ||
* `[in] parent_constructor`: Constructor of the parent class. | ||
* `[in] utf8name`: Name of the JavaScript constructor function; this is | ||
not required to be the same as the C++ class name, though it is recommended |
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.
This is talking about C++ everywhere, but C is what we mean, right?
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.
Yeah, I copied the doc mostly from napi_define_class
, updating for the different functionality. I suppose it might be worth updating the doc for napi_define_class
to not assume folks write C++ classes. I'd like to place that beyond the scope of this PR though.
PostProcess(const std::string& key, const napi_property_descriptor* prop) { | ||
if (prop->attributes == napi_default && prop->value == nullptr) return; | ||
|
||
post_process += " " + key + ": {\n"; |
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.
What if key
contains characters that are not valid identifiers?
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.
napi_run_script
will return non-napi_ok
if compiling the script fails. The caller can deal with that.
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.
Yes, but that’s still a bug?
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.
You're right. We should never concat user-supplied strings into the JS snippet directly, because they can contain weird characters. I changed the code so that key is always [propn]
, and, if the prop descriptor provides a utf8name
instead of a name
, it napi_create_string_utf8()
first and then passes that as a parameter into the resulting function. So, the JS snippet never actually contains literal strings supplied by the user.
(method == desc->getter) ? "get " + key + "()" : | ||
"set " + key + "(x)") + " {\n" | ||
" if (!(this instanceof " + classname + "))\n" | ||
" throw new Error('Illegal invocation');\n" |
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.
Why are we adding this brand check? JS classes don’t do it, and for C++ classes, it’s not necessarily the right thing to do in the presence of prototype manipulation
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.
Classes created using napi_define_class
have such a brand check built-in to ensure that internal field counts line up. The engine does this. Check the assert.throws()
I added to test_constructor. It also throws for a class created using napi_define_class
, which I have not modified (other than to factor out the code that adds static members for reuse with napi_define_subclass
).
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.
Yeah, the point is that this does not actually ensure that the internal fields are present as expected.
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.
@addaleax in that case I think I need to back it up with type tags and throw the exception if the tag is absent. This also means that I have to likely subclass v8impl::CallbackWrapper
to do the check before calling into the add-on.
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.
@addaleax ... or, rather, replace the instanceof
check in the JS snippet with a type-tag check on the native side.
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.
@addaleax ... which also means that, for efficiency's sake, it might be best located in core 😉🙂
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.
I think if we’re talking efficiency then generating a new script per subclass is already throwing that out of the window ;)
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.
🙂
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.
@addaleax actually, unless one generates subclasses in a tight loop, only the startup performance is hit by the need to compile JS code. I guess I was talking about the price for a call.
JS -> v8impl::CallbackWrapper ----> (napi_env env, napi_callback_info info) -> (napi_env env, napi_callback_info info)
type tag check happens here
as would be an external implementation, is more expensive than
JS -> v8impl::CallbackWrapper ----> (napi_env env, napi_callback_info info)
type tag check happens here
which is how it would be done internally.
assert.strictEqual(superItem.chainableMethod('something'), 'something-1'); | ||
assert.strictEqual(subItem.chainableMethod('something'), '1-something-1'); | ||
assert.strictEqual(superItem.value, 5); | ||
assert.strictEqual(subItem.value, 5); |
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.
It’s worth verifying that the prototype chains are set up correctly, I think
@jasnell in those scenarios, are you using node-addon-api? The reason I ask is that if we land this in node-addon-api and you want to use plain C, we'd have to land it both as-is, and make available a In effect, this would re-animate the concept that parts of N-API ship with node-addon-api, whereas the rest is in core. We had a very similar setup (where we ship a complete copy of N-API) in the beginning, when not all maintained versions of Node.js contained N-API and so, when building an addon against those versions, we'd build N-API as a static library and link against that rather than core. |
3b20d4b
to
6c3a53d
Compare
94521d3
to
a5c8e6c
Compare
I'll need more time to better understand the balance between keeping N-API lean to avoid problems with keeping it stable versus the benefit. @gabrielschulhof could you give the team an overview in the next meeting? Since you are "ambivalent" about landing it, I'd initially lean towards waiting until we have "pull/demand" since it does add significant complexity. |
@mhdawson the one thing that has changed since nodejs/node-addon-api#229 (comment) is that there is no doubt that any and all engines can implement this functionality, because they can just do what this implementation does, meaning compile a JS snippet to generate a function that then performs the subclassing and plugs in the native bindings. The N-API implementation would need only provide
in order to implement There is AFAICT "pull/demand" to have this, both from @jasnell above and from nodejs/node-addon-api#229, the initial comment of which has received 5 👍s, and all of them (AFAICT) from folks outside of the core collaborators. The OP of nodejs/node-addon-api#661 also stumbled upon nodejs/node-addon-api#229 and closed their own issue as a duplicate, so I'd say that adds a 👍 because the OP is not among the 5 who have 👍-ed nodejs/node-addon-api#229. I expressed ambivalence because I am aware that this is a big addition and so I am prepared to abandon this change, or relocate it to another repo. I wanted to collect responses while having concrete code to look at. I still need to implement the brand check on the native side because, as @addaleax pointed out, |
@mhdawson found another person who needs this: nodejs/node-addon-api#246 |
Add the ability to define a subclass of a JS class by generating a JS
snippet that uses the
extends
syntax and calls the native bindingswhich are passed in as parameters to the generated function responsible
for defining the subclass.
An example of a generated JS snippet might be:
where
ctor
,getSuperParams
,prop0
, andprop1
are nativefunctions supplied to
napi_define_subclass
.Signed-off-by: @gabrielschulhof
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes