Skip to content

N-API v7 across runtimes (V8 / JavaScriptCore / Chakra) - #189

Draft
matthargett wants to merge 62 commits into
BabylonJS:mainfrom
rebeckerspecialties:napi-v7
Draft

N-API v7 across runtimes (V8 / JavaScriptCore / Chakra)#189
matthargett wants to merge 62 commits into
BabylonJS:mainfrom
rebeckerspecialties:napi-v7

Conversation

@matthargett

Copy link
Copy Markdown

Third PR in the N-API stack (after #116 napi-tests, #183 napi-shared-android). Raises the default
NAPI_VERSION 5 -> 7 and brings the v6/v7 surface across every runtime. Fork CI: 22/22 green.

Engine BigInt instance_data ArrayBuffer detach
V8 (Android/Win/Linux) native yes yes
JavaScriptCore (macOS/iOS) C-API (macOS 15+) + JS-eval fallback yes transfer()
JavaScriptCore (jsc-android ~2020) ENOTSUP feature-detect (engine has no BigInt) yes n/a
Chakra (Win32/UWP) ENOTSUP feature-detect yes
JSI n/a — backend is the node-addon-api C++ wrapper, not the C API

Conformance is honest per engine: test_bigint runs for real on V8 + Apple JSC; a literal-free
test_bigint_unsupported fallback asserts the ENOTSUP throw on engines with no BigInt (jsc-android,
Chakra). test_instance_data runs everywhere v6 is supported. detach + get_all_property_names land at
the v9 bump (their vendored tests need node_api_basic_env); the detach feature is implemented now.

Notable: jsc-android (~2020) has no BigInt at all — its parser rejects 0n literals — so the JSC
backend feature-detects BigInt at env init and reports ENOTSUP, exactly like Chakra. The JSI backend
implements only the C++ Napi:: wrapper (zero C napi_*), so the C-API conformance suite is gated off
it; covering JSI would mean a full C-API-over-jsi port, out of scope here.

Also fixed several pre-existing cross-platform CI failures this surfaced (first broad fork CI of the
napi stack): node_lite engine selection by compile-define (Android-JSC, Linux-V8), <cstdint> on
Linux, napi-into-addon linking on Windows/UWP, the GSL C4875 warnings-as-error toolchain drift, the
JSI shared-header include path, and on-device surfacing of in-process node_lite failures.

Draft — stacked; base is upstream main so the diff includes the #116 + #183 commits until those merge
(merge order: #183, then #116, then this). Validated 22/22 on the fork CI matrix via the twin rebeckerspecialties#4.


Landing sequence

Inter-related N-API PRs; intended order (✅ done / ⏳ pending):

  1. Fix Windows/UWP CI build (MSVC C4875 + JSI include path) #185 — Fix Windows/UWP CI build (MSVC C4875 + JSI include path). Independent build/toolchain fix; lands first so CI is green under everything below. (fork twin Fix Windows/UWP CI build (MSVC C4875 + JSI include path) #185 — 22/22 green)
  2. Build napi as a shared library on Android #183 — Build napi as a shared library on Android (so in-process addons resolve napi_*).
  3. Add N-API compliance tests #116 — N-API compliance tests (the Android addons resolve napi_* via the shared libnapi.so from the step above).
  4. N-API v7 across runtimes (V8 / JSC / Chakra) — this PR. (fork twin N-API v7 across runtimes (V8 / JavaScriptCore / Chakra) rebeckerspecialties/JsRuntimeHost#4 — 22/22 green on the fork CI matrix.)this PR

Motivating RFCs: #186 (jsc-android → maintained JSC) · #187 (WebWorker via N-API → Factotum into BabylonNative).

…ction when the receiver was undefined, so the VM forcibly substituted the global object even in strict mode. The new implementation always routes through Function.prototype.call, preserving the exact thisArg. This only affected JSC: Chakra already pushes recv onto the argv array before invoking JsCallFunction, and V8 hands the raw recv value to Function::Call. Neither engine coerces in strict mode, so no additional fixes were required.
…bug fix in strict mode was actually found by the suite! The failing behavior was exercised by Tests/NodeApi/test/js-native-api/3_callbacks/test.js. New cmake targets emit a node-lite binary and a NodeApiTests binary, all currently enabled tests for currently supported NAPI v5 pass on Mac. Next step is to enable them for running in Android simulator.
…x handles during instrumentation, causing crashes, and now route console output through the new NodeLiteRuntime::Callbacks. On Android we forward stdout/stderr to logcat via callbacks to work around this for now. Added Android-specific shims (node_lite_android.cpp, child_process_android.cpp) so native module loading uses dlopen and JS child_process.spawnSync safely reports “unsupported”. Extended the Node‑API harness to allow in-process execution: RunNodeLiteScript captures output, SetNodeApiTestEnvironment lets the JNI layer provide a base directory and asset manager, and the GTest registration path uses that configuration instead of shelling out to the node_lite executable
…-- a use-after-free. Will check sanitizers under Android next
Two build-restoration fixes (no behavior/impl or NAPI-version changes), needed
after rebasing onto upstream HEAD and building with the current Xcode/libc++:

- node_lite: NodeApi::CallFunction took std::span<napi_value> but is only ever
  called with braced-init-lists ({a,b,c}). Newer libc++ correctly rejects
  constructing a non-const std::span from an initializer_list (that ctor is
  C++26). Switch the parameter to std::initializer_list<napi_value> (begin()
  yields the const napi_value* napi_call_function wants).

- Tests/NodeApi: the POST_BUILD copy_directory of each .node runs as an Xcode
  script phase BEFORE Xcode's implicit CodeSign phase signs the original, so the
  copied addons that node_lite/NodeApiTests dlopen are unsigned on a clean build
  and macOS refuses to load them. Ad-hoc sign the copies directly (APPLE only).
…mulator)

Build-restoration fixes for the Android in-process NodeApi harness after rebasing
onto upstream HEAD (no impl/NAPI-version changes). Each was a latent break in the
napi-tests Android integration, surfaced by a clean build on a current toolchain:

- CMakeLists.txt: drop the AndroidExtensions Globals.cpp 'patch' step. It file(COPY)'d
  patches/AndroidExtensions/Globals.cpp, which was never committed in any ref (author's
  local-only file). Upstream uses a newer AndroidExtensions pin and needs no patch.
- build.gradle: bump default ndkVersion 23.1.7779620 -> 28.2.13676358 (matches CI's
  NDK_VERSION). NDK 23's libc++ can't compile googletest 1.17.0's <=> usage. Also map the
  Android sanitizer flag JSR_ENABLE_ASAN -> ENABLE_SANITIZERS (the upstream option kept
  during the rebase).
- Tests/NodeApi/CMakeLists.txt: use ${JsRuntimeHost_SOURCE_DIR} instead of
  ${CMAKE_SOURCE_DIR} for Core/Node-API include paths. On Android JsRuntimeHost is added
  as a subdirectory of the app, so CMAKE_SOURCE_DIR was the app dir (headers not found);
  the project-scoped var is correct in both standalone (macOS) and nested (Android) builds.
- Tests/NodeApi/CMakeLists.txt: allow the .node modules to link with unresolved napi_*
  symbols on Android (-Wl,--unresolved-symbols=ignore-all), the ELF equivalent of Apple's
  -undefined dynamic_lookup; they bind at dlopen time from the host (UnitTestsJNI).
- Shared.cpp: gate the Android NodeApi-harness block on NODE_API_AVAILABLE_NATIVE_TESTS
  (defined only by UnitTestsJNI) so the standalone UnitTests target -- built but unused on
  Android -- doesn't try to compile AndroidExtensions/NodeApi code it doesn't link.
The instrumented run aborted with 'use of deleted global reference': the harness fell
back to android::global::GetAppContext() (GetFilesDir -> GetObjectClass) whose JNI global
ref is not valid during the instrumented run. JNI.cpp now computes a writable base dir from
the still-valid instrumentation Context and passes it plus the native AAssetManager to
SetNodeApiTestEnvironment() before RunTests() -- the wiring the harness was designed for
(see e1fce6b) but which was never actually connected.

This removes the crash and lets ConfigureNodeApiTests run. NOTE: on-device execution of the
NodeApi conformance tests is still not achieved -- CopyAssetsRecursive relies on
AAssetManager subdirectory enumeration (AAssetDir_getNextFileName lists files only, not
dirs) so the nested test tree isn't copied, and the native .node modules are neither
packaged nor loadable from an app-writable dir on API 29+. Tracked as follow-up.
Before this, the instrumented run passed vacuously -- no NodeApi tests ran. Several
layered fixes get them executing on the emulator (macOS path unchanged: still 12/12):

#1 Asset enumeration: AAssetManager can't list subdirectories, so CopyAssetsRecursive
   copied nothing. copyNodeApiTests now emits a file manifest (manifest.txt -- not a
   dotfile, which aapt would drop) and Shared.cpp copies each listed file.

#2 Native module packaging/loading: build each addon as lib<name>.so on Android so AGP
   packages it into lib/<abi>/ (nativeLibraryDir, the only dlopen-able location on API
   29+); node_lite_android loads it by soname; ResolveModulePath resolves the (on-disk
   absent) .node so LoadNativeModule runs.

V8 lifecycle (in-process node_lite shares the host's V8): reuse the host's already-
   initialized V8 platform (fixes 'Wrong initialization order'); hold a Locker +
   Isolate::Scope so multi-isolate access is locked (fixes 'Entering the V8 API without
   proper locking').

KNOWN REMAINING (tracked): node_lite calls Node-API outside any napi callback during
   NodeLiteRuntime::Initialize/script execution, which on V8 needs a live HandleScope +
   current Context. v8::HandleScope/Context::Scope are stack-only (operator new is
   private) so they can't be held across the holder; this needs a scope-wrapping rework
   of node_lite's V8 entry points (or napi_open_handle_scope + context enter). Until then
   the on-device native tests segfault in napi_create_object.
…eate_object segfault)

NodeApiEnvScope -> jsr_open_napi_env_scope was a no-op stub: it allocated a scope
struct but never entered the env's V8 isolate/context. On JSC that's fine (the env
carries its context explicitly), but on V8 node_lite then calls Node-API outside any
napi callback with no *current context*, so napi_create_object -> v8::Object::New(isolate)
segfaulted during NodeLiteRuntime::Initialize. Enter the env's context on open and exit
it on close (Android only). The in-process V8 runtime now initializes and runs tests.
… error

Step toward in-process error handling: ExitOnException was noexcept, but the in-process
runner installs a fatal handler that throws NodeLiteFatalError (rather than std::exit) so
the harness can turn a JS error into a ProcessResult. Throwing from the noexcept function
std::terminate'd the test process. Dropped noexcept so it propagates to RunNodeLiteScript.

(Partial: other noexcept teardown paths -- NodeApiHandleScope/NodeApiEnvScope dtors calling
NODE_LITE_CALL, and the env-holder dtor's onUnhandledError -> ExitWithJSError -- can still
throw during unwinding when a test errors. Full in-process error-path exception-safety is
the remaining Android item.)
NodeApiHandleScope/NodeApiEnvScope destructors used the throwing NODE_LITE_CALL, and the
JsRuntimeHostEnvHolder destructor's onUnhandledError can invoke the throwing in-process fatal
handler -- both std::terminate if they fire while a NodeLiteFatalError is unwinding. Make the
scope dtors ignore the close status and wrap onUnhandledError in try/catch.

Correct robustness fixes, but they do NOT yet resolve the remaining in-process failure: when a
test errors, a *second* NodeLiteFatalError is thrown during unwinding (double-exception ->
std::terminate). The escaping throw site isn't visible in the tombstone (stack already unwound)
and needs on-device lldb to pinpoint. macOS unaffected (12/12).
…winding

Don't re-throw NodeLiteFatalError from the in-process fatal handler when std::uncaught_exceptions()
> 0, to avoid a double-exception std::terminate. (Correct hardening, but the remaining in-process
abort is a *single* uncaught NodeLiteFatalError escaping RunNodeLiteScript's catch -- a scope-exit
destructor throw on a test that leaves a pending exception; needs on-device lldb to pinpoint.)
node_lite and the js-native-api addons are written against the C napi_* API. Core/Node-API-JSI
implements only the C++ Napi:: wrapper over jsi (no C napi_* symbols), and its napi.h carries its own
copy of the napi types, which collides with the shared <napi/js_native_api_types.h> the harness
includes (C2365 redefinition in node_lite). Skip the harness for JSI.
…ootstrap)

This branch had replaced upstream's plain cached JAVASCRIPTCORE_LIBRARY path with find_library +
FATAL_ERROR. Upstream's Hermes support configures a *host* build of this same source tree to bootstrap
hermesc/shermes (-D NAPI_JAVASCRIPT_ENGINE=Hermes), and that configure runs the elseif(UNIX) branch on
CI runners that have no libwebkit2gtk installed -- so the fatal error aborts the bootstrap and
Android_Hermes fails. Restore upstream's non-fatal form; the engine isn't even JavaScriptCore there.
…d engines

The harness sources were compiled into UnitTestsJNI for every Android engine. Hermes supplies napi via
its own hermesNapi library, which does not export napi_create_external / napi_get_value_external, so
Android_Hermes failed to link; QuickJS has not been validated on-device. Gate the harness sources (and
the NODE_API_AVAILABLE_NATIVE_TESTS list that drives them) behind an engine allow-list of V8 and
JavaScriptCore, matching the desktop gating in Tests/NodeApi/CMakeLists.txt.
@matthargett
matthargett force-pushed the napi-v7 branch 2 times, most recently from 26f9881 to e813292 Compare September 1, 2026 03:33
Hermes does not ship a js_native_api_hermes.cc -- its C napi_* functions live in the hermesNapi static
library. Building napi as a SHARED library therefore produces a libnapi.so that does not carry those
symbols, and everything linking it fails with undefined references (napi_wrap, napi_create_arraybuffer,
napi_create_external, ...). The shared-napi default exists so dlopen'd .node addons can resolve napi_*
at load; that harness is not built for Hermes, so keep napi static there.
Bump the [BABYLON-NATIVE-ADDITION] default (still -DNAPI_VERSION-overridable) so the v6/v7 surface is
exposed. Non-breaking: the enabled conformance addons use only v1-v5, and the JSC/macOS build stays
green at v7 (13 pass / 1 skip). V8 implements the full surface for free; JSC/Chakra v6/v7 functions
are added incrementally in follow-ups, feature-detect-failing where the engine C API can't express
them (BigInt, ArrayBuffer detach).
Per-env instance data + a finalizer that runs at env teardown. Enables the test_instance_data
conformance test, green on JSC/macOS at v7.
BigInt: create via the macOS-15+ C API (JSBigIntCreateWith*) with a JS-BigInt-global eval fallback
for jsc-android/older; extract (get_value_*) via BigInt.asIntN/asUintN + toString round-trips; typeof
reports napi_bigint (kJSTypeBigInt on macOS 15+); create_bigint_words enforces the Node INT_MAX /
RangeError size limits before reading the words buffer. Detach: ArrayBuffer.prototype.transfer()
(ES2024) with .detached for is_detached; ENOTSUP throw where transfer() is absent. test_bigint green
on JSC/macOS.
…cate)

macOS 15+/iOS 18+ use the kJSTypeBigInt fast path; older JSC (jsc-android ~2020) does not surface
kJSTypeBigInt through the C API, so fall back to a cached `typeof v === 'bigint'` predicate created at
env init. Keeps the v6 BigInt conformance test green on the jsc-android target.
Chakra: add napi_set/get_instance_data (per-env slot, finalized at env teardown by ~napi_env__) and the
BigInt create/get functions as feature-detection stubs that throw a JS-catchable ENOTSUP error -- the
Win10 OS edge-mode Chakra predates BigInt and exposes no JsBigInt* API, so there is no value-preserving
fallback. JSI (Core/Node-API-JSI) does not implement the v6/v7 C surface, so its conformance addons are
gated to the v1-v5 set. V8 + JavaScriptCore execute the v6/v7 tests for real; Chakra links them.
…him not green on-device)

