Skip to content

Add initial Node-API (N-API) PoC scaffolding#1606

Open
ksh8281 wants to merge 11 commits into
Samsung:masterfrom
ksh8281:napi
Open

Add initial Node-API (N-API) PoC scaffolding#1606
ksh8281 wants to merge 11 commits into
Samsung:masterfrom
ksh8281:napi

Conversation

@ksh8281

@ksh8281 ksh8281 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Vendor node-api-headers and a sparse nodejs/node test corpus, implement enough js_native_api.h to pass test/js-native-api/2_function_arguments.

Vendor node-api-headers and a sparse nodejs/node test corpus, implement
enough js_native_api.h to pass test/js-native-api/2_function_arguments.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
ksh8281 added 10 commits July 13, 2026 17:14
…ty/call_function

Unlocks 3_callbacks, 4_object_factory, 5_function_factory TCs; cctest napi
addon build list generalized into a FOREACH loop instead of per-TC blocks.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
…object_wrap

napi_define_class uses FunctionTemplateRef (auto-allocates `this` on
construct, unlike plain FunctionObjectRef); napi_ref strong/weak refcounting
maps onto PersistentValueRefMap::add()/remove(). Also generalizes the cctest
napi addon build list to handle mixed .c/.cc TC sources, and auto-enables
ESCARGOT_USE_EXTENDED_API when ESCARGOT_NAPI is on.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
…zer env

Add napi_create_uint32/napi_get_value_uint32. Generalize the cctest addon
build list to support multi-source addons (7_factory_wrap needs two .cc
files). Fix a segfault: ObjectWrap's wrapped-instance finalizers captured a
napi_env__ that lived in a nested lambda's stack frame, so once that frame
returned, a later test's GC pass could invoke the finalizer with a dangling
env. Move envData to the test's own frame and force collection before it
returns, matching the WeakPtr/Finalizer GC-forcing pattern already used
elsewhere in cctest.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
Move napi_env__ into a NapiEnv member (NapiEnv::env()) so a napi_callback
captured by something long-lived (e.g. a napi_wrap finalizer) never captures
a dangling env once the Evaluator::execute call that created it returns.
Simplifies all cctest napi_env__ boilerplace down to napiEnv->env().

This is a distinct fix from the "flush wrapped objects via forced GC before
a test ends" pattern already used in ObjectWrap/FactoryWrap - both are still
needed, since leftover GC garbage can otherwise get finalized at an unsafe
moment (verified via gdb: mid-construction of an unrelated test's VMInstance).

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
Flush wrap'd/finalizer-bearing garbage before a NapiEnv's Context/VMInstance
tear down, so nothing lingers to be opportunistically finalized later against
a torn-down env. Not exercised by current tests (none call delete on a
NapiEnv yet - they intentionally leak like the rest of this codebase's
cctest style), verified manually with a temporary delete call.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
napi_wrap's always-weak ref and napi_create_reference(initial_refcount==0)
now register a finalizer on the target that clears ref->value when it is
GC'd, so napi_get_reference_value stops returning a dangling pointer.
napi_delete_reference unregisters that finalizer before freeing ref, since
ref is a plain (non-GC) allocation and a later collection would otherwise
invoke the finalizer with a dangling data pointer.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
napi_remove_wrap only cleared the object's extraData() before, leaving the
napi_wrap finalizer registered; a real Node-API napi_remove_wrap must
suppress it instead. NapiEnv now keeps a side table (HashMap<ObjectRef*,
void*>) from wrapped object to its WrapFinalizeData*, since extraData()
itself must stay exactly the caller's native_object for napi_unwrap.
napi_remove_wrap looks up and unregisters the finalizer via this table;
NapiWrapFinalizer itself also clears its own entry when the object is
actually collected, so a later object reusing the same address never sees
a stale entry.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
Each call adds/removes exactly one PersistentValueRefMap rooting unit,
keeping napi_ref's refcount field equal to how many add() calls it has
made for its target - the same invariant napi_create_reference's initial
loop and napi_delete_reference's teardown loop already rely on. The
weak-staleness finalizer (NapiWeakRefFinalizer) is registered/unregistered
only at the weak<->strong boundary, guarded by a new
napi_ref__::weakFinalizerRegistered flag so repeated ref()/unref() calls
don't register it more than once. napi_reference_ref errors on a weak ref
whose target is already collected; napi_reference_unref errors when the
refcount is already 0, matching js_native_api.h's documented contract.

While writing the regression test for the weak-after-unref path, found
that PersistentValueRefMap::remove() could leave a value rooted forever:
tsl::robin_map's erase() only marks a bucket empty (its destructor call on
a raw pointer key is a no-op) without clearing the bucket's memory, and
since the map's backing array is itself GC-managed and thus conservatively
scanned, the leftover ValueRef* bytes kept looking like a live reference
until some unrelated future insert() happened to reuse that exact bucket.
Fixed by forcing a rehash right after the erase, which allocates a fresh
(GC_malloc-zeroed) backing array containing only the currently-valid
entries. PersistentValueRefMap has no other consumer in the engine today,
so this is scoped to napi.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
~NapiEnv() is the only environment-teardown hook this PoC has, so run the
instance data finalizer there, once, before the wrap/finalizer GC-flush
safety net that already lives in that destructor. The finalizer pointer is
cleared before invocation to avoid any re-entrant double-call.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
napi_open_handle_scope/napi_close_handle_scope/
napi_open_escapable_handle_scope/napi_close_escapable_handle_scope/
napi_escape_handle are pure bookkeeping (LIFO nesting + escape-once
enforcement) since napi_value is already a GC-rooted pointer here, unlike
V8's buffered handles.

While adding the real test_handle_scope TC as a regression test, found
that napi_call_function let a JS exception escape as a raw C++ exception
across the N-API call boundary instead of reporting napi_pending_exception.
Fixed by running the call through Evaluator::execute's nested-SandBox
overload.

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant