feat: Node-API (napi) surface for plugin developers - #437
Draft
edusperoni wants to merge 6 commits into
Draft
Conversation
Six files copied byte-identical from nodejs/node tag v26.7.0 (b4f23d3619c98bed09af93a21192f6080197a8c6): the engine-independent Node-API headers, the V8 implementation, and its impl header. node_api.cc is intentionally not vendored; its role is filled by the runtime-side embed layer added separately. vendor/README.md records provenance, build accommodations, and the re-sync procedure; NOTICE carries the Node.js license.
One napi_env per runtime (main and each Worker), created at the end of Runtime::Init and destroyed under the teardown Locker before DisposeAllRegistered, since its reference lists hold v8::Globals. The shim supplies the idioms js_native_api_v8.cc expects from Node's internal headers, keeping the vendored files byte-identical. NodeApiEmbed covers the node_api.h surface Node implements in node_api.cc: constructor-based module registration (with a node_module_register alias for napi-ios addon compatibility), version queries, fatal errors, and buffers over Uint8Array; async and threadsafe-function entry points are stubs returning napi_generic_failure until the async layer lands. Plugins reach the env through NativeScriptNapiEnv() (NapiRuntime.h, mirroring JSIRuntime), and registered addons load through require(name): bare specifiers only, consulted after the builtin fast path, with exports cached per env. Finalizers queued from V8 weak callbacks drain on the next runloop turn, matching Node's SetImmediate placement.
Exercises the napitestmodule fixture: value round-trips, property definition, error propagation with code, napi_wrap/unwrap, references, per-env exports caching, and the finalizer draining on the runloop turn after collection.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Replaces the Phase-1 stubs with the full async surface. Threadsafe functions follow Node's queue semantics (bounded queue, blocking and nonblocking call modes, acquire/release, abort -> napi_closing) with every JS-side step posted to the owning runtime's runloop — producer threads never take the isolate's Locker, which is the same invariant the Worker class-initialization deadlock taught us. napi_async_work executes on a GCD global queue and completes on the owning runloop, with napi_cancelled reported when cancel wins the race. Env teardown now enters the isolate (the destructor holds the Locker but no scopes), then runs cleanup hooks (one LIFO list, drained against the live registry so hooks may add and remove hooks), aborts surviving threadsafe functions — releasing blocked producers and handing undelivered items to the callback with a null env — and only then finalizes references, since each TSFN holds a ref to its JS callback. Known divergences, documented inline: ref/unref of a TSFN gates nothing (the app runloop never exits because of NAPI), a JS-thread call into its own full blocking queue returns napi_would_deadlock rather than blocking, and napi_delete_async_work refuses while the work is in flight.
Ports a high-value subset of Node's test/js-native-api assertions (strings with exact truncation and NUL semantics, number conversions including the int32/int64 sentinel tables, symbols, typed arrays and dataviews with bounds and detach, promises, errors with codes, references across the weak/strong boundary, exceptions, coercions, property definition) into a second fixture module and 70+ specs. Shared fixture helpers move to NapiTestSupport.h, which also fixes error reporting: the pending-exception probe clears the last error code, so the real message has to be captured before it. One ported expectation was wrong for this runtime and is now asserted as the actual behavior and documented: the global Promise is replaced by a Proxy (promise-proxy.js), so napi_is_promise reports false for promises constructed through it, while napi_create_promise and async-function promises report true. docs/node-api.md is the plugin-author guide: quickstart addon, registration and require() loading, threading contract, finalizer timing, version story, and the divergences-from-Node table.
The cross-thread resolution marshaling lives entirely in the wrapped executor; the per-instance Proxy only rebound then/catch/finally to the underlying promise, so removing it changes no scheduling behavior while making constructed promises real V8 promises again. Engine-level checks now hold for them — napi_is_promise reports true (divergence entry removed from docs/node-api.md), and the promise user code holds is the same object the unhandledrejection/rejectionhandled events carry, which the late-handler test now asserts directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Exposes a standard Node-API (
napi_*) surface to plugin authors, alongside the existing JSI facade — plugins can be written against the Node-API C ABI instead of raw V8.js_native_api_v8.cc+ public headers) from nodejs/node v26.7.0 (b4f23d36), byte-identical; provenance, license (MIT/OpenJS NOTICE), and a 3-line re-sync procedure live inNativeScript/napi/vendor/README.md. A small shim (NativeScript/napi/shim/) supplies the handful of idioms upstream expects from Node internals.napi_envper runtime (main + each Worker), created at the end ofRuntime::Init, destroyed under the teardown Locker beforeObjectManager::DisposeAllRegistered(env ref lists holdv8::Globals). NAPI refs/finalizers stay on stockv8::Global+SetWeak, byte-compatible with upstream — deliberately independent of the planned cppgc migration of runtime wrappers.napi_closing) where every JS-side step is posted to the owning runtime's runloop — producer threads never take the isolate'sLocker;napi_async_workexecutes on a GCD global queue and completes on the owning runloop; async contexts,napi_make_callback, callback scopes, and env/async cleanup hooks (LIFO, hooks may add/remove hooks while running).#include <NativeScript/NapiRuntime.h>→NativeScriptNapiEnv()(mirrorsJSIRuntime). Headers ship in the framework automatically.napi_module_register(plus anode_module_registeralias for napi-ios addon compatibility) and load from JS withrequire("name")— bare specifiers only, consulted after the builtin fast path, exports cached per env (Workers get their own instance).napi_is_promiseand other engine-level checks hold for promises made via the globalPromise, and rejection events carry the same object user code holds.SetImmediateplacement), never during GC.napi_get_version); envs use module API version 8 (Node's default), with the implications documented.docs/node-api.md— plugin-author quickstart (working addon + JS caller, registration, loading, threading contract, finalizer timing, version story) plus the full divergence reference.Divergences from Node
Everything in
js_native_api.his upstream, compiled unmodified. The differences are confined to thenode_api.hsurface, where Node's implementation depends on libuv,node::Buffer, or its module loader — All are written up indocs/node-api.md.napi_get_uv_event_loopandnode_api_get_module_file_namereturnnapi_generic_failure. There is nouv_loop_t— the runtime drives aCFRunLoop— and addons are linked into the app binary rather than loaded from a file, so nothing identifies the calling module.Uint8Arrays. There is nonode::Buffer, sonapi_is_bufferis exactly "is this aUint8Array".napi_create_external_bufferis still zero-copy; its finalizer runs from V8's backing-store deleter, and is skipped (leaking the data) if the isolate is already disposing.ref/unrefare no-ops. The runloop belongs to the app or the worker; it does not exit because an addon released its last reference. The flag is tracked so calls pair up, and gates nothing.napi_call_threadsafe_functionreturnsnapi_would_deadlockinstead of blocking, when a blocking call is made from the env's own thread with a full queue — the thread that would drain the queue is the caller. Node blocks regardless and never returns this status; wedging the runloop is worse than a status an addon may not expect.napi_delete_async_workrefuses queued or executing work (napi_generic_failure) rather than deleting it and leaving the queue holding a dangling pointer.napi_remove_async_cleanup_hookis safe.napi_fatal_exceptionreports and continues through the runtime's error handlers;napi_fatal_errorstill aborts, as upstream.NAPI_MODULEmacro is not the entry point. It only emits the symbols adlopenloader would scan for, and there is no such loader here; addons register from a constructor callingnapi_module_register.node_api.his the whole native surface — noprocess, nofs, no libuv handles, nonode.h/v8.h/uv.haccess.Testing
NapiTests.jsandNapiCoverageTests.js).test/js-native-apisuites: string encodings/truncation/NUL semantics, number conversion sentinel tables, symbols, typed arrays + dataviews (bounds, alignment, detach), promises, errors with codes, references (weak/strong transitions, GC-driven), exceptions, coercions, property definition — plus TSFN ordering/backpressure/reentry/abort, async work + cancel, cleanup hooks, and Worker isolation.nmon the built framework (Debug simulator + Release device): 145napi_*symbols +NativeScriptNapiEnvexported.Follow-ups (not in this PR)
require("<path>.node")dylib loading (dlopen+dlsym("napi_register_module_v1"), as napi-ios does).-fmodulesconsumers:NapiRuntime.hincludes non-modularnapi/vendor/*.hheaders; likely moot since the framework setsDEFINES_MODULE = NO, but worth confirming with a real plugin build.napi_create_external_buffer's backing-store deleter holds a rawnapi_env; guarded against teardown races (leaks instead of dangling), a full fix would mirror Node'sv8impl::Referenceownership.