test_instance_data (v6) runs on both Android engines; BigInt runs for real on V8. On jsc-android the
eval-based BigInt shim fails on-device with no error detail from the in-process runner (macOS JSC + V8
both pass test_bigint), so it's gated to V8 and tracked as a follow-up.
…jsc-android ~2020)

jsc-android (~2020) ships without BigInt -- its parser even rejects `0n` literals -- so the eval-based
BigInt shim can't work there. Detect BigInt once at env init (`typeof BigInt === 'function' && typeof
BigInt(1) === 'bigint'`, which parses on every JSC) and have the create/get paths throw a JS-catchable
ENOTSUP when it's absent, matching the Chakra backend. macOS/iOS JSC (which have BigInt) are unaffected.
…e BigInt by engine support

The standard test_bigint uses `0n` literals, which don't parse on engines without BigInt (jsc-android
~2020, Win10 Chakra). Add a feature-detection fallback that asserts napi_create_bigint_int64 throws
ENOTSUP, and gate it to run exactly where BigInt is absent (Chakra; jsc-android) while the real
test_bigint runs on V8 + Apple JSC. Keeps the desktop + Android run-lists in sync.
The in-process runner keeps the assertion/exception message + stack in ProcessResult.std_error, which
never reached the device log -- making on-device conformance failures undebuggable (a 0 ms FAILED with
no detail). Log it on non-zero status. This is what surfaced the jsc-android BigInt SyntaxError.
The addons were enabled for every engine except JSI, so the backends upstream has added since
(QuickJS, Hermes) picked up addons whose napi_* symbols they do not export and failed at link --
QuickJS has napi_create_bigint_int64 but no napi_create_bigint_words / napi_get_value_bigint_words,
and no napi_add_finalizer (test_instance_data). Switch to an allow-list of the engines whose v6/v7
surface is implemented and which the suite has actually been run against (V8, JavaScriptCore, Chakra),
so a newly added backend is opted in deliberately rather than by default.
…e suite

The Android app carries its own NODE_API_AVAILABLE_NATIVE_TESTS list, which gave every non-V8 engine
test_instance_data plus the ENOTSUP BigInt fallback. On QuickJS that is wrong twice over: it has no
napi_add_finalizer (test_instance_data), and it *does* have BigInt, so the ENOTSUP fallback fails.
Restrict the v6/v7 suites to V8 + JavaScriptCore, matching Tests/NodeApi/CMakeLists.txt.
The NODE_API_AVAILABLE_NATIVE_TESTS list is only meaningful when the in-process harness sources are
actually compiled, which is now gated to the validated engines (V8, JavaScriptCore).
…e conformance addons

`if(JSR_NODE_API_HARNESS_SRC)` guarded the NODE_API_AVAILABLE_NATIVE_TESTS
definition, but the variable stopped being set once the harness sources became
unconditional (the engine-specific `#ifdef`s they needed are now JSR_NAPI_ENGINE_*
compile definitions, so they build for every engine). The guard was therefore
always false: the define was never emitted, and Shared.cpp ran no Node-API
conformance addons at all on device. Both Android jobs were green because the
suite was empty, not because it passed.

Gate on the engine allow-list directly instead. Android now runs the addons
again: 20 tests / 18 passed / 2 intentional skips on both V8 and JavaScriptCore.
…s BigInt

jsc-android's `latest` tag is still 250231.0.0 (WebKit r250230, Sept 2019), which
compiles BigInt behind the `useBigInt` runtime option, defaulted off -- so its
parser rejects `0n` and the standard test_bigint cannot even be parsed. The `next`
tag, 294992.0.0 (WebKit r294992, July 2022), is the newest published build and has
BigInt enabled by default.

Verified on-device (arm64-v8a, API 29) rather than inferred:

                                    r250231              r294992
  typeof BigInt                     undefined            function
  typeof BigInt(1)                  ReferenceError       bigint
  typeof 0n                         SyntaxError          bigint
  BigInt.asIntN / asUintN           ReferenceError       function / function
  BigInt.asUintN(64, -1n)           SyntaxError          18446744073709551615
  BigInt64Array                     undefined            function
  ArrayBuffer.prototype.transfer    undefined            undefined
  structuredClone                   undefined            undefined

So Android JavaScriptCore now runs the real test_bigint instead of the ENOTSUP
test_bigint_unsupported fallback; Chakra keeps the fallback. napi_detach_arraybuffer
stays feature-detected/ENOTSUP on Android: neither build has
ArrayBuffer.prototype.transfer, which is the only public detach path, and that is a
hard limit of the newest jsc-android.

Two mechanical consequences of the bump: r294992 publishes only the `-intl`
variant, so the AAR path changes, and it statically links libc++ (no
libc++_shared.so DT_NEEDED, unlike r250231), removing an ABI coupling.

Full on-device suite, both engines: 20 tests, 18 passed, 2 intentional skips, 0 failures.
…d at env init

Two independent problems in the same code, both about reaching JS intrinsics from
the C entry points.

1. Every BigInt entry point resolved its intrinsics on the live global object, once
   per call: BigIntLow64 re-read `BigInt` and `BigInt.asIntN`/`asUintN`,
   BigIntToString read `toString` off the boxed value (so through
   BigInt.prototype), and the create path evaluated a `BigInt("...")` source
   string. napi_detach_arraybuffer and napi_is_detached_arraybuffer did the same
   with `transfer` and `detached`. All of those are reachable from script, so a
   page could replace them and steer an addon's napi_*_bigint_* results -- the
   invariant BabylonJS#116 established for napi_call_function with a cached
   Function.prototype.call, not carried through to these.

   Capture them once at env construction instead, before any user script can run:
   the constructor, asIntN/asUintN, BigInt.prototype.toString, a `(v) => -v`
   helper (StringToBigInt accepts a sign only on decimal literals, so the hex
   words path cannot spell a negative value), ArrayBuffer.prototype.transfer, and
   the `detached` getter out of its property descriptor. The create path now calls
   the captured constructor rather than evaluating source, which also drops a
   parse per BigInt created.

   Capturing by property lookup doubles as the feature probe, replacing the eval
   probe: on an engine without BigInt the lookups just fail. That matters because
   a probe containing a `0n` literal is a SyntaxError -- not a false result -- on
   jsc-android r250231.

   NodeApi.BigIntIgnoresMonkeyPatchedIntrinsics covers this. Verified it fails
   against the old lookup (returns the patched 1234, lossless false) and passes
   with the fix, on both the C API and the fallback path.

2. The BigInt C API guards were wrong on two axes, verified by compiling against
   each SDK rather than reading the annotations:

     * `#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && >= 150000` is false on every
       non-macOS Apple SDK, so the whole fast path was compiled out of iOS and
       visionOS builds -- including iOS 18.3 and visionOS 26.1, where the API is
       available. visionOS additionally reports __IPHONE_OS_VERSION_MAX_ALLOWED as
       17.0, so it needs a clause of its own.
     * `__builtin_available(macOS 15.0, *)` does not guard iOS or visionOS at all:
       the `*` asserts availability on unlisted platforms. Had the `#if` been
       "fixed" without this, an iOS 17 deployment would have called a null weak
       symbol. clang confirms it -- the call still warns as unguarded under that
       form at iOS 17 / visionOS 1.

   Both are replaced by one JSR_JSC_HAS_BIGINT_C_API macro pair naming every
   platform. Compiles clean and takes the intended path at macOS 13/15,
   iOS 17/18.3, visionOS 1/26.1 and tvOS 18.

Also makes deinit_symbol null-safe: the intrinsics are null on an engine without
BigInt, and it unprotected unconditionally.

Verified: macOS JavaScriptCore 13/13 UnitTests and 15/16 conformance (1 quarantined),
on both the C API path and with it forced off; Android arm64 JavaScriptCore and V8
21 tests / 19 passed / 2 intentional skips each.
js_native_api.h and js_native_api_types.h already default NAPI_VERSION to 7 on this
branch, but napi.h still pinned 5 -- and because all three use `#ifndef`, whichever
is reached first wins. Every consumer includes <napi/napi.h> (all of Babylon Native,
and every node-addon-api addon), so 5 won essentially everywhere: Napi::BigInt,
Napi::BigInt64Array, Value::IsBigInt(), GetInstanceData<T>() and Napi::Addon stayed
compiled out even though the C entry points behind them are implemented and pass
conformance.

Found because a test in Tests/UnitTests could not name napi_create_bigint_int64:
that translation unit was seeing NAPI_VERSION=5 through napi.h, not the 7 the C
headers declare.

Note for BabylonJS#229: this makes the three headers agree, it does not decide the policy
question. If NAPI_VERSION goes back to 5 with features ungated individually, all
three move together.
Dropping the platform #ifdef around the `typeof v === 'bigint'` fallback left it
running in napi_typeof's default branch on every engine -- so every napi_typeof of
an ordinary object paid a JS call before reaching the object handling, on a very
hot path.

Probe once at env init whether JSValueGetType actually classifies a BigInt on this
engine and OS, and consult the predicate only when it does not. That is also
strictly better than gating on the SDK or __builtin_available: the enumerator can be
compiled in while the JSC actually deployed never produces it, and the probe asks
the engine instead of assuming.

macOS JavaScriptCore 13/13 + 15/16 conformance; Android JavaScriptCore 21 tests /
19 passed / 2 intentional skips.
It had no coverage at all: the upstream conformance test for detach lives in
test_typedarray, which needs v9's node_api_basic_env and so cannot be enabled yet.

Detach is not uniform across the engines here -- ArrayBuffer.prototype.transfer()
(ES2024) is the only public detach path, since the JavaScriptCore C API has no
detach entry point -- so the test asserts whichever half of the contract applies:
the buffer really detaches, or the failure is a catchable JS error carrying code
ENOTSUP rather than a bare napi_status an addon cannot tell from a real error.

Both halves are exercised by the matrix: macOS JavaScriptCore and V8 take the
detach path, Android JavaScriptCore takes ENOTSUP (neither jsc-android r250231 nor
r294992 has transfer -- verified on device).

macOS 14/14; Android JavaScriptCore 22 tests / 20 passed / 2 intentional skips.
The body was commented out and replaced with a bare `*result = true;` in BabylonJS#70
("Update Node-API to latest from node.js"), so the API answered "detached" for
every value handed to it, including a freshly created, perfectly live ArrayBuffer.
Anything branching on it -- an addon guarding a zero-copy path, or a
detach-then-verify sequence -- got the wrong answer with no error to notice.

The pinned V8 does declare ArrayBuffer::WasDetached() (v8-array-buffer.h), so the
commented-out implementation compiles as written; restore it.

Caught by NodeApi.DetachArrayBufferOrReportsUnsupported on Android_V8 and
Win32_x64_V8. Also record the napi_status of each napi_is_detached_arraybuffer call
in that test, so an engine that fails the query is not mistaken for one that
answered "detached".

Android arm64 V8: 22 tests / 20 passed / 2 intentional skips.
napi_detach_arraybuffer and napi_is_detached_arraybuffer were absent from the
Chakra backend entirely, so linking anything that referenced them failed -- which
is how this surfaced (Win32_x64_Chakra and Win32_x86_Chakra failed at Build
Solution once a test called them).

Win10's OS Chakra has no way to detach an ArrayBuffer: the jsrt runtime exposes no
detach entry point, and the engine predates ES2024 ArrayBuffer.prototype.transfer.
So napi_detach_arraybuffer follows the same feature-detection-by-exception pattern
as BigInt here -- a JS-catchable error tagged ENOTSUP -- rather than a bare status
an addon cannot distinguish from a real failure.

napi_is_detached_arraybuffer can be answered truthfully rather than stubbed: since
nothing can detach a buffer on this engine, an ArrayBuffer that still reports
storage through JsGetArrayBufferStorage is live, and a non-ArrayBuffer is not
detached either (matching Node's contract).
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