diff --git a/docs/README.md b/docs/README.md index 7c5d73d8c..318a25341 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,9 @@ # Runtime documentation +- [Node-API](node-api.md) — the standard `napi_*` C ABI for native addons: + building against the prefab package, registering and requiring addons, + threading and finalizer contracts, and the documented divergences from Node + (shared with the iOS runtime). - [Performance API](performance.md) — WHATWG `performance` (hr-time, user timing, performance timeline with `PerformanceObserver`), per-isolate time origins for workers, the native clock hook that future `requestAnimationFrame` diff --git a/docs/node-api.md b/docs/node-api.md new file mode 100644 index 000000000..1903d1e92 --- /dev/null +++ b/docs/node-api.md @@ -0,0 +1,204 @@ +# Node-API + +The runtime implements [Node-API](https://nodejs.org/api/n-api.html), the ABI-stable C interface Node.js addons are written against. The implementation is Node v26.7.0's own `js_native_api` sources, vendored unmodified under `test-app/runtime/src/main/cpp/napi/vendor/`; only the pieces Node implements on top of libuv and its module loader are reimplemented here, against the runtime's per-runtime event loop and its own `require()`. The surface, the divergences and the vendored sources are shared byte-for-byte with the iOS runtime, so one addon source compiles against Node, the iOS runtime and this one. + +An addon for this runtime is ordinary C or C++ compiled with the NDK — either linked into a library the app already loads, or built as its own `.so`. The addon registers itself from a static constructor and JS reaches it through `require("")`. + +```c +#include + +static napi_value Add(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) { + return NULL; + } + if (argc < 2) { + napi_throw_type_error(env, NULL, "add expects two numbers"); + return NULL; + } + + double a = 0, b = 0; + if (napi_get_value_double(env, args[0], &a) != napi_ok || + napi_get_value_double(env, args[1], &b) != napi_ok) { + napi_throw_type_error(env, NULL, "add expects two numbers"); + return NULL; + } + + napi_value result = NULL; + if (napi_create_double(env, a + b, &result) != napi_ok) { + return NULL; + } + return result; +} + +static napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + {"add", NULL, Add, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, 1, properties); + return exports; +} + +static napi_module sModule = { + NAPI_MODULE_VERSION, 0, __FILE__, Init, "myaddon", NULL, {0}, +}; + +__attribute__((constructor)) static void RegisterMyModule(void) { + napi_module_register(&sModule); +} +``` + +```js +const addon = require("myaddon"); +addon.add(1, 2); // 3 +``` + +`test-app/runtime/src/main/cpp/napi/tests/NapiTestModule.cpp` and `NapiCoverageModule.cpp` are complete working addons in this shape, and `test-app/app/src/main/assets/app/tests/NapiTests.js` / `NapiCoverageTests.js` are their specs. + +## Building an addon (plugin authors) + +Two pieces: headers come from the runtime's prefab package; linking follows the same convention native V8 plugins (e.g. `@nativescript/canvas`) already use. + +**Headers.** The runtime `.aar` embeds a header-only [Prefab](https://google.github.io/prefab/) package. In the plugin's (or app's) Android library module: + +```groovy +android { + buildFeatures { + prefab true + } +} +``` + +and in its `CMakeLists.txt`: + +```cmake +find_package(NativeScript REQUIRED CONFIG) +target_link_libraries(myaddon NativeScript::NativeScript) +``` + +That puts the headers on the include path — so the ecosystem-standard bare include, exactly as an addon for Node.js writes it, compiles unchanged: + +```c +#include +``` + +This is the portable form: the same source compiles against Node itself, the iOS runtime and this runtime, and it is required if you use the `node-addon-api` C++ wrapper (its `napi.h` hard-codes `#include `). The exported headers are `node_api.h`, `node_api_types.h`, `js_native_api.h`, `js_native_api_types.h` and `NapiRuntime.h` (the NativeScript-specific `NativeScriptNapiEnv()` declaration, see below). + +**Linking.** The prefab package is deliberately header-only, so the addon `.so` links against the runtime the way native V8 plugins do today: against a local copy of `libNativeScript.so` that exists *only* to satisfy the linker at build time and is never shipped. + +1. Extract `libNativeScript.so` per ABI from the runtime `.aar` in the `@nativescript/android` npm package (`framework/app/libs/runtime-libs/nativescript-optimized.aar` → `jni//`), into e.g. `src/main/libs//`. +2. Link it: `target_link_libraries(myaddon ${CMAKE_SOURCE_DIR}/src/main/libs/${ANDROID_ABI}/libNativeScript.so)`. This gives full link-time symbol checking and a `DT_NEEDED libNativeScript.so` entry. +3. Exclude the copy from the plugin's own packaging: `packagingOptions { jniLibs { excludes += "**/libNativeScript.so" } }`. + +At runtime the addon's `DT_NEEDED` binds against the runtime library the app already loaded. Unlike direct-V8 plugins, a Node-API addon does **not** need the app to set `useV8Symbols`: every runtime flavor exports the full `napi_*`/`node_api_*` surface and `NativeScriptNapiEnv`. + +## Registering a module + +**Prefer constructor registration over the `NAPI_MODULE` / `NAPI_MODULE_INIT` macros.** The macros emit the exported symbols (`napi_register_module_v1`, `node_api_module_get_api_version_v1`) that a dlopen loader scans for — and the `.so` require path here does scan for them — but the symbols carry no module name (so a macro-registered addon is only reachable by path, never by bare `require("name")`) and only one of each can exist per binary. + +The pattern that supports both loading forms is the one above: fill in a `napi_module` and call `napi_module_register` from a constructor. + +- `nm_version` must be `NAPI_MODULE_VERSION`. +- `nm_modname` is the name JS passes to `require()`. It is the key in a process-wide registry, so it must be unique across every addon loaded into the app. +- `nm_register_func` is called once per environment, lazily, the first time that environment requires the module. +- `napi_module_register` is the only registration entry point. There is deliberately no `node_module_register` alias: Node's symbol of that name takes a `node_module*` with a different field layout, so accepting it would misread the struct rather than help old `NODE_MODULE`-style addons. + +The constructor runs when the addon's library is loaded — at app start if the addon is linked into a library the app loads eagerly, or at the first `require()` of the `.so` (below) otherwise. + +## Loading from JS + +`require("myaddon")` returns the addon's exports. Resolution order inside `require()` is: + +1. `ns:` / `node:` builtin modules, +2. registered Node-API addons — bare specifiers only, so `require("./myaddon")` or `require("~/myaddon")` will never reach an addon, +3. ordinary file and `node_modules` resolution. + +An unregistered name is not a Node-API error; it falls through to step 3 and fails (or succeeds) as any other package name would. + +An addon shipped as its own `.so` can also be loaded by path: `require("path/to/libmyaddon.so")` (or `require("system_lib://libmyaddon.so")` for a library packaged in the APK's `jniLibs`) `dlopen`s the library and initializes it Node's way — if its constructors registered a Node-API module, the call returns that module's exports (Node's `modpending` dance); otherwise, if the library carries the `napi_register_module_v1` symbol that the `NAPI_MODULE` / `node-addon-api` registration macros emit, that entry point is called instead, so stock ecosystem addons load unmodified by path. After a constructor-registered addon's first load, the bare `require("myaddon")` also resolves (macro-registered addons carry no name, so they are only reachable by path). Libraries that do neither keep the pre-existing `NSMain` protocol. + +Exports are cached **per environment**, not per process. `require("myaddon") === require("myaddon")` within one isolate, but a `Worker` gets its own environment, runs `nm_register_func` again, and receives a different exports object with different native state. Anything an addon keeps in file-scope statics is shared across every environment in the process; anything it wants to keep per-environment belongs in `napi_set_instance_data`. + +## The environment + +Every runtime — the main one and each `Worker` — owns one `napi_env`. Native code that is not already inside a Node-API callback gets it from `NapiRuntime.h`: + +```c +#include + +napi_env env = NativeScriptNapiEnv(); +if (env != NULL) { + // ... +} +``` + +The lookup is thread-local: it returns the env of the runtime running **on the calling thread**, and `NULL` on any other thread, before the runtime finishes initializing, or after it has torn down. There is no way to obtain another thread's env, by design — see below. + +A `Worker`'s env is destroyed when the worker terminates: cleanup hooks run, threadsafe functions are aborted, and every finalizer fires. The **main** runtime's env lives for the process — nothing invokes the runtime's teardown path (`DestroyRuntime`, which is where env destruction is wired) for the main runtime today, so its cleanup hooks and instance-data finalizers only run if an embedder drives that path; on a normal Android app, process death does the reaping. + +## Threading + +**Every Node-API call must happen on the thread that owns the env.** That thread holds the isolate's lock and drives the looper; entering the isolate from anywhere else deadlocks against the runtime's cross-isolate locking. The API does not check this for you (`napi_make_callback` and blocking threadsafe-function calls are the only two that do), so a wrong-thread call is undefined behaviour rather than an error status. + +Two supported ways to get work off that thread: + +- **`napi_create_async_work` / `napi_queue_async_work`** for background compute. The `execute` callback runs on a shared worker pool bounded at 4 concurrent tasks — matching Node's default libuv pool, so an addon that fans out more than 4 *interdependent* blocking executes deadlocks here exactly as it would on Node — and must not touch the isolate or the `napi_env` at all; the `complete` callback is posted to the env's event loop and may. The pool threads are plain native threads, not attached to the JVM. Every completion entry ends with a microtask checkpoint, so a promise resolved from `complete` settles promptly even if nothing else enters JS. Note the divergence on `napi_delete_async_work` below. +- **Threadsafe functions** for calling into JS from a thread you own. `napi_create_threadsafe_function` on the JS thread, then `napi_call_threadsafe_function` from anywhere. Calls are queued and drained on the env's event loop, at most 64 per entry so a fast producer cannot starve timers or the UI. + +Finalizer drains, threadsafe-function callbacks and async-work completions ride the runtime's event loop (the internal lane added in the per-runtime EventLoop work), which drops everything still queued when the runtime shuts down — so work in flight when a `Worker` terminates is dropped rather than delivered to a dead isolate. For async work specifically: an `execute` that has not started when the worker terminates is skipped, an in-flight completion is dropped, and the work item is parked in the completed state so a cleanup hook's `napi_delete_async_work` still succeeds; the addon's `data` for dropped items is never handed back (the same trade Node makes at environment shutdown). + +An exception thrown by JS during one of these entries is always **contained**: it is routed through the runtime's error pipeline (`error` event, then the uncaught-error hooks). It never propagates out of the entry — including under `uncaughtErrorPolicy: "throw"`, where the error is still fully reported but there is no native caller beneath a loop entry to hand it to. + +## Finalizers + +Finalizers registered through `napi_wrap`, `napi_add_finalizer`, `napi_create_external` and friends **never run during garbage collection**. V8's weak callback only enqueues them; they are drained on a later entry of the owning thread's event loop, with the isolate locked and entered, so a finalizer may call back into JS. The drain shares the loop's internal lane with V8's own tasks, so its order relative to timers is not specified — code waiting for a finalizer should poll, not count looper turns. + +Practically this means code that drops the last reference to a wrapped object and immediately checks whether the finalizer ran will always see "no" — it has to yield to the looper first. In the test runner, where `__collect()` forces a collection: + +```js +__collect(); +(function poll() { + if (!finalizerRan()) { + setTimeout(poll, 0); + return; + } + // the finalizer has run +})(); +``` + +At environment teardown the queue is flushed synchronously instead, so nothing is left unfinalized when a `Worker` exits. + +External `ArrayBuffer`s (`napi_create_external_buffer`, `napi_create_external_arraybuffer`) hand the pointer to V8 without copying, and V8's backing-store deleter can fire on any thread — so the deleter never runs the callback itself. The finalizer is registered with the env: the deleter posts it to the event loop, and anything still unclaimed when the environment tears down runs in the teardown sweep, while the env is alive. Either way it runs exactly once, on the env's thread. + +## Versions + +`napi_get_version` reports **10**, the highest Node-API version the vendored sources implement. + +Separately, every env is created at **module API version 8**, which is what determines *behaviour*. An addon that includes the headers without asking for anything else also compiles at `NAPI_VERSION 8`, so declarations and behaviour line up by default. Two things follow: + +- An addon can opt into the version 9 and 10 *declarations* by defining `NAPI_VERSION` before including the headers (`node_api_symbol_for`, `node_api_create_syntax_error`, the `node_api_create_property_key_*` family, external strings). Those functions are implemented and work. +- It cannot opt into version 10 *semantics*, because the env is fixed at 8. The one that bites in practice is `napi_create_reference`: at version 8 it only accepts objects, functions and symbols, and returns `napi_invalid_arg` for strings, numbers, booleans, `null` and `undefined`. Version 10 would allow all of them. + +`napi_get_node_version` reports `26.7.0`, the Node release the sources were taken from. It says nothing about a Node process being present — there isn't one. + +## Divergences + +Everything in `js_native_api.h` behaves exactly as upstream: it is upstream, compiled unmodified. The differences are confined to the `node_api.h` surface, where Node's implementation depends on libuv, `node::Buffer` or the module loader — and they are identical to the iOS runtime's, so an addon written against one runtime's divergence table holds on the other. + +| API | Behaviour here | Why | +| --- | --- | --- | +| `napi_get_uv_event_loop` | Always `napi_generic_failure` | There is no `uv_loop_t`. The runtime drives an Android Looper; handing out a fabricated loop would be worse than reporting failure. | +| `node_api_get_module_file_name` | Always `napi_generic_failure` | Nothing identifies the calling module at that point — addons are either linked into a larger library or dlopened by the runtime without per-module bookkeeping. | +| `napi_create_buffer`, `napi_create_buffer_copy`, `napi_create_external_buffer`, `node_api_create_buffer_from_arraybuffer` | Produce a plain `Uint8Array` | There is no `node::Buffer` class. `napi_get_buffer_info` returns the same thing either way. | +| `napi_is_buffer` | Exactly "is this a `Uint8Array`" | Follows from the above. An `Int8Array` is not a buffer; a `Uint8Array` from any source is. | +| `napi_ref_threadsafe_function`, `napi_unref_threadsafe_function` | Track the flag, affect nothing; always `napi_ok` | The looper belongs to the app or the worker and does not exit because an addon released its last reference. | +| `napi_call_threadsafe_function`, blocking mode, called on the env's own thread with a full queue | `napi_would_deadlock` instead of blocking | The thread that would have to drain the queue is the caller. Node blocks regardless and never returns this status; wedging the looper is worse than a status an addon may not expect. Non-blocking calls still return `napi_queue_full`, and a closed function still returns `napi_closing`. | +| `napi_delete_async_work` on work that is queued or executing | `napi_generic_failure`, work not deleted | Node deletes unconditionally and leaves the queued work holding a dangling pointer. Deleting after a *successful* cancel, or after completion, works as usual. | +| `napi_add_async_cleanup_hook` | Hook runs at teardown but is **not awaited** | The thread running teardown is the one that would have to run the completion, so a hook that defers is never given the chance to report back. Its handle stays valid, so a late `napi_remove_async_cleanup_hook` is still safe, but it can no longer reach JS. | +| `napi_fatal_exception` | Reports through the runtime's error pipeline (`error` event, uncaught-error hooks, `uncaughtErrorPolicy`) and returns; does not abort by itself | `napi_fatal_error` still aborts the process, as upstream. | +| `napi_async_init`, `napi_open_callback_scope` | Accepted; the resource and name arguments are inert | There is no `async_hooks`, so there is nothing to report the async context to. `napi_make_callback` itself works, and rejects a call from the wrong thread with `napi_generic_failure`. | + +## Not supported + +`node_api.h` is the entire native surface. The Node **runtime** is not here: no `process`, no `fs`, no `require()` of Node core modules from native, no libuv handles, no worker_threads C API. An addon that only uses Node-API works; an addon that reaches into `node.h`, `v8.h` or `uv.h` does not. diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 809f0cb88..208901d6e 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -87,6 +87,9 @@ require('./tests/testPrimordials'); require('./tests/testInspect'); // The ns:/node: builtin modules require('./tests/testNsUtil'); +// Node-API addon surface +require('./tests/NapiTests'); +require('./tests/NapiCoverageTests'); require("./tests/testConcurrentAccess"); require("./tests/testESModules.mjs"); diff --git a/test-app/app/src/main/assets/app/tests/NapiCoverageTests.js b/test-app/app/src/main/assets/app/tests/NapiCoverageTests.js new file mode 100644 index 000000000..fbe6f3522 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/NapiCoverageTests.js @@ -0,0 +1,857 @@ +// The addons are compiled into libNativeScript.so for local Debug builds +// only; on any other runtime flavor the suite skips rather than fails. +var napiCoverageModuleAvailable = true; +try { + require("napicoveragemodule"); +} catch (e) { + napiCoverageModuleAvailable = false; +} + +(napiCoverageModuleAvailable ? describe : xdescribe)("Node-API value surface", function () { + // Conditional so the disabled suite's declaration body stays throw-free + // (jasmine executes it even for xdescribe). + var napi = napiCoverageModuleAvailable ? require("napicoveragemodule") : {}; + + it("is a separate addon from napitestmodule, cached per env", function () { + expect(napi).not.toBe(require("napitestmodule")); + expect(require("napicoveragemodule")).toBe(napi); + }); + + describe("versions", function () { + it("reports the highest supported Node-API version", function () { + expect(napi.napiVersion()).toBe(10); + }); + + it("reports the Node release the surface was vendored from", function () { + expect(napi.nodeVersion()).toEqual({ + major: 26, + minor: 7, + patch: 0, + release: "node" + }); + }); + + it("refuses the APIs that have no equivalent here", function () { + expect(napi.unsupportedApiStatuses()).toEqual({ + uvEventLoop: "generic_failure", + moduleFileName: "generic_failure" + }); + }); + }); + + describe("strings", function () { + it("measures a string differently per encoding", function () { + expect(napi.stringLengths("abc")).toEqual({ utf8: 3, utf16: 3, latin1: 3 }); + expect(napi.stringLengths("")).toEqual({ utf8: 0, utf16: 0, latin1: 0 }); + // U+2603 is three UTF-8 bytes but a single code unit. + expect(napi.stringLengths("☃")).toEqual({ utf8: 3, utf16: 1, latin1: 1 }); + // U+1F600 is four UTF-8 bytes and a surrogate pair. + expect(napi.stringLengths("😀")).toEqual({ utf8: 4, utf16: 2, latin1: 2 }); + }); + + it("creates strings in each encoding", function () { + var created = napi.createStrings(); + expect(created.utf8).toBe("utf8 ü ☃"); + expect(created.utf16).toBe("utf16 ü 😀"); + expect(created.latin1).toBe("Aéÿ"); + expect(created.empty).toBe(""); + }); + + it("honours an explicit length when creating", function () { + expect(napi.createStrings().utf8Sized).toBe("abc"); + }); + + it("copies at most bufsize - 1 units and terminates the rest", function () { + expect(napi.copyString("hello", "utf8", 6)).toEqual({ copied: 5, text: "hello" }); + expect(napi.copyString("hello", "utf8", 3)).toEqual({ copied: 2, text: "he" }); + expect(napi.copyString("hello", "utf8", 1)).toEqual({ copied: 0, text: "" }); + + expect(napi.copyString("hello", "utf16", 6)).toEqual({ copied: 5, text: "hello" }); + expect(napi.copyString("hello", "utf16", 3)).toEqual({ copied: 2, text: "he" }); + + expect(napi.copyString("héllo", "latin1", 6)).toEqual({ copied: 5, text: "héllo" }); + expect(napi.copyString("héllo", "latin1", 3)).toEqual({ copied: 2, text: "hé" }); + }); + + it("reports zero copied for an empty buffer", function () { + expect(napi.copyString("hello", "utf8", 0)).toEqual({ copied: 0, text: "" }); + expect(napi.copyString("hello", "utf16", 0)).toEqual({ copied: 0, text: "" }); + }); + + it("never splits a UTF-8 sequence when truncating", function () { + // V8 stops short of the buffer rather than emitting a partial + // sequence, so two bytes of room hold none of the three-byte U+2603. + expect(napi.copyString("☃x", "utf8", 3)).toEqual({ copied: 0, text: "" }); + expect(napi.copyString("☃x", "utf8", 4)).toEqual({ copied: 3, text: "☃" }); + expect(napi.copyString("☃x", "utf8", 5)).toEqual({ copied: 4, text: "☃x" }); + }); + + it("rejects a non-string", function () { + expect(napi.stringStatus("x")).toBe("ok"); + expect(napi.stringStatus(42)).toBe("string_expected"); + expect(napi.stringStatus(null)).toBe("string_expected"); + expect(napi.stringStatus({})).toBe("string_expected"); + }); + }); + + describe("numbers", function () { + it("round-trips a fractional value with truncation toward zero", function () { + expect(napi.numberParts(1.5)).toEqual({ + double: 1.5, + int32: 1, + uint32: 1, + int64: 1 + }); + expect(napi.numberParts(-1.5)).toEqual({ + double: -1.5, + int32: -1, + uint32: 4294967295, + int64: -1 + }); + }); + + it("wraps out-of-range values the way ToInt32/ToUint32 do", function () { + expect(napi.numberParts(-1)).toEqual({ + double: -1, + int32: -1, + uint32: 4294967295, + int64: -1 + }); + expect(napi.numberParts(2147483648)).toEqual({ + double: 2147483648, + int32: -2147483648, + uint32: 2147483648, + int64: 2147483648 + }); + expect(napi.numberParts(4294967297)).toEqual({ + double: 4294967297, + int32: 1, + uint32: 1, + int64: 4294967297 + }); + }); + + it("turns every non-finite value into zero", function () { + var nan = napi.numberParts(NaN); + expect(isNaN(nan.double)).toBe(true); + expect(nan.int32).toBe(0); + expect(nan.uint32).toBe(0); + expect(nan.int64).toBe(0); + + expect(napi.numberParts(Infinity)).toEqual({ + double: Infinity, + int32: 0, + uint32: 0, + int64: 0 + }); + expect(napi.numberParts(-Infinity)).toEqual({ + double: -Infinity, + int32: 0, + uint32: 0, + int64: 0 + }); + }); + + it("creates the edges of each integer width", function () { + var created = napi.createNumbers(); + expect(created.int32Min).toBe(-2147483648); + expect(created.int32Max).toBe(2147483647); + expect(created.uint32Max).toBe(4294967295); + expect(created.int64Max).toBe(9007199254740991); + expect(created.int64Min).toBe(-9007199254740991); + expect(isNaN(created.nan)).toBe(true); + expect(created.posInf).toBe(Infinity); + expect(created.negInf).toBe(-Infinity); + expect(Object.is(created.negZero, -0)).toBe(true); + }); + + it("rejects a non-number without coercing it", function () { + expect(napi.numberStatus(1)).toBe("ok"); + expect(napi.numberStatus("1")).toBe("number_expected"); + expect(napi.numberStatus(true)).toBe("number_expected"); + expect(napi.numberStatus(null)).toBe("number_expected"); + }); + }); + + describe("symbols", function () { + it("creates a unique symbol carrying its description", function () { + var symbol = napi.createSymbol("napi-desc"); + expect(typeof symbol).toBe("symbol"); + expect(symbol.description).toBe("napi-desc"); + expect(napi.createSymbol("napi-desc")).not.toBe(symbol); + }); + + it("creates a symbol without a description", function () { + var symbol = napi.createSymbol(undefined); + expect(typeof symbol).toBe("symbol"); + expect(symbol.description).toBeUndefined(); + }); + + it("requires a string description", function () { + expect(napi.createSymbolStatus("ok-desc")).toBe("ok"); + expect(napi.createSymbolStatus(42)).toBe("string_expected"); + expect(napi.createSymbolStatus({})).toBe("string_expected"); + }); + + it("resolves registered symbols from the same table as Symbol.for", function () { + expect(napi.symbolFor("napi.coverage.key")).toBe(Symbol.for("napi.coverage.key")); + }); + + it("uses a symbol as a property key", function () { + var target = {}; + var key = Symbol("napi-key"); + expect(napi.symbolKeyRoundTrip(target, key, 7)).toEqual({ + hasBefore: false, + hasAfter: true, + read: 7, + deleted: true, + hasAfterDelete: false + }); + expect(target[key]).toBeUndefined(); + }); + }); + + describe("array buffers and views", function () { + it("fills a new array buffer through its data pointer", function () { + var buffer = napi.createArrayBuffer(8); + expect(buffer instanceof ArrayBuffer).toBe(true); + expect(buffer.byteLength).toBe(8); + expect(Array.prototype.slice.call(new Uint8Array(buffer))).toEqual([ + 0, 1, 2, 3, 4, 5, 6, 7 + ]); + }); + + it("reads back the same memory JS sees", function () { + var buffer = napi.createArrayBuffer(4); + new Uint8Array(buffer)[3] = 99; + expect(napi.arrayBufferInfo(buffer)).toEqual({ + byteLength: 4, + hasData: true, + firstByte: 0, + lastByte: 99 + }); + + napi.writeByte(buffer, 0, 200); + expect(new Uint8Array(buffer)[0]).toBe(200); + }); + + it("creates a typed array over a JS array buffer at an offset", function () { + var buffer = new ArrayBuffer(8); + new Uint8Array(buffer).set([0, 0, 5, 6, 7, 8, 9, 10]); + + var view = napi.createTypedArray("uint16", buffer, 3, 2); + expect(view instanceof Uint16Array).toBe(true); + expect(view.length).toBe(3); + expect(view.byteOffset).toBe(2); + expect(view.byteLength).toBe(6); + expect(view.buffer).toBe(buffer); + }); + + it("describes a typed array from the native side", function () { + var buffer = napi.createArrayBuffer(8); + var view = napi.createTypedArray("uint8", buffer, 5, 3); + + var info = napi.typedArrayInfo(view); + expect(info.type).toBe("uint8"); + expect(info.length).toBe(5); + expect(info.byteOffset).toBe(3); + expect(info.buffer).toBe(buffer); + // The data pointer already has the offset applied. + expect(info.firstByte).toBe(3); + }); + + it("keeps every element type distinguishable", function () { + var buffer = new ArrayBuffer(8); + expect(napi.typedArrayInfo(napi.createTypedArray("int8", buffer, 8, 0)).type).toBe( + "int8" + ); + expect(napi.typedArrayInfo(napi.createTypedArray("float64", buffer, 1, 0)).type).toBe( + "float64" + ); + expect(napi.typedArrayInfo(napi.createTypedArray("biguint64", buffer, 1, 0)).type).toBe( + "biguint64" + ); + }); + + it("wraps the addon's own allocation without copying it", function () { + var view = napi.createExternalTypedArray(); + expect(view instanceof Uint8Array).toBe(true); + expect(view.byteLength).toBe(8); + expect(Array.prototype.slice.call(view)).toEqual([10, 11, 12, 13, 14, 15, 16, 17]); + + // Writes land in the addon's buffer, which is the one JS is reading. + napi.writeByte(view.buffer, 0, 42); + expect(view[0]).toBe(42); + }); + + it("creates and describes a DataView", function () { + var buffer = napi.createArrayBuffer(8); + var view = napi.createDataView(buffer, 4, 2); + expect(view instanceof DataView).toBe(true); + expect(view.byteLength).toBe(4); + expect(view.byteOffset).toBe(2); + expect(view.buffer).toBe(buffer); + expect(view.getUint8(0)).toBe(2); + + expect(napi.dataViewInfo(view)).toEqual({ + byteLength: 4, + byteOffset: 2, + buffer: buffer, + firstByte: 2 + }); + }); + + it("detaches an array buffer", function () { + var buffer = napi.createArrayBuffer(4); + expect(napi.detachArrayBuffer(buffer)).toEqual({ + status: "ok", + before: false, + after: true + }); + expect(buffer.byteLength).toBe(0); + }); + + it("refuses to detach something that is not an array buffer", function () { + expect(napi.detachArrayBuffer({})).toEqual({ + status: "arraybuffer_expected", + before: false, + after: false + }); + }); + }); + + describe("promises", function () { + // The addon holds one deferred at a time, so a spec that fails before + // settling would make every later createPromise() throw. + beforeEach(function () { + napi.settlePromise(true, undefined); + }); + + it("resolves a deferred", function (done) { + var promise = napi.createPromise(); + expect(promise instanceof Promise).toBe(true); + expect(napi.predicates(promise).promise).toBe(true); + + promise.then(function (value) { + expect(value).toBe(42); + done(); + }); + + expect(napi.settlePromise(true, 42)).toBe(true); + }); + + it("rejects a deferred with the value it was given", function (done) { + var promise = napi.createPromise(); + var reason = new Error("rejected by the addon"); + + promise.then( + function () { + expect("resolved").toBe("rejected"); + done(); + }, + function (caught) { + expect(caught).toBe(reason); + done(); + } + ); + + expect(napi.settlePromise(false, reason)).toBe(true); + }); + + it("reports that there is nothing left to settle", function () { + expect(napi.settlePromise(true, 1)).toBe(false); + }); + + it("does not see a plain thenable as a promise", function () { + expect(napi.predicates({ then: function () {} }).promise).toBe(false); + }); + + it("sees every kind of JS promise as a promise", function () { + expect(napi.predicates(Promise.resolve()).promise).toBe(true); + expect(napi.predicates(new Promise(function () {})).promise).toBe(true); + expect(napi.predicates((async function () {})()).promise).toBe(true); + }); + }); + + describe("errors", function () { + it("creates each error type with its code", function () { + var error = napi.createError("error", "ECODE", "plain"); + expect(error instanceof Error).toBe(true); + expect(error.name).toBe("Error"); + expect(error.message).toBe("plain"); + expect(error.code).toBe("ECODE"); + + var typeError = napi.createError("type", "ETYPE", "typed"); + expect(typeError instanceof TypeError).toBe(true); + expect(typeError.code).toBe("ETYPE"); + + var rangeError = napi.createError("range", "ERANGE", "ranged"); + expect(rangeError instanceof RangeError).toBe(true); + expect(rangeError.code).toBe("ERANGE"); + + var syntaxError = napi.createError("syntax", "ESYNTAX", "syntactic"); + expect(syntaxError instanceof SyntaxError).toBe(true); + expect(syntaxError.code).toBe("ESYNTAX"); + }); + + it("leaves out the code when none is given", function () { + var error = napi.createError("type", null, "no code"); + expect(error instanceof TypeError).toBe(true); + expect(error.message).toBe("no code"); + expect("code" in error).toBe(false); + }); + + it("requires a string message", function () { + expect(napi.createErrorStatus("fine")).toBe("ok"); + expect(napi.createErrorStatus(42)).toBe("string_expected"); + }); + + it("throws each error type", function () { + var caught; + try { + napi.throwErrorKind("type", "ETHROWN", "thrown type error"); + } catch (e) { + caught = e; + } + expect(caught instanceof TypeError).toBe(true); + expect(caught.message).toBe("thrown type error"); + expect(caught.code).toBe("ETHROWN"); + + caught = undefined; + try { + napi.throwErrorKind("range", null, "thrown range error"); + } catch (e) { + caught = e; + } + expect(caught instanceof RangeError).toBe(true); + expect(caught.message).toBe("thrown range error"); + expect("code" in caught).toBe(false); + + caught = undefined; + try { + napi.throwErrorKind("syntax", null, "thrown syntax error"); + } catch (e) { + caught = e; + } + expect(caught instanceof SyntaxError).toBe(true); + }); + + it("throws a value that is not an Error", function () { + var caught; + try { + napi.throwValue("just a string"); + } catch (e) { + caught = e; + } + expect(caught).toBe("just a string"); + + var sentinel = { tag: "thrown object" }; + caught = undefined; + try { + napi.throwValue(sentinel); + } catch (e) { + caught = e; + } + expect(caught).toBe(sentinel); + }); + + it("recognises errors and only errors", function () { + expect(napi.predicates(new Error("x")).error).toBe(true); + expect(napi.predicates(new TypeError("x")).error).toBe(true); + expect(napi.predicates({ message: "x" }).error).toBe(false); + expect(napi.predicates("x").error).toBe(false); + }); + }); + + describe("exceptions", function () { + it("surfaces, blocks on, and clears a pending exception", function () { + var result = napi.callAndCatch(function () { + throw new Error("boom"); + }); + + expect(result.callStatus).toBe("pending_exception"); + expect(result.pendingBefore).toBe(true); + // A second call refuses to run while the first exception is pending. + expect(result.blockedStatus).toBe("pending_exception"); + expect(result.clearStatus).toBe("ok"); + expect(result.pendingAfter).toBe(false); + expect(result.caught instanceof Error).toBe(true); + expect(result.caught.message).toBe("boom"); + }); + + it("leaves nothing pending after a clean call", function () { + var result = napi.callAndCatch(function () { + return 1; + }); + + expect(result.callStatus).toBe("ok"); + expect(result.pendingBefore).toBe(false); + expect(result.blockedStatus).toBe("ok"); + expect(result.pendingAfter).toBe(false); + expect(result.caught).toBeUndefined(); + }); + + it("clears to undefined when nothing was thrown", function () { + expect(napi.clearWithoutException()).toEqual({ status: "ok", isUndefined: true }); + }); + + it("rethrows the identical value it caught", function () { + var sentinel = new Error("rethrown"); + var caught; + try { + napi.callAndRethrow(function () { + throw sentinel; + }); + } catch (e) { + caught = e; + } + expect(caught).toBe(sentinel); + }); + }); + + describe("references", function () { + it("counts up and down and hands back the value", function () { + var target = { id: "counted" }; + var ref = napi.refCreate(target, 1); + + expect(napi.refGet(ref)).toBe(target); + expect(napi.refRef(ref)).toBe(2); + expect(napi.refRef(ref)).toBe(3); + expect(napi.refUnref(ref)).toBe(2); + expect(napi.refUnref(ref)).toBe(1); + expect(napi.refUnref(ref)).toBe(0); + expect(napi.refGet(ref)).toBe(target); + + expect(napi.refDelete(ref)).toBe(true); + expect(napi.refDelete(ref)).toBe(false); + }); + + it("only references objects, functions and symbols at module API version 8", function () { + expect(napi.refCreateStatus({})).toBe("ok"); + expect(napi.refCreateStatus(function () {})).toBe("ok"); + expect(napi.refCreateStatus(Symbol("ref"))).toBe("ok"); + + expect(napi.refCreateStatus("a string")).toBe("invalid_arg"); + expect(napi.refCreateStatus(42)).toBe("invalid_arg"); + expect(napi.refCreateStatus(true)).toBe("invalid_arg"); + expect(napi.refCreateStatus(null)).toBe("invalid_arg"); + expect(napi.refCreateStatus(undefined)).toBe("invalid_arg"); + }); + + it("keeps a weak reference alive once it is strengthened", function () { + var target = { tag: "strengthened" }; + var ref = napi.refCreate(target, 0); + expect(napi.refRef(ref)).toBe(1); + target = null; + + // Conservative stack scanning keeps the dropped value alive until + // the loop below overwrites the frame that held it. + __collect(); + var sink = 0; + for (var i = 0; i < 200000; i++) { + sink += i % 7; + } + __collect(); + + expect(sink).toBeGreaterThan(0); + expect(napi.refGet(ref).tag).toBe("strengthened"); + expect(napi.refDelete(ref)).toBe(true); + }); + + it("drops a weak reference once its value is collected", function (done) { + var target = { tag: "weak" }; + var ref = napi.refCreate(target, 0); + // Deliberately not refGet: handing the object back to JS would put + // it on the stack again, where conservative scanning keeps it alive. + expect(napi.refIsLive(ref)).toBe(true); + target = null; + + __collect(); + var sink = 0; + for (var i = 0; i < 200000; i++) { + sink += i % 7; + } + __collect(); + + expect(sink).toBeGreaterThan(0); + + setTimeout(function () { + expect(napi.refIsLive(ref)).toBe(false); + expect(napi.refGet(ref)).toBeUndefined(); + expect(napi.refDelete(ref)).toBe(true); + done(); + }, 0); + }); + }); + + describe("conversions", function () { + it("coerces to boolean", function () { + expect(napi.coerce("bool", 0)).toBe(false); + expect(napi.coerce("bool", NaN)).toBe(false); + expect(napi.coerce("bool", "")).toBe(false); + expect(napi.coerce("bool", null)).toBe(false); + expect(napi.coerce("bool", undefined)).toBe(false); + expect(napi.coerce("bool", "x")).toBe(true); + expect(napi.coerce("bool", {})).toBe(true); + }); + + it("coerces to number", function () { + expect(napi.coerce("number", "42")).toBe(42); + expect(napi.coerce("number", "")).toBe(0); + expect(napi.coerce("number", true)).toBe(1); + expect(napi.coerce("number", null)).toBe(0); + expect(isNaN(napi.coerce("number", undefined))).toBe(true); + expect(isNaN(napi.coerce("number", "not a number"))).toBe(true); + }); + + it("coerces to string", function () { + expect(napi.coerce("string", 42)).toBe("42"); + expect(napi.coerce("string", null)).toBe("null"); + expect(napi.coerce("string", undefined)).toBe("undefined"); + expect(napi.coerce("string", true)).toBe("true"); + expect(napi.coerce("string", [1, 2])).toBe("1,2"); + expect(napi.coerce("string", {})).toBe("[object Object]"); + }); + + it("coerces to object", function () { + var boxed = napi.coerce("object", 42); + expect(typeof boxed).toBe("object"); + expect(boxed instanceof Number).toBe(true); + expect(boxed.valueOf()).toBe(42); + + expect(napi.coerce("object", "s") instanceof String).toBe(true); + expect(napi.coerce("object", true) instanceof Boolean).toBe(true); + + var target = { already: "an object" }; + expect(napi.coerce("object", target)).toBe(target); + }); + + it("propagates the TypeError a coercion throws", function () { + expect(function () { + napi.coerce("number", Symbol("no number")); + }).toThrowError(TypeError); + + expect(function () { + napi.coerce("object", null); + }).toThrowError(TypeError); + }); + }); + + describe("properties", function () { + it("distinguishes own from inherited", function () { + var own = { a: 1 }; + expect(napi.propertyOps(own, "a")).toEqual({ + has: true, + hasOwn: true, + read: 1, + deleted: true, + hasAfterDelete: false + }); + + var child = Object.create({ p: 5 }); + expect(napi.propertyOps(child, "p")).toEqual({ + has: true, + hasOwn: false, + read: 5, + // Deleting a non-own property succeeds without touching the + // prototype, so the lookup still finds it. + deleted: true, + hasAfterDelete: true + }); + }); + + it("reports a missing property", function () { + expect(napi.propertyOps({}, "missing")).toEqual({ + has: false, + hasOwn: false, + read: undefined, + deleted: true, + hasAfterDelete: false + }); + }); + + it("operates on array elements by index", function () { + var values = [10, 20, 30]; + expect(napi.elementOps(values, 1)).toEqual({ + has: true, + read: 20, + deleted: true, + hasAfterDelete: false + }); + expect(values.length).toBe(3); + expect(values[1]).toBeUndefined(); + }); + + it("lists enumerable string keys including inherited ones", function () { + var target = Object.create({ inherited: 1 }); + target.own = 2; + Object.defineProperty(target, "hidden", { value: 3, enumerable: false }); + target[Symbol("skipped")] = 4; + + expect(napi.propertyNames(target)).toEqual(["own", "inherited"]); + }); + + it("filters keys with napi_get_all_property_names", function () { + var target = Object.create({ inherited: 1 }); + target.own = 2; + Object.defineProperty(target, "hidden", { value: 3, enumerable: false }); + var symbolKey = Symbol("kept"); + target[symbolKey] = 4; + + var ownAll = napi.allPropertyNames(target, true, "all"); + expect(ownAll.indexOf("own")).toBeGreaterThan(-1); + expect(ownAll.indexOf("hidden")).toBeGreaterThan(-1); + expect(ownAll.indexOf(symbolKey)).toBeGreaterThan(-1); + expect(ownAll.indexOf("inherited")).toBe(-1); + + var ownStrings = napi.allPropertyNames(target, true, "skip_symbols"); + expect(ownStrings.indexOf(symbolKey)).toBe(-1); + expect(ownStrings.indexOf("hidden")).toBeGreaterThan(-1); + + var enumerable = napi.allPropertyNames(target, false, "enumerable"); + expect(enumerable.indexOf("own")).toBeGreaterThan(-1); + expect(enumerable.indexOf("inherited")).toBeGreaterThan(-1); + expect(enumerable.indexOf("hidden")).toBe(-1); + + var symbolsOnly = napi.allPropertyNames(target, true, "skip_strings"); + expect(symbolsOnly).toEqual([symbolKey]); + }); + + it("keeps or stringifies index keys per the conversion mode", function () { + var keys = napi.indexKeyTypes([1, 2]); + expect(typeof keys.kept[0]).toBe("number"); + expect(keys.kept[0]).toBe(0); + expect(typeof keys.converted[0]).toBe("string"); + expect(keys.converted[0]).toBe("0"); + expect(keys.kept.length).toBe(keys.converted.length); + }); + + it("defines an accessor pair sharing one callback", function () { + var target = napi.defineOnTarget({}); + + target.base = 5; + expect(target.base).toBe(5); + // Same getter, different descriptor `data`. + expect(target.offset).toBe(105); + + var base = Object.getOwnPropertyDescriptor(target, "base"); + expect(typeof base.get).toBe("function"); + expect(typeof base.set).toBe("function"); + expect(base.enumerable).toBe(true); + + var offset = Object.getOwnPropertyDescriptor(target, "offset"); + expect(typeof offset.get).toBe("function"); + expect(offset.set).toBeUndefined(); + }); + + it("maps napi_property_attributes onto the descriptor", function () { + var target = napi.defineOnTarget({}); + + expect(Object.getOwnPropertyDescriptor(target, "locked")).toEqual({ + value: 1, + writable: false, + enumerable: false, + configurable: false + }); + expect(Object.getOwnPropertyDescriptor(target, "open")).toEqual({ + value: 1, + writable: true, + enumerable: true, + configurable: true + }); + }); + }); + + describe("types and identity", function () { + it("names every value type", function () { + expect(napi.typeOf(undefined)).toBe("undefined"); + expect(napi.typeOf(null)).toBe("null"); + expect(napi.typeOf(true)).toBe("boolean"); + expect(napi.typeOf(1)).toBe("number"); + expect(napi.typeOf("s")).toBe("string"); + expect(napi.typeOf(Symbol("s"))).toBe("symbol"); + expect(napi.typeOf({})).toBe("object"); + expect(napi.typeOf([])).toBe("object"); + expect(napi.typeOf(function () {})).toBe("function"); + expect(napi.typeOf(BigInt(1))).toBe("bigint"); + expect(napi.typeOf(napi.createExternal(1))).toBe("external"); + }); + + it("classifies built-in object kinds", function () { + expect(napi.predicates([]).array).toBe(true); + expect(napi.predicates({ length: 0 }).array).toBe(false); + expect(napi.predicates(new Date()).date).toBe(true); + expect(napi.predicates(new ArrayBuffer(1)).arraybuffer).toBe(true); + expect(napi.predicates(new Uint8Array(1)).typedarray).toBe(true); + expect(napi.predicates(new DataView(new ArrayBuffer(1))).dataview).toBe(true); + expect(napi.predicates(new ArrayBuffer(1)).typedarray).toBe(false); + }); + + it("treats a Uint8Array, and only a Uint8Array, as a buffer", function () { + // There is no node::Buffer here, so napi_is_buffer is exactly + // "is this a Uint8Array". + expect(napi.predicates(new Uint8Array(1)).buffer).toBe(true); + expect(napi.predicates(new Int8Array(1)).buffer).toBe(false); + expect(napi.predicates(new ArrayBuffer(1)).buffer).toBe(false); + }); + + it("compares with strict equality semantics", function () { + var target = {}; + expect(napi.strictEquals(target, target)).toBe(true); + expect(napi.strictEquals({}, {})).toBe(false); + expect(napi.strictEquals("a", "a")).toBe(true); + expect(napi.strictEquals(1, "1")).toBe(false); + expect(napi.strictEquals(NaN, NaN)).toBe(false); + expect(napi.strictEquals(0, -0)).toBe(true); + }); + + it("walks the prototype chain for instanceof", function () { + expect(napi.instanceOf(new TypeError("x"), Error)).toBe(true); + expect(napi.instanceOf(new Error("x"), TypeError)).toBe(false); + expect(napi.instanceOf([], Array)).toBe(true); + expect(napi.instanceOf({}, Error)).toBe(false); + }); + + it("constructs and reads a prototype", function () { + function Point(x) { + this.x = x; + } + + var point = napi.newInstance(Point, 7); + expect(point instanceof Point).toBe(true); + expect(point.x).toBe(7); + expect(napi.getPrototype(point)).toBe(Point.prototype); + expect(napi.getPrototype({})).toBe(Object.prototype); + }); + + it("freezes and seals", function () { + var frozen = napi.freezeObject({ a: 1 }); + expect(Object.isFrozen(frozen)).toBe(true); + frozen.a = 2; + expect(frozen.a).toBe(1); + + var sealed = napi.sealObject({ a: 1 }); + expect(Object.isSealed(sealed)).toBe(true); + expect(Object.isFrozen(sealed)).toBe(false); + sealed.a = 2; + expect(sealed.a).toBe(2); + sealed.b = 3; + expect(sealed.b).toBeUndefined(); + }); + + it("round-trips a date", function () { + var date = napi.createDate(1234567); + expect(date instanceof Date).toBe(true); + expect(date.getTime()).toBe(1234567); + expect(napi.dateValue(date)).toBe(1234567); + expect(napi.predicates(date).date).toBe(true); + expect(napi.dateValue({})).toBe("date_expected"); + }); + + it("round-trips an external pointer", function () { + var external = napi.createExternal(2.5); + expect(napi.externalValue(external)).toBe(2.5); + expect(napi.externalValue({})).toBe("invalid_arg"); + }); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/NapiTests.js b/test-app/app/src/main/assets/app/tests/NapiTests.js new file mode 100644 index 000000000..9fcacbcaf --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/NapiTests.js @@ -0,0 +1,324 @@ +// The addons are compiled into libNativeScript.so for local Debug builds +// only; on any other runtime flavor the suite skips rather than fails. +var napiTestModuleAvailable = true; +try { + require("napitestmodule"); +} catch (e) { + napiTestModuleAvailable = false; +} + +(napiTestModuleAvailable ? describe : xdescribe)("Node-API addon", function () { + // Conditional so the disabled suite's declaration body stays throw-free + // (jasmine executes it even for xdescribe). + var napi = napiTestModuleAvailable ? require("napitestmodule") : {}; + + it("exports the addon's functions", function () { + expect(typeof napi).toBe("object"); + expect(typeof napi.echoString).toBe("function"); + expect(typeof napi.doubleNumber).toBe("function"); + expect(typeof napi.negateBool).toBe("function"); + expect(typeof napi.transformObject).toBe("function"); + expect(typeof napi.transformArray).toBe("function"); + expect(typeof napi.throwError).toBe("function"); + expect(typeof napi.wrapValue).toBe("function"); + expect(typeof napi.unwrapValue).toBe("function"); + expect(typeof napi.finalizerRan).toBe("function"); + expect(typeof napi.resetFinalizerFlag).toBe("function"); + expect(typeof napi.holdRef).toBe("function"); + expect(typeof napi.getRef).toBe("function"); + expect(typeof napi.releaseRef).toBe("function"); + expect(typeof napi.startAsyncWork).toBe("function"); + expect(typeof napi.startCancelledWork).toBe("function"); + expect(typeof napi.startTsfn).toBe("function"); + expect(typeof napi.pushTsfn).toBe("function"); + expect(typeof napi.probeTsfnAbort).toBe("function"); + expect(typeof napi.invokeViaMakeCallback).toBe("function"); + expect(typeof napi.exerciseCleanupHooks).toBe("function"); + }); + + it("is instantiated once per env", function () { + expect(require("napitestmodule")).toBe(napi); + }); + + it("round-trips a string", function () { + expect(napi.echoString("hello")).toBe("hello"); + expect(napi.echoString("")).toBe(""); + expect(napi.echoString("ünïcödé ☃")).toBe("ünïcödé ☃"); + }); + + it("round-trips a number", function () { + expect(napi.doubleNumber(21)).toBe(42); + expect(napi.doubleNumber(-1.5)).toBe(-3); + }); + + it("round-trips a bool", function () { + expect(napi.negateBool(true)).toBe(false); + expect(napi.negateBool(false)).toBe(true); + }); + + it("reads a property and builds a new object", function () { + var result = napi.transformObject({ value: 4, ignored: "x" }); + expect(result.value).toBe(8); + expect(result.tag).toBe("napi"); + expect(result.ignored).toBeUndefined(); + }); + + it("reads an array and builds a new array", function () { + var result = napi.transformArray([7, 8, 9]); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + expect(result[0]).toBe(3); + expect(result[1]).toBe(7); + + expect(napi.transformArray([])).toEqual([0, 0]); + }); + + it("throws a catchable Error carrying a code", function () { + var error; + try { + napi.throwError(); + } catch (e) { + error = e; + } + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("napi test failure"); + expect(error.code).toBe("ERR_TEST_CODE"); + }); + + describe("napi_define_properties", function () { + it("defines a value property", function () { + var descriptor = Object.getOwnPropertyDescriptor(napi, "moduleName"); + expect(descriptor.value).toBe("napitestmodule"); + expect(descriptor.get).toBeUndefined(); + expect(descriptor.enumerable).toBe(true); + }); + + it("defines an accessor property", function () { + var descriptor = Object.getOwnPropertyDescriptor(napi, "wrapCount"); + expect(typeof descriptor.get).toBe("function"); + expect(descriptor.value).toBeUndefined(); + + var before = napi.wrapCount; + napi.wrapValue({}, 1); + expect(napi.wrapCount).toBe(before + 1); + }); + }); + + describe("napi_wrap", function () { + it("unwraps the payload it wrapped", function () { + var target = {}; + expect(napi.wrapValue(target, 3.5)).toBe(target); + expect(napi.unwrapValue(target)).toBe(3.5); + }); + + it("runs the finalizer once the wrapper is collected", function (done) { + napi.resetFinalizerFlag(); + expect(napi.finalizerRan()).toBe(false); + + (function () { + napi.wrapValue({}, 11); + })(); + + // Conservative stack scanning keeps the dead wrapper alive until + // the loop below overwrites the frame that held it. + __collect(); + var sink = 0; + for (var i = 0; i < 200000; i++) { + sink += i % 7; + } + __collect(); + + expect(sink).toBeGreaterThan(0); + + // Node-API finalizers are queued from the weak callback and drained + // on a later event-loop entry, never inside the collection itself. + expect(napi.finalizerRan()).toBe(false); + + // The drain shares the internal lane with V8's own GC tasks and + // runs one entry per pass, so there is no ordering guarantee + // against timers — poll instead of assuming the first tick. + var attempts = 0; + (function poll() { + if (napi.finalizerRan() || ++attempts > 50) { + expect(napi.finalizerRan()).toBe(true); + done(); + return; + } + setTimeout(poll, 0); + })(); + }); + }); + + describe("napi_make_callback", function () { + it("calls the function and returns its result", function () { + var seen; + var result = napi.invokeViaMakeCallback(function (value) { + seen = value; + return value + 1; + }, 41); + + expect(seen).toBe(41); + expect(result).toBe(42); + }); + + it("propagates a throw from the callback", function () { + var error; + try { + napi.invokeViaMakeCallback(function () { + throw new Error("boom"); + }, 1); + } catch (e) { + error = e; + } + + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("boom"); + }); + }); + + describe("napi_async_work", function () { + it("executes off the JS thread and completes on it", function (done) { + napi.startAsyncWork(21, function (status, result, ranOffJsThread) { + expect(status).toBe("ok"); + expect(result).toBe(42); + expect(ranOffJsThread).toBe(true); + done(); + }); + }); + + it("completes cancelled work with a cancelled status", function (done) { + // Whether the cancel wins the race against the queue is not ours to + // decide; both outcomes have to hold up. + var cancelStatus = napi.startCancelledWork(function (status) { + if (cancelStatus === "ok") { + expect(status).toBe("cancelled"); + } else { + expect(cancelStatus).toBe("generic_failure"); + expect(status).toBe("ok"); + } + done(); + }); + }); + }); + + describe("napi_threadsafe_function", function () { + it("delivers every value in order, then finalizes", function (done) { + var values = []; + + napi.startTsfn( + 5, + 0, + function (value) { + values.push(value); + }, + function (lastCallStatus) { + expect(lastCallStatus).toBe("ok"); + expect(values).toEqual([1, 2, 3, 4, 5]); + done(); + } + ); + }); + + it("blocks the producer on a full queue instead of dropping values", function (done) { + var values = []; + + napi.startTsfn( + 50, + 2, + function (value) { + values.push(value); + }, + function (lastCallStatus) { + expect(lastCallStatus).toBe("ok"); + expect(values.length).toBe(50); + expect(values[0]).toBe(1); + expect(values[49]).toBe(50); + done(); + } + ); + }); + + it("accepts a call made from inside its own callback", function (done) { + var values = []; + var pushStatus; + + napi.startTsfn( + 3, + 0, + function (value) { + values.push(value); + if (value === 1) { + pushStatus = napi.pushTsfn(100); + } + }, + function () { + expect(pushStatus).toBe("ok"); + expect(values.length).toBe(4); + // The producer may already have queued 2 and 3 by then, so + // only "after the first value" is guaranteed. + expect(values.indexOf(100)).toBeGreaterThan(0); + done(); + } + ); + }); + + it("reports closing after an abort and drops the queued call", function (done) { + var delivered = []; + var probe = napi.probeTsfnAbort(function (value) { + delivered.push(value); + }); + + expect(probe.queued).toBe("ok"); + expect(probe.released).toBe("ok"); + expect(probe.afterAbort).toBe("closing"); + + setTimeout(function () { + expect(delivered).toEqual([]); + done(); + }, 0); + }); + }); + + describe("cleanup hooks", function () { + it("rejects removing a hook that is no longer registered", function () { + expect(napi.exerciseCleanupHooks()).toBe("invalid_arg"); + }); + }); + + describe("workers", function () { + it("instantiates the addon separately in each isolate", function (done) { + var worker = new Worker("./napiEvalWorker.js"); + worker.onmessage = function (msg) { + worker.terminate(); + expect(msg.data.doubled).toBe(42); + expect(msg.data.cached).toBe(true); + done(); + }; + worker.postMessage({ + eval: + "var m = require('napitestmodule'); " + + "postMessage({ doubled: m.doubleNumber(21), cached: require('napitestmodule') === m });" + }); + }); + }); + + describe("napi_create_reference", function () { + afterEach(function () { + napi.releaseRef(); + }); + + it("holds and returns the referenced value", function () { + var held = { id: "held" }; + napi.holdRef(held); + expect(napi.getRef()).toBe(held); + expect(napi.getRef()).toBe(held); + }); + + it("returns undefined once released", function () { + napi.holdRef({ id: "held" }); + expect(napi.releaseRef()).toBe(true); + expect(napi.getRef()).toBeUndefined(); + expect(napi.releaseRef()).toBe(false); + }); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/napiEvalWorker.js b/test-app/app/src/main/assets/app/tests/napiEvalWorker.js new file mode 100644 index 000000000..66b05d502 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/napiEvalWorker.js @@ -0,0 +1,3 @@ +onmessage = function (msg) { + eval(msg.data.eval || ""); +}; diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 71a2ab7f8..a092d3058 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -56,6 +56,10 @@ include_directories( src/main/cpp/include src/main/cpp/v8_inspector src/main/cpp/ada + # Node-API: the vendored upstream sources include their own headers and + # the shim's Node-internal stand-ins (env-inl.h, util-inl.h) bare. + src/main/cpp/napi/vendor + src/main/cpp/napi/shim ) # The runtime's builtin JavaScript (src/main/cpp/js) embedded into a generated @@ -129,6 +133,20 @@ else () set(INSPECTOR_SOURCES) endif () +# The Node-API test addons back the NapiTests/NapiCoverageTests specs of the +# test app. Local Debug builds only — exactly the variant runtests installs — +# so no published .aar ever carries them. +if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT OPTIMIZED_BUILD AND NOT OPTIMIZED_WITH_INSPECTOR_BUILD) + set( + NAPI_TEST_SOURCES + + src/main/cpp/napi/tests/NapiTestModule.cpp + src/main/cpp/napi/tests/NapiCoverageModule.cpp + ) +else () + set(NAPI_TEST_SOURCES) +endif () + # Command info: https://cmake.org/cmake/help/v3.4/command/add_library.html # Creates(shared static) and names a library given relative sources # Gradle automatically packages shared libraries with your APK. @@ -209,10 +227,21 @@ add_library( src/main/cpp/HMRSupport.cpp src/main/cpp/DevFlags.cpp + # Node-API: vendored upstream implementation plus the embedder half + # (env lifecycle, module registry, async work, threadsafe functions) + src/main/cpp/napi/vendor/js_native_api_v8.cc + src/main/cpp/napi/NapiEnv.cpp + src/main/cpp/napi/NapiRuntime.cpp + src/main/cpp/napi/NapiThreadSafeFunction.cpp + src/main/cpp/napi/NodeApiEmbed.cpp + ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp # V8 inspector source files will be included only in Release mode ${INSPECTOR_SOURCES} + + # Node-API test addons, local Debug builds only + ${NAPI_TEST_SOURCES} ) set(NATIVES_BLOB_INCLUDE_DIRECTORIES ${PROJECT_SOURCE_DIR}/src/main/libs/${ANDROID_ABI}/include) diff --git a/test-app/runtime/build.gradle b/test-app/runtime/build.gradle index 0bc3c4d39..0efc7dde5 100644 --- a/test-app/runtime/build.gradle +++ b/test-app/runtime/build.gradle @@ -161,6 +161,51 @@ android { } +// Ships the Node-API headers inside the .aar as a header-only prefab package, +// so a plugin's native build gets the ecosystem-standard bare +// `#include ` (required by node-addon-api) with +// android { buildFeatures { prefab true } } +// find_package(NativeScript REQUIRED CONFIG) +// target_link_libraries( NativeScript::NativeScript) +// The package is hand-authored rather than AGP-generated: AGP's +// prefabPublishing would name the package after the gradle project, record +// the c++_static STL (which prefab's consumer check rejects for a shared +// library no matter the consumer's STL), and bundle the unstripped .so. +// Header-only carries none of that. Linking follows the same convention V8 +// plugins already use: link against the runtime .so extracted from this .aar +// and exclude it from packaging (see docs/node-api.md). +// +// Only the public Node-API surface is exported: the vendored engine headers +// (js_native_api_v8.h) and the shim stay internal. +def napiPrefabDir = layout.buildDirectory.dir("napi-prefab") +tasks.register("stageNapiPrefab", Sync) { + into napiPrefabDir + from("prefab-package") { + // prefab.json at the package root, module.json inside the module + eachFile { f -> + if (f.name == "module.json") { + f.path = "prefab/modules/NativeScript/module.json" + } else { + f.path = "prefab/${f.name}" + } + } + } + from("src/main/cpp/napi/vendor") { + include "node_api.h", "node_api_types.h", "js_native_api.h", "js_native_api_types.h" + into "prefab/modules/NativeScript/include" + } + from("src/main/cpp/napi/NapiRuntime.h") { + into "prefab/modules/NativeScript/include" + } + includeEmptyDirs = false +} +afterEvaluate { + tasks.withType(com.android.build.gradle.tasks.BundleAar).configureEach { + dependsOn("stageNapiPrefab") + from(napiPrefabDir) + } +} + allprojects { afterEvaluate { tasks.withType(JavaCompile).configureEach { diff --git a/test-app/runtime/exported-symbols.map b/test-app/runtime/exported-symbols.map index 49ee740cd..9598f58f8 100644 --- a/test-app/runtime/exported-symbols.map +++ b/test-app/runtime/exported-symbols.map @@ -12,6 +12,9 @@ global: Java_*; JNI_OnLoad; + napi_*; + node_api_*; + NativeScriptNapiEnv; extern "C++" { v8::[A-Z]*; v8::api_internal::*; diff --git a/test-app/runtime/prefab-package/module.json b/test-app/runtime/prefab-package/module.json new file mode 100644 index 000000000..1a7d2db95 --- /dev/null +++ b/test-app/runtime/prefab-package/module.json @@ -0,0 +1,3 @@ +{ + "export_libraries": [] +} diff --git a/test-app/runtime/prefab-package/prefab.json b/test-app/runtime/prefab-package/prefab.json new file mode 100644 index 000000000..5c6b06f63 --- /dev/null +++ b/test-app/runtime/prefab-package/prefab.json @@ -0,0 +1,5 @@ +{ + "schema_version": 2, + "name": "NativeScript", + "dependencies": [] +} diff --git a/test-app/runtime/src/main/cpp/ModuleInternal.cpp b/test-app/runtime/src/main/cpp/ModuleInternal.cpp index eb214512f..be8ad4024 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternal.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternal.cpp @@ -15,6 +15,7 @@ #include "Constants.h" #include "NativeScriptException.h" #include "NsBuiltinModules.h" +#include "napi/NapiModules.h" #include "Util.h" #include "SimpleProfiler.h" #include "include/v8.h" @@ -47,6 +48,17 @@ bool ModuleInternal::IsLikelyOptionalModule(const std::string& moduleName) { return false; } +// A package-style specifier: neither a path nor a scheme, so it may be claimed +// by a registry rather than resolved on disk. +static bool IsBareSpecifier(const std::string& specifier) { + if (specifier.empty() || specifier[0] == '.' || specifier[0] == '/' || + specifier[0] == '~') { + return false; + } + + return specifier.find(':') == std::string::npos; +} + // Helper function to check if a file path is an ES module (.mjs) but not a source map (.mjs.map) bool ModuleInternal::IsESModule(const std::string& path) { return path.size() >= 4 && path.compare(path.size() - 4, 4, ".mjs") == 0 && @@ -192,6 +204,20 @@ void ModuleInternal::RequireCallbackImpl(const v8::FunctionCallbackInfoGetCurrentContext(); + Local exports; + if (NapiModules::GetExports(context, moduleName).ToLocal(&exports)) { + args.GetReturnValue().Set(exports); + } else if (!isolate->HasPendingException()) { + isolate->ThrowException(Exception::Error(ArgConverter::ConvertToV8String( + isolate, "Node-API module '" + moduleName + "' failed to initialize"))); + } + return; + } + tns::instrumentation::Frame frame("RequireCallback " + moduleName); string callingModuleDirName = ArgConverter::ConvertToString(args[1].As()); auto isData = false; @@ -377,12 +403,44 @@ Local ModuleInternal::LoadModule(Isolate* isolate, const string& moduleP } moduleFunc = moduleFuncValue.As(); } else if (Util::EndsWith(modulePath, ".so")) { + // Registrations from libraries this loader did not dlopen (statically + // linked addons whose constructors ran at app start) must not be + // attributed to whatever `.so` happens to load next. + NapiModules::ClaimPendingModule(); + auto handle = dlopen(modulePath.c_str(), RTLD_LAZY); if (handle == nullptr) { auto error = dlerror(); - string errMsg(error); + string errMsg(error != nullptr ? error : "dlopen failed for " + modulePath); throw NativeScriptException(errMsg); } + + // A library whose constructors registered a Node-API module is a + // Node-API addon: its exports come from the addon registry (Node's + // dlopen consumes `modpending` the same way). A library carrying the + // NAPI_MODULE / node-addon-api registration symbol instead is + // initialized through it — this is also the path a *re*-dlopen of an + // already-loaded addon takes, since constructors only run on first + // load. NSMain remains the protocol for plain native modules. + string napiModuleName = NapiModules::ClaimPendingModule(); + void* napiInitSymbol = napiModuleName.empty() + ? dlsym(handle, "napi_register_module_v1") + : nullptr; + if (!napiModuleName.empty() || napiInitSymbol != nullptr) { + Local napiExports; + bool initialized = napiModuleName.empty() + ? NapiModules::InitAddonFromSymbol(context, napiInitSymbol, modulePath).ToLocal(&napiExports) + : NapiModules::GetExports(context, napiModuleName).ToLocal(&napiExports); + if (!initialized || tc.HasCaught()) { + throw NativeScriptException(tc, "Error initializing Node-API module " + + (napiModuleName.empty() ? modulePath : napiModuleName)); + } + moduleObj->Set(context, exportsPropName, napiExports); + tempModule.SaveToCache(); + result = moduleObj; + return result; + } + auto func = dlsym(handle, "NSMain"); if (func == nullptr) { string errMsg("Cannot find 'NSMain' in " + modulePath); diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index 2713aeae6..c218dcdf0 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -32,6 +32,7 @@ #include "NativeScriptAssert.h" #include "NativeScriptException.h" #include "NativeScriptPlatform.h" +#include "napi/NapiEnv.h" #include "Performance.h" #include "SimpleAllocator.h" #include "SimpleProfiler.h" @@ -891,6 +892,9 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, this->m_context = new Persistent(isolate, context); + this->m_napiEnv = NapiEnv::Create(context, m_eventLoop); + s_currentRuntime = this; + s_mainThreadInitialized = true; return isolate; @@ -934,6 +938,18 @@ void Runtime::DestroyRuntime() { // and v8 teardown posts have their work dropped from now on m_eventLoop->Shutdown(); } + if (m_napiEnv != nullptr) { + // After Shutdown so no queued Node-API entry can run against a dying env, + // and before disposeIsolate: the env's reference lists hold v8::Globals, + // so its teardown needs the isolate alive and locked. The Locker is + // reentrant for the worker path, which already holds it here. + v8::Locker locker(m_isolate); + NapiEnv::Destroy(static_cast(m_napiEnv)); + m_napiEnv = nullptr; + } + if (s_currentRuntime == this) { + s_currentRuntime = nullptr; + } // The events state holds v8::Global handles (backing event target, dispatch // closures and tracked promise rejections) - reset them while the isolate // is still alive. @@ -962,3 +978,27 @@ bool Runtime::s_mainThreadInitialized = false; v8::Platform* Runtime::platform = nullptr; int Runtime::m_androidVersion = Runtime::GetAndroidVersion(); std::shared_ptr Runtime::s_mainEventLoop; + +thread_local Runtime* Runtime::s_currentRuntime = nullptr; + +napi_env Runtime::GetNapiEnvIfAlive(const Runtime* runtime) { + if (runtime == nullptr) { + return nullptr; + } + + std::lock_guard lock(s_runtimeCacheMutex); + for (const auto& entry : s_isolate2RuntimesCache) { + Runtime* candidate = entry.second; + // The home-thread comparison closes the allocator-reuse (ABA) hole: a + // stale thread-local can only exist on the dead runtime's home thread, + // and a recycled same-address Runtime homed on this thread would have + // overwritten that thread-local — so an address match homed on a foreign + // thread can only be a recycled pointer. + if (candidate == runtime && candidate->m_napiEnv != nullptr && + static_cast(candidate->m_napiEnv)->HomeThread() == + std::this_thread::get_id()) { + return candidate->m_napiEnv; + } + } + return nullptr; +} diff --git a/test-app/runtime/src/main/cpp/Runtime.h b/test-app/runtime/src/main/cpp/Runtime.h index 9a587a4f3..8cecb1c26 100644 --- a/test-app/runtime/src/main/cpp/Runtime.h +++ b/test-app/runtime/src/main/cpp/Runtime.h @@ -17,6 +17,10 @@ #include #include +// Declared rather than included: js_native_api_types.h pins NAPI_VERSION for +// the whole translation unit, and Runtime.h reaches nearly all of them. +typedef struct napi_env__* napi_env; + namespace tns { class PromiseRejectionTracker; @@ -48,6 +52,34 @@ class Runtime { static Runtime* GetRuntimeFromIsolateData(v8::Isolate* isolate); + /* + * The runtime whose home thread is the calling thread, or null. Set at + * the end of PrepareV8Runtime; may be stale after a Runtime destroyed + * on another thread, so consumers must validate through + * GetNapiEnvIfAlive rather than dereference it. + */ + static Runtime* GetCurrentRuntime() { + return s_currentRuntime; + } + + /* + * The Node-API environment for this runtime's context. Null before + * PrepareV8Runtime creates it and after DestroyRuntime destroys it. + */ + napi_env GetNapiEnv() const { + return m_napiEnv; + } + + /* + * Resolves the env while holding the registry lock, so a possibly- + * stale pointer (e.g. the thread-local left behind when a Runtime was + * destroyed on another thread) is never dereferenced outside it. The + * returned env's validity is governed by the Node-API threading + * contract: it is only safe to use on the runtime's own thread, where + * teardown cannot race it. + */ + static napi_env GetNapiEnvIfAlive(const Runtime* runtime); + static ObjectManager* GetObjectManager(v8::Isolate* isolate); static void Init(JavaVM* vm, void* reserved); @@ -224,6 +256,8 @@ class Runtime { std::shared_ptr m_eventLoop; + napi_env m_napiEnv = nullptr; + v8::Global m_globalEventTarget; v8::Global m_dispatchErrorEventFunc; v8::Global m_dispatchUnhandledRejectionFunc; @@ -273,6 +307,8 @@ class Runtime { static std::shared_ptr s_mainEventLoop; + static thread_local Runtime* s_currentRuntime; + #ifdef APPLICATION_IN_DEBUG std::mutex m_fileWriteMutex; #endif diff --git a/test-app/runtime/src/main/cpp/napi/NapiEnv.cpp b/test-app/runtime/src/main/cpp/napi/NapiEnv.cpp new file mode 100644 index 000000000..6b250d423 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiEnv.cpp @@ -0,0 +1,234 @@ +#include "NapiEnv.h" + +#include + +#include +#include + +#include "ArgConverter.h" +#include "EventLoop.h" +#include "NapiThreadSafeFunction.h" +#include "NativeScriptException.h" +#include "Runtime.h" + +using namespace v8; + +namespace tns { + +NapiEnv::NapiEnv(Local context, + const std::shared_ptr& eventLoop) + : napi_env__(context, NODE_API_DEFAULT_MODULE_API_VERSION), + homeThread_(std::this_thread::get_id()), + eventLoop_(eventLoop), + aliveFlag_(std::make_shared>(true)) {} + +NapiEnv::~NapiEnv() = default; + +NapiEnv* NapiEnv::Create(Local context, + const std::shared_ptr& eventLoop) { + return new NapiEnv(context, eventLoop); +} + +void NapiEnv::Destroy(NapiEnv* env) { + if (env == nullptr) { + return; + } + + // Drops the reference taken at construction, which runs the finalizer drain + // and deletes the env. + env->Unref(); +} + +NapiEnv* NapiEnv::ForIsolate(Isolate* isolate) { + if (isolate == nullptr) { + return nullptr; + } + + // Read the isolate slot directly: the Runtime::GetRuntime* accessors throw + // NativeScriptException when the slot is unset, and a C++ exception must + // not cross the extern "C" Node-API surface this is called under. + Runtime* runtime = static_cast( + isolate->GetData((uint32_t)Runtime::IsolateData::RUNTIME)); + if (runtime == nullptr) { + return nullptr; + } + + return static_cast(runtime->GetNapiEnv()); +} + +void NapiEnv::CallFinalizer(napi_finalize cb, void* data, void* hint) { + if (cb == nullptr) { + return; + } + + HandleScope handle_scope(this->isolate); + Context::Scope context_scope(this->context()); + + CallIntoModule([&](napi_env env) { cb(env, data, hint); }, + NapiReportModuleException); +} + +void NapiEnv::EnqueueFinalizer(v8impl::RefTracker* finalizer) { + // Runs inside V8's weak callback, where calling into JS is forbidden. The + // queue is drained on a later event-loop entry instead, which is where Node + // puts it too (a SetImmediate there, an internal-lane post here). One drain + // is scheduled per non-empty stretch of the queue. + bool scheduled = !this->pending_finalizers.empty(); + napi_env__::EnqueueFinalizer(finalizer); + + if (scheduled || this->tearingDown_) { + return; + } + + std::shared_ptr loop = this->GetEventLoop(); + if (loop == nullptr) { + return; + } + + // The entry runs under the loop's Locker/scopes; EventLoop::Shutdown drops + // queued entries before DestroyRuntime destroys this env, so `this` is live + // here. + NapiEnv* env = this; + loop->PostInternal([env]() { env->DrainFinalizers(); }); +} + +void NapiEnv::RegisterExternalFinalizer( + const std::shared_ptr& finalizer) { + this->externalFinalizers_.insert(finalizer); +} + +void NapiEnv::RunExternalFinalizer( + std::shared_ptr finalizer) { + // By value: the argument may alias the registry entry erased below. + if (finalizer->claimed.exchange(true)) { + return; + } + + this->CallFinalizer(finalizer->cb, finalizer->data, finalizer->hint); + this->externalFinalizers_.erase(finalizer); +} + +void NapiEnv::DrainFinalizers() { + while (!this->pending_finalizers.empty()) { + v8impl::RefTracker* finalizer = *this->pending_finalizers.begin(); + this->pending_finalizers.erase(finalizer); + finalizer->Finalize(); + } +} + +void NapiEnv::DeleteMe() { + // DestroyRuntime holds the Locker but may not have entered the isolate + // (the worker teardown path has, entering again is free), so teardown + // enters it here before anything below touches handles or the context. + Isolate::Scope isolate_scope(this->isolate); + HandleScope handle_scope(this->isolate); + + aliveFlag_->store(false); + + // From here on can_call_into_js() is false: hooks and finalizers still run + // and may release env-bound resources (delete refs, release threadsafe + // functions), but any Node-API call that would enter JS is refused, matching + // Node's teardown contract. + this->tearingDown_ = true; + + // Cleanup hooks come first, so an addon gets to release its threadsafe + // functions and other env-bound resources itself. Whatever survives is + // closed below, before any reference is finalized — a threadsafe function + // holds one to its JS callback. + NapiRunEnvCleanupHooks(this); + NapiAbortThreadSafeFunctions(this); + + this->DrainFinalizers(); + + v8impl::RefTracker::FinalizeAll(&this->finalizing_reflist); + v8impl::RefTracker::FinalizeAll(&this->reflist); + + // External-buffer finalizers whose backing-store deleter has not fired (or + // whose posted run was dropped by Shutdown) run here, while the env can + // still make the callback; the deleter finds them claimed and does nothing. + while (!this->externalFinalizers_.empty()) { + this->RunExternalFinalizer(*this->externalFinalizers_.begin()); + } + + this->moduleExports_.clear(); + + delete this; +} + +Local NapiEnv::PrivateKey(NapiPrivateKeySlot slot) { + size_t index = static_cast(slot); + if (this->privateKeys_[index].IsEmpty()) { + const char* name = slot == NapiPrivateKeySlot::wrapper + ? "node_api.wrapper" + : "node_api.type_tag"; + Local key = Private::New( + this->isolate, ArgConverter::ConvertToV8String(this->isolate, name)); + this->privateKeys_[index].Set(this->isolate, key); + } + + return this->privateKeys_[index].Get(this->isolate); +} + +MaybeLocal NapiEnv::CachedModuleExports(const std::string& name) { + auto it = this->moduleExports_.find(name); + if (it == this->moduleExports_.end()) { + return MaybeLocal(); + } + + return it->second.Get(this->isolate); +} + +void NapiEnv::CacheModuleExports(const std::string& name, + Local exports) { + this->moduleExports_[name].Reset(this->isolate, exports); +} + +void NapiReportModuleException(napi_env env, Local exception) { + if (env->terminatedOrTerminating()) { + return; + } + + Isolate* isolate = env->isolate; + TryCatch tc(isolate); + isolate->ThrowException(exception); + if (!NativeScriptException::ContainUncaughtCallbackException(isolate, tc)) { + // Node-API entries always contain. These run from event-loop entries with + // no Java caller below them, so propagating (what a false return asks + // for) would leave a pending JNI exception under a loop that keeps + // executing. Under uncaughtErrorPolicy "throw" the error was already + // fully reported before containment declined; the remaining false paths + // (JS-initiated chain, escapeException) have no JS frame reachable from + // here, so the error is logged and dropped. + __android_log_print(ANDROID_LOG_ERROR, "TNS.Napi", + "Uncontained exception in Node-API callback: %s", + ArgConverter::ToString(isolate, exception).c_str()); + tc.Reset(); + } +} + +Local NapiPrivateKey(Local context, NapiPrivateKeySlot slot) { + // The context argument exists to match upstream's macro; one env per isolate + // makes it redundant, and V8 no longer exposes Context::GetIsolate. + (void)context; + Isolate* isolate = Isolate::GetCurrent(); + NapiEnv* env = NapiEnv::ForIsolate(isolate); + if (env == nullptr) { + v8impl::OnFatalError(nullptr, + "Node-API private key requested without a napi_env"); + } + return env->PrivateKey(slot); +} + +} // namespace tns + +namespace v8impl { + +void OnFatalError(const char* location, const char* message) { + __android_log_print(ANDROID_LOG_FATAL, "TNS.Napi", + "NativeScript Node-API fatal error: %s%s%s", message, + location != nullptr ? " at " : "", + location != nullptr ? location : ""); + abort(); +} + +} // namespace v8impl diff --git a/test-app/runtime/src/main/cpp/napi/NapiEnv.h b/test-app/runtime/src/main/cpp/napi/NapiEnv.h new file mode 100644 index 000000000..f93dbdb96 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiEnv.h @@ -0,0 +1,123 @@ +#ifndef NAPIENV_H_ +#define NAPIENV_H_ + +// Pins NAPI_VERSION for every consumer of this header, so napi_env__ is seen +// identically wherever it is compiled. Must precede the napi includes below. +#ifndef NAPI_EXPERIMENTAL +#define NAPI_EXPERIMENTAL +#endif +#ifndef NODE_API_EXPERIMENTAL_NO_WARNING +#define NODE_API_EXPERIMENTAL_NO_WARNING +#endif + +#include +#include +#include +#include +#include +#include + +#include "js_native_api_v8.h" + +namespace tns { + +class EventLoop; + +// The finalizer of one external buffer/arraybuffer. Its callback must run +// exactly once, on the env's thread, while the env is alive — but V8's +// backing-store deleter fires on arbitrary threads, including during isolate +// disposal after the env died. So the deleter only *posts* the callback, the +// env's teardown sweep runs whatever has not run yet, and `claimed` (flipped +// exclusively on the env's thread) arbitrates between the two. +struct NapiExternalFinalizer { + std::atomic claimed{false}; + napi_finalize cb = nullptr; + void* data = nullptr; + void* hint = nullptr; +}; + +// The napi_env behind every Node-API call, one per runtime isolate/context. +// Node's equivalent (node_napi_env__) lives in node_api.cc, which is not +// vendored; this is its replacement. +class NapiEnv : public napi_env__ { + public: + // Creates the env for `context` on the runtime's home thread and hands + // ownership to the caller, which must eventually pass it to Destroy while + // the isolate is alive and locked. + static NapiEnv* Create(v8::Local context, + const std::shared_ptr& eventLoop); + static void Destroy(NapiEnv* env); + + // Null when the isolate has no runtime, or before PrepareV8Runtime reaches + // the env, or after teardown. + static NapiEnv* ForIsolate(v8::Isolate* isolate); + + bool can_call_into_js() const override { return !tearingDown_; } + void CallFinalizer(napi_finalize cb, void* data, void* hint) override; + void EnqueueFinalizer(v8impl::RefTracker* finalizer) override; + void DeleteMe() override; + + v8::Local PrivateKey(NapiPrivateKeySlot slot); + + // The thread that owns this env — identity checks only + // (is-this-the-env's-thread); work is posted through GetEventLoop(). + std::thread::id HomeThread() const { return homeThread_; } + + // The runtime's event loop, or null once the runtime released it. Between + // Shutdown and that release it is still returned, and posts to it are + // silently dropped. Posted internal-lane entries run on the env's thread + // under the loop's Locker/scopes and end with a microtask checkpoint. + std::shared_ptr GetEventLoop() const { return eventLoop_.lock(); } + + // Exports of an addon already initialized in this env, or an empty handle. + v8::MaybeLocal CachedModuleExports(const std::string& name); + void CacheModuleExports(const std::string& name, + v8::Local exports); + + // External-buffer finalizer registry, env thread only. Registered entries + // are claimed+run either by a posted backing-store deleter or by the + // teardown sweep in DeleteMe, whichever gets there first. + void RegisterExternalFinalizer( + const std::shared_ptr& finalizer); + void RunExternalFinalizer(std::shared_ptr finalizer); + + // Flips to false at the head of DeleteMe. Held (shared) by async work + // queued to the background pool, whose threads outlive any single env: a + // pool job that finds it false must not run the execute callback — the raw + // env it captured is gone. + const std::shared_ptr>& AliveFlag() const { + return aliveFlag_; + } + + private: + NapiEnv(v8::Local context, + const std::shared_ptr& eventLoop); + ~NapiEnv() override; + + void DrainFinalizers(); + + std::thread::id homeThread_; + std::weak_ptr eventLoop_; + std::shared_ptr> aliveFlag_; + bool tearingDown_ = false; + v8::Eternal privateKeys_[2]; + std::unordered_map> moduleExports_; + std::unordered_set> + externalFinalizers_; +}; + +// Runs the env's cleanup hooks, most recently added first, at the head of +// teardown. Defined in NodeApiEmbed.cpp, next to the hook registry. +void NapiRunEnvCleanupHooks(NapiEnv* env); + +// The shared exception tail for every Node-API entry into JS (finalizers, +// threadsafe-function callbacks, async-work completions): routes the pending +// exception through the runtime's 9.1 containment pipeline (`error` event -> +// legacy hook -> log), and rethrows to Java when policy or a JS-initiated +// chain demands propagation. Runs on the env's thread with the isolate locked +// and a context entered. +void NapiReportModuleException(napi_env env, v8::Local exception); + +} // namespace tns + +#endif /* NAPIENV_H_ */ diff --git a/test-app/runtime/src/main/cpp/napi/NapiModules.h b/test-app/runtime/src/main/cpp/napi/NapiModules.h new file mode 100644 index 000000000..8111830ca --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiModules.h @@ -0,0 +1,41 @@ +#ifndef NAPIMODULES_H_ +#define NAPIMODULES_H_ + +#include + +#include "v8.h" + +namespace tns { + +// The process-global table of Node-API addons registered through +// napi_module_register, and their per-env instantiation. Shaped after +// NsBuiltinModules so require() can consult both the same way. +class NapiModules { + public: + static bool IsRegistered(const std::string& name); + + // Returns the addon's exports for the context's env, initializing it on + // first use and reusing that object afterwards. Empty on failure, with the + // exception left pending on the isolate. + static v8::MaybeLocal GetExports(v8::Local context, + const std::string& name); + + // The name of the module registered on this thread since the last claim, or + // empty. Node's dlopen does the same dance (modpending): a `.so` addon that + // self-registers from a static constructor has no other way to tell the + // loader which module the library it just opened contributed. + static std::string ClaimPendingModule(); + + // Initializes an addon through a `napi_register_module_v1` symbol found in + // a dlopen'd library (the NAPI_MODULE / node-addon-api registration form, + // which emits no constructor). Exports are cached per env under `cacheKey` + // (the resolved library path — the symbol carries no module name). Empty on + // failure, with the exception left pending on the isolate. + static v8::MaybeLocal InitAddonFromSymbol( + v8::Local context, void* initSymbol, + const std::string& cacheKey); +}; + +} // namespace tns + +#endif /* NAPIMODULES_H_ */ diff --git a/test-app/runtime/src/main/cpp/napi/NapiRuntime.cpp b/test-app/runtime/src/main/cpp/napi/NapiRuntime.cpp new file mode 100644 index 000000000..d830798a5 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiRuntime.cpp @@ -0,0 +1,10 @@ +#include "NapiRuntime.h" + +#include "Runtime.h" + +extern "C" napi_env NativeScriptNapiEnv(void) { + // The thread-local can go stale when a Runtime is destroyed on a different + // thread than the one that created it, so the env is resolved through the + // registry without dereferencing the pointer outside its lock. + return tns::Runtime::GetNapiEnvIfAlive(tns::Runtime::GetCurrentRuntime()); +} diff --git a/test-app/runtime/src/main/cpp/napi/NapiRuntime.h b/test-app/runtime/src/main/cpp/napi/NapiRuntime.h new file mode 100644 index 000000000..27bcc6c06 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiRuntime.h @@ -0,0 +1,26 @@ +#ifndef NAPIRUNTIME_H_ +#define NAPIRUNTIME_H_ + +// The NativeScript-specific piece of the Node-API surface: obtaining the env +// outside a Node-API callback. Everything else an addon needs is the standard +// , which ships next to this header. +#include "node_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// The Node-API environment of the runtime on the calling thread, or NULL when +// this thread has no runtime (or its runtime has torn down). Each runtime — +// the main one and every Worker — owns a separate env. +// +// The explicit visibility attribute keeps the symbol exported under the +// -fvisibility=hidden the release builds compile with (a version script +// cannot resurrect a hidden symbol). +__attribute__((visibility("default"))) napi_env NativeScriptNapiEnv(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NAPIRUNTIME_H_ */ diff --git a/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.cpp b/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.cpp new file mode 100644 index 000000000..a6e33bc62 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.cpp @@ -0,0 +1,501 @@ +// napi_threadsafe_function over the runtime's EventLoop. +// +// The invariant the whole file is built around: a producer thread never enters +// the target isolate. It takes this object's mutex, appends to the queue and +// leaves; every step that touches JS runs in an entry posted to the env's own +// event loop. Taking the isolate's Locker from a foreign thread would sidestep +// the loop's ordering and deadlock against multithreaded-JS entry paths. + +// Must precede every include: without NAPI_EXPERIMENTAL, NAPI_VERSION defaults +// to 8 and the version-gated declarations in node_api.h stay invisible, so the +// definitions below would silently not match anything. +#define NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_NO_WARNING + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "node_api.h" + +#include "js_native_api_v8.h" + +#include "EventLoop.h" +#include "NapiEnv.h" +#include "NapiThreadSafeFunction.h" + +struct napi_threadsafe_function__ + : public std::enable_shared_from_this { + // Read from producer threads, so they are fixed at construction and only + // cleared once the function is closed for good. The home thread id is used + // for identity checks only: a producer may compare against it after the + // owning thread exits, and comparing an id is fine where entering the + // isolate would not be. Work is posted through the event loop, whose + // weak_ptr goes null once the runtime shuts it down. + tns::NapiEnv* env = nullptr; + v8::Isolate* isolate = nullptr; + std::thread::id homeThread; + std::weak_ptr eventLoop; + + // Touched on the env's thread only, which is where the abort path and every + // dispatch run. + napi_ref callbackRef = nullptr; + napi_threadsafe_function_call_js callJs = nullptr; + napi_finalize finalizeCb = nullptr; + void* finalizeData = nullptr; + void* context = nullptr; + + std::mutex mutex; + std::condition_variable spaceAvailable; + std::queue queue; + size_t maxQueueSize = 0; + size_t threadCount = 0; + bool closing = false; + bool envAlive = true; + bool dispatchPosted = false; + bool finalized = false; + bool refed = true; +}; + +namespace { + +using TsfnRef = std::shared_ptr; + +// Owns the reference that stands in for the handle the addon holds: a +// threadsafe function outlives its JS side (entries in flight keep their own +// reference) and its JS side outlives the handle, so neither can own it alone. +struct TsfnRegistry { + std::mutex mutex; + std::unordered_map live; +}; + +TsfnRegistry& Registry() { + static TsfnRegistry* registry = new TsfnRegistry(); + return *registry; +} + +void DropHandleRef(napi_threadsafe_function__* tsfn) { + // Declared before the lock so it outlives it: dropping the last reference + // runs the destructor, which must not happen under the registry's mutex. + TsfnRef dropped; + + TsfnRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + auto it = registry.live.find(tsfn); + if (it == registry.live.end()) { + return; + } + + dropped = std::move(it->second); + registry.live.erase(it); +} + +// Node deletes the function as soon as it is finalized, which leaves the +// handles of threads that have not released yet dangling. Holding on until +// every thread has released costs one map entry and makes a late +// napi_release_threadsafe_function safe. +void DropHandleRefIfDone(const TsfnRef& tsfn) { + { + std::lock_guard lock(tsfn->mutex); + if (!tsfn->finalized || tsfn->threadCount > 0) { + return; + } + } + + DropHandleRef(tsfn.get()); +} + +// The env's thread, with the isolate locked and a handle scope open. +void FinalizeOnJsThread(const TsfnRef& tsfn) { + std::queue undelivered; + { + std::lock_guard lock(tsfn->mutex); + if (tsfn->finalized) { + return; + } + tsfn->finalized = true; + undelivered.swap(tsfn->queue); + } + + if (tsfn->callbackRef != nullptr) { + napi_delete_reference(tsfn->env, tsfn->callbackRef); + tsfn->callbackRef = nullptr; + } + + // Node hands undelivered items back with a null env so the producer's data + // can still be freed; there is no JS left to run for them. This must happen + // before the finalize callback, which is where addons free the context these + // calls receive. + while (!undelivered.empty()) { + if (tsfn->callJs != nullptr) { + tsfn->callJs(nullptr, nullptr, tsfn->context, undelivered.front()); + } + undelivered.pop(); + } + + if (tsfn->finalizeCb != nullptr) { + tsfn->env->CallFinalizer(tsfn->finalizeCb, tsfn->finalizeData, + tsfn->context); + } +} + +void CallJsOnJsThread(const TsfnRef& tsfn, void* data) { + napi_env env = tsfn->env; + + v8::HandleScope handle_scope(env->isolate); + v8::Context::Scope context_scope(env->context()); + + napi_value callback = nullptr; + if (tsfn->callbackRef != nullptr) { + napi_get_reference_value(env, tsfn->callbackRef, &callback); + } + + env->CallIntoModule( + [&](napi_env moduleEnv) { + if (tsfn->callJs != nullptr) { + tsfn->callJs(moduleEnv, callback, tsfn->context, data); + return; + } + if (callback != nullptr) { + napi_value recv = nullptr; + napi_get_undefined(moduleEnv, &recv); + napi_call_function(moduleEnv, recv, callback, 0, nullptr, nullptr); + } + }, + tns::NapiReportModuleException); +} + +void PostDispatch(const TsfnRef& tsfn); + +// A producer that pushes faster than the callback returns would otherwise keep +// one loop entry busy forever, starving timers, messages and the UI. Node +// re-arms its async handle per item; this yields every so many. +constexpr size_t kMaxCallsPerDispatch = 64; + +void RunDispatch(const TsfnRef& tsfn) { + size_t delivered = 0; + + for (;;) { + void* data = nullptr; + bool haveData = false; + bool finalize = false; + { + std::lock_guard lock(tsfn->mutex); + if (!tsfn->envAlive) { + tsfn->dispatchPosted = false; + return; + } + + if (tsfn->closing) { + finalize = true; + } else if (!tsfn->queue.empty()) { + data = tsfn->queue.front(); + tsfn->queue.pop(); + haveData = true; + } else if (tsfn->threadCount == 0) { + tsfn->closing = true; + finalize = true; + } else { + tsfn->dispatchPosted = false; + return; + } + } + + tsfn->spaceAvailable.notify_all(); + + if (finalize) { + FinalizeOnJsThread(tsfn); + { + std::lock_guard lock(tsfn->mutex); + tsfn->dispatchPosted = false; + } + DropHandleRefIfDone(tsfn); + return; + } + + if (haveData) { + CallJsOnJsThread(tsfn, data); + + if (++delivered >= kMaxCallsPerDispatch) { + { + std::lock_guard lock(tsfn->mutex); + tsfn->dispatchPosted = false; + } + PostDispatch(tsfn); + return; + } + } + } +} + +void PostDispatch(const TsfnRef& tsfn) { + std::weak_ptr weakLoop; + { + // Read under the mutex: this runs on producer threads, and teardown + // closes the function as soon as the env starts tearing down. + std::lock_guard lock(tsfn->mutex); + if (tsfn->dispatchPosted || !tsfn->envAlive) { + return; + } + tsfn->dispatchPosted = true; + weakLoop = tsfn->eventLoop; + } + + // The entry owns a reference: the handle may be released, and the queue + // drained by an earlier dispatch, before this one runs. It runs under the + // loop's Locker/scopes, and EventLoop::Shutdown drops queued entries before + // the env dies, so no liveness re-check is needed inside. + // + // If Shutdown runs between the IsStopped check and the post, the post is + // silently dropped and dispatchPosted stays latched; that is harmless + // because the abort path that follows Shutdown never consults it. + TsfnRef ref = tsfn; + + std::shared_ptr loop = weakLoop.lock(); + if (loop == nullptr || loop->IsStopped()) { + std::lock_guard lock(ref->mutex); + ref->dispatchPosted = false; + return; + } + + loop->PostInternal([ref]() { RunDispatch(ref); }); +} + +} // namespace + +namespace tns { + +void NapiAbortThreadSafeFunctions(NapiEnv* env) { + // Looped rather than a single sweep: teardown finalizers running after the + // first pass may create new threadsafe functions on this env, and those + // must be closed too or they leak holding a dangling env pointer. + for (;;) { + std::vector victims; + { + TsfnRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + for (auto& entry : registry.live) { + if (entry.second->env == env) { + victims.push_back(entry.second); + } + } + } + if (victims.empty()) { + return; + } + + for (const TsfnRef& tsfn : victims) { + { + std::lock_guard lock(tsfn->mutex); + tsfn->closing = true; + tsfn->envAlive = false; + } + // Producers blocked on a full queue have to be let go before anything + // else: they are answered with napi_closing. + tsfn->spaceAvailable.notify_all(); + + v8::Isolate::Scope isolate_scope(env->isolate); + v8::HandleScope handle_scope(env->isolate); + v8::Context::Scope context_scope(env->context()); + FinalizeOnJsThread(tsfn); + + // The home thread id stays comparable forever; only `envAlive` gates + // posting work. + { + std::lock_guard lock(tsfn->mutex); + tsfn->env = nullptr; + tsfn->isolate = nullptr; + } + + DropHandleRefIfDone(tsfn); + } + } +} + +} // namespace tns + +//=== Entry points ========================================================= + +napi_status NAPI_CDECL +napi_create_threadsafe_function(napi_env env, + napi_value func, + napi_value async_resource, + napi_value async_resource_name, + size_t max_queue_size, + size_t initial_thread_count, + void* thread_finalize_data, + napi_finalize thread_finalize_cb, + void* context, + napi_threadsafe_function_call_js call_js_cb, + napi_threadsafe_function* result) { + CHECK_ENV(env); + CHECK_ARG(env, async_resource_name); + RETURN_STATUS_IF_FALSE(env, initial_thread_count > 0, napi_invalid_arg); + CHECK_ARG(env, result); + + // There is no async_hooks here, so the resource object carries no behaviour. + (void)async_resource; + + if (func == nullptr) { + CHECK_ARG(env, call_js_cb); + } + + tns::NapiEnv* tnsEnv = static_cast(env); + RETURN_STATUS_IF_FALSE(env, tnsEnv->GetEventLoop() != nullptr, + napi_generic_failure); + + TsfnRef tsfn = std::make_shared(); + tsfn->env = tnsEnv; + tsfn->isolate = env->isolate; + tsfn->homeThread = tnsEnv->HomeThread(); + tsfn->eventLoop = tnsEnv->GetEventLoop(); + tsfn->callJs = call_js_cb; + tsfn->finalizeCb = thread_finalize_cb; + tsfn->finalizeData = thread_finalize_data; + tsfn->context = context; + tsfn->maxQueueSize = max_queue_size; + tsfn->threadCount = initial_thread_count; + + if (func != nullptr) { + napi_status status = + napi_create_reference(env, func, 1, &tsfn->callbackRef); + if (status != napi_ok) { + return napi_set_last_error(env, status); + } + } + + { + TsfnRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + registry.live[tsfn.get()] = tsfn; + } + + *result = tsfn.get(); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_threadsafe_function_context( + napi_threadsafe_function func, void** result) { + if (func == nullptr || result == nullptr) { + return napi_invalid_arg; + } + + *result = func->context; + return napi_ok; +} + +napi_status NAPI_CDECL +napi_call_threadsafe_function(napi_threadsafe_function func, + void* data, + napi_threadsafe_function_call_mode is_blocking) { + if (func == nullptr) { + return napi_invalid_arg; + } + + { + std::unique_lock lock(func->mutex); + while (!func->closing && func->maxQueueSize > 0 && + func->queue.size() >= func->maxQueueSize) { + if (is_blocking == napi_tsfn_nonblocking) { + return napi_queue_full; + } + // The thread that drains the queue is the one that would have to wake + // this wait, so blocking on it there can only stall forever. Node blocks + // regardless and leaves this status unused; wedging the event loop is + // worse than a status an addon may not expect. + if (std::this_thread::get_id() == func->homeThread) { + return napi_would_deadlock; + } + func->spaceAvailable.wait(lock); + } + + if (func->closing) { + return napi_closing; + } + + func->queue.push(data); + } + + PostDispatch(func->shared_from_this()); + return napi_ok; +} + +napi_status NAPI_CDECL +napi_acquire_threadsafe_function(napi_threadsafe_function func) { + if (func == nullptr) { + return napi_invalid_arg; + } + + std::lock_guard lock(func->mutex); + if (func->closing) { + return napi_closing; + } + + func->threadCount++; + return napi_ok; +} + +napi_status NAPI_CDECL napi_release_threadsafe_function( + napi_threadsafe_function func, napi_threadsafe_function_release_mode mode) { + if (func == nullptr) { + return napi_invalid_arg; + } + + TsfnRef tsfn = func->shared_from_this(); + bool dispatch = false; + { + std::lock_guard lock(tsfn->mutex); + if (tsfn->threadCount == 0) { + return napi_invalid_arg; + } + + tsfn->threadCount--; + if (!tsfn->closing && + (mode == napi_tsfn_abort || tsfn->threadCount == 0)) { + tsfn->closing = (mode == napi_tsfn_abort); + dispatch = true; + } + } + + // A producer waiting for queue space has to see the close, and the JS side + // has to notice that it can finalize. + tsfn->spaceAvailable.notify_all(); + if (dispatch) { + PostDispatch(tsfn); + } + DropHandleRefIfDone(tsfn); + + return napi_ok; +} + +// The looper this runtime drives has no libuv-style reference count: it +// belongs to the app (or the worker) and never exits because a Node-API addon +// asked it to. The flag is tracked so napi_ref/unref pair up, and gates +// nothing. + +napi_status NAPI_CDECL napi_unref_threadsafe_function( + node_api_basic_env env, napi_threadsafe_function func) { + CHECK_ENV(env); + CHECK_ARG(env, func); + + std::lock_guard lock(func->mutex); + func->refed = false; + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_ref_threadsafe_function( + node_api_basic_env env, napi_threadsafe_function func) { + CHECK_ENV(env); + CHECK_ARG(env, func); + + std::lock_guard lock(func->mutex); + func->refed = true; + + return napi_clear_last_error(env); +} diff --git a/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.h b/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.h new file mode 100644 index 000000000..aac4cfaba --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NapiThreadSafeFunction.h @@ -0,0 +1,21 @@ +#ifndef NAPITHREADSAFEFUNCTION_H_ +#define NAPITHREADSAFEFUNCTION_H_ + +namespace tns { + +class NapiEnv; + +// Closes every threadsafe function still bound to `env`: queued calls are +// dropped, producer threads blocked on a full queue are woken and answered +// with napi_closing, and each function's finalizer runs here. +// +// Runs on the env's own thread with the isolate locked, at the head of env +// teardown: each function holds a reference to its JS callback, which must be +// deleted before the env finalizes its reference lists. Handles stay +// dereferenceable until their owning threads release them; they simply stop +// being able to reach JS. +void NapiAbortThreadSafeFunctions(NapiEnv* env); + +} // namespace tns + +#endif /* NAPITHREADSAFEFUNCTION_H_ */ diff --git a/test-app/runtime/src/main/cpp/napi/NodeApiEmbed.cpp b/test-app/runtime/src/main/cpp/napi/NodeApiEmbed.cpp new file mode 100644 index 000000000..5888b389e --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/NodeApiEmbed.cpp @@ -0,0 +1,1008 @@ +// The embedder half of Node-API: everything node_api.h declares that the +// vendored js_native_api_v8.cc does not implement. Upstream this is +// src/node_api.cc, which is bound to node::Environment and libuv and so cannot +// be vendored. + +// Must precede every include: without NAPI_EXPERIMENTAL, NAPI_VERSION defaults +// to 8 and the version-gated declarations in node_api.h stay invisible, so the +// definitions below would silently not match anything. +#define NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_NO_WARNING + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "node_api.h" + +#include "js_native_api_v8.h" + +#include "EventLoop.h" +#include "NapiEnv.h" +#include "NapiModules.h" + +namespace { + +struct ModuleRegistry { + std::mutex mutex; + std::unordered_map modules; + // Node's `modpending`: the module most recently registered on this thread, + // consumed by the `.so` loader right after its dlopen returns. + static thread_local napi_module* pending; +}; + +thread_local napi_module* ModuleRegistry::pending = nullptr; + +// Addons register from static constructors at image-load time and are read +// much later from JS threads, so the table must outlive both the static +// initialization order and every other TU's static destructors. +ModuleRegistry& Registry() { + static ModuleRegistry* registry = new ModuleRegistry(); + return *registry; +} + +napi_module* FindModule(const std::string& name) { + ModuleRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + auto it = registry.modules.find(name); + return it == registry.modules.end() ? nullptr : it->second; +} + +} // namespace + +//=== Module registration ================================================== + +void NAPI_CDECL napi_module_register(napi_module* mod) { + if (mod == nullptr || mod->nm_modname == nullptr) { + return; + } + + ModuleRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + // First registration wins: exports are cached per env under this name, so + // letting a late duplicate replace the module would hand different callers + // different addons under one identity. + auto inserted = registry.modules.emplace(mod->nm_modname, mod); + if (!inserted.second && inserted.first->second != mod) { + __android_log_print(ANDROID_LOG_WARN, "TNS.Napi", + "Ignoring duplicate Node-API module registration for " + "'%s'", + mod->nm_modname); + return; + } + ModuleRegistry::pending = mod; +} + +// No `node_module_register` alias is exported: Node's symbol of that name +// takes a `node_module*` (a different layout than `napi_module`) and +// napi-ios's takes (name, init) — a single-pointer cast would misread both. +// Addons register through `napi_module_register`. + +namespace tns { + +bool NapiModules::IsRegistered(const std::string& name) { + return FindModule(name) != nullptr; +} + +std::string NapiModules::ClaimPendingModule() { + ModuleRegistry& registry = Registry(); + std::lock_guard lock(registry.mutex); + napi_module* mod = ModuleRegistry::pending; + ModuleRegistry::pending = nullptr; + return mod == nullptr ? std::string() : std::string(mod->nm_modname); +} + +namespace { + +// The shared instantiation core: runs an addon's register function against +// the context's env, caching the exports per env under `cacheKey`. +v8::MaybeLocal InstantiateAddon(v8::Local context, + napi_addon_register_func registerFunc, + const std::string& cacheKey) { + if (registerFunc == nullptr) { + return v8::MaybeLocal(); + } + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + tns::NapiEnv* env = tns::NapiEnv::ForIsolate(isolate); + if (env == nullptr) { + return v8::MaybeLocal(); + } + + v8::Local cached; + if (env->CachedModuleExports(cacheKey).ToLocal(&cached)) { + return cached; + } + + v8::Local exports = v8::Object::New(isolate); + + v8::Local envContext = env->context(); + v8::Context::Scope contextScope(envContext); + v8::TryCatch tc(isolate); + + napi_value returned = + registerFunc(env, v8impl::JsValueFromV8LocalValue(exports)); + if (tc.HasCaught()) { + tc.ReThrow(); + return v8::MaybeLocal(); + } + + if (returned != nullptr) { + v8::Local value = v8impl::V8LocalValueFromJsValue(returned); + // A primitive return has no way to become a module namespace, so the + // object handed to the register func stays authoritative. + if (value->IsObject()) { + exports = value.As(); + } + } + + env->CacheModuleExports(cacheKey, exports); + return exports; +} + +} // namespace + +v8::MaybeLocal NapiModules::GetExports( + v8::Local context, const std::string& name) { + napi_module* mod = FindModule(name); + if (mod == nullptr) { + return v8::MaybeLocal(); + } + + return InstantiateAddon(context, mod->nm_register_func, name); +} + +v8::MaybeLocal NapiModules::InitAddonFromSymbol( + v8::Local context, void* initSymbol, + const std::string& cacheKey) { + return InstantiateAddon( + context, reinterpret_cast(initSymbol), + cacheKey); +} + +} // namespace tns + +//=== Version and fatal errors ============================================= + +napi_status NAPI_CDECL napi_get_node_version( + node_api_basic_env env, const napi_node_version** version) { + CHECK_ENV(env); + CHECK_ARG(env, version); + + static const napi_node_version node_version = {26, 7, 0, "node"}; + *version = &node_version; + + return napi_clear_last_error(env); +} + +void NAPI_CDECL napi_fatal_error(const char* location, + size_t location_len, + const char* message, + size_t message_len) { + std::string location_string; + if (location != nullptr) { + location_string.assign(location, location_len == NAPI_AUTO_LENGTH + ? strlen(location) + : location_len); + } + + std::string message_string; + if (message != nullptr) { + message_string.assign( + message, + message_len == NAPI_AUTO_LENGTH ? strlen(message) : message_len); + } + + __android_log_print(ANDROID_LOG_FATAL, "TNS.Napi", "FATAL ERROR: %s %s", + location_string.c_str(), message_string.c_str()); + abort(); +} + +napi_status NAPI_CDECL napi_fatal_exception(napi_env env, napi_value err) { + CHECK_ENV(env); + CHECK_ARG(env, err); + + v8::HandleScope scope(env->isolate); + v8::Context::Scope context_scope(env->context()); + tns::NapiReportModuleException(env, + v8impl::V8LocalValueFromJsValue(err)); + + return napi_clear_last_error(env); +} + +//=== Buffers ============================================================== +// +// There is no node::Buffer here: a napi buffer is a plain Uint8Array, which is +// what addons observe through napi_get_buffer_info either way. + +napi_status NAPI_CDECL napi_create_buffer(napi_env env, + size_t length, + void** data, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local buffer = + v8::ArrayBuffer::New(env->isolate, length); + v8::Local array = v8::Uint8Array::New(buffer, 0, length); + + if (data != nullptr) { + *data = buffer->Data(); + } + + *result = v8impl::JsValueFromV8LocalValue(array); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_create_buffer_copy(napi_env env, + size_t length, + const void* data, + void** result_data, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local buffer = + v8::ArrayBuffer::New(env->isolate, length); + if (length > 0) { + CHECK_ARG(env, data); + memcpy(buffer->Data(), data, length); + } + + v8::Local array = v8::Uint8Array::New(buffer, 0, length); + + if (result_data != nullptr) { + *result_data = buffer->Data(); + } + + *result = v8impl::JsValueFromV8LocalValue(array); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +napi_create_external_buffer(napi_env env, + size_t length, + void* data, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + // The deleter can fire on any thread, including during isolate disposal + // after the env died, so it never runs the callback itself: it posts the + // env-registered finalizer to the event loop, and whatever the post cannot + // reach (loop shut down, or already gone) the env teardown sweep has run or + // will run. DeleterState carries one strong ref so the finalizer outlives a + // posted run even after teardown erased the registry entry. + struct DeleterState { + tns::NapiEnv* env; + std::shared_ptr finalizer; + std::weak_ptr loop; + }; + auto deleter = [](void*, size_t, void* deleter_data) { + std::unique_ptr state( + static_cast(deleter_data)); + if (state == nullptr || state->finalizer->claimed.load()) { + return; + } + + std::shared_ptr loop = state->loop.lock(); + if (loop == nullptr || loop->IsStopped()) { + return; + } + tns::NapiEnv* stateEnv = state->env; + std::shared_ptr finalizer = state->finalizer; + loop->PostInternal( + [stateEnv, finalizer]() { stateEnv->RunExternalFinalizer(finalizer); }); + }; + + tns::NapiEnv* tnsEnv = static_cast(env); + DeleterState* deleter_data = nullptr; + if (finalize_cb != nullptr) { + auto finalizer = std::make_shared(); + finalizer->cb = reinterpret_cast(finalize_cb); + finalizer->data = data; + finalizer->hint = finalize_hint; + tnsEnv->RegisterExternalFinalizer(finalizer); + deleter_data = + new DeleterState{tnsEnv, std::move(finalizer), tnsEnv->GetEventLoop()}; + } + + std::unique_ptr backing_store = + v8::ArrayBuffer::NewBackingStore( + data, length, deleter, reinterpret_cast(deleter_data)); + CHECK(!!backing_store); // Cannot fail. + + v8::Local buffer = + v8::ArrayBuffer::New(env->isolate, std::move(backing_store)); + v8::Local array = v8::Uint8Array::New(buffer, 0, length); + + *result = v8impl::JsValueFromV8LocalValue(array); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +node_api_create_buffer_from_arraybuffer(napi_env env, + napi_value arraybuffer, + size_t byte_offset, + size_t byte_length, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, arraybuffer); + CHECK_ARG(env, result); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + RETURN_STATUS_IF_FALSE(env, value->IsArrayBuffer(), napi_invalid_arg); + + v8::Local buffer = value.As(); + THROW_RANGE_ERROR_IF_FALSE( + env, + byte_offset <= buffer->ByteLength() && + byte_length <= buffer->ByteLength() - byte_offset, + "ERR_OUT_OF_RANGE", + "The byte offset + length is out of range"); + + v8::Local array = + v8::Uint8Array::New(buffer, byte_offset, byte_length); + + *result = v8impl::JsValueFromV8LocalValue(array); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_is_buffer(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsUint8Array(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_buffer_info(napi_env env, + napi_value value, + void** data, + size_t* length) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsUint8Array(), napi_invalid_arg); + + v8::Local array = val.As(); + + if (data != nullptr) { + // Calling Buffer() may have the side effect of allocating the buffer, + // so only do this when it's needed. + *data = + static_cast(array->Buffer()->Data()) + array->ByteOffset(); + } + + if (length != nullptr) { + *length = array->ByteLength(); + } + + return napi_clear_last_error(env); +} + +//=== Async contexts and callback scopes =================================== +// +// There is no async_hooks here, so an async context is an opaque token that +// keeps its resource objects alive for as long as the addon holds it, and a +// callback scope is the depth counter the vendored sources balance against. + +struct napi_async_context__ { + napi_env env = nullptr; + napi_ref resource = nullptr; +}; + +struct napi_callback_scope__ { + napi_env env = nullptr; +}; + +napi_status NAPI_CDECL napi_async_init(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_context* result) { + CHECK_ENV(env); + CHECK_ARG(env, async_resource_name); + CHECK_ARG(env, result); + + std::unique_ptr context(new napi_async_context__()); + context->env = env; + + if (async_resource != nullptr) { + STATUS_CALL( + napi_create_reference(env, async_resource, 1, &context->resource)); + } + // The name only feeds async_hooks, which don't exist here — and referencing + // it would fail anyway: a string ref needs module API version >= 10 and envs + // default to 8. + + *result = context.release(); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_async_destroy(napi_env env, + napi_async_context async_context) { + CHECK_ENV(env); + CHECK_ARG(env, async_context); + RETURN_STATUS_IF_FALSE(env, async_context->env == env, napi_invalid_arg); + + if (async_context->resource != nullptr) { + napi_delete_reference(env, async_context->resource); + } + delete async_context; + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_open_callback_scope(napi_env env, + napi_value resource_object, + napi_async_context context, + napi_callback_scope* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + // Both only matter to async_hooks listeners, which do not exist here. + (void)resource_object; + (void)context; + + napi_callback_scope__* scope = new napi_callback_scope__(); + scope->env = env; + env->open_callback_scopes++; + + *result = scope; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_close_callback_scope(napi_env env, + napi_callback_scope scope) { + CHECK_ENV(env); + CHECK_ARG(env, scope); + RETURN_STATUS_IF_FALSE(env, scope->env == env, napi_callback_scope_mismatch); + RETURN_STATUS_IF_FALSE(env, env->open_callback_scopes > 0, + napi_callback_scope_mismatch); + + env->open_callback_scopes--; + delete scope; + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_make_callback(napi_env env, + napi_async_context async_context, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, recv); + if (argc > 0) { + CHECK_ARG(env, argv); + } + + // Entering the isolate is only ever legal from the thread that owns it; + // reaching JS from anywhere else is what threadsafe functions are for. + RETURN_STATUS_IF_FALSE( + env, + std::this_thread::get_id() == + static_cast(env)->HomeThread(), + napi_generic_failure); + + (void)async_context; + + v8::Local context = env->context(); + + v8::Local v8recv; + CHECK_TO_OBJECT(env, context, v8recv, recv); + + v8::Local v8func; + CHECK_TO_FUNCTION(env, v8func, func); + + env->open_callback_scopes++; + v8::MaybeLocal callback_result = v8func->Call( + context, v8recv, static_cast(argc), + reinterpret_cast*>(const_cast(argv))); + env->open_callback_scopes--; + + // Node drains microtasks as the outermost callback scope closes; this + // isolate is left on V8's automatic policy, which already does that when the + // call returns to native code. + + if (try_catch.HasCaught()) { + return napi_set_last_error(env, napi_pending_exception); + } + + CHECK_MAYBE_EMPTY(env, callback_result, napi_generic_failure); + if (result != nullptr) { + *result = v8impl::JsValueFromV8LocalValue(callback_result.ToLocalChecked()); + } + + return GET_RETURN_STATUS(env); +} + +//=== Async work =========================================================== + +struct napi_async_work__ { + enum class State { idle, queued, executing, completed }; + + tns::NapiEnv* env = nullptr; + v8::Isolate* isolate = nullptr; + // Goes null once the runtime shuts the loop down; a completion that finds + // it null is dropped, the same fate Shutdown gives already-queued entries. + std::weak_ptr eventLoop; + napi_async_execute_callback execute = nullptr; + napi_async_complete_callback complete = nullptr; + void* data = nullptr; + + std::mutex mutex; + State state = State::idle; + bool cancelled = false; +}; + +namespace { + +// The env's thread. `work` belongs to the addon, which is free to delete it +// from the complete callback, so nothing may touch it afterwards. +// Runs as an internal-lane entry, under the loop's Locker/scopes. Shutdown +// drops queued completions before the env dies (the addon's `data` is dropped +// with them — the same trade Node makes at environment shutdown), so no +// liveness check or isolate ceremony is needed here. +void CompleteAsyncWork(napi_async_work work, napi_status status) { + tns::NapiEnv* env = work->env; + { + std::lock_guard lock(work->mutex); + work->state = napi_async_work__::State::completed; + } + + napi_async_complete_callback complete = work->complete; + if (complete == nullptr) { + return; + } + + // The loop entry supplies Locker/Isolate::Scope/HandleScope but no context; + // the complete callback runs with the env's context entered (Node's + // contract), and the exception handler below needs one too. + v8::HandleScope handle_scope(env->isolate); + v8::Context::Scope context_scope(env->context()); + + void* data = work->data; + env->CallIntoModule( + [&](napi_env moduleEnv) { complete(moduleEnv, status, data); }, + tns::NapiReportModuleException); +} + +// Node runs async work on a fixed libuv pool (4 threads by default) and +// addons write execute callbacks that assume a bounded worker count — an +// unbounded pool would let N blocking callbacks spawn N threads. The threads +// are detached and live for the process, exactly like the libuv pool; they +// never touch the JVM or any isolate. +class AsyncWorkPool { + public: + static AsyncWorkPool& Instance() { + static AsyncWorkPool* pool = new AsyncWorkPool(); + return *pool; + } + + void Submit(std::function job) { + { + std::lock_guard lock(mutex_); + jobs_.push(std::move(job)); + } + jobAvailable_.notify_one(); + } + + private: + static constexpr int kThreadCount = 4; + + AsyncWorkPool() { + for (int i = 0; i < kThreadCount; i++) { + std::thread([this]() { this->Run(); }).detach(); + } + } + + void Run() { + for (;;) { + std::function job; + { + std::unique_lock lock(mutex_); + jobAvailable_.wait(lock, [this]() { return !jobs_.empty(); }); + job = std::move(jobs_.front()); + jobs_.pop(); + } + try { + job(); + } catch (const std::exception& e) { + // An exception escaping an execute callback would otherwise + // std::terminate with no diagnostic. It is still fatal (there is no + // one to hand it to, matching Node), but it dies with a name. + __android_log_print(ANDROID_LOG_FATAL, "TNS.Napi", + "Uncaught C++ exception in napi_async_work " + "execute callback: %s", + e.what()); + abort(); + } catch (...) { + __android_log_print(ANDROID_LOG_FATAL, "TNS.Napi", + "Uncaught C++ exception in napi_async_work " + "execute callback"); + abort(); + } + } + } + + std::mutex mutex_; + std::condition_variable jobAvailable_; + std::queue> jobs_; +}; + +} // namespace + +napi_status NAPI_CDECL +napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result) { + CHECK_ENV(env); + CHECK_ARG(env, async_resource_name); + CHECK_ARG(env, execute); + CHECK_ARG(env, result); + + // Only async_hooks listeners would observe the resource, and there are none. + (void)async_resource; + + tns::NapiEnv* tnsEnv = static_cast(env); + RETURN_STATUS_IF_FALSE(env, tnsEnv->GetEventLoop() != nullptr, + napi_generic_failure); + + napi_async_work__* work = new napi_async_work__(); + work->env = tnsEnv; + work->isolate = env->isolate; + work->eventLoop = tnsEnv->GetEventLoop(); + work->execute = execute; + work->complete = complete; + work->data = data; + + *result = work; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_delete_async_work(napi_env env, + napi_async_work work) { + CHECK_ENV(env); + CHECK_ARG(env, work); + + { + // Node deletes unconditionally, which leaves the pointer the queued work + // still holds dangling. Refusing leaks the work instead, which an addon + // deleting straight after a cancel will notice. + std::lock_guard lock(work->mutex); + RETURN_STATUS_IF_FALSE(env, + work->state == napi_async_work__::State::idle || + work->state == + napi_async_work__::State::completed, + napi_generic_failure); + } + + delete work; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_queue_async_work(node_api_basic_env basic_env, + napi_async_work work) { + CHECK_ENV(basic_env); + CHECK_ARG(basic_env, work); + + napi_env env = const_cast(basic_env); + RETURN_STATUS_IF_FALSE(env, work->env == env, napi_invalid_arg); + + { + std::lock_guard lock(work->mutex); + RETURN_STATUS_IF_FALSE(env, work->state == napi_async_work__::State::idle || + work->state == + napi_async_work__::State::completed, + napi_generic_failure); + work->state = napi_async_work__::State::queued; + work->cancelled = false; + } + + std::shared_ptr> envAlive = + static_cast(env)->AliveFlag(); + AsyncWorkPool::Instance().Submit([work, envAlive]() { + // The env can die (Worker terminated) while this job waits in the pool; + // the flag flip happens-before the queued-entry drop in + // EventLoop::Shutdown, so a false read here means the raw `work->env` must + // not be dereferenced. The work is parked in `completed` so a cleanup + // hook's napi_delete_async_work still succeeds; execute/complete are + // dropped — the same trade Node makes at environment shutdown. + if (!envAlive->load()) { + std::lock_guard lock(work->mutex); + work->state = napi_async_work__::State::completed; + return; + } + + napi_status status = napi_ok; + { + std::lock_guard lock(work->mutex); + if (work->cancelled) { + status = napi_cancelled; + } else { + work->state = napi_async_work__::State::executing; + } + } + + // Off the env's thread: the execute callback may not touch the isolate, + // which is exactly the contract Node states for it. + if (status == napi_ok) { + work->execute(work->env, work->data); + } + + std::shared_ptr loop = work->eventLoop.lock(); + if (loop != nullptr && !loop->IsStopped()) { + loop->PostInternal([work, status]() { CompleteAsyncWork(work, status); }); + } else { + // Completion cannot be delivered; park the work so it stays deletable. + std::lock_guard lock(work->mutex); + work->state = napi_async_work__::State::completed; + } + }); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_cancel_async_work(node_api_basic_env basic_env, + napi_async_work work) { + CHECK_ENV(basic_env); + CHECK_ARG(basic_env, work); + + napi_env env = const_cast(basic_env); + RETURN_STATUS_IF_FALSE(env, work->env == env, napi_invalid_arg); + + std::lock_guard lock(work->mutex); + // Once the execute callback is running there is nothing to cancel; the same + // mutex is what makes the answer truthful. + RETURN_STATUS_IF_FALSE(env, work->state == napi_async_work__::State::queued, + napi_generic_failure); + work->cancelled = true; + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_uv_event_loop(node_api_basic_env env, + struct uv_loop_s** loop) { + // This runtime drives an Android Looper; there is no uv_loop_t to hand out, + // and inventing one would be worse than saying so. + CHECK_ENV(env); + CHECK_ARG(env, loop); + return napi_set_last_error(env, napi_generic_failure); +} + +//=== Cleanup hooks ======================================================== + +struct napi_async_cleanup_hook_handle__ { + napi_env env = nullptr; + napi_async_cleanup_hook hook = nullptr; + void* data = nullptr; +}; + +namespace { + +// One entry per registered hook, in registration order: Node runs both kinds +// off a single list, most recently added first, and addons rely on that to +// undo their work in the reverse order they set it up. +struct CleanupEntry { + napi_cleanup_hook hook = nullptr; + void* arg = nullptr; + napi_async_cleanup_hook_handle__* asyncHandle = nullptr; +}; + +struct CleanupRegistry { + std::mutex mutex; + std::unordered_map> byEnv; + // Handles outlive their env's entry, so removal can be answered without + // reading through a `napi_env` that may already be gone. + std::unordered_set liveHandles; +}; + +CleanupRegistry& Cleanups() { + static CleanupRegistry* registry = new CleanupRegistry(); + return *registry; +} + +} // namespace + +napi_status NAPI_CDECL napi_add_env_cleanup_hook(node_api_basic_env basic_env, + napi_cleanup_hook fun, + void* arg) { + CHECK_ENV(basic_env); + CHECK_ARG(basic_env, fun); + + napi_env env = const_cast(basic_env); + + CleanupRegistry& registry = Cleanups(); + std::lock_guard lock(registry.mutex); + std::vector& entries = registry.byEnv[env]; + // Node keeps these in a set keyed by (fun, arg): re-registering an existing + // pair is a no-op there, and running the hook twice at teardown would hand + // an addon written against that contract a double free. + auto existing = std::find_if( + entries.begin(), entries.end(), [&](const CleanupEntry& candidate) { + return candidate.asyncHandle == nullptr && candidate.hook == fun && + candidate.arg == arg; + }); + if (existing == entries.end()) { + entries.push_back(CleanupEntry{fun, arg, nullptr}); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_remove_env_cleanup_hook( + node_api_basic_env basic_env, napi_cleanup_hook fun, void* arg) { + CHECK_ENV(basic_env); + CHECK_ARG(basic_env, fun); + + napi_env env = const_cast(basic_env); + + CleanupRegistry& registry = Cleanups(); + std::lock_guard lock(registry.mutex); + auto it = registry.byEnv.find(env); + if (it == registry.byEnv.end()) { + return napi_set_last_error(env, napi_invalid_arg); + } + + std::vector& entries = it->second; + auto entry = std::find_if( + entries.rbegin(), entries.rend(), [&](const CleanupEntry& candidate) { + return candidate.asyncHandle == nullptr && candidate.hook == fun && + candidate.arg == arg; + }); + if (entry == entries.rend()) { + return napi_set_last_error(env, napi_invalid_arg); + } + + entries.erase(std::next(entry).base()); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL +napi_add_async_cleanup_hook(node_api_basic_env basic_env, + napi_async_cleanup_hook hook, + void* arg, + napi_async_cleanup_hook_handle* remove_handle) { + CHECK_ENV(basic_env); + CHECK_ARG(basic_env, hook); + + napi_env env = const_cast(basic_env); + + napi_async_cleanup_hook_handle__* handle = + new napi_async_cleanup_hook_handle__(); + handle->env = env; + handle->hook = hook; + handle->data = arg; + + { + CleanupRegistry& registry = Cleanups(); + std::lock_guard lock(registry.mutex); + registry.byEnv[env].push_back(CleanupEntry{nullptr, nullptr, handle}); + registry.liveHandles.insert(handle); + } + + if (remove_handle != nullptr) { + *remove_handle = handle; + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_remove_async_cleanup_hook( + napi_async_cleanup_hook_handle remove_handle) { + if (remove_handle == nullptr) { + return napi_invalid_arg; + } + + CleanupRegistry& registry = Cleanups(); + { + std::lock_guard lock(registry.mutex); + if (registry.liveHandles.erase(remove_handle) == 0) { + return napi_invalid_arg; + } + + auto it = registry.byEnv.find(remove_handle->env); + if (it != registry.byEnv.end()) { + std::vector& entries = it->second; + entries.erase(std::remove_if(entries.begin(), entries.end(), + [&](const CleanupEntry& candidate) { + return candidate.asyncHandle == + remove_handle; + }), + entries.end()); + } + } + + delete remove_handle; + return napi_ok; +} + +namespace tns { + +void NapiRunEnvCleanupHooks(NapiEnv* env) { + CleanupRegistry& registry = Cleanups(); + + // A hook reaches back into Node-API through an env it stashed away, and + // teardown holds the isolate's lock but opens no scopes of its own. Note + // that execution is already terminating by then: a hook can still release + // env-bound resources, but nothing it does will reach JS. + v8::HandleScope handle_scope(env->isolate); + v8::Context::Scope context_scope(env->context()); + + // Hooks add and remove other hooks as they run, so each round is taken from + // the live list rather than a snapshot — one that dropped an entry would + // call through a hook another hook already deleted. + for (;;) { + CleanupEntry entry; + { + std::lock_guard lock(registry.mutex); + auto it = registry.byEnv.find(env); + if (it == registry.byEnv.end()) { + return; + } + + std::vector& entries = it->second; + if (entries.empty()) { + registry.byEnv.erase(it); + return; + } + + entry = entries.back(); + entries.pop_back(); + } + + if (entry.asyncHandle == nullptr) { + entry.hook(entry.arg); + continue; + } + + // Node waits for every async hook to report back through + // napi_remove_async_cleanup_hook before the env goes away. Nothing can + // wait here: the thread running this teardown is the one that would have + // to run the completion, so a hook that defers is simply not awaited. Its + // handle stays live so a late removal is still safe, and it can no longer + // reach JS. + entry.asyncHandle->hook(entry.asyncHandle, entry.asyncHandle->data); + } +} + +} // namespace tns + +//=== Not implemented ====================================================== + +napi_status NAPI_CDECL node_api_get_module_file_name(node_api_basic_env env, + const char** result) { + // Addons are linked into the app or loaded through the runtime's own `.so` + // require path; nothing identifies the calling module at this point. + CHECK_ENV(env); + CHECK_ARG(env, result); + return napi_set_last_error(env, napi_generic_failure); +} diff --git a/test-app/runtime/src/main/cpp/napi/shim/env-inl.h b/test-app/runtime/src/main/cpp/napi/shim/env-inl.h new file mode 100644 index 000000000..348cc8ebc --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/shim/env-inl.h @@ -0,0 +1,16 @@ +#ifndef SRC_ENV_INL_H_ +#define SRC_ENV_INL_H_ + +// js_native_api_v8.cc includes this name expecting Node's environment header. +// It is the first include after the NAPI_EXPERIMENTAL opt-in, which is why the +// shim's own definitions are pulled in from here rather than only through +// js_native_api_v8.h. + +#include "js_native_api_v8_internals.h" + +// napi_create_external_arraybuffer delegates to napi_create_external_buffer, +// which js_native_api_v8.cc never includes a header for: upstream it arrives +// through node.h, pulled in by the real env.h. +#include "node_api.h" + +#endif // SRC_ENV_INL_H_ diff --git a/test-app/runtime/src/main/cpp/napi/shim/js_native_api_v8_internals.h b/test-app/runtime/src/main/cpp/napi/shim/js_native_api_v8_internals.h new file mode 100644 index 000000000..fc6ad3f54 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/shim/js_native_api_v8_internals.h @@ -0,0 +1,141 @@ +#ifndef SRC_JS_NATIVE_API_V8_INTERNALS_H_ +#define SRC_JS_NATIVE_API_V8_INTERNALS_H_ + +// Supplies the idioms `js_native_api_v8.{h,cc}` expect from their embedder. +// Upstream bridges these to Node's internal headers; here they are defined +// against this runtime instead, so the vendored files stay byte-identical. +// +// Anything needing the runtime (private keys, fatal errors) is declared here +// and defined in NapiEnv.cpp. + +// js_native_api_v8.cc opts into NAPI_EXPERIMENTAL so it implements every +// versioned entry point; the accompanying #warning is not actionable here and +// -Werror would turn it into a build failure. +#ifndef NODE_API_EXPERIMENTAL_NO_WARNING +#define NODE_API_EXPERIMENTAL_NO_WARNING +#endif + +#include +#include +#include +#include +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#include "v8.h" +#pragma clang diagnostic pop + +// Mirrors src/node_version.h of the vendored Node.js release. +#define NODE_API_SUPPORTED_VERSION_MAX 10 +#define NODE_API_SUPPORTED_VERSION_MIN 1 +#define NODE_API_DEFAULT_MODULE_API_VERSION 8 + +#define NAPI_ARRAYSIZE(array) (sizeof(::v8impl::ArraySizeHelper(array))) + +#define NAPI_FIXED_ONE_BYTE_STRING(isolate, string) \ + (::v8impl::OneByteString((isolate), (string), sizeof(string) - 1)) + +namespace tns { + +// The suffixes js_native_api_v8.cc passes to NAPI_PRIVATE_KEY. Named to match +// the macro argument so the macro can paste them unchanged. +enum class NapiPrivateKeySlot { wrapper, type_tag }; + +// Node keys these off node::Environment; we key off the napi_env owning the +// context, which is reachable from the isolate. +v8::Local NapiPrivateKey(v8::Local context, + NapiPrivateKeySlot slot); + +} // namespace tns + +#define NAPI_PRIVATE_KEY(context, suffix) \ + (::tns::NapiPrivateKey((context), ::tns::NapiPrivateKeySlot::suffix)) + +namespace v8impl { + +template +using Persistent = v8::Global; + +// Reads a v8::Global as a v8::Local without allocating a handle. Sound only +// for strong references: a weak one may have been cleared, so the slot has to +// be re-read through the isolate. +class PersistentToLocal { + public: + template + static inline v8::Local Strong( + const v8::PersistentBase& persistent) { + return *reinterpret_cast*>( + const_cast*>(&persistent)); + } + + template + static inline v8::Local Weak( + v8::Isolate* isolate, const v8::PersistentBase& persistent) { + return v8::Local::New(isolate, persistent); + } + + template + static inline v8::Local Default( + v8::Isolate* isolate, const v8::PersistentBase& persistent) { + return persistent.IsWeak() ? Weak(isolate, persistent) : Strong(persistent); + } +}; + +template +char (&ArraySizeHelper(T (&array)[N]))[N]; + +inline v8::Local OneByteString(v8::Isolate* isolate, + const char* data, int length) { + return v8::String::NewFromOneByte(isolate, + reinterpret_cast(data), + v8::NewStringType::kInternalized, length) + .ToLocalChecked(); +} + +[[noreturn]] void OnFatalError(const char* location, const char* message); + +} // end of namespace v8impl + +#define NAPI_STRINGIFY_HELPER(x) #x +#define NAPI_STRINGIFY(x) NAPI_STRINGIFY_HELPER(x) + +// Unprefixed because the vendored sources spell them that way. The inspector +// headers define the same names as equivalent abort-on-false assertions, so a +// translation unit that already has them keeps its own rather than clashing. +#ifndef CHECK +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + ::v8impl::OnFatalError(__FILE__ ":" NAPI_STRINGIFY(__LINE__), \ + "CHECK(" #expr ") failed"); \ + } \ + } while (0) +#endif + +#ifndef CHECK_EQ +#define CHECK_EQ(a, b) CHECK((a) == (b)) +#endif +#ifndef CHECK_NE +#define CHECK_NE(a, b) CHECK((a) != (b)) +#endif +#ifndef CHECK_LE +#define CHECK_LE(a, b) CHECK((a) <= (b)) +#endif +#ifndef CHECK_LT +#define CHECK_LT(a, b) CHECK((a) < (b)) +#endif +#ifndef CHECK_GE +#define CHECK_GE(a, b) CHECK((a) >= (b)) +#endif +#ifndef CHECK_GT +#define CHECK_GT(a, b) CHECK((a) > (b)) +#endif +#ifndef CHECK_NULL +#define CHECK_NULL(val) CHECK((val) == nullptr) +#endif +#ifndef CHECK_NOT_NULL +#define CHECK_NOT_NULL(val) CHECK((val) != nullptr) +#endif + +#endif // SRC_JS_NATIVE_API_V8_INTERNALS_H_ diff --git a/test-app/runtime/src/main/cpp/napi/shim/util-inl.h b/test-app/runtime/src/main/cpp/napi/shim/util-inl.h new file mode 100644 index 000000000..ca1176f96 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/shim/util-inl.h @@ -0,0 +1,39 @@ +#ifndef SRC_UTIL_INL_H_ +#define SRC_UTIL_INL_H_ + +// js_native_api_v8.cc includes this name expecting Node's util header; of that +// header it uses only node::OnScopeLeave. + +#include + +#include "js_native_api_v8_internals.h" + +namespace node { + +template +struct OnScopeLeaveImpl { + Fn fn_; + bool active_; + + explicit OnScopeLeaveImpl(Fn&& fn) : fn_(std::move(fn)), active_(true) {} + ~OnScopeLeaveImpl() { + if (active_) fn_(); + } + + OnScopeLeaveImpl(const OnScopeLeaveImpl& other) = delete; + OnScopeLeaveImpl& operator=(const OnScopeLeaveImpl& other) = delete; + OnScopeLeaveImpl(OnScopeLeaveImpl&& other) + : fn_(std::move(other.fn_)), active_(other.active_) { + other.active_ = false; + } +}; + +// Runs `fn` when the returned guard leaves scope, however it is left. +template +inline OnScopeLeaveImpl OnScopeLeave(Fn&& fn) { + return OnScopeLeaveImpl(std::move(fn)); +} + +} // namespace node + +#endif // SRC_UTIL_INL_H_ diff --git a/test-app/runtime/src/main/cpp/napi/tests/NapiCoverageModule.cpp b/test-app/runtime/src/main/cpp/napi/tests/NapiCoverageModule.cpp new file mode 100644 index 000000000..1d206b089 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/tests/NapiCoverageModule.cpp @@ -0,0 +1,1415 @@ +// Addons see NAPI_VERSION 8 unless they ask for more; asking must come before +// the headers. The env still reports module API version 8 either way — this +// only decides which declarations exist. +#define NAPI_VERSION 9 + +#include +#include +#include +#include + +#include "NapiTestSupport.h" + +// A second addon, kept apart from NapiTestModule so that module stays a +// readable example of the embedding surface while this one sweeps the value, +// property and error APIs the way Node's test/js-native-api suites do. + +// Reads a short ASCII selector argument ("utf8", "range", ...). Longer input is +// truncated, which can only turn into a "no such selector" throw. +static bool ReadSelector(napi_env env, napi_value value, char* buffer, size_t size) { + size_t copied = 0; + return napi_get_value_string_utf8(env, value, buffer, size, &copied) == napi_ok; +} + +static napi_value UnknownSelector(napi_env env, const char* what) { + napi_throw_error(env, NULL, what); + return NULL; +} + +//=== Strings ============================================================== + +// The three encodings measure a string differently: utf8 counts bytes, utf16 +// counts code units, latin1 counts characters (V8's String::Length). +static napi_value StringLengths(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + size_t utf8 = 0; + size_t utf16 = 0; + size_t latin1 = 0; + NAPI_CALL(env, napi_get_value_string_utf8(env, args[0], NULL, 0, &utf8)); + NAPI_CALL(env, napi_get_value_string_utf16(env, args[0], NULL, 0, &utf16)); + NAPI_CALL(env, napi_get_value_string_latin1(env, args[0], NULL, 0, &latin1)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "utf8", NapiDouble(env, (double)utf8))); + NAPI_CALL(env, napi_set_named_property(env, result, "utf16", NapiDouble(env, (double)utf16))); + NAPI_CALL(env, napi_set_named_property(env, result, "latin1", NapiDouble(env, (double)latin1))); + return result; +} + +// Copies into a buffer of exactly `bufsize`, so a spec can pin the truncation +// contract: at most bufsize - 1 units are written and the rest is the +// terminator. +static napi_value CopyString(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char encoding[16]; + if (!ReadSelector(env, args[1], encoding, sizeof(encoding))) { + NapiThrowLastError(env); + return NULL; + } + + int32_t requested = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[2], &requested)); + size_t bufsize = requested < 0 ? 0 : (size_t)requested; + + size_t copied = 0; + napi_value text = NULL; + napi_status status = napi_ok; + + if (strcmp(encoding, "utf16") == 0) { + char16_t* buffer = (char16_t*)calloc(bufsize + 1, sizeof(char16_t)); + if (buffer == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + status = napi_get_value_string_utf16(env, args[0], buffer, bufsize, &copied); + if (status == napi_ok) { + status = napi_create_string_utf16(env, buffer, copied, &text); + } + free(buffer); + } else { + char* buffer = (char*)calloc(bufsize + 1, sizeof(char)); + if (buffer == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + if (strcmp(encoding, "utf8") == 0) { + status = napi_get_value_string_utf8(env, args[0], buffer, bufsize, &copied); + if (status == napi_ok) { + status = napi_create_string_utf8(env, buffer, copied, &text); + } + } else if (strcmp(encoding, "latin1") == 0) { + status = napi_get_value_string_latin1(env, args[0], buffer, bufsize, &copied); + if (status == napi_ok) { + status = napi_create_string_latin1(env, buffer, copied, &text); + } + } else { + free(buffer); + return UnknownSelector(env, "unknown string encoding"); + } + free(buffer); + } + + if (status != napi_ok) { + NapiThrowLastError(env); + return NULL; + } + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "copied", NapiDouble(env, (double)copied))); + NAPI_CALL(env, napi_set_named_property(env, result, "text", text)); + return result; +} + +static napi_value CreateStrings(napi_env env, napi_callback_info info) { + (void)info; + + static const char16_t kUtf16[] = u"utf16 ü \U0001F600"; + // Bytes above 0x7F are single latin1 characters, not UTF-8 sequences. + static const char kLatin1[] = {'A', (char)0xE9, (char)0xFF, '\0'}; + + napi_value utf8 = NULL; + napi_value utf8Sized = NULL; + napi_value utf16 = NULL; + napi_value latin1 = NULL; + napi_value empty = NULL; + NAPI_CALL(env, napi_create_string_utf8(env, "utf8 ü ☃", NAPI_AUTO_LENGTH, &utf8)); + NAPI_CALL(env, napi_create_string_utf8(env, "abcdef", 3, &utf8Sized)); + NAPI_CALL(env, napi_create_string_utf16(env, kUtf16, NAPI_AUTO_LENGTH, &utf16)); + NAPI_CALL(env, napi_create_string_latin1(env, kLatin1, 3, &latin1)); + NAPI_CALL(env, napi_create_string_utf8(env, "", 0, &empty)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "utf8", utf8)); + NAPI_CALL(env, napi_set_named_property(env, result, "utf8Sized", utf8Sized)); + NAPI_CALL(env, napi_set_named_property(env, result, "utf16", utf16)); + NAPI_CALL(env, napi_set_named_property(env, result, "latin1", latin1)); + NAPI_CALL(env, napi_set_named_property(env, result, "empty", empty)); + return result; +} + +static napi_value StringStatus(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + size_t length = 0; + return NapiStatusValue(env, napi_get_value_string_utf8(env, args[0], NULL, 0, &length)); +} + +//=== Numbers ============================================================== + +static napi_value NumberParts(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double asDouble = 0; + int32_t asInt32 = 0; + uint32_t asUint32 = 0; + int64_t asInt64 = 0; + NAPI_CALL(env, napi_get_value_double(env, args[0], &asDouble)); + NAPI_CALL(env, napi_get_value_int32(env, args[0], &asInt32)); + NAPI_CALL(env, napi_get_value_uint32(env, args[0], &asUint32)); + NAPI_CALL(env, napi_get_value_int64(env, args[0], &asInt64)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "double", NapiDouble(env, asDouble))); + NAPI_CALL(env, napi_set_named_property(env, result, "int32", NapiDouble(env, (double)asInt32))); + NAPI_CALL(env, napi_set_named_property(env, result, "uint32", NapiDouble(env, (double)asUint32))); + NAPI_CALL(env, napi_set_named_property(env, result, "int64", NapiDouble(env, (double)asInt64))); + return result; +} + +static napi_value CreateNumbers(napi_env env, napi_callback_info info) { + (void)info; + + napi_value int32Min = NULL; + napi_value int32Max = NULL; + napi_value uint32Max = NULL; + napi_value int64Max = NULL; + napi_value int64Min = NULL; + NAPI_CALL(env, napi_create_int32(env, INT32_MIN, &int32Min)); + NAPI_CALL(env, napi_create_int32(env, INT32_MAX, &int32Max)); + NAPI_CALL(env, napi_create_uint32(env, UINT32_MAX, &uint32Max)); + NAPI_CALL(env, napi_create_int64(env, 9007199254740991LL, &int64Max)); + NAPI_CALL(env, napi_create_int64(env, -9007199254740991LL, &int64Min)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "int32Min", int32Min)); + NAPI_CALL(env, napi_set_named_property(env, result, "int32Max", int32Max)); + NAPI_CALL(env, napi_set_named_property(env, result, "uint32Max", uint32Max)); + NAPI_CALL(env, napi_set_named_property(env, result, "int64Max", int64Max)); + NAPI_CALL(env, napi_set_named_property(env, result, "int64Min", int64Min)); + NAPI_CALL(env, napi_set_named_property(env, result, "nan", NapiDouble(env, NAN))); + NAPI_CALL(env, napi_set_named_property(env, result, "posInf", NapiDouble(env, INFINITY))); + NAPI_CALL(env, napi_set_named_property(env, result, "negInf", NapiDouble(env, -INFINITY))); + NAPI_CALL(env, napi_set_named_property(env, result, "negZero", NapiDouble(env, -0.0))); + return result; +} + +static napi_value NumberStatus(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double value = 0; + return NapiStatusValue(env, napi_get_value_double(env, args[0], &value)); +} + +//=== Symbols ============================================================== + +static napi_value CreateSymbol(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_valuetype type = napi_undefined; + NAPI_CALL(env, napi_typeof(env, args[0], &type)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_symbol(env, type == napi_undefined ? NULL : args[0], &result)); + return result; +} + +static napi_value CreateSymbolStatus(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value symbol = NULL; + return NapiStatusValue(env, napi_create_symbol(env, args[0], &symbol)); +} + +// Resolves in the same global registry Symbol.for uses. +static napi_value SymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char key[64]; + if (!ReadSelector(env, args[0], key, sizeof(key))) { + NapiThrowLastError(env); + return NULL; + } + + napi_value result = NULL; + NAPI_CALL(env, node_api_symbol_for(env, key, NAPI_AUTO_LENGTH, &result)); + return result; +} + +// A symbol is a valid property key everywhere a string is; the four operations +// have to agree about that. +static napi_value SymbolKeyRoundTrip(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool hasBefore = false; + bool hasAfter = false; + bool deleted = false; + bool hasAfterDelete = false; + napi_value read = NULL; + NAPI_CALL(env, napi_has_property(env, args[0], args[1], &hasBefore)); + NAPI_CALL(env, napi_set_property(env, args[0], args[1], args[2])); + NAPI_CALL(env, napi_has_property(env, args[0], args[1], &hasAfter)); + NAPI_CALL(env, napi_get_property(env, args[0], args[1], &read)); + NAPI_CALL(env, napi_delete_property(env, args[0], args[1], &deleted)); + NAPI_CALL(env, napi_has_property(env, args[0], args[1], &hasAfterDelete)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "hasBefore", NapiBool(env, hasBefore))); + NAPI_CALL(env, napi_set_named_property(env, result, "hasAfter", NapiBool(env, hasAfter))); + NAPI_CALL(env, napi_set_named_property(env, result, "read", read)); + NAPI_CALL(env, napi_set_named_property(env, result, "deleted", NapiBool(env, deleted))); + NAPI_CALL(env, + napi_set_named_property(env, result, "hasAfterDelete", NapiBool(env, hasAfterDelete))); + return result; +} + +//=== ArrayBuffers, typed arrays and DataViews ============================= + +static const struct { + const char* name; + napi_typedarray_type type; +} kTypedArrayTypes[] = { + {"int8", napi_int8_array}, + {"uint8", napi_uint8_array}, + {"uint8clamped", napi_uint8_clamped_array}, + {"int16", napi_int16_array}, + {"uint16", napi_uint16_array}, + {"int32", napi_int32_array}, + {"uint32", napi_uint32_array}, + {"float32", napi_float32_array}, + {"float64", napi_float64_array}, + {"bigint64", napi_bigint64_array}, + {"biguint64", napi_biguint64_array}, +}; + +static const size_t kTypedArrayTypeCount = sizeof(kTypedArrayTypes) / sizeof(kTypedArrayTypes[0]); + +static bool LookupTypedArrayType(const char* name, napi_typedarray_type* type) { + for (size_t i = 0; i < kTypedArrayTypeCount; i++) { + if (strcmp(kTypedArrayTypes[i].name, name) == 0) { + *type = kTypedArrayTypes[i].type; + return true; + } + } + return false; +} + +static const char* TypedArrayTypeName(napi_typedarray_type type) { + for (size_t i = 0; i < kTypedArrayTypeCount; i++) { + if (kTypedArrayTypes[i].type == type) { + return kTypedArrayTypes[i].name; + } + } + return "unknown"; +} + +// Bytes are written through the raw pointer, so JS reading them back proves the +// handle and the memory refer to the same buffer. +static napi_value CreateArrayBuffer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t byteLength = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[0], &byteLength)); + if (byteLength < 0) { + napi_throw_error(env, NULL, "negative byte length"); + return NULL; + } + + void* data = NULL; + napi_value result = NULL; + NAPI_CALL(env, napi_create_arraybuffer(env, (size_t)byteLength, &data, &result)); + + uint8_t* bytes = (uint8_t*)data; + for (int32_t i = 0; i < byteLength; i++) { + bytes[i] = (uint8_t)i; + } + return result; +} + +static napi_value ArrayBufferInfo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + void* data = NULL; + size_t byteLength = 0; + NAPI_CALL(env, napi_get_arraybuffer_info(env, args[0], &data, &byteLength)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL( + env, napi_set_named_property(env, result, "byteLength", NapiDouble(env, (double)byteLength))); + NAPI_CALL(env, napi_set_named_property(env, result, "hasData", NapiBool(env, data != NULL))); + if (byteLength > 0) { + NAPI_CALL(env, napi_set_named_property(env, result, "firstByte", + NapiDouble(env, ((uint8_t*)data)[0]))); + NAPI_CALL(env, napi_set_named_property(env, result, "lastByte", + NapiDouble(env, ((uint8_t*)data)[byteLength - 1]))); + } + return result; +} + +static napi_value WriteByte(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + void* data = NULL; + size_t byteLength = 0; + NAPI_CALL(env, napi_get_arraybuffer_info(env, args[0], &data, &byteLength)); + + int32_t index = 0; + int32_t value = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[1], &index)); + NAPI_CALL(env, napi_get_value_int32(env, args[2], &value)); + if (index < 0 || (size_t)index >= byteLength) { + napi_throw_range_error(env, NULL, "byte index out of range"); + return NULL; + } + + ((uint8_t*)data)[index] = (uint8_t)value; + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value CreateTypedArray(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value args[4]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char typeName[24]; + if (!ReadSelector(env, args[0], typeName, sizeof(typeName))) { + NapiThrowLastError(env); + return NULL; + } + + napi_typedarray_type type = napi_uint8_array; + if (!LookupTypedArrayType(typeName, &type)) { + return UnknownSelector(env, "unknown typed array type"); + } + + int32_t length = 0; + int32_t byteOffset = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[2], &length)); + NAPI_CALL(env, napi_get_value_int32(env, args[3], &byteOffset)); + + napi_value result = NULL; + NAPI_CALL( + env, napi_create_typedarray(env, type, (size_t)length, args[1], (size_t)byteOffset, &result)); + return result; +} + +static napi_value TypedArrayInfo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_typedarray_type type = napi_uint8_array; + size_t length = 0; + void* data = NULL; + napi_value buffer = NULL; + size_t byteOffset = 0; + NAPI_CALL(env, + napi_get_typedarray_info(env, args[0], &type, &length, &data, &buffer, &byteOffset)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL( + env, napi_set_named_property(env, result, "type", NapiString(env, TypedArrayTypeName(type)))); + NAPI_CALL(env, napi_set_named_property(env, result, "length", NapiDouble(env, (double)length))); + NAPI_CALL( + env, napi_set_named_property(env, result, "byteOffset", NapiDouble(env, (double)byteOffset))); + NAPI_CALL(env, napi_set_named_property(env, result, "buffer", buffer)); + // Reading the first element as raw bytes needs no knowledge of the element + // type, and still proves `data` points past `byteOffset`. + NAPI_CALL(env, napi_set_named_property(env, result, "firstByte", + NapiDouble(env, length > 0 ? ((uint8_t*)data)[0] : -1))); + return result; +} + +static void FinalizeExternalBytes(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + free(data); +} + +// napi_create_external_arraybuffer hands V8 the addon's own allocation without +// copying, so the bytes JS reads back are the ones written below. +static napi_value CreateExternalTypedArray(napi_env env, napi_callback_info info) { + (void)info; + + const size_t byteLength = 8; + uint8_t* bytes = (uint8_t*)malloc(byteLength); + if (bytes == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + for (size_t i = 0; i < byteLength; i++) { + bytes[i] = (uint8_t)(10 + i); + } + + napi_value buffer = NULL; + napi_status status = napi_create_external_arraybuffer(env, bytes, byteLength, + FinalizeExternalBytes, NULL, &buffer); + if (status != napi_ok) { + free(bytes); + NapiThrowLastError(env); + return NULL; + } + + napi_value result = NULL; + NAPI_CALL(env, napi_create_typedarray(env, napi_uint8_array, byteLength, buffer, 0, &result)); + return result; +} + +static napi_value CreateDataView(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t byteLength = 0; + int32_t byteOffset = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[1], &byteLength)); + NAPI_CALL(env, napi_get_value_int32(env, args[2], &byteOffset)); + + napi_value result = NULL; + NAPI_CALL(env, + napi_create_dataview(env, (size_t)byteLength, args[0], (size_t)byteOffset, &result)); + return result; +} + +static napi_value DataViewInfo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + size_t byteLength = 0; + void* data = NULL; + napi_value buffer = NULL; + size_t byteOffset = 0; + NAPI_CALL(env, napi_get_dataview_info(env, args[0], &byteLength, &data, &buffer, &byteOffset)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL( + env, napi_set_named_property(env, result, "byteLength", NapiDouble(env, (double)byteLength))); + NAPI_CALL( + env, napi_set_named_property(env, result, "byteOffset", NapiDouble(env, (double)byteOffset))); + NAPI_CALL(env, napi_set_named_property(env, result, "buffer", buffer)); + NAPI_CALL(env, + napi_set_named_property(env, result, "firstByte", + NapiDouble(env, byteLength > 0 ? ((uint8_t*)data)[0] : -1))); + return result; +} + +static napi_value DetachArrayBuffer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool before = false; + NAPI_CALL(env, napi_is_detached_arraybuffer(env, args[0], &before)); + napi_status status = napi_detach_arraybuffer(env, args[0]); + bool after = false; + NAPI_CALL(env, napi_is_detached_arraybuffer(env, args[0], &after)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "status", NapiStatusValue(env, status))); + NAPI_CALL(env, napi_set_named_property(env, result, "before", NapiBool(env, before))); + NAPI_CALL(env, napi_set_named_property(env, result, "after", NapiBool(env, after))); + return result; +} + +//=== Promises ============================================================= + +static napi_deferred sDeferred = NULL; + +static napi_value CreatePromise(napi_env env, napi_callback_info info) { + (void)info; + if (sDeferred != NULL) { + napi_throw_error(env, NULL, "a deferred is already outstanding"); + return NULL; + } + + napi_value promise = NULL; + NAPI_CALL(env, napi_create_promise(env, &sDeferred, &promise)); + return promise; +} + +// Settling frees the deferred, so the handle is cleared before the call rather +// than after: a second settle must not reach the same pointer. +static napi_value SettlePromise(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + if (sDeferred == NULL) { + return NapiBool(env, false); + } + + bool resolve = false; + NAPI_CALL(env, napi_get_value_bool(env, args[0], &resolve)); + + napi_deferred deferred = sDeferred; + sDeferred = NULL; + NAPI_CALL(env, resolve ? napi_resolve_deferred(env, deferred, args[1]) + : napi_reject_deferred(env, deferred, args[1])); + return NapiBool(env, true); +} + +//=== Errors =============================================================== + +static napi_value CreateError(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char kind[16]; + if (!ReadSelector(env, args[0], kind, sizeof(kind))) { + NapiThrowLastError(env); + return NULL; + } + + napi_valuetype codeType = napi_undefined; + NAPI_CALL(env, napi_typeof(env, args[1], &codeType)); + napi_value code = codeType == napi_string ? args[1] : NULL; + + napi_value result = NULL; + napi_status status; + if (strcmp(kind, "error") == 0) { + status = napi_create_error(env, code, args[2], &result); + } else if (strcmp(kind, "type") == 0) { + status = napi_create_type_error(env, code, args[2], &result); + } else if (strcmp(kind, "range") == 0) { + status = napi_create_range_error(env, code, args[2], &result); + } else if (strcmp(kind, "syntax") == 0) { + status = node_api_create_syntax_error(env, code, args[2], &result); + } else { + return UnknownSelector(env, "unknown error kind"); + } + + if (status != napi_ok) { + NapiThrowLastError(env); + return NULL; + } + return result; +} + +static napi_value CreateErrorStatus(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value error = NULL; + return NapiStatusValue(env, napi_create_error(env, NULL, args[0], &error)); +} + +static napi_value ThrowErrorKind(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char kind[16]; + char code[32]; + char message[64]; + if (!ReadSelector(env, args[0], kind, sizeof(kind)) || + !ReadSelector(env, args[2], message, sizeof(message))) { + NapiThrowLastError(env); + return NULL; + } + + napi_valuetype codeType = napi_undefined; + NAPI_CALL(env, napi_typeof(env, args[1], &codeType)); + const char* codePtr = NULL; + if (codeType == napi_string) { + if (!ReadSelector(env, args[1], code, sizeof(code))) { + NapiThrowLastError(env); + return NULL; + } + codePtr = code; + } + + if (strcmp(kind, "error") == 0) { + napi_throw_error(env, codePtr, message); + } else if (strcmp(kind, "type") == 0) { + napi_throw_type_error(env, codePtr, message); + } else if (strcmp(kind, "range") == 0) { + napi_throw_range_error(env, codePtr, message); + } else if (strcmp(kind, "syntax") == 0) { + node_api_throw_syntax_error(env, codePtr, message); + } else { + return UnknownSelector(env, "unknown error kind"); + } + return NULL; +} + +// napi_throw takes any value, not only an Error. +static napi_value ThrowValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_throw(env, args[0]); + return NULL; +} + +//=== Exceptions =========================================================== + +static napi_value CallAndCatch(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value recv = NULL; + NAPI_CALL(env, napi_get_undefined(env, &recv)); + + napi_status callStatus = napi_call_function(env, recv, args[0], 0, NULL, NULL); + + bool pendingBefore = false; + napi_is_exception_pending(env, &pendingBefore); + + // Anything with a preamble refuses to run while an exception is pending; the + // status is what an addon sees if it ignores the first failure. + napi_status blockedStatus = napi_call_function(env, recv, args[0], 0, NULL, NULL); + + napi_value caught = NULL; + napi_status clearStatus = napi_get_and_clear_last_exception(env, &caught); + + bool pendingAfter = true; + napi_is_exception_pending(env, &pendingAfter); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, + napi_set_named_property(env, result, "callStatus", NapiStatusValue(env, callStatus))); + NAPI_CALL(env, + napi_set_named_property(env, result, "pendingBefore", NapiBool(env, pendingBefore))); + NAPI_CALL(env, napi_set_named_property(env, result, "blockedStatus", + NapiStatusValue(env, blockedStatus))); + NAPI_CALL(env, + napi_set_named_property(env, result, "clearStatus", NapiStatusValue(env, clearStatus))); + NAPI_CALL(env, napi_set_named_property(env, result, "pendingAfter", NapiBool(env, pendingAfter))); + NAPI_CALL(env, napi_set_named_property(env, result, "caught", caught)); + return result; +} + +static napi_value CallAndRethrow(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value recv = NULL; + NAPI_CALL(env, napi_get_undefined(env, &recv)); + + if (napi_call_function(env, recv, args[0], 0, NULL, NULL) == napi_ok) { + napi_throw_error(env, NULL, "the callback was expected to throw"); + return NULL; + } + + napi_value caught = NULL; + NAPI_CALL(env, napi_get_and_clear_last_exception(env, &caught)); + napi_throw(env, caught); + return NULL; +} + +// A clean call must leave nothing pending and yield no exception to clear. +static napi_value ClearWithoutException(napi_env env, napi_callback_info info) { + (void)info; + + napi_value cleared = NULL; + napi_status status = napi_get_and_clear_last_exception(env, &cleared); + + napi_valuetype type = napi_object; + NAPI_CALL(env, napi_typeof(env, cleared, &type)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "status", NapiStatusValue(env, status))); + NAPI_CALL(env, napi_set_named_property(env, result, "isUndefined", + NapiBool(env, type == napi_undefined))); + return result; +} + +//=== References =========================================================== + +#define NAPI_COVERAGE_REF_SLOTS 16 + +static napi_ref sRefs[NAPI_COVERAGE_REF_SLOTS]; + +static bool ReadRefSlot(napi_env env, napi_value value, int32_t* slot) { + if (napi_get_value_int32(env, value, slot) != napi_ok) { + NapiThrowLastError(env); + return false; + } + if (*slot < 0 || *slot >= NAPI_COVERAGE_REF_SLOTS || sRefs[*slot] == NULL) { + napi_throw_error(env, NULL, "no reference in that slot"); + return false; + } + return true; +} + +static napi_value RefCreate(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t initialCount = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[1], &initialCount)); + + int32_t slot = 0; + while (slot < NAPI_COVERAGE_REF_SLOTS && sRefs[slot] != NULL) { + slot++; + } + if (slot == NAPI_COVERAGE_REF_SLOTS) { + napi_throw_error(env, NULL, "no free reference slot"); + return NULL; + } + + NAPI_CALL(env, napi_create_reference(env, args[0], (uint32_t)initialCount, &sRefs[slot])); + return NapiDouble(env, slot); +} + +// The reference is deleted again straight away: only the status is under test, +// and a leaked slot would starve the table. +static napi_value RefCreateStatus(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_ref ref = NULL; + napi_status status = napi_create_reference(env, args[0], 1, &ref); + if (status == napi_ok) { + napi_delete_reference(env, ref); + } + return NapiStatusValue(env, status); +} + +static napi_value RefRef(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t slot = 0; + if (!ReadRefSlot(env, args[0], &slot)) { + return NULL; + } + + uint32_t count = 0; + NAPI_CALL(env, napi_reference_ref(env, sRefs[slot], &count)); + return NapiDouble(env, count); +} + +static napi_value RefUnref(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t slot = 0; + if (!ReadRefSlot(env, args[0], &slot)) { + return NULL; + } + + uint32_t count = 0; + NAPI_CALL(env, napi_reference_unref(env, sRefs[slot], &count)); + return NapiDouble(env, count); +} + +static napi_value RefGet(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t slot = 0; + if (!ReadRefSlot(env, args[0], &slot)) { + return NULL; + } + + napi_value value = NULL; + NAPI_CALL(env, napi_get_reference_value(env, sRefs[slot], &value)); + + // A weak reference whose value has been collected yields NULL, not an error. + if (value == NULL) { + NAPI_CALL(env, napi_get_undefined(env, &value)); + } + return value; +} + +// Answers "is the referent still there" without handing the value to JS, which +// would put it back on the stack and defeat the collection the caller is +// trying to observe. +static napi_value RefIsLive(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t slot = 0; + if (!ReadRefSlot(env, args[0], &slot)) { + return NULL; + } + + napi_value value = NULL; + NAPI_CALL(env, napi_get_reference_value(env, sRefs[slot], &value)); + return NapiBool(env, value != NULL); +} + +static napi_value RefDelete(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t slot = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[0], &slot)); + if (slot < 0 || slot >= NAPI_COVERAGE_REF_SLOTS || sRefs[slot] == NULL) { + return NapiBool(env, false); + } + + NAPI_CALL(env, napi_delete_reference(env, sRefs[slot])); + sRefs[slot] = NULL; + return NapiBool(env, true); +} + +//=== Conversions ========================================================== + +static napi_value Coerce(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char kind[16]; + if (!ReadSelector(env, args[0], kind, sizeof(kind))) { + NapiThrowLastError(env); + return NULL; + } + + napi_value result = NULL; + if (strcmp(kind, "bool") == 0) { + NAPI_CALL(env, napi_coerce_to_bool(env, args[1], &result)); + } else if (strcmp(kind, "number") == 0) { + NAPI_CALL(env, napi_coerce_to_number(env, args[1], &result)); + } else if (strcmp(kind, "string") == 0) { + NAPI_CALL(env, napi_coerce_to_string(env, args[1], &result)); + } else if (strcmp(kind, "object") == 0) { + NAPI_CALL(env, napi_coerce_to_object(env, args[1], &result)); + } else { + return UnknownSelector(env, "unknown coercion kind"); + } + return result; +} + +//=== Properties =========================================================== + +static napi_value PropertyOps(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool has = false; + bool hasOwn = false; + bool deleted = false; + bool hasAfterDelete = false; + napi_value read = NULL; + NAPI_CALL(env, napi_has_property(env, args[0], args[1], &has)); + NAPI_CALL(env, napi_has_own_property(env, args[0], args[1], &hasOwn)); + NAPI_CALL(env, napi_get_property(env, args[0], args[1], &read)); + NAPI_CALL(env, napi_delete_property(env, args[0], args[1], &deleted)); + NAPI_CALL(env, napi_has_property(env, args[0], args[1], &hasAfterDelete)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "has", NapiBool(env, has))); + NAPI_CALL(env, napi_set_named_property(env, result, "hasOwn", NapiBool(env, hasOwn))); + NAPI_CALL(env, napi_set_named_property(env, result, "read", read)); + NAPI_CALL(env, napi_set_named_property(env, result, "deleted", NapiBool(env, deleted))); + NAPI_CALL(env, + napi_set_named_property(env, result, "hasAfterDelete", NapiBool(env, hasAfterDelete))); + return result; +} + +static napi_value ElementOps(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t index = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[1], &index)); + + bool has = false; + bool deleted = false; + bool hasAfterDelete = false; + napi_value read = NULL; + NAPI_CALL(env, napi_has_element(env, args[0], (uint32_t)index, &has)); + NAPI_CALL(env, napi_get_element(env, args[0], (uint32_t)index, &read)); + NAPI_CALL(env, napi_delete_element(env, args[0], (uint32_t)index, &deleted)); + NAPI_CALL(env, napi_has_element(env, args[0], (uint32_t)index, &hasAfterDelete)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "has", NapiBool(env, has))); + NAPI_CALL(env, napi_set_named_property(env, result, "read", read)); + NAPI_CALL(env, napi_set_named_property(env, result, "deleted", NapiBool(env, deleted))); + NAPI_CALL(env, + napi_set_named_property(env, result, "hasAfterDelete", NapiBool(env, hasAfterDelete))); + return result; +} + +static napi_value PropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_property_names(env, args[0], &result)); + return result; +} + +static napi_value AllPropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool ownOnly = false; + NAPI_CALL(env, napi_get_value_bool(env, args[1], &ownOnly)); + + char filterName[24]; + if (!ReadSelector(env, args[2], filterName, sizeof(filterName))) { + NapiThrowLastError(env); + return NULL; + } + + napi_key_filter filter = napi_key_all_properties; + if (strcmp(filterName, "all") == 0) { + filter = napi_key_all_properties; + } else if (strcmp(filterName, "writable") == 0) { + filter = napi_key_writable; + } else if (strcmp(filterName, "enumerable") == 0) { + filter = napi_key_enumerable; + } else if (strcmp(filterName, "configurable") == 0) { + filter = napi_key_configurable; + } else if (strcmp(filterName, "skip_strings") == 0) { + filter = napi_key_skip_strings; + } else if (strcmp(filterName, "skip_symbols") == 0) { + filter = napi_key_skip_symbols; + } else { + return UnknownSelector(env, "unknown key filter"); + } + + napi_value result = NULL; + NAPI_CALL(env, napi_get_all_property_names( + env, args[0], ownOnly ? napi_key_own_only : napi_key_include_prototypes, + filter, napi_key_numbers_to_strings, &result)); + return result; +} + +// Index keys survive napi_key_keep_numbers as numbers; the other conversion +// stringifies them. +static napi_value IndexKeyTypes(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value kept = NULL; + napi_value converted = NULL; + NAPI_CALL(env, + napi_get_all_property_names(env, args[0], napi_key_own_only, napi_key_all_properties, + napi_key_keep_numbers, &kept)); + NAPI_CALL(env, + napi_get_all_property_names(env, args[0], napi_key_own_only, napi_key_all_properties, + napi_key_numbers_to_strings, &converted)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "kept", kept)); + NAPI_CALL(env, napi_set_named_property(env, result, "converted", converted)); + return result; +} + +static double sAccessorValue = 0; + +static napi_value AccessorGet(napi_env env, napi_callback_info info) { + void* data = NULL; + NAPI_CALL(env, napi_get_cb_info(env, info, NULL, NULL, NULL, &data)); + // `data` distinguishes two properties sharing one callback. + return NapiDouble(env, sAccessorValue + (double)(intptr_t)data); +} + +static napi_value AccessorSet(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_CALL(env, napi_get_value_double(env, args[0], &sAccessorValue)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value DefineOnTarget(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value one = NULL; + NAPI_CALL(env, napi_create_double(env, 1, &one)); + + napi_property_descriptor properties[] = { + {"base", NULL, NULL, AccessorGet, AccessorSet, NULL, napi_enumerable, (void*)(intptr_t)0}, + {"offset", NULL, NULL, AccessorGet, NULL, NULL, napi_enumerable, (void*)(intptr_t)100}, + {"locked", NULL, NULL, NULL, NULL, one, napi_default, NULL}, + {"open", NULL, NULL, NULL, NULL, one, + (napi_property_attributes)(napi_writable | napi_enumerable | napi_configurable), NULL}, + }; + + NAPI_CALL(env, napi_define_properties(env, args[0], sizeof(properties) / sizeof(properties[0]), + properties)); + return args[0]; +} + +//=== Types, identity and object shape ===================================== + +static const char* ValueTypeName(napi_valuetype type) { + switch (type) { + case napi_undefined: + return "undefined"; + case napi_null: + return "null"; + case napi_boolean: + return "boolean"; + case napi_number: + return "number"; + case napi_string: + return "string"; + case napi_symbol: + return "symbol"; + case napi_object: + return "object"; + case napi_function: + return "function"; + case napi_external: + return "external"; + case napi_bigint: + return "bigint"; + default: + return "unknown"; + } +} + +static napi_value TypeOf(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_valuetype type = napi_undefined; + NAPI_CALL(env, napi_typeof(env, args[0], &type)); + return NapiString(env, ValueTypeName(type)); +} + +static napi_value Predicates(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool isArray = false; + bool isDate = false; + bool isError = false; + bool isPromise = false; + bool isArrayBuffer = false; + bool isTypedArray = false; + bool isDataView = false; + bool isBuffer = false; + NAPI_CALL(env, napi_is_array(env, args[0], &isArray)); + NAPI_CALL(env, napi_is_date(env, args[0], &isDate)); + NAPI_CALL(env, napi_is_error(env, args[0], &isError)); + NAPI_CALL(env, napi_is_promise(env, args[0], &isPromise)); + NAPI_CALL(env, napi_is_arraybuffer(env, args[0], &isArrayBuffer)); + NAPI_CALL(env, napi_is_typedarray(env, args[0], &isTypedArray)); + NAPI_CALL(env, napi_is_dataview(env, args[0], &isDataView)); + NAPI_CALL(env, napi_is_buffer(env, args[0], &isBuffer)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "array", NapiBool(env, isArray))); + NAPI_CALL(env, napi_set_named_property(env, result, "date", NapiBool(env, isDate))); + NAPI_CALL(env, napi_set_named_property(env, result, "error", NapiBool(env, isError))); + NAPI_CALL(env, napi_set_named_property(env, result, "promise", NapiBool(env, isPromise))); + NAPI_CALL(env, napi_set_named_property(env, result, "arraybuffer", NapiBool(env, isArrayBuffer))); + NAPI_CALL(env, napi_set_named_property(env, result, "typedarray", NapiBool(env, isTypedArray))); + NAPI_CALL(env, napi_set_named_property(env, result, "dataview", NapiBool(env, isDataView))); + NAPI_CALL(env, napi_set_named_property(env, result, "buffer", NapiBool(env, isBuffer))); + return result; +} + +static napi_value StrictEquals(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool equal = false; + NAPI_CALL(env, napi_strict_equals(env, args[0], args[1], &equal)); + return NapiBool(env, equal); +} + +static napi_value InstanceOf(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool result = false; + NAPI_CALL(env, napi_instanceof(env, args[0], args[1], &result)); + return NapiBool(env, result); +} + +static napi_value NewInstance(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result = NULL; + NAPI_CALL(env, napi_new_instance(env, args[0], 1, &args[1], &result)); + return result; +} + +static napi_value GetPrototype(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_prototype(env, args[0], &result)); + return result; +} + +static napi_value FreezeObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NAPI_CALL(env, napi_object_freeze(env, args[0])); + return args[0]; +} + +static napi_value SealObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NAPI_CALL(env, napi_object_seal(env, args[0])); + return args[0]; +} + +static napi_value CreateDate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double millis = 0; + NAPI_CALL(env, napi_get_value_double(env, args[0], &millis)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_date(env, millis, &result)); + return result; +} + +static napi_value DateValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double millis = 0; + napi_status status = napi_get_date_value(env, args[0], &millis); + if (status != napi_ok) { + return NapiStatusValue(env, status); + } + return NapiDouble(env, millis); +} + +static void FinalizeExternalData(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + free(data); +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double* payload = (double*)malloc(sizeof(double)); + if (payload == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + NAPI_CALL(env, napi_get_value_double(env, args[0], payload)); + + napi_value result = NULL; + napi_status status = napi_create_external(env, payload, FinalizeExternalData, NULL, &result); + if (status != napi_ok) { + free(payload); + NapiThrowLastError(env); + return NULL; + } + return result; +} + +static napi_value ExternalValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + void* payload = NULL; + napi_status status = napi_get_value_external(env, args[0], &payload); + if (status != napi_ok) { + return NapiStatusValue(env, status); + } + return NapiDouble(env, *(double*)payload); +} + +static napi_value NapiVersion(napi_env env, napi_callback_info info) { + (void)info; + + uint32_t version = 0; + NAPI_CALL(env, napi_get_version(env, &version)); + return NapiDouble(env, version); +} + +static napi_value NodeVersion(napi_env env, napi_callback_info info) { + (void)info; + + const napi_node_version* version = NULL; + NAPI_CALL(env, napi_get_node_version(env, &version)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "major", NapiDouble(env, version->major))); + NAPI_CALL(env, napi_set_named_property(env, result, "minor", NapiDouble(env, version->minor))); + NAPI_CALL(env, napi_set_named_property(env, result, "patch", NapiDouble(env, version->patch))); + NAPI_CALL(env, + napi_set_named_property(env, result, "release", NapiString(env, version->release))); + return result; +} + +// The two APIs with no runtime-event-loop-shaped answer report failure rather than +// improvising one. +static napi_value UnsupportedApiStatuses(napi_env env, napi_callback_info info) { + (void)info; + + struct uv_loop_s* loop = NULL; + napi_status eventLoop = napi_get_uv_event_loop(env, &loop); + + const char* fileName = NULL; + napi_status moduleFileName = node_api_get_module_file_name(env, &fileName); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, + napi_set_named_property(env, result, "uvEventLoop", NapiStatusValue(env, eventLoop))); + NAPI_CALL(env, napi_set_named_property(env, result, "moduleFileName", + NapiStatusValue(env, moduleFileName))); + return result; +} + +//=== Registration ========================================================= + +static napi_value InitNapiCoverageModule(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + NAPI_METHOD("stringLengths", StringLengths), + NAPI_METHOD("copyString", CopyString), + NAPI_METHOD("createStrings", CreateStrings), + NAPI_METHOD("stringStatus", StringStatus), + NAPI_METHOD("numberParts", NumberParts), + NAPI_METHOD("createNumbers", CreateNumbers), + NAPI_METHOD("numberStatus", NumberStatus), + NAPI_METHOD("createSymbol", CreateSymbol), + NAPI_METHOD("createSymbolStatus", CreateSymbolStatus), + NAPI_METHOD("symbolFor", SymbolFor), + NAPI_METHOD("symbolKeyRoundTrip", SymbolKeyRoundTrip), + NAPI_METHOD("createArrayBuffer", CreateArrayBuffer), + NAPI_METHOD("arrayBufferInfo", ArrayBufferInfo), + NAPI_METHOD("writeByte", WriteByte), + NAPI_METHOD("createTypedArray", CreateTypedArray), + NAPI_METHOD("typedArrayInfo", TypedArrayInfo), + NAPI_METHOD("createExternalTypedArray", CreateExternalTypedArray), + NAPI_METHOD("createDataView", CreateDataView), + NAPI_METHOD("dataViewInfo", DataViewInfo), + NAPI_METHOD("detachArrayBuffer", DetachArrayBuffer), + NAPI_METHOD("createPromise", CreatePromise), + NAPI_METHOD("settlePromise", SettlePromise), + NAPI_METHOD("createError", CreateError), + NAPI_METHOD("createErrorStatus", CreateErrorStatus), + NAPI_METHOD("throwErrorKind", ThrowErrorKind), + NAPI_METHOD("throwValue", ThrowValue), + NAPI_METHOD("callAndCatch", CallAndCatch), + NAPI_METHOD("callAndRethrow", CallAndRethrow), + NAPI_METHOD("clearWithoutException", ClearWithoutException), + NAPI_METHOD("refCreate", RefCreate), + NAPI_METHOD("refCreateStatus", RefCreateStatus), + NAPI_METHOD("refRef", RefRef), + NAPI_METHOD("refUnref", RefUnref), + NAPI_METHOD("refGet", RefGet), + NAPI_METHOD("refIsLive", RefIsLive), + NAPI_METHOD("refDelete", RefDelete), + NAPI_METHOD("coerce", Coerce), + NAPI_METHOD("propertyOps", PropertyOps), + NAPI_METHOD("elementOps", ElementOps), + NAPI_METHOD("propertyNames", PropertyNames), + NAPI_METHOD("allPropertyNames", AllPropertyNames), + NAPI_METHOD("indexKeyTypes", IndexKeyTypes), + NAPI_METHOD("defineOnTarget", DefineOnTarget), + NAPI_METHOD("typeOf", TypeOf), + NAPI_METHOD("predicates", Predicates), + NAPI_METHOD("strictEquals", StrictEquals), + NAPI_METHOD("instanceOf", InstanceOf), + NAPI_METHOD("newInstance", NewInstance), + NAPI_METHOD("getPrototype", GetPrototype), + NAPI_METHOD("freezeObject", FreezeObject), + NAPI_METHOD("sealObject", SealObject), + NAPI_METHOD("createDate", CreateDate), + NAPI_METHOD("dateValue", DateValue), + NAPI_METHOD("createExternal", CreateExternal), + NAPI_METHOD("externalValue", ExternalValue), + NAPI_METHOD("napiVersion", NapiVersion), + NAPI_METHOD("nodeVersion", NodeVersion), + NAPI_METHOD("unsupportedApiStatuses", UnsupportedApiStatuses), + }; + + NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), + properties)); + return exports; +} + +static napi_module sNapiCoverageModule = { + NAPI_MODULE_VERSION, 0, __FILE__, InitNapiCoverageModule, "napicoveragemodule", NULL, {0}, +}; + +__attribute__((constructor)) static void RegisterNapiCoverageModule(void) { + napi_module_register(&sNapiCoverageModule); +} diff --git a/test-app/runtime/src/main/cpp/napi/tests/NapiTestModule.cpp b/test-app/runtime/src/main/cpp/napi/tests/NapiTestModule.cpp new file mode 100644 index 000000000..4d1ef7c02 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/tests/NapiTestModule.cpp @@ -0,0 +1,619 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include "NapiTestSupport.h" + +typedef struct { + double value; +} NapiTestPayload; + +static int sFinalizerRuns = 0; +static int sWrapCount = 0; +static napi_ref sHeldRef = NULL; + +static napi_value EchoString(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + size_t length = 0; + NAPI_CALL(env, napi_get_value_string_utf8(env, args[0], NULL, 0, &length)); + + char* buffer = (char*)malloc(length + 1); + if (buffer == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + + napi_value result = NULL; + napi_status status = napi_get_value_string_utf8(env, args[0], buffer, length + 1, &length); + if (status == napi_ok) { + status = napi_create_string_utf8(env, buffer, length, &result); + } + free(buffer); + + if (status != napi_ok) { + NapiThrowLastError(env); + return NULL; + } + + return result; +} + +static napi_value DoubleNumber(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double value = 0; + NAPI_CALL(env, napi_get_value_double(env, args[0], &value)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_double(env, value * 2, &result)); + return result; +} + +static napi_value NegateBool(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool value = false; + NAPI_CALL(env, napi_get_value_bool(env, args[0], &value)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_boolean(env, !value, &result)); + return result; +} + +static napi_value TransformObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value input = NULL; + NAPI_CALL(env, napi_get_named_property(env, args[0], "value", &input)); + + double value = 0; + NAPI_CALL(env, napi_get_value_double(env, input, &value)); + + napi_value doubled = NULL; + NAPI_CALL(env, napi_create_double(env, value * 2, &doubled)); + + napi_value tag = NULL; + NAPI_CALL(env, napi_create_string_utf8(env, "napi", NAPI_AUTO_LENGTH, &tag)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "value", doubled)); + NAPI_CALL(env, napi_set_named_property(env, result, "tag", tag)); + return result; +} + +static napi_value TransformArray(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + uint32_t length = 0; + NAPI_CALL(env, napi_get_array_length(env, args[0], &length)); + + napi_value first = NULL; + double firstValue = 0; + if (length > 0) { + NAPI_CALL(env, napi_get_element(env, args[0], 0, &first)); + NAPI_CALL(env, napi_get_value_double(env, first, &firstValue)); + } + + napi_value result = NULL; + NAPI_CALL(env, napi_create_array_with_length(env, 2, &result)); + + napi_value lengthValue = NULL; + NAPI_CALL(env, napi_create_double(env, (double)length, &lengthValue)); + NAPI_CALL(env, napi_set_element(env, result, 0, lengthValue)); + + napi_value firstOut = NULL; + NAPI_CALL(env, napi_create_double(env, firstValue, &firstOut)); + NAPI_CALL(env, napi_set_element(env, result, 1, firstOut)); + return result; +} + +static napi_value ThrowError(napi_env env, napi_callback_info info) { + (void)info; + napi_throw_error(env, "ERR_TEST_CODE", "napi test failure"); + return NULL; +} + +static void FinalizePayload(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + free(data); + sFinalizerRuns++; +} + +static napi_value WrapValue(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double value = 0; + NAPI_CALL(env, napi_get_value_double(env, args[1], &value)); + + NapiTestPayload* payload = (NapiTestPayload*)malloc(sizeof(NapiTestPayload)); + if (payload == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + payload->value = value; + + napi_status status = napi_wrap(env, args[0], payload, FinalizePayload, NULL, NULL); + if (status != napi_ok) { + free(payload); + NapiThrowLastError(env); + return NULL; + } + + sWrapCount++; + return args[0]; +} + +static napi_value UnwrapValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + void* payload = NULL; + NAPI_CALL(env, napi_unwrap(env, args[0], &payload)); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_double(env, ((NapiTestPayload*)payload)->value, &result)); + return result; +} + +static napi_value FinalizerRan(napi_env env, napi_callback_info info) { + (void)info; + napi_value result = NULL; + NAPI_CALL(env, napi_get_boolean(env, sFinalizerRuns > 0, &result)); + return result; +} + +static napi_value ResetFinalizerFlag(napi_env env, napi_callback_info info) { + (void)info; + sFinalizerRuns = 0; + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value HoldRef(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + if (sHeldRef != NULL) { + napi_delete_reference(env, sHeldRef); + sHeldRef = NULL; + } + NAPI_CALL(env, napi_create_reference(env, args[0], 1, &sHeldRef)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value GetRef(napi_env env, napi_callback_info info) { + (void)info; + napi_value result = NULL; + if (sHeldRef != NULL) { + NAPI_CALL(env, napi_get_reference_value(env, sHeldRef, &result)); + } + + // A weak or already-collected reference yields NULL rather than an error. + if (result == NULL) { + NAPI_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +static napi_value ReleaseRef(napi_env env, napi_callback_info info) { + (void)info; + bool released = false; + if (sHeldRef != NULL) { + NAPI_CALL(env, napi_delete_reference(env, sHeldRef)); + sHeldRef = NULL; + released = true; + } + + napi_value result = NULL; + NAPI_CALL(env, napi_get_boolean(env, released, &result)); + return result; +} + +static napi_value GetWrapCount(napi_env env, napi_callback_info info) { + (void)info; + napi_value result = NULL; + NAPI_CALL(env, napi_create_double(env, (double)sWrapCount, &result)); + return result; +} + +//=== Async work =========================================================== + +typedef struct { + napi_async_work work; + napi_ref callbackRef; + double input; + double result; + bool ranOffJsThread; + bool sleepBeforeWork; + pthread_t jsThread; +} NapiAsyncWorkContext; + +static void ExecuteAsyncWork(napi_env env, void* data) { + (void)env; + NapiAsyncWorkContext* context = (NapiAsyncWorkContext*)data; + if (context->sleepBeforeWork) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + context->ranOffJsThread = !pthread_equal(pthread_self(), context->jsThread); + context->result = context->input * 2; +} + +static void CompleteAsyncWork(napi_env env, napi_status status, void* data) { + NapiAsyncWorkContext* context = (NapiAsyncWorkContext*)data; + + napi_value callback = NULL; + napi_get_reference_value(env, context->callbackRef, &callback); + if (callback != NULL) { + napi_value recv = NULL; + napi_value args[3]; + napi_get_undefined(env, &recv); + args[0] = NapiStatusValue(env, status); + napi_create_double(env, context->result, &args[1]); + napi_get_boolean(env, context->ranOffJsThread, &args[2]); + napi_call_function(env, recv, callback, 3, args, NULL); + } + + napi_delete_reference(env, context->callbackRef); + napi_delete_async_work(env, context->work); + free(context); +} + +static NapiAsyncWorkContext* CreateAsyncWorkContext(napi_env env, napi_value callback, double input, + bool sleepBeforeWork) { + NapiAsyncWorkContext* context = (NapiAsyncWorkContext*)calloc(1, sizeof(NapiAsyncWorkContext)); + if (context == NULL) { + napi_throw_error(env, NULL, "out of memory"); + return NULL; + } + + context->input = input; + context->sleepBeforeWork = sleepBeforeWork; + context->jsThread = pthread_self(); + + napi_value name = NULL; + if (napi_create_string_utf8(env, "napi-test-work", NAPI_AUTO_LENGTH, &name) != napi_ok || + napi_create_reference(env, callback, 1, &context->callbackRef) != napi_ok || + napi_create_async_work(env, NULL, name, ExecuteAsyncWork, CompleteAsyncWork, context, + &context->work) != napi_ok) { + if (context->callbackRef != NULL) { + napi_delete_reference(env, context->callbackRef); + } + free(context); + NapiThrowLastError(env); + return NULL; + } + + return context; +} + +static napi_value StartAsyncWork(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double input = 0; + NAPI_CALL(env, napi_get_value_double(env, args[0], &input)); + + NapiAsyncWorkContext* context = CreateAsyncWorkContext(env, args[1], input, false); + if (context == NULL) { + return NULL; + } + + NAPI_CALL(env, napi_queue_async_work(env, context->work)); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +// Cancellation is inherently racy: the work may already be running by the time +// the cancel lands. The status is returned so the spec can hold both outcomes +// to their own contract instead of guessing which one happened. +static napi_value StartCancelledWork(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NapiAsyncWorkContext* context = CreateAsyncWorkContext(env, args[0], 1, true); + if (context == NULL) { + return NULL; + } + + NAPI_CALL(env, napi_queue_async_work(env, context->work)); + return NapiStatusValue(env, napi_cancel_async_work(env, context->work)); +} + +//=== Threadsafe functions ================================================= + +// Shared between the producer thread and the finalizer, which runs on the JS +// thread and may fire while the producer is still unwinding — env teardown +// closes the function out from under it. Whichever side finishes last frees. +struct NapiTsfnContext { + napi_threadsafe_function tsfn = NULL; + napi_ref doneRef = NULL; + int count = 0; + std::atomic owners{1}; + std::atomic lastCallStatus{napi_ok}; +}; + +static void ReleaseTsfnContext(NapiTsfnContext* context) { + if (context->owners.fetch_sub(1) == 1) { + delete context; + } +} + +// Only ever touched on the JS thread: set when a run starts, cleared by the +// finalizer, read by the reentrant push below. +static napi_threadsafe_function sActiveTsfn = NULL; + +static void TsfnCallJs(napi_env env, napi_value js_callback, void* context, void* data) { + (void)context; + + // A null env means the call was dropped: the function closed before it could + // be delivered, and this is the addon's chance to free `data`. + if (env == NULL || js_callback == NULL) { + return; + } + + napi_value recv = NULL; + napi_value arg = NULL; + napi_get_undefined(env, &recv); + napi_create_int32(env, (int32_t)(intptr_t)data, &arg); + napi_call_function(env, recv, js_callback, 1, &arg, NULL); +} + +static void TsfnFinalize(napi_env env, void* data, void* hint) { + (void)hint; + NapiTsfnContext* context = (NapiTsfnContext*)data; + sActiveTsfn = NULL; + + if (context->doneRef != NULL) { + napi_value done = NULL; + napi_get_reference_value(env, context->doneRef, &done); + if (done != NULL) { + napi_value recv = NULL; + napi_value arg = NapiStatusValue(env, (napi_status)context->lastCallStatus.load()); + napi_get_undefined(env, &recv); + napi_call_function(env, recv, done, 1, &arg, NULL); + } + napi_delete_reference(env, context->doneRef); + } + + ReleaseTsfnContext(context); +} + +static void TsfnProducer(NapiTsfnContext* context) { + napi_threadsafe_function tsfn = context->tsfn; + + for (int i = 1; i <= context->count; i++) { + napi_status status = + napi_call_threadsafe_function(tsfn, (void*)(intptr_t)i, napi_tsfn_blocking); + context->lastCallStatus.store(status); + if (status != napi_ok) { + break; + } + } + + napi_release_threadsafe_function(tsfn, napi_tsfn_release); + ReleaseTsfnContext(context); +} + +static napi_value StartTsfn(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value args[4]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t count = 0; + int32_t maxQueueSize = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[0], &count)); + NAPI_CALL(env, napi_get_value_int32(env, args[1], &maxQueueSize)); + + NapiTsfnContext* context = new NapiTsfnContext(); + context->count = count; + + napi_value name = NULL; + if (napi_create_string_utf8(env, "napi-test-tsfn", NAPI_AUTO_LENGTH, &name) != napi_ok || + napi_create_reference(env, args[3], 1, &context->doneRef) != napi_ok || + napi_create_threadsafe_function(env, args[2], NULL, name, (size_t)maxQueueSize, 1, context, + TsfnFinalize, NULL, TsfnCallJs, &context->tsfn) != napi_ok) { + if (context->doneRef != NULL) { + napi_delete_reference(env, context->doneRef); + } + delete context; + NapiThrowLastError(env); + return NULL; + } + + // Handed to the producer thread; the finalizer holds the reference the + // context was created with. + context->owners.fetch_add(1); + sActiveTsfn = context->tsfn; + std::thread(TsfnProducer, context).detach(); + + napi_value result = NULL; + NAPI_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +// Pushes from the JS thread, which is legal while a call is being delivered: +// the function cannot close underneath its own callback. +static napi_value PushTsfn(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t value = 0; + NAPI_CALL(env, napi_get_value_int32(env, args[0], &value)); + + if (sActiveTsfn == NULL) { + return NapiStatusValue(env, napi_closing); + } + + return NapiStatusValue(env, napi_call_threadsafe_function(sActiveTsfn, (void*)(intptr_t)value, + napi_tsfn_nonblocking)); +} + +static napi_value ProbeTsfnAbort(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value name = NULL; + NAPI_CALL(env, napi_create_string_utf8(env, "napi-test-abort", NAPI_AUTO_LENGTH, &name)); + + napi_threadsafe_function tsfn = NULL; + NAPI_CALL(env, napi_create_threadsafe_function(env, args[0], NULL, name, 0, 1, NULL, NULL, NULL, + TsfnCallJs, &tsfn)); + + napi_status queued = + napi_call_threadsafe_function(tsfn, (void*)(intptr_t)1, napi_tsfn_nonblocking); + napi_status released = napi_release_threadsafe_function(tsfn, napi_tsfn_abort); + napi_status afterAbort = + napi_call_threadsafe_function(tsfn, (void*)(intptr_t)2, napi_tsfn_nonblocking); + + napi_value result = NULL; + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, napi_set_named_property(env, result, "queued", NapiStatusValue(env, queued))); + NAPI_CALL(env, napi_set_named_property(env, result, "released", NapiStatusValue(env, released))); + NAPI_CALL(env, + napi_set_named_property(env, result, "afterAbort", NapiStatusValue(env, afterAbort))); + return result; +} + +//=== Callback scopes ====================================================== + +static napi_value InvokeViaMakeCallback(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value resourceName = NULL; + NAPI_CALL(env, + napi_create_string_utf8(env, "napi-test-callback", NAPI_AUTO_LENGTH, &resourceName)); + + napi_value resource = NULL; + NAPI_CALL(env, napi_create_object(env, &resource)); + + napi_async_context context = NULL; + NAPI_CALL(env, napi_async_init(env, resource, resourceName, &context)); + + napi_value recv = NULL; + NAPI_CALL(env, napi_get_global(env, &recv)); + + napi_value result = NULL; + napi_status status = napi_make_callback(env, context, recv, args[0], 1, &args[1], &result); + napi_async_destroy(env, context); + + if (status != napi_ok) { + NapiThrowLastError(env); + return NULL; + } + + return result; +} + +//=== Cleanup hooks ======================================================== + +static void FreeCleanupToken(void* arg) { free(arg); } + +static void RemoveAsyncCleanupHook(napi_async_cleanup_hook_handle handle, void* data) { + free(data); + napi_remove_async_cleanup_hook(handle); +} + +static napi_value ExerciseCleanupHooks(napi_env env, napi_callback_info info) { + (void)info; + + void* token = malloc(1); + NAPI_CALL(env, napi_add_env_cleanup_hook(env, FreeCleanupToken, token)); + NAPI_CALL(env, napi_remove_env_cleanup_hook(env, FreeCleanupToken, token)); + + // Removing twice must be rejected, not silently accepted. + napi_status removedTwice = napi_remove_env_cleanup_hook(env, FreeCleanupToken, token); + + napi_async_cleanup_hook_handle handle = NULL; + NAPI_CALL(env, napi_add_async_cleanup_hook(env, RemoveAsyncCleanupHook, malloc(1), &handle)); + if (handle == NULL) { + napi_throw_error(env, NULL, "expected an async cleanup hook handle"); + return NULL; + } + NAPI_CALL(env, napi_remove_async_cleanup_hook(handle)); + + // Both kinds are left registered so env teardown has something to run; the + // tokens they free are what would leak if it never did. + NAPI_CALL(env, napi_add_env_cleanup_hook(env, FreeCleanupToken, token)); + NAPI_CALL(env, napi_add_async_cleanup_hook(env, RemoveAsyncCleanupHook, malloc(1), NULL)); + + return NapiStatusValue(env, removedTwice); +} + +static napi_value InitNapiTestModule(napi_env env, napi_value exports) { + napi_value moduleName = NULL; + NAPI_CALL(env, napi_create_string_utf8(env, "napitestmodule", NAPI_AUTO_LENGTH, &moduleName)); + + napi_property_descriptor properties[] = { + NAPI_METHOD("echoString", EchoString), + NAPI_METHOD("doubleNumber", DoubleNumber), + NAPI_METHOD("negateBool", NegateBool), + NAPI_METHOD("transformObject", TransformObject), + NAPI_METHOD("transformArray", TransformArray), + NAPI_METHOD("throwError", ThrowError), + NAPI_METHOD("wrapValue", WrapValue), + NAPI_METHOD("unwrapValue", UnwrapValue), + NAPI_METHOD("finalizerRan", FinalizerRan), + NAPI_METHOD("resetFinalizerFlag", ResetFinalizerFlag), + NAPI_METHOD("holdRef", HoldRef), + NAPI_METHOD("getRef", GetRef), + NAPI_METHOD("releaseRef", ReleaseRef), + NAPI_METHOD("startAsyncWork", StartAsyncWork), + NAPI_METHOD("startCancelledWork", StartCancelledWork), + NAPI_METHOD("startTsfn", StartTsfn), + NAPI_METHOD("pushTsfn", PushTsfn), + NAPI_METHOD("probeTsfnAbort", ProbeTsfnAbort), + NAPI_METHOD("invokeViaMakeCallback", InvokeViaMakeCallback), + NAPI_METHOD("exerciseCleanupHooks", ExerciseCleanupHooks), + NAPI_GETTER("wrapCount", GetWrapCount), + NAPI_VALUE("moduleName", moduleName), + }; + + NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), + properties)); + return exports; +} + +// A statically linked addon cannot use NAPI_MODULE_INIT: the generated symbol +// carries no name and only one can exist per image. +static napi_module sNapiTestModule = { + NAPI_MODULE_VERSION, 0, __FILE__, InitNapiTestModule, "napitestmodule", NULL, {0}, +}; + +__attribute__((constructor)) static void RegisterNapiTestModule(void) { + napi_module_register(&sNapiTestModule); +} diff --git a/test-app/runtime/src/main/cpp/napi/tests/NapiTestSupport.h b/test-app/runtime/src/main/cpp/napi/tests/NapiTestSupport.h new file mode 100644 index 000000000..5347471df --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/tests/NapiTestSupport.h @@ -0,0 +1,131 @@ +#ifndef NapiTestSupport_h +#define NapiTestSupport_h + +#include + +#include "napi/vendor/node_api.h" + +// A failed call leaves either a pending JS exception or only an extended error +// info record; JS must see a throw either way. +static inline void NapiThrowLastError(napi_env env) { + // The message has to be copied out before anything else runs: every napi + // call clears the error record on its way out, the pending-exception check + // below included, after which napi_get_last_error_info reports napi_ok — + // whose message is null. + char message[256]; + const napi_extended_error_info* info = NULL; + napi_get_last_error_info(env, &info); + if (info != NULL && info->error_message != NULL) { + strncpy(message, info->error_message, sizeof(message) - 1); + message[sizeof(message) - 1] = '\0'; + } else { + strcpy(message, "napi call failed"); + } + + bool pending = false; + if (napi_is_exception_pending(env, &pending) == napi_ok && pending) { + return; + } + + napi_throw_error(env, NULL, message); +} + +#define NAPI_CALL(env, call) \ + do { \ + if ((call) != napi_ok) { \ + NapiThrowLastError(env); \ + return NULL; \ + } \ + } while (0) + +#define NAPI_METHOD(name, fn) \ + {(name), NULL, (fn), NULL, NULL, NULL, napi_default, NULL} +#define NAPI_GETTER(name, fn) \ + {(name), NULL, NULL, (fn), NULL, NULL, napi_enumerable, NULL} +#define NAPI_VALUE(name, val) \ + {(name), NULL, NULL, NULL, NULL, (val), napi_enumerable, NULL} + +// Statuses cross into JS as names so a spec can assert on them. +static inline const char* NapiStatusName(napi_status status) { + switch (status) { + case napi_ok: + return "ok"; + case napi_invalid_arg: + return "invalid_arg"; + case napi_object_expected: + return "object_expected"; + case napi_string_expected: + return "string_expected"; + case napi_name_expected: + return "name_expected"; + case napi_function_expected: + return "function_expected"; + case napi_number_expected: + return "number_expected"; + case napi_boolean_expected: + return "boolean_expected"; + case napi_array_expected: + return "array_expected"; + case napi_generic_failure: + return "generic_failure"; + case napi_pending_exception: + return "pending_exception"; + case napi_cancelled: + return "cancelled"; + case napi_escape_called_twice: + return "escape_called_twice"; + case napi_handle_scope_mismatch: + return "handle_scope_mismatch"; + case napi_callback_scope_mismatch: + return "callback_scope_mismatch"; + case napi_queue_full: + return "queue_full"; + case napi_closing: + return "closing"; + case napi_bigint_expected: + return "bigint_expected"; + case napi_date_expected: + return "date_expected"; + case napi_arraybuffer_expected: + return "arraybuffer_expected"; + case napi_detachable_arraybuffer_expected: + return "detachable_arraybuffer_expected"; + case napi_would_deadlock: + return "would_deadlock"; + case napi_no_external_buffers_allowed: + return "no_external_buffers_allowed"; + case napi_cannot_run_js: + return "cannot_run_js"; + default: + return "other"; + } +} + +static inline napi_value NapiStatusValue(napi_env env, napi_status status) { + napi_value result = NULL; + napi_create_string_utf8(env, NapiStatusName(status), NAPI_AUTO_LENGTH, + &result); + return result; +} + +// Result builders for the object-shaped returns the specs assert against. A +// failed create yields NULL, which napi_set_named_property then rejects. +static inline napi_value NapiDouble(napi_env env, double value) { + napi_value result = NULL; + napi_create_double(env, value, &result); + return result; +} + +static inline napi_value NapiBool(napi_env env, bool value) { + napi_value result = NULL; + napi_get_boolean(env, value, &result); + return result; +} + +static inline napi_value NapiString(napi_env env, const char* value) { + napi_value result = NULL; + napi_create_string_utf8(env, value, NAPI_AUTO_LENGTH, &result); + return result; +} + +#endif /* NapiTestSupport_h */ diff --git a/test-app/runtime/src/main/cpp/napi/vendor/NOTICE b/test-app/runtime/src/main/cpp/napi/vendor/NOTICE new file mode 100644 index 000000000..d7ea15dd3 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/NOTICE @@ -0,0 +1,24 @@ +The files in this directory are copied verbatim from the Node.js project +(https://github.com/nodejs/node), tag v26.7.0, and are licensed as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/test-app/runtime/src/main/cpp/napi/vendor/README.md b/test-app/runtime/src/main/cpp/napi/vendor/README.md new file mode 100644 index 000000000..7440eb24b --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/README.md @@ -0,0 +1,55 @@ +# Vendored Node-API sources + +Node.js's engine-independent Node-API implementation and its public headers, +copied here verbatim so plugins get the standard ABI without this runtime +reimplementing it. + +- Source: https://github.com/nodejs/node +- Tag: `v26.7.0` +- Commit: `b4f23d3619c98bed09af93a21192f6080197a8c6` + +Files, all from `src/` at that commit: + +| File | Role | +| --- | --- | +| `js_native_api.h` | Engine-independent public API | +| `js_native_api_types.h` | Public types shared by both headers | +| `js_native_api_v8.h` | `napi_env__` and the V8 implementation's internals | +| `js_native_api_v8.cc` | The implementation | +| `node_api.h` | Node-specific public API (modules, buffers, async) | +| `node_api_types.h` | Node-specific public types | + +`src/node_api.cc` is deliberately *not* vendored: it is bound to Node's event +loop and environment. Its role is filled by `../NodeApiEmbed.cpp`, +`../NapiThreadSafeFunction.cpp` and `../NapiEnv.cpp`, and the idioms +`js_native_api_v8.cc` expects from Node's +internal headers are supplied by `../shim/`. + +## Local deviations + +None. Every file is byte-identical to upstream (and to the copy vendored in +the iOS runtime). + +One build-level accommodation keeps it that way: + +- `../shim/env-inl.h` includes `node_api.h`, because + `napi_create_external_arraybuffer` calls `napi_create_external_buffer` + without including a header for it; upstream that declaration arrives + transitively through Node's `env.h`. + +## Re-syncing + +```sh +V=v26.7.0 +for f in js_native_api.h js_native_api_types.h js_native_api_v8.h js_native_api_v8.cc node_api.h node_api_types.h; do + curl -sSfo "test-app/runtime/src/main/cpp/napi/vendor/$f" "https://raw.githubusercontent.com/nodejs/node/$V/src/$f" +done +``` + +Then update the tag and commit above, re-check `../shim/` against upstream's +`src/js_native_api_v8_internals.h` and `src/node_version.h` (the shim mirrors +`NODE_API_SUPPORTED_VERSION_MAX/MIN` and +`NODE_API_DEFAULT_MODULE_API_VERSION`), and cover any new `node_api.h` entry +point in `../NodeApiEmbed.cpp` (or `../NapiThreadSafeFunction.cpp`, which owns +the threadsafe function surface) — nothing declared there may be left +undefined. diff --git a/test-app/runtime/src/main/cpp/napi/vendor/js_native_api.h b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api.h new file mode 100644 index 000000000..d2375ecd3 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api.h @@ -0,0 +1,634 @@ +#ifndef SRC_JS_NATIVE_API_H_ +#define SRC_JS_NATIVE_API_H_ + +// This file needs to be compatible with C compilers. +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) + +#include "js_native_api_types.h" + +// If you need __declspec(dllimport), either include instead, or +// define NAPI_EXTERN as __declspec(dllimport) on the compiler's command line. +#ifndef NAPI_EXTERN +#ifdef _WIN32 +#define NAPI_EXTERN __declspec(dllexport) +#elif defined(__wasm__) +#define NAPI_EXTERN \ + __attribute__((visibility("default"))) \ + __attribute__((__import_module__("napi"))) +#else +#define NAPI_EXTERN __attribute__((visibility("default"))) +#endif +#endif + +#define NAPI_AUTO_LENGTH SIZE_MAX + +#ifdef __cplusplus +#define EXTERN_C_START extern "C" { +#define EXTERN_C_END } +#else +#define EXTERN_C_START +#define EXTERN_C_END +#endif + +EXTERN_C_START + +NAPI_EXTERN napi_status NAPI_CDECL napi_get_last_error_info( + node_api_basic_env env, const napi_extended_error_info** result); + +// Getters for defined singletons +NAPI_EXTERN napi_status NAPI_CDECL napi_get_undefined(napi_env env, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_null(napi_env env, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_global(napi_env env, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_boolean(napi_env env, + bool value, + napi_value* result); + +// Methods to create Primitive types/Objects +NAPI_EXTERN napi_status NAPI_CDECL napi_create_object(napi_env env, + napi_value* result); +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_CREATE_OBJECT_WITH_PROPERTIES +NAPI_EXTERN napi_status NAPI_CDECL +node_api_create_object_with_properties(napi_env env, + napi_value prototype_or_null, + napi_value* property_names, + napi_value* property_values, + size_t property_count, + napi_value* result); +#endif // NAPI_EXPERIMENTAL + +NAPI_EXTERN napi_status NAPI_CDECL napi_create_array(napi_env env, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_array_with_length(napi_env env, size_t length, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_double(napi_env env, + double value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_int32(napi_env env, + int32_t value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_uint32(napi_env env, + uint32_t value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_int64(napi_env env, + int64_t value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_string_latin1( + napi_env env, const char* str, size_t length, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_string_utf8(napi_env env, + const char* str, + size_t length, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_string_utf16(napi_env env, + const char16_t* str, + size_t length, + napi_value* result); +#if NAPI_VERSION >= 10 +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_external_string_latin1( + napi_env env, + char* str, + size_t length, + node_api_basic_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); +NAPI_EXTERN napi_status NAPI_CDECL +node_api_create_external_string_utf16(napi_env env, + char16_t* str, + size_t length, + node_api_basic_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); + +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_property_key_latin1( + napi_env env, const char* str, size_t length, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_property_key_utf8( + napi_env env, const char* str, size_t length, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_property_key_utf16( + napi_env env, const char16_t* str, size_t length, napi_value* result); +#endif // NAPI_VERSION >= 10 + +NAPI_EXTERN napi_status NAPI_CDECL napi_create_symbol(napi_env env, + napi_value description, + napi_value* result); +#if NAPI_VERSION >= 9 +NAPI_EXTERN napi_status NAPI_CDECL +node_api_symbol_for(napi_env env, + const char* utf8description, + size_t length, + napi_value* result); +#endif // NAPI_VERSION >= 9 +NAPI_EXTERN napi_status NAPI_CDECL napi_create_function(napi_env env, + const char* utf8name, + size_t length, + napi_callback cb, + void* data, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_type_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_range_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); +#if NAPI_VERSION >= 9 +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_syntax_error( + napi_env env, napi_value code, napi_value msg, napi_value* result); +#endif // NAPI_VERSION >= 9 + +// Methods to get the native napi_value from Primitive type +NAPI_EXTERN napi_status NAPI_CDECL napi_typeof(napi_env env, + napi_value value, + napi_valuetype* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_double(napi_env env, + napi_value value, + double* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_int32(napi_env env, + napi_value value, + int32_t* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_uint32(napi_env env, + napi_value value, + uint32_t* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_int64(napi_env env, + napi_value value, + int64_t* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_bool(napi_env env, + napi_value value, + bool* result); + +// Copies LATIN-1 encoded bytes from a string into a buffer. +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_string_latin1( + napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result); + +// Copies UTF-8 encoded bytes from a string into a buffer. +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_string_utf8( + napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result); + +// Copies UTF-16 encoded bytes from a string into a buffer. +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_string_utf16(napi_env env, + napi_value value, + char16_t* buf, + size_t bufsize, + size_t* result); + +// Methods to coerce values +// These APIs may execute user scripts +NAPI_EXTERN napi_status NAPI_CDECL napi_coerce_to_bool(napi_env env, + napi_value value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_coerce_to_number(napi_env env, + napi_value value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_coerce_to_object(napi_env env, + napi_value value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_coerce_to_string(napi_env env, + napi_value value, + napi_value* result); + +// Methods to work with Objects +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE +NAPI_EXTERN napi_status NAPI_CDECL node_api_set_prototype(napi_env env, + napi_value object, + napi_value value); +#endif +NAPI_EXTERN napi_status NAPI_CDECL napi_get_prototype(napi_env env, + napi_value object, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_property_names(napi_env env, + napi_value object, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_set_property(napi_env env, + napi_value object, + napi_value key, + napi_value value); +NAPI_EXTERN napi_status NAPI_CDECL napi_has_property(napi_env env, + napi_value object, + napi_value key, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_property(napi_env env, + napi_value object, + napi_value key, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_delete_property(napi_env env, + napi_value object, + napi_value key, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_has_own_property(napi_env env, + napi_value object, + napi_value key, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_set_named_property(napi_env env, + napi_value object, + const char* utf8name, + napi_value value); +NAPI_EXTERN napi_status NAPI_CDECL napi_has_named_property(napi_env env, + napi_value object, + const char* utf8name, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_named_property(napi_env env, + napi_value object, + const char* utf8name, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_set_element(napi_env env, + napi_value object, + uint32_t index, + napi_value value); +NAPI_EXTERN napi_status NAPI_CDECL napi_has_element(napi_env env, + napi_value object, + uint32_t index, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_element(napi_env env, + napi_value object, + uint32_t index, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_delete_element(napi_env env, + napi_value object, + uint32_t index, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_define_properties(napi_env env, + napi_value object, + size_t property_count, + const napi_property_descriptor* properties); + +// Methods to work with Arrays +NAPI_EXTERN napi_status NAPI_CDECL napi_is_array(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_array_length(napi_env env, + napi_value value, + uint32_t* result); + +// Methods to compare values +NAPI_EXTERN napi_status NAPI_CDECL napi_strict_equals(napi_env env, + napi_value lhs, + napi_value rhs, + bool* result); + +// Methods to work with Functions +NAPI_EXTERN napi_status NAPI_CDECL napi_call_function(napi_env env, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_new_instance(napi_env env, + napi_value constructor, + size_t argc, + const napi_value* argv, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_instanceof(napi_env env, + napi_value object, + napi_value constructor, + bool* result); + +// Methods to work with napi_callbacks + +// Gets all callback info in a single call. (Ugly, but faster.) +NAPI_EXTERN napi_status NAPI_CDECL napi_get_cb_info( + napi_env env, // [in] Node-API environment handle + napi_callback_info cbinfo, // [in] Opaque callback-info handle + size_t* argc, // [in-out] Specifies the size of the provided argv array + // and receives the actual count of args. + napi_value* argv, // [out] Array of values + napi_value* this_arg, // [out] Receives the JS 'this' arg for the call + void** data); // [out] Receives the data pointer for the callback. + +NAPI_EXTERN napi_status NAPI_CDECL napi_get_new_target( + napi_env env, napi_callback_info cbinfo, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_define_class(napi_env env, + const char* utf8name, + size_t length, + napi_callback constructor, + void* data, + size_t property_count, + const napi_property_descriptor* properties, + napi_value* result); + +// Methods to work with external data objects +NAPI_EXTERN napi_status NAPI_CDECL +napi_wrap(napi_env env, + napi_value js_object, + void* native_object, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_ref* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_unwrap(napi_env env, + napi_value js_object, + void** result); +NAPI_EXTERN napi_status NAPI_CDECL napi_remove_wrap(napi_env env, + napi_value js_object, + void** result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_external(napi_env env, + void* data, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_external(napi_env env, + napi_value value, + void** result); + +// Methods to control object lifespan + +// Set initial_refcount to 0 for a weak reference, >0 for a strong reference. +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_reference(napi_env env, + napi_value value, + uint32_t initial_refcount, + napi_ref* result); + +// Deletes a reference. The referenced value is released, and may +// be GC'd unless there are other references to it. +NAPI_EXTERN napi_status NAPI_CDECL napi_delete_reference(node_api_basic_env env, + napi_ref ref); + +// Increments the reference count, optionally returning the resulting count. +// After this call the reference will be a strong reference because its +// refcount is >0, and the referenced object is effectively "pinned". +// Calling this when the refcount is 0 and the object is unavailable +// results in an error. +NAPI_EXTERN napi_status NAPI_CDECL napi_reference_ref(napi_env env, + napi_ref ref, + uint32_t* result); + +// Decrements the reference count, optionally returning the resulting count. +// If the result is 0 the reference is now weak and the object may be GC'd +// at any time if there are no other references. Calling this when the +// refcount is already 0 results in an error. +NAPI_EXTERN napi_status NAPI_CDECL napi_reference_unref(napi_env env, + napi_ref ref, + uint32_t* result); + +// Attempts to get a referenced value. If the reference is weak, +// the value might no longer be available, in that case the call +// is still successful but the result is NULL. +NAPI_EXTERN napi_status NAPI_CDECL napi_get_reference_value(napi_env env, + napi_ref ref, + napi_value* result); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_open_handle_scope(napi_env env, napi_handle_scope* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_close_handle_scope(napi_env env, napi_handle_scope scope); +NAPI_EXTERN napi_status NAPI_CDECL napi_open_escapable_handle_scope( + napi_env env, napi_escapable_handle_scope* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_close_escapable_handle_scope( + napi_env env, napi_escapable_handle_scope scope); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_escape_handle(napi_env env, + napi_escapable_handle_scope scope, + napi_value escapee, + napi_value* result); + +// Methods to support error handling +NAPI_EXTERN napi_status NAPI_CDECL napi_throw(napi_env env, napi_value error); +NAPI_EXTERN napi_status NAPI_CDECL napi_throw_error(napi_env env, + const char* code, + const char* msg); +NAPI_EXTERN napi_status NAPI_CDECL napi_throw_type_error(napi_env env, + const char* code, + const char* msg); +NAPI_EXTERN napi_status NAPI_CDECL napi_throw_range_error(napi_env env, + const char* code, + const char* msg); +#if NAPI_VERSION >= 9 +NAPI_EXTERN napi_status NAPI_CDECL node_api_throw_syntax_error(napi_env env, + const char* code, + const char* msg); +#endif // NAPI_VERSION >= 9 +NAPI_EXTERN napi_status NAPI_CDECL napi_is_error(napi_env env, + napi_value value, + bool* result); + +// Methods to support catching exceptions +NAPI_EXTERN napi_status NAPI_CDECL napi_is_exception_pending(napi_env env, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_and_clear_last_exception(napi_env env, napi_value* result); + +// Methods to work with array buffers and typed arrays +NAPI_EXTERN napi_status NAPI_CDECL napi_is_arraybuffer(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_create_arraybuffer(napi_env env, + size_t byte_length, + void** data, + napi_value* result); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_external_arraybuffer(napi_env env, + void* external_data, + size_t byte_length, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_CREATE_EXTERNAL_SHAREDARRAYBUFFER +NAPI_EXTERN napi_status NAPI_CDECL +node_api_create_external_sharedarraybuffer(napi_env env, + void* external_data, + size_t byte_length, + node_api_noenv_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +#endif // NAPI_EXPERIMENTAL +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +NAPI_EXTERN napi_status NAPI_CDECL napi_get_arraybuffer_info( + napi_env env, napi_value arraybuffer, void** data, size_t* byte_length); +NAPI_EXTERN napi_status NAPI_CDECL napi_is_typedarray(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_typedarray(napi_env env, + napi_typedarray_type type, + size_t length, + napi_value arraybuffer, + size_t byte_offset, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_typedarray_info(napi_env env, + napi_value typedarray, + napi_typedarray_type* type, + size_t* length, + void** data, + napi_value* arraybuffer, + size_t* byte_offset); + +NAPI_EXTERN napi_status NAPI_CDECL napi_create_dataview(napi_env env, + size_t length, + napi_value arraybuffer, + size_t byte_offset, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_is_dataview(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_dataview_info(napi_env env, + napi_value dataview, + size_t* bytelength, + void** data, + napi_value* arraybuffer, + size_t* byte_offset); + +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER +NAPI_EXTERN napi_status NAPI_CDECL +node_api_is_sharedarraybuffer(napi_env env, napi_value value, bool* result); +NAPI_EXTERN napi_status NAPI_CDECL node_api_create_sharedarraybuffer( + napi_env env, size_t byte_length, void** data, napi_value* result); +#endif // NAPI_EXPERIMENTAL + +// version management +NAPI_EXTERN napi_status NAPI_CDECL napi_get_version(node_api_basic_env env, + uint32_t* result); + +// Promises +NAPI_EXTERN napi_status NAPI_CDECL napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise); +NAPI_EXTERN napi_status NAPI_CDECL napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution); +NAPI_EXTERN napi_status NAPI_CDECL napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value rejection); +NAPI_EXTERN napi_status NAPI_CDECL napi_is_promise(napi_env env, + napi_value value, + bool* is_promise); + +// Running a script +NAPI_EXTERN napi_status NAPI_CDECL napi_run_script(napi_env env, + napi_value script, + napi_value* result); + +// Memory management +NAPI_EXTERN napi_status NAPI_CDECL napi_adjust_external_memory( + node_api_basic_env env, int64_t change_in_bytes, int64_t* adjusted_value); + +#if NAPI_VERSION >= 5 + +// Dates +NAPI_EXTERN napi_status NAPI_CDECL napi_create_date(napi_env env, + double time, + napi_value* result); + +NAPI_EXTERN napi_status NAPI_CDECL napi_is_date(napi_env env, + napi_value value, + bool* is_date); + +NAPI_EXTERN napi_status NAPI_CDECL napi_get_date_value(napi_env env, + napi_value value, + double* result); + +// Add finalizer for pointer +NAPI_EXTERN napi_status NAPI_CDECL +napi_add_finalizer(napi_env env, + napi_value js_object, + void* finalize_data, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_ref* result); + +#endif // NAPI_VERSION >= 5 + +#ifdef NAPI_EXPERIMENTAL +#define NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + +NAPI_EXTERN napi_status NAPI_CDECL +node_api_post_finalizer(node_api_basic_env env, + napi_finalize finalize_cb, + void* finalize_data, + void* finalize_hint); + +#endif // NAPI_EXPERIMENTAL + +#if NAPI_VERSION >= 6 + +// BigInt +NAPI_EXTERN napi_status NAPI_CDECL napi_create_bigint_int64(napi_env env, + int64_t value, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_bigint_words(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_bigint_int64(napi_env env, + napi_value value, + int64_t* result, + bool* lossless); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_bigint_uint64( + napi_env env, napi_value value, uint64_t* result, bool* lossless); +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_value_bigint_words(napi_env env, + napi_value value, + int* sign_bit, + size_t* word_count, + uint64_t* words); + +// Object +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_all_property_names(napi_env env, + napi_value object, + napi_key_collection_mode key_mode, + napi_key_filter key_filter, + napi_key_conversion key_conversion, + napi_value* result); + +// Instance data +NAPI_EXTERN napi_status NAPI_CDECL +napi_set_instance_data(node_api_basic_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_instance_data(node_api_basic_env env, void** data); +#endif // NAPI_VERSION >= 6 + +#if NAPI_VERSION >= 7 +// ArrayBuffer detaching +NAPI_EXTERN napi_status NAPI_CDECL +napi_detach_arraybuffer(napi_env env, napi_value arraybuffer); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_is_detached_arraybuffer(napi_env env, napi_value value, bool* result); +#endif // NAPI_VERSION >= 7 + +#if NAPI_VERSION >= 8 +// Type tagging +NAPI_EXTERN napi_status NAPI_CDECL napi_type_tag_object( + napi_env env, napi_value value, const napi_type_tag* type_tag); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_check_object_type_tag(napi_env env, + napi_value value, + const napi_type_tag* type_tag, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_object_freeze(napi_env env, + napi_value object); +NAPI_EXTERN napi_status NAPI_CDECL napi_object_seal(napi_env env, + napi_value object); +#endif // NAPI_VERSION >= 8 + +EXTERN_C_END + +#endif // SRC_JS_NATIVE_API_H_ diff --git a/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_types.h b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_types.h new file mode 100644 index 000000000..1de8e29d6 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_types.h @@ -0,0 +1,244 @@ +#ifndef SRC_JS_NATIVE_API_TYPES_H_ +#define SRC_JS_NATIVE_API_TYPES_H_ + +// Use INT_MAX, this should only be consumed by the pre-processor anyway. +#define NAPI_VERSION_EXPERIMENTAL 2147483647 +#ifndef NAPI_VERSION +#ifdef NAPI_EXPERIMENTAL +#define NAPI_VERSION NAPI_VERSION_EXPERIMENTAL +#else +// The baseline version for Node-API. +// NAPI_VERSION controls which version is used by default when compiling +// a native addon. If the addon developer wants to use functions from a +// newer Node-API version not yet available in all LTS versions, they can +// set NAPI_VERSION to explicitly depend on that version. +#define NAPI_VERSION 8 +#endif +#endif + +#if defined(NAPI_EXPERIMENTAL) && \ + !defined(NODE_API_EXPERIMENTAL_NO_WARNING) && \ + !defined(NODE_WANT_INTERNALS) +#ifdef _MSC_VER +#pragma message("NAPI_EXPERIMENTAL is enabled. " \ + "Experimental features may be unstable.") +#else +#warning "NAPI_EXPERIMENTAL is enabled. " \ + "Experimental features may be unstable." +#endif +#endif + +// This file needs to be compatible with C compilers. +// This is a public include file, and these includes have essentially +// become part of its API. +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) + +#if !defined __cplusplus || (defined(_MSC_VER) && _MSC_VER < 1900) +typedef uint16_t char16_t; +#endif + +#ifndef NAPI_CDECL +#ifdef _WIN32 +#define NAPI_CDECL __cdecl +#else +#define NAPI_CDECL +#endif +#endif + +// JSVM API types are all opaque pointers for ABI stability +// typedef undefined structs instead of void* for compile time type safety +typedef struct napi_env__* napi_env; + +// We need to mark APIs which can be called during garbage collection (GC), +// meaning that they do not affect the state of the JS engine, and can +// therefore be called synchronously from a finalizer that itself runs +// synchronously during GC. Such APIs can receive either a `napi_env` or a +// `node_api_basic_env` as their first parameter, because we should be able to +// also call them during normal, non-garbage-collecting operations, whereas +// APIs that affect the state of the JS engine can only receive a `napi_env` as +// their first parameter, because we must not call them during GC. In lieu of +// inheritance, we use the properties of the const qualifier to accomplish +// this, because both a const and a non-const value can be passed to an API +// expecting a const value, but only a non-const value can be passed to an API +// expecting a non-const value. +// +// In conjunction with appropriate CFLAGS to warn us if we're passing a const +// (basic) environment into an API that expects a non-const environment, and +// the definition of basic finalizer function pointer types below, which +// receive a basic environment as their first parameter, and can thus only call +// basic APIs (unless the user explicitly casts the environment), we achieve +// the ability to ensure at compile time that we do not call APIs that affect +// the state of the JS engine from a synchronous (basic) finalizer. +#if !defined(NAPI_EXPERIMENTAL) || \ + (defined(NAPI_EXPERIMENTAL) && \ + (defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT) || \ + defined(NODE_API_EXPERIMENTAL_BASIC_ENV_OPT_OUT))) +typedef struct napi_env__* node_api_nogc_env; +#else +typedef const struct napi_env__* node_api_nogc_env; +#endif +typedef node_api_nogc_env node_api_basic_env; + +typedef struct napi_value__* napi_value; +typedef struct napi_ref__* napi_ref; +typedef struct napi_handle_scope__* napi_handle_scope; +typedef struct napi_escapable_handle_scope__* napi_escapable_handle_scope; +typedef struct napi_callback_info__* napi_callback_info; +typedef struct napi_deferred__* napi_deferred; + +typedef enum { + napi_default = 0, + napi_writable = 1 << 0, + napi_enumerable = 1 << 1, + napi_configurable = 1 << 2, + + // Used with napi_define_class to distinguish static properties + // from instance properties. Ignored by napi_define_properties. + napi_static = 1 << 10, + +#if NAPI_VERSION >= 8 + // Default for class methods. + napi_default_method = napi_writable | napi_configurable, + + // Default for object properties, like in JS obj[prop]. + napi_default_jsproperty = napi_writable | napi_enumerable | napi_configurable, +#endif // NAPI_VERSION >= 8 +} napi_property_attributes; + +typedef enum { + // ES6 types (corresponds to typeof) + napi_undefined, + napi_null, + napi_boolean, + napi_number, + napi_string, + napi_symbol, + napi_object, + napi_function, + napi_external, + napi_bigint, +} napi_valuetype; + +typedef enum { + napi_int8_array, + napi_uint8_array, + napi_uint8_clamped_array, + napi_int16_array, + napi_uint16_array, + napi_int32_array, + napi_uint32_array, + napi_float32_array, + napi_float64_array, + napi_bigint64_array, + napi_biguint64_array, +#define NODE_API_HAS_FLOAT16_ARRAY + napi_float16_array, +} napi_typedarray_type; + +typedef enum { + napi_ok, + napi_invalid_arg, + napi_object_expected, + napi_string_expected, + napi_name_expected, + napi_function_expected, + napi_number_expected, + napi_boolean_expected, + napi_array_expected, + napi_generic_failure, + napi_pending_exception, + napi_cancelled, + napi_escape_called_twice, + napi_handle_scope_mismatch, + napi_callback_scope_mismatch, + napi_queue_full, + napi_closing, + napi_bigint_expected, + napi_date_expected, + napi_arraybuffer_expected, + napi_detachable_arraybuffer_expected, + napi_would_deadlock, // unused + napi_no_external_buffers_allowed, + napi_cannot_run_js, +} napi_status; +// Note: when adding a new enum value to `napi_status`, please also update +// * `const int last_status` in the definition of `napi_get_last_error_info()' +// in file js_native_api_v8.cc. +// * `const char* error_messages[]` in file js_native_api_v8.cc with a brief +// message explaining the error. +// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly +// added value(s). + +typedef napi_value(NAPI_CDECL* napi_callback)(napi_env env, + napi_callback_info info); +typedef void(NAPI_CDECL* napi_finalize)(napi_env env, + void* finalize_data, + void* finalize_hint); + +#if !defined(NAPI_EXPERIMENTAL) || \ + (defined(NAPI_EXPERIMENTAL) && \ + (defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT) || \ + defined(NODE_API_EXPERIMENTAL_BASIC_ENV_OPT_OUT))) +typedef napi_finalize node_api_nogc_finalize; +#else +typedef void(NAPI_CDECL* node_api_nogc_finalize)(node_api_nogc_env env, + void* finalize_data, + void* finalize_hint); +#endif +typedef node_api_nogc_finalize node_api_basic_finalize; + +// A finalizer that can be called from any thread and at any time. +typedef void(NAPI_CDECL* node_api_noenv_finalize)(void* finalize_data, + void* finalize_hint); + +typedef struct { + // One of utf8name or name should be NULL. + const char* utf8name; + napi_value name; + + napi_callback method; + napi_callback getter; + napi_callback setter; + napi_value value; + + napi_property_attributes attributes; + void* data; +} napi_property_descriptor; + +typedef struct { + const char* error_message; + void* engine_reserved; + uint32_t engine_error_code; + napi_status error_code; +} napi_extended_error_info; + +#if NAPI_VERSION >= 6 +typedef enum { + napi_key_include_prototypes, + napi_key_own_only +} napi_key_collection_mode; + +typedef enum { + napi_key_all_properties = 0, + napi_key_writable = 1, + napi_key_enumerable = 1 << 1, + napi_key_configurable = 1 << 2, + napi_key_skip_strings = 1 << 3, + napi_key_skip_symbols = 1 << 4 +} napi_key_filter; + +typedef enum { + napi_key_keep_numbers, + napi_key_numbers_to_strings +} napi_key_conversion; +#endif // NAPI_VERSION >= 6 + +#if NAPI_VERSION >= 8 +typedef struct { + uint64_t lower; + uint64_t upper; +} napi_type_tag; +#endif // NAPI_VERSION >= 8 + +#endif // SRC_JS_NATIVE_API_TYPES_H_ diff --git a/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.cc b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.cc new file mode 100644 index 000000000..5e533d2a6 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.cc @@ -0,0 +1,3748 @@ +#include +#include // INT_MAX +#include +#ifndef NAPI_EXPERIMENTAL +#define NAPI_EXPERIMENTAL +#endif +#include "env-inl.h" +#include "js_native_api.h" +#include "js_native_api_v8.h" +#include "util-inl.h" + +#define CHECK_MAYBE_NOTHING(env, maybe, status) \ + RETURN_STATUS_IF_FALSE((env), !((maybe).IsNothing()), (status)) + +#define CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe, status) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsNothing()), (status)) + +#define CHECK_TO_NUMBER(env, context, result, src) \ + CHECK_TO_TYPE((env), Number, (context), (result), (src), napi_number_expected) + +// Node-API defines NAPI_AUTO_LENGTH as the indicator that a string +// is null terminated. For V8 the equivalent is -1. The assert +// validates that our cast of NAPI_AUTO_LENGTH results in -1 as +// needed by V8. +#define CHECK_NEW_FROM_UTF8_LEN(env, result, str, len) \ + do { \ + static_assert(static_cast(NAPI_AUTO_LENGTH) == -1, \ + "Casting NAPI_AUTO_LENGTH to int must result in -1"); \ + RETURN_STATUS_IF_FALSE( \ + (env), (len == NAPI_AUTO_LENGTH) || len <= INT_MAX, napi_invalid_arg); \ + RETURN_STATUS_IF_FALSE((env), (str) != nullptr, napi_invalid_arg); \ + auto str_maybe = v8::String::NewFromUtf8((env)->isolate, \ + (str), \ + v8::NewStringType::kInternalized, \ + static_cast(len)); \ + CHECK_MAYBE_EMPTY((env), str_maybe, napi_generic_failure); \ + (result) = str_maybe.ToLocalChecked(); \ + } while (0) + +#define CHECK_NEW_FROM_UTF8(env, result, str) \ + CHECK_NEW_FROM_UTF8_LEN((env), (result), (str), NAPI_AUTO_LENGTH) + +#define CHECK_NEW_STRING_ARGS(env, str, length, result) \ + do { \ + CHECK_ENV_NOT_IN_GC((env)); \ + if ((length) > 0) CHECK_ARG((env), (str)); \ + CHECK_ARG((env), (result)); \ + RETURN_STATUS_IF_FALSE( \ + (env), \ + ((length) == NAPI_AUTO_LENGTH) || (length) <= INT_MAX, \ + napi_invalid_arg); \ + } while (0) + +#define CREATE_TYPED_ARRAY( \ + env, type, size_of_element, buffer, byte_offset, length, out) \ + do { \ + if ((size_of_element) > 1) { \ + THROW_RANGE_ERROR_IF_FALSE( \ + (env), \ + (byte_offset) % (size_of_element) == 0, \ + "ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT", \ + "start offset of " #type \ + " should be a multiple of " #size_of_element); \ + } \ + THROW_RANGE_ERROR_IF_FALSE( \ + (env), \ + (length) * (size_of_element) + (byte_offset) <= buffer->ByteLength(), \ + "ERR_NAPI_INVALID_TYPEDARRAY_LENGTH", \ + "Invalid typed array length"); \ + (out) = v8::type::New((buffer), (byte_offset), (length)); \ + } while (0) + +void napi_env__::InvokeFinalizerFromGC(v8impl::RefTracker* finalizer) { + if (module_api_version != NAPI_VERSION_EXPERIMENTAL) { + EnqueueFinalizer(finalizer); + } else { + // The experimental code calls finalizers immediately to release native + // objects as soon as possible. In that state any code that may affect GC + // state causes a fatal error. To work around this issue the finalizer code + // can call node_api_post_finalizer. + auto restore_state = node::OnScopeLeave( + [this, saved = in_gc_finalizer] { in_gc_finalizer = saved; }); + in_gc_finalizer = true; + finalizer->Finalize(); + } +} + +namespace v8impl { +namespace { + +template +napi_status NewString(napi_env env, + const CCharType* str, + size_t length, + napi_value* result, + StringMaker string_maker) { + CHECK_NEW_STRING_ARGS(env, str, length, result); + + auto isolate = env->isolate; + auto str_maybe = string_maker(isolate); + CHECK_MAYBE_EMPTY(env, str_maybe, napi_generic_failure); + *result = v8impl::JsValueFromV8LocalValue(str_maybe.ToLocalChecked()); + return napi_clear_last_error(env); +} + +template +napi_status NewExternalString(napi_env env, + CharType* str, + size_t length, + napi_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied, + CreateAPI create_api, + StringMaker string_maker) { + CHECK_NEW_STRING_ARGS(env, str, length, result); + + napi_status status; +#ifdef V8_ENABLE_SANDBOX + status = create_api(env, str, length, result); + if (status == napi_ok) { + if (copied != nullptr) { + *copied = true; + } + if (finalize_callback) { + env->CallFinalizer( + finalize_callback, static_cast(str), finalize_hint); + } + } +#else + status = NewString(env, str, length, result, string_maker); + if (status == napi_ok && copied != nullptr) { + *copied = false; + } +#endif // V8_ENABLE_SANDBOX + return status; +} + +class TrackedStringResource : private RefTracker { + public: + TrackedStringResource(napi_env env, + napi_finalize finalize_callback, + void* data, + void* finalize_hint) + : RefTracker(), finalizer_(env, finalize_callback, data, finalize_hint) { + Link(finalize_callback == nullptr ? &env->reflist + : &env->finalizing_reflist); + } + + protected: + // The only time Finalize() gets called before destructor is if the + // environment is dying. Finalize() expects that the item will be unlinked, + // so we do it here. V8 will still call destructor on us later, so we don't do + // any deleting here. We just null out env to avoid passing a stale pointer + // to the user's finalizer when V8 does finally call destructor. + void Finalize() override { + Unlink(); + finalizer_.ResetEnv(); + } + + ~TrackedStringResource() override { + Unlink(); + finalizer_.CallFinalizer(); + } + + private: + Finalizer finalizer_; +}; + +class ExternalOneByteStringResource final + : public v8::String::ExternalOneByteStringResource, + TrackedStringResource { + public: + ExternalOneByteStringResource(napi_env env, + char* string, + const size_t length, + napi_finalize finalize_callback, + void* finalize_hint) + : TrackedStringResource(env, finalize_callback, string, finalize_hint), + string_(string), + length_(length) {} + + const char* data() const override { return string_; } + size_t length() const override { return length_; } + + private: + const char* string_; + const size_t length_; +}; + +class ExternalStringResource final : public v8::String::ExternalStringResource, + TrackedStringResource { + public: + ExternalStringResource(napi_env env, + char16_t* string, + const size_t length, + napi_finalize finalize_callback, + void* finalize_hint) + : TrackedStringResource(env, finalize_callback, string, finalize_hint), + string_(reinterpret_cast(string)), + length_(length) {} + + const uint16_t* data() const override { return string_; } + size_t length() const override { return length_; } + + private: + const uint16_t* string_; + const size_t length_; +}; + +inline napi_status V8NameFromPropertyDescriptor( + napi_env env, + const napi_property_descriptor* p, + v8::Local* result) { + if (p->utf8name != nullptr) { + CHECK_NEW_FROM_UTF8(env, *result, p->utf8name); + } else { + v8::Local property_value = + v8impl::V8LocalValueFromJsValue(p->name); + + RETURN_STATUS_IF_FALSE(env, property_value->IsName(), napi_name_expected); + *result = property_value.As(); + } + + return napi_ok; +} + +// convert from Node-API property attributes to v8::PropertyAttribute +inline v8::PropertyAttribute V8PropertyAttributesFromDescriptor( + const napi_property_descriptor* descriptor) { + unsigned int attribute_flags = v8::PropertyAttribute::None; + + // The napi_writable attribute is ignored for accessor descriptors, but + // V8 would throw `TypeError`s on assignment with nonexistence of a setter. + if ((descriptor->getter == nullptr && descriptor->setter == nullptr) && + (descriptor->attributes & napi_writable) == 0) { + attribute_flags |= v8::PropertyAttribute::ReadOnly; + } + + if ((descriptor->attributes & napi_enumerable) == 0) { + attribute_flags |= v8::PropertyAttribute::DontEnum; + } + if ((descriptor->attributes & napi_configurable) == 0) { + attribute_flags |= v8::PropertyAttribute::DontDelete; + } + + return static_cast(attribute_flags); +} + +inline napi_deferred JsDeferredFromNodePersistent( + v8impl::Persistent* local) { + return reinterpret_cast(local); +} + +inline v8impl::Persistent* NodePersistentFromJsDeferred( + napi_deferred local) { + return reinterpret_cast*>(local); +} + +class HandleScopeWrapper { + public: + explicit HandleScopeWrapper(v8::Isolate* isolate) : scope(isolate) {} + + private: + v8::HandleScope scope; +}; + +// In node v0.10 version of v8, there is no EscapableHandleScope and the +// node v0.10 port use HandleScope::Close(Local v) to mimic the behavior +// of a EscapableHandleScope::Escape(Local v), but it is not the same +// semantics. This is an example of where the api abstraction fail to work +// across different versions. +class EscapableHandleScopeWrapper { + public: + explicit EscapableHandleScopeWrapper(v8::Isolate* isolate) + : scope(isolate), escape_called_(false) {} + bool escape_called() const { return escape_called_; } + template + v8::Local Escape(v8::Local handle) { + escape_called_ = true; + return scope.Escape(handle); + } + + private: + v8::EscapableHandleScope scope; + bool escape_called_; +}; + +inline napi_handle_scope JsHandleScopeFromV8HandleScope(HandleScopeWrapper* s) { + return reinterpret_cast(s); +} + +inline HandleScopeWrapper* V8HandleScopeFromJsHandleScope(napi_handle_scope s) { + return reinterpret_cast(s); +} + +inline napi_escapable_handle_scope +JsEscapableHandleScopeFromV8EscapableHandleScope( + EscapableHandleScopeWrapper* s) { + return reinterpret_cast(s); +} + +inline EscapableHandleScopeWrapper* +V8EscapableHandleScopeFromJsEscapableHandleScope( + napi_escapable_handle_scope s) { + return reinterpret_cast(s); +} + +inline napi_status ConcludeDeferred(napi_env env, + napi_deferred deferred, + napi_value result, + bool is_resolved) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8impl::Persistent* deferred_ref = + NodePersistentFromJsDeferred(deferred); + v8::Local v8_deferred = + v8::Local::New(env->isolate, *deferred_ref); + + auto v8_resolver = v8_deferred.As(); + + v8::Maybe success = + is_resolved ? v8_resolver->Resolve( + context, v8impl::V8LocalValueFromJsValue(result)) + : v8_resolver->Reject( + context, v8impl::V8LocalValueFromJsValue(result)); + + delete deferred_ref; + + RETURN_STATUS_IF_FALSE(env, success.FromMaybe(false), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +enum UnwrapAction { KeepWrap, RemoveWrap }; + +inline napi_status Unwrap(napi_env env, + napi_value js_object, + void** result, + UnwrapAction action) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, js_object); + if (action == KeepWrap) { + CHECK_ARG(env, result); + } + + v8::Local context = env->context(); + + v8::Local value = v8impl::V8LocalValueFromJsValue(js_object); + RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); + v8::Local obj = value.As(); + + auto val = obj->GetPrivate(context, NAPI_PRIVATE_KEY(context, wrapper)) + .ToLocalChecked(); + RETURN_STATUS_IF_FALSE(env, val->IsExternal(), napi_invalid_arg); + Reference* reference = static_cast( + val.As()->Value(v8::kExternalPointerTypeTagDefault)); + + if (result) { + *result = reference->Data(); + } + + if (action == RemoveWrap) { + CHECK(obj->DeletePrivate(context, NAPI_PRIVATE_KEY(context, wrapper)) + .FromJust()); + if (reference->ownership() == ReferenceOwnership::kUserland) { + // When the wrap is been removed, the finalizer should be reset. + reference->ResetFinalizer(); + } else { + delete reference; + } + } + + return GET_RETURN_STATUS(env); +} + +//=== Function napi_callback wrapper ================================= + +// Use this data structure to associate callback data with each Node-API +// function exposed to JavaScript. The structure is stored in a v8::External +// which gets passed into our callback wrapper. This reduces the performance +// impact of calling through Node-API. Ref: benchmark/misc/function_call +// Discussion (incl. perf. data): https://github.com/nodejs/node/pull/21072 +class CallbackBundle { + public: + // Creates an object to be made available to the static function callback + // wrapper, used to retrieve the native callback function and data pointer. + static inline v8::Local New(napi_env env, + napi_callback cb, + void* data) { + CallbackBundle* bundle = new CallbackBundle(); + bundle->cb = cb; + bundle->cb_data = data; + bundle->env = env; + + v8::Local cbdata = v8::External::New( + env->isolate, bundle, v8::kExternalPointerTypeTagDefault); + ReferenceWithFinalizer::New( + env, cbdata, 0, ReferenceOwnership::kRuntime, Delete, bundle, nullptr); + return cbdata; + } + + static CallbackBundle* FromCallbackData(v8::Local data) { + return reinterpret_cast( + data.As()->Value(v8::kExternalPointerTypeTagDefault)); + } + + public: + napi_env env; // Necessary to invoke C++ Node-API callback + void* cb_data; // The user provided callback data + napi_callback cb; + + private: + static void Delete(napi_env env, void* data, void* hint) { + CallbackBundle* bundle = static_cast(data); + delete bundle; + } +}; + +// Wraps up v8::FunctionCallbackInfo. +// The class must be stack allocated. +class FunctionCallbackWrapper { + public: + static void Invoke(const v8::FunctionCallbackInfo& info) { + FunctionCallbackWrapper cbwrapper(info); + cbwrapper.InvokeCallback(); + } + + static inline napi_status NewFunction(napi_env env, + napi_callback cb, + void* cb_data, + v8::Local* result) { + v8::Local cbdata = v8impl::CallbackBundle::New(env, cb, cb_data); + RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure); + + v8::MaybeLocal maybe_function = + v8::Function::New(env->context(), Invoke, cbdata); + CHECK_MAYBE_EMPTY(env, maybe_function, napi_generic_failure); + + *result = maybe_function.ToLocalChecked(); + return napi_clear_last_error(env); + } + + static inline napi_status NewTemplate( + napi_env env, + napi_callback cb, + void* cb_data, + v8::Local* result, + v8::Local sig = v8::Local()) { + v8::Local cbdata = v8impl::CallbackBundle::New(env, cb, cb_data); + RETURN_STATUS_IF_FALSE(env, !cbdata.IsEmpty(), napi_generic_failure); + + *result = v8::FunctionTemplate::New(env->isolate, Invoke, cbdata, sig); + return napi_clear_last_error(env); + } + + napi_value GetNewTarget() { + if (cbinfo_.IsConstructCall()) { + return v8impl::JsValueFromV8LocalValue(cbinfo_.NewTarget()); + } else { + return nullptr; + } + } + + void Args(napi_value* buffer, size_t buffer_length) { + size_t i = 0; + size_t min_arg_count = std::min(buffer_length, ArgsLength()); + + for (; i < min_arg_count; ++i) { + buffer[i] = JsValueFromV8LocalValue(cbinfo_[i]); + } + + if (i < buffer_length) { + napi_value undefined = + JsValueFromV8LocalValue(v8::Undefined(cbinfo_.GetIsolate())); + for (; i < buffer_length; ++i) { + buffer[i] = undefined; + } + } + } + + napi_value This() { return JsValueFromV8LocalValue(cbinfo_.This()); } + + size_t ArgsLength() { return static_cast(cbinfo_.Length()); } + + void* Data() { return bundle_->cb_data; } + + private: + explicit FunctionCallbackWrapper( + const v8::FunctionCallbackInfo& cbinfo) + : cbinfo_(cbinfo), + bundle_(CallbackBundle::FromCallbackData(cbinfo.Data())) {} + + void InvokeCallback() { + napi_callback_info cbinfo_wrapper = + reinterpret_cast(this); + + // All other pointers we need are stored in `_bundle` + napi_env env = bundle_->env; + napi_callback cb = bundle_->cb; + + napi_value result = nullptr; + bool exceptionOccurred = false; + env->CallIntoModule([&](napi_env env) { result = cb(env, cbinfo_wrapper); }, + [&](napi_env env, v8::Local value) { + exceptionOccurred = true; + if (env->terminatedOrTerminating()) { + return; + } + env->isolate->ThrowException(value); + }); + + if (!exceptionOccurred && (result != nullptr)) { + cbinfo_.GetReturnValue().Set(V8LocalValueFromJsValue(result)); + } + } + + private: + const v8::FunctionCallbackInfo& cbinfo_; + CallbackBundle* bundle_; +}; + +inline napi_status Wrap(napi_env env, + napi_value js_object, + void* native_object, + napi_finalize finalize_cb, + void* finalize_hint, + napi_ref* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, js_object); + + v8::Local context = env->context(); + + v8::Local value = v8impl::V8LocalValueFromJsValue(js_object); + RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg); + v8::Local obj = value.As(); + + // If we've already wrapped this object, we error out. + RETURN_STATUS_IF_FALSE( + env, + !obj->HasPrivate(context, NAPI_PRIVATE_KEY(context, wrapper)).FromJust(), + napi_invalid_arg); + + v8impl::Reference* reference = nullptr; + if (result != nullptr) { + // The returned reference should be deleted via napi_delete_reference() + // ONLY in response to the finalize callback invocation. (If it is deleted + // before then, then the finalize callback will never be invoked.) + // Therefore a finalize callback is required when returning a reference. + CHECK_ARG(env, finalize_cb); + reference = v8impl::ReferenceWithFinalizer::New( + env, + obj, + 0, + v8impl::ReferenceOwnership::kUserland, + finalize_cb, + native_object, + finalize_hint); + *result = reinterpret_cast(reference); + } else if (finalize_cb != nullptr) { + // Create a self-deleting reference. + reference = v8impl::ReferenceWithFinalizer::New( + env, + obj, + 0, + v8impl::ReferenceOwnership::kRuntime, + finalize_cb, + native_object, + finalize_hint); + } else { + // Create a self-deleting reference. + reference = v8impl::ReferenceWithData::New( + env, obj, 0, v8impl::ReferenceOwnership::kRuntime, native_object); + } + + CHECK(obj->SetPrivate( + context, + NAPI_PRIVATE_KEY(context, wrapper), + v8::External::New( + env->isolate, reference, v8::kExternalPointerTypeTagDefault)) + .FromJust()); + + return GET_RETURN_STATUS(env); +} + +// In JavaScript, weak references can be created for object types (Object, +// Function, and external Object) and for local symbols that are created with +// the `Symbol` function call. Global symbols created with the `Symbol.for` +// method cannot be weak references because they are never collected. +// +// Currently, V8 has no API to detect if a symbol is local or global. +// Until we have a V8 API for it, we consider that all symbols can be weak. +// This matches the current Node-API behavior. +inline bool CanBeHeldWeakly(v8::Local value) { + return value->IsObject() || value->IsSymbol(); +} + +} // end of anonymous namespace + +void Finalizer::ResetEnv() { + env_ = nullptr; +} + +void Finalizer::ResetFinalizer() { + finalize_callback_ = nullptr; + finalize_data_ = nullptr; + finalize_hint_ = nullptr; +} + +void Finalizer::CallFinalizer() { + napi_finalize finalize_callback = finalize_callback_; + void* finalize_data = finalize_data_; + void* finalize_hint = finalize_hint_; + ResetFinalizer(); + + if (finalize_callback == nullptr) return; + if (env_ == nullptr) { + // The environment is dead. Call the finalizer directly. + finalize_callback(nullptr, finalize_data, finalize_hint); + } else { + env_->CallFinalizer(finalize_callback, finalize_data, finalize_hint); + } +} + +TrackedFinalizer::TrackedFinalizer(napi_env env, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint) + : RefTracker(), + finalizer_(env, finalize_callback, finalize_data, finalize_hint) {} + +TrackedFinalizer* TrackedFinalizer::New(napi_env env, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint) { + TrackedFinalizer* finalizer = new TrackedFinalizer( + env, finalize_callback, finalize_data, finalize_hint); + finalizer->Link(&env->finalizing_reflist); + return finalizer; +} + +// When a TrackedFinalizer is being deleted, it may have been queued to call its +// finalizer. +TrackedFinalizer::~TrackedFinalizer() { + // Remove from the env's tracked list. + Unlink(); + // Try to remove the finalizer from the scheduled second pass callback. + finalizer_.env()->DequeueFinalizer(this); +} + +void TrackedFinalizer::Finalize() { + Unlink(); + finalizer_.CallFinalizer(); + delete this; +} + +Reference::Reference(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership) + : RefTracker(), + persistent_(env->isolate, value), + refcount_(initial_refcount), + ownership_(ownership), + can_be_weak_(CanBeHeldWeakly(value)) { + if (refcount_ == 0) { + SetWeak(); + } +} + +Reference::~Reference() { + // Reset the handle. And no weak callback will be invoked. + persistent_.Reset(); + + // Remove from the env's tracked list. + Unlink(); +} + +Reference* Reference::New(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership) { + Reference* reference = new Reference(env, value, initial_refcount, ownership); + reference->Link(&env->reflist); + return reference; +} + +uint32_t Reference::Ref() { + // When the persistent_ is cleared in the WeakCallback, and a second pass + // callback is pending, return 0 unconditionally. + if (persistent_.IsEmpty()) { + return 0; + } + if (++refcount_ == 1 && can_be_weak_) { + persistent_.ClearWeak(); + } + return refcount_; +} + +uint32_t Reference::Unref() { + // When the persistent_ is cleared in the WeakCallback, and a second pass + // callback is pending, return 0 unconditionally. + if (persistent_.IsEmpty() || refcount_ == 0) { + return 0; + } + if (--refcount_ == 0) { + SetWeak(); + } + return refcount_; +} + +v8::Local Reference::Get(napi_env env) { + if (persistent_.IsEmpty()) { + return v8::Local(); + } else { + return v8::Local::New(env->isolate, persistent_); + } +} + +void Reference::Finalize() { + // Unconditionally reset the persistent handle so that no weak callback will + // be invoked again. + persistent_.Reset(); + + // If the Reference is not ReferenceOwnership::kRuntime, userland code should + // delete it. Delete it if it is ReferenceOwnership::kRuntime. + bool deleteMe = ownership_ == ReferenceOwnership::kRuntime; + + // Whether the Reference is going to be deleted in the finalize_callback + // or not, it should be removed from the tracked list. + Unlink(); + + // If the finalize_callback is present, it should either delete the + // derived Reference, or the Reference ownership was set to + // ReferenceOwnership::kRuntime and the deleteMe parameter is true. + CallUserFinalizer(); + + if (deleteMe) { + delete this; + } +} + +// Call the Finalize immediately since there is no user finalizer to call. +void Reference::InvokeFinalizerFromGC() { + Finalize(); +} + +// Mark the reference as weak and eligible for collection by the GC. +void Reference::SetWeak() { + if (can_be_weak_) { + persistent_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); + } else { + persistent_.Reset(); + } +} + +// Static function called by GC. Delegate the call to the reference instance. +void Reference::WeakCallback(const v8::WeakCallbackInfo& data) { + Reference* reference = data.GetParameter(); + // The reference must be reset during the weak callback per V8 API protocol. + reference->persistent_.Reset(); + reference->InvokeFinalizerFromGC(); +} + +ReferenceWithData* ReferenceWithData::New(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + void* data) { + ReferenceWithData* reference = + new ReferenceWithData(env, value, initial_refcount, ownership, data); + reference->Link(&env->reflist); + return reference; +} + +ReferenceWithData::ReferenceWithData(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + void* data) + : Reference(env, value, initial_refcount, ownership), data_(data) {} + +ReferenceWithFinalizer* ReferenceWithFinalizer::New( + napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint) { + ReferenceWithFinalizer* reference = + new ReferenceWithFinalizer(env, + value, + initial_refcount, + ownership, + finalize_callback, + finalize_data, + finalize_hint); + reference->Link(&env->finalizing_reflist); + return reference; +} + +ReferenceWithFinalizer::ReferenceWithFinalizer(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint) + : Reference(env, value, initial_refcount, ownership), + finalizer_(env, finalize_callback, finalize_data, finalize_hint) {} + +ReferenceWithFinalizer::~ReferenceWithFinalizer() { + // Try to remove the finalizer from the scheduled second pass callback. + finalizer_.env()->DequeueFinalizer(this); +} + +void ReferenceWithFinalizer::CallUserFinalizer() { + finalizer_.CallFinalizer(); +} + +// The Node-API finalizer callback may make calls into the engine. V8's heap is +// not in a consistent state during the weak callback, and therefore it does +// not support calls back into it. Enqueue the invocation of the finalizer. +void ReferenceWithFinalizer::InvokeFinalizerFromGC() { + finalizer_.env()->InvokeFinalizerFromGC(this); +} + +/** + * A wrapper for `v8::External` to support type-tagging. `v8::External` doesn't + * support defining any properties and private properties on it, even though it + * is an object. This wrapper is used to store the type tag and the data of the + * external value. + */ +class ExternalWrapper { + private: + explicit ExternalWrapper(void* data) : data_(data), type_tag_{0, 0} {} + + static void WeakCallback(const v8::WeakCallbackInfo& data) { + ExternalWrapper* wrapper = data.GetParameter(); + delete wrapper; + } + + public: + static v8::Local New(napi_env env, void* data) { + ExternalWrapper* wrapper = new ExternalWrapper(data); + v8::Local external = v8::External::New( + env->isolate, wrapper, v8::kExternalPointerTypeTagDefault); + wrapper->persistent_.Reset(env->isolate, external); + wrapper->persistent_.SetWeak( + wrapper, WeakCallback, v8::WeakCallbackType::kParameter); + + return external; + } + + static ExternalWrapper* From(v8::Local external) { + return static_cast( + external->Value(v8::kExternalPointerTypeTagDefault)); + } + + void* Data() { return data_; } + + bool TypeTag(const napi_type_tag* type_tag) { + if (has_tag_) { + return false; + } + type_tag_ = *type_tag; + has_tag_ = true; + return true; + } + + bool CheckTypeTag(const napi_type_tag* type_tag) { + return has_tag_ && type_tag->lower == type_tag_.lower && + type_tag->upper == type_tag_.upper; + } + + private: + v8impl::Persistent persistent_; + void* data_; + napi_type_tag type_tag_; + bool has_tag_ = false; +}; + +} // end of namespace v8impl + +// Warning: Keep in-sync with napi_status enum +static const char* error_messages[] = { + nullptr, + "Invalid argument", + "An object was expected", + "A string was expected", + "A string or symbol was expected", + "A function was expected", + "A number was expected", + "A boolean was expected", + "An array was expected", + "Unknown failure", + "An exception is pending", + "The async work item was cancelled", + "napi_escape_handle already called on scope", + "Invalid handle scope usage", + "Invalid callback scope usage", + "Thread-safe function queue is full", + "Thread-safe function handle is closing", + "A bigint was expected", + "A date was expected", + "An arraybuffer was expected", + "A detachable arraybuffer was expected", + "Main thread would deadlock", + "External buffers are not allowed", + "Cannot run JavaScript", +}; + +napi_status NAPI_CDECL napi_get_last_error_info( + node_api_basic_env basic_env, const napi_extended_error_info** result) { + napi_env env = const_cast(basic_env); + CHECK_ENV(env); + CHECK_ARG(env, result); + + // The value of the constant below must be updated to reference the last + // message in the `napi_status` enum each time a new error message is added. + // We don't have a napi_status_last as this would result in an ABI + // change each time a message was added. + const int last_status = napi_cannot_run_js; + + static_assert(NAPI_ARRAYSIZE(error_messages) == last_status + 1, + "Count of error messages must match count of error values"); + CHECK_LE(env->last_error.error_code, last_status); + // Wait until someone requests the last error information to fetch the error + // message string + env->last_error.error_message = error_messages[env->last_error.error_code]; + + if (env->last_error.error_code == napi_ok) { + napi_clear_last_error(env); + } + *result = &(env->last_error); + return napi_ok; +} + +napi_status NAPI_CDECL napi_create_function(napi_env env, + const char* utf8name, + size_t length, + napi_callback cb, + void* callback_data, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + CHECK_ARG(env, cb); + + v8::Local return_value; + v8::EscapableHandleScope scope(env->isolate); + v8::Local fn; + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewFunction( + env, cb, callback_data, &fn)); + return_value = scope.Escape(fn); + + if (utf8name != nullptr) { + v8::Local name_string; + CHECK_NEW_FROM_UTF8_LEN(env, name_string, utf8name, length); + return_value->SetName(name_string); + } + + *result = v8impl::JsValueFromV8LocalValue(return_value); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +napi_define_class(napi_env env, + const char* utf8name, + size_t length, + napi_callback constructor, + void* callback_data, + size_t property_count, + const napi_property_descriptor* properties, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + CHECK_ARG(env, constructor); + + if (property_count > 0) { + CHECK_ARG(env, properties); + } + + v8::Isolate* isolate = env->isolate; + + v8::EscapableHandleScope scope(isolate); + v8::Local tpl; + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewTemplate( + env, constructor, callback_data, &tpl)); + + v8::Local name_string; + CHECK_NEW_FROM_UTF8_LEN(env, name_string, utf8name, length); + tpl->SetClassName(name_string); + + size_t static_property_count = 0; + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor* p = properties + i; + + if ((p->attributes & napi_static) != 0) { + // Static properties are handled separately below. + static_property_count++; + continue; + } + + v8::Local property_name; + STATUS_CALL(v8impl::V8NameFromPropertyDescriptor(env, p, &property_name)); + + v8::PropertyAttribute attributes = + v8impl::V8PropertyAttributesFromDescriptor(p); + + // This code is similar to that in napi_define_properties(); the + // difference is it applies to a template instead of an object, + // and preferred PropertyAttribute for lack of PropertyDescriptor + // support on ObjectTemplate. + if (p->getter != nullptr || p->setter != nullptr) { + v8::Local getter_tpl; + v8::Local setter_tpl; + if (p->getter != nullptr) { + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewTemplate( + env, p->getter, p->data, &getter_tpl)); + } + if (p->setter != nullptr) { + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewTemplate( + env, p->setter, p->data, &setter_tpl)); + } + + tpl->PrototypeTemplate()->SetAccessorProperty( + property_name, getter_tpl, setter_tpl, attributes); + } else if (p->method != nullptr) { + v8::Local t; + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewTemplate( + env, p->method, p->data, &t, v8::Signature::New(isolate, tpl))); + + tpl->PrototypeTemplate()->Set(property_name, t, attributes); + } else { + v8::Local value = v8impl::V8LocalValueFromJsValue(p->value); + tpl->PrototypeTemplate()->Set(property_name, value, attributes); + } + } + + v8::Local context = env->context(); + *result = v8impl::JsValueFromV8LocalValue( + scope.Escape(tpl->GetFunction(context).ToLocalChecked())); + + if (static_property_count > 0) { + std::vector static_descriptors; + static_descriptors.reserve(static_property_count); + + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor* p = properties + i; + if ((p->attributes & napi_static) != 0) { + static_descriptors.push_back(*p); + } + } + + STATUS_CALL(napi_define_properties( + env, *result, static_descriptors.size(), static_descriptors.data())); + } + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_property_names(napi_env env, + napi_value object, + napi_value* result) { + return napi_get_all_property_names( + env, + object, + napi_key_include_prototypes, + static_cast(napi_key_enumerable | napi_key_skip_symbols), + napi_key_numbers_to_strings, + result); +} + +napi_status NAPI_CDECL +napi_get_all_property_names(napi_env env, + napi_value object, + napi_key_collection_mode key_mode, + napi_key_filter key_filter, + napi_key_conversion key_conversion, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local obj; + CHECK_TO_OBJECT(env, context, obj, object); + + v8::PropertyFilter filter = v8::PropertyFilter::ALL_PROPERTIES; + if (key_filter & napi_key_writable) { + filter = static_cast(filter | + v8::PropertyFilter::ONLY_WRITABLE); + } + if (key_filter & napi_key_enumerable) { + filter = static_cast( + filter | v8::PropertyFilter::ONLY_ENUMERABLE); + } + if (key_filter & napi_key_configurable) { + filter = static_cast( + filter | v8::PropertyFilter::ONLY_CONFIGURABLE); + } + if (key_filter & napi_key_skip_strings) { + filter = static_cast(filter | + v8::PropertyFilter::SKIP_STRINGS); + } + if (key_filter & napi_key_skip_symbols) { + filter = static_cast(filter | + v8::PropertyFilter::SKIP_SYMBOLS); + } + v8::KeyCollectionMode collection_mode; + v8::KeyConversionMode conversion_mode; + + switch (key_mode) { + case napi_key_include_prototypes: + collection_mode = v8::KeyCollectionMode::kIncludePrototypes; + break; + case napi_key_own_only: + collection_mode = v8::KeyCollectionMode::kOwnOnly; + break; + default: + return napi_set_last_error(env, napi_invalid_arg); + } + + switch (key_conversion) { + case napi_key_keep_numbers: + conversion_mode = v8::KeyConversionMode::kKeepNumbers; + break; + case napi_key_numbers_to_strings: + conversion_mode = v8::KeyConversionMode::kConvertToString; + break; + default: + return napi_set_last_error(env, napi_invalid_arg); + } + + v8::MaybeLocal maybe_all_propertynames = + obj->GetPropertyNames(context, + collection_mode, + filter, + v8::IndexFilter::kIncludeIndices, + conversion_mode); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE( + env, maybe_all_propertynames, napi_generic_failure); + + *result = + v8impl::JsValueFromV8LocalValue(maybe_all_propertynames.ToLocalChecked()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_set_property(napi_env env, + napi_value object, + napi_value key, + napi_value value) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, key); + CHECK_ARG(env, value); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local k = v8impl::V8LocalValueFromJsValue(key); + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + v8::Maybe set_maybe = obj->Set(context, k, val); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_maybe.FromMaybe(false), napi_generic_failure); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_has_property(napi_env env, + napi_value object, + napi_value key, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + CHECK_ARG(env, key); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local k = v8impl::V8LocalValueFromJsValue(key); + v8::Maybe has_maybe = obj->Has(context, k); + + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, has_maybe, napi_generic_failure); + + *result = has_maybe.FromMaybe(false); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_property(napi_env env, + napi_value object, + napi_value key, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, key); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local k = v8impl::V8LocalValueFromJsValue(key); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + auto get_maybe = obj->Get(context, k); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, get_maybe, napi_generic_failure); + + v8::Local val = get_maybe.ToLocalChecked(); + *result = v8impl::JsValueFromV8LocalValue(val); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_delete_property(napi_env env, + napi_value object, + napi_value key, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, key); + + v8::Local context = env->context(); + v8::Local k = v8impl::V8LocalValueFromJsValue(key); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + v8::Maybe delete_maybe = obj->Delete(context, k); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, delete_maybe, napi_generic_failure); + + if (result != nullptr) *result = delete_maybe.FromMaybe(false); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_has_own_property(napi_env env, + napi_value object, + napi_value key, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, key); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + v8::Local k = v8impl::V8LocalValueFromJsValue(key); + RETURN_STATUS_IF_FALSE(env, k->IsName(), napi_name_expected); + v8::Maybe has_maybe = obj->HasOwnProperty(context, k.As()); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, has_maybe, napi_generic_failure); + *result = has_maybe.FromMaybe(false); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_set_named_property(napi_env env, + napi_value object, + const char* utf8name, + napi_value value) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local key; + CHECK_NEW_FROM_UTF8(env, key, utf8name); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + v8::Maybe set_maybe = obj->Set(context, key, val); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_maybe.FromMaybe(false), napi_generic_failure); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_has_named_property(napi_env env, + napi_value object, + const char* utf8name, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local key; + CHECK_NEW_FROM_UTF8(env, key, utf8name); + + v8::Maybe has_maybe = obj->Has(context, key); + + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, has_maybe, napi_generic_failure); + + *result = has_maybe.FromMaybe(false); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_named_property(napi_env env, + napi_value object, + const char* utf8name, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + + v8::Local key; + CHECK_NEW_FROM_UTF8(env, key, utf8name); + + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + auto get_maybe = obj->Get(context, key); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, get_maybe, napi_generic_failure); + + v8::Local val = get_maybe.ToLocalChecked(); + *result = v8impl::JsValueFromV8LocalValue(val); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_set_element(napi_env env, + napi_value object, + uint32_t index, + napi_value value) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + auto set_maybe = obj->Set(context, index, val); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_maybe.FromMaybe(false), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_has_element(napi_env env, + napi_value object, + uint32_t index, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Maybe has_maybe = obj->Has(context, index); + + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, has_maybe, napi_generic_failure); + + *result = has_maybe.FromMaybe(false); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_element(napi_env env, + napi_value object, + uint32_t index, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + auto get_maybe = obj->Get(context, index); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, get_maybe, napi_generic_failure); + + *result = v8impl::JsValueFromV8LocalValue(get_maybe.ToLocalChecked()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_delete_element(napi_env env, + napi_value object, + uint32_t index, + bool* result) { + NAPI_PREAMBLE(env); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + v8::Maybe delete_maybe = obj->Delete(context, index); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, delete_maybe, napi_generic_failure); + + if (result != nullptr) *result = delete_maybe.FromMaybe(false); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +napi_define_properties(napi_env env, + napi_value object, + size_t property_count, + const napi_property_descriptor* properties) { + NAPI_PREAMBLE(env); + if (property_count > 0) { + CHECK_ARG(env, properties); + } + + v8::Local context = env->context(); + + v8::Local obj; + CHECK_TO_OBJECT(env, context, obj, object); + + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor* p = &properties[i]; + + v8::Local property_name; + STATUS_CALL(v8impl::V8NameFromPropertyDescriptor(env, p, &property_name)); + + if (p->getter != nullptr || p->setter != nullptr) { + v8::Local local_getter; + v8::Local local_setter; + + if (p->getter != nullptr) { + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewFunction( + env, p->getter, p->data, &local_getter)); + } + if (p->setter != nullptr) { + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewFunction( + env, p->setter, p->data, &local_setter)); + } + + v8::PropertyDescriptor descriptor(local_getter, local_setter); + descriptor.set_enumerable((p->attributes & napi_enumerable) != 0); + descriptor.set_configurable((p->attributes & napi_configurable) != 0); + + auto define_maybe = + obj->DefineProperty(context, property_name, descriptor); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, define_maybe.FromMaybe(false), napi_invalid_arg); + } else if (p->method != nullptr) { + v8::Local method; + STATUS_CALL(v8impl::FunctionCallbackWrapper::NewFunction( + env, p->method, p->data, &method)); + v8::PropertyDescriptor descriptor(method, + (p->attributes & napi_writable) != 0); + descriptor.set_enumerable((p->attributes & napi_enumerable) != 0); + descriptor.set_configurable((p->attributes & napi_configurable) != 0); + + auto define_maybe = + obj->DefineProperty(context, property_name, descriptor); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, define_maybe.FromMaybe(false), napi_generic_failure); + } else { + v8::Local value = v8impl::V8LocalValueFromJsValue(p->value); + v8::Maybe define_maybe = v8::Just(false); + + if ((p->attributes & napi_enumerable) && + (p->attributes & napi_writable) && + (p->attributes & napi_configurable)) { + // Use a fast path for this type of data property. + define_maybe = obj->CreateDataProperty(context, property_name, value); + } else { + v8::PropertyDescriptor descriptor(value, + (p->attributes & napi_writable) != 0); + descriptor.set_enumerable((p->attributes & napi_enumerable) != 0); + descriptor.set_configurable((p->attributes & napi_configurable) != 0); + + define_maybe = obj->DefineProperty(context, property_name, descriptor); + } + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, define_maybe.FromMaybe(false), napi_invalid_arg); + } + } + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_object_freeze(napi_env env, napi_value object) { + NAPI_PREAMBLE(env); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Maybe set_frozen = + obj->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_frozen.FromMaybe(false), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_object_seal(napi_env env, napi_value object) { + NAPI_PREAMBLE(env); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Maybe set_sealed = + obj->SetIntegrityLevel(context, v8::IntegrityLevel::kSealed); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_sealed.FromMaybe(false), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_is_array(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + *result = val->IsArray(); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_array_length(napi_env env, + napi_value value, + uint32_t* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsArray(), napi_array_expected); + + v8::Local arr = val.As(); + *result = arr->Length(); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_strict_equals(napi_env env, + napi_value lhs, + napi_value rhs, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, lhs); + CHECK_ARG(env, rhs); + CHECK_ARG(env, result); + + v8::Local a = v8impl::V8LocalValueFromJsValue(lhs); + v8::Local b = v8impl::V8LocalValueFromJsValue(rhs); + + *result = a->StrictEquals(b); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL node_api_set_prototype(napi_env env, + napi_value object, + napi_value value) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + + v8::Local context = env->context(); + v8::Local obj; + + CHECK_TO_OBJECT(env, context, obj, object); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + v8::Maybe set_maybe = obj->SetPrototypeV2(context, val); + + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, set_maybe.FromMaybe(false), napi_generic_failure); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_prototype(napi_env env, + napi_value object, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + + v8::Local obj; + CHECK_TO_OBJECT(env, context, obj, object); + + // This doesn't invokes Proxy's [[GetPrototypeOf]] handler. + v8::Local val = obj->GetPrototypeV2(); + *result = v8impl::JsValueFromV8LocalValue(val); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_create_object(napi_env env, napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue(v8::Object::New(env->isolate)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL +node_api_create_object_with_properties(napi_env env, + napi_value prototype_or_null, + napi_value* property_names, + napi_value* property_values, + size_t property_count, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + if (property_count > 0) { + CHECK_ARG(env, property_names); + CHECK_ARG(env, property_values); + } + + v8::Local v8_prototype_or_null; + if (prototype_or_null == nullptr) { + v8_prototype_or_null = v8::Null(env->isolate); + } else { + v8_prototype_or_null = v8impl::V8LocalValueFromJsValue(prototype_or_null); + } + + v8::LocalVector v8_names(env->isolate, property_count); + v8::LocalVector v8_values(env->isolate, property_count); + + for (size_t i = 0; i < property_count; i++) { + v8::Local name_value = + v8impl::V8LocalValueFromJsValue(property_names[i]); + RETURN_STATUS_IF_FALSE(env, name_value->IsName(), napi_name_expected); + v8_names[i] = name_value.As(); + v8_values[i] = v8impl::V8LocalValueFromJsValue(property_values[i]); + } + + v8::Local obj = v8::Object::New(env->isolate, + v8_prototype_or_null, + v8_names.data(), + v8_values.data(), + property_count); + + RETURN_STATUS_IF_FALSE(env, !obj.IsEmpty(), napi_generic_failure); + *result = v8impl::JsValueFromV8LocalValue(obj); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_array(napi_env env, napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue(v8::Array::New(env->isolate)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_array_with_length(napi_env env, + size_t length, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = + v8impl::JsValueFromV8LocalValue(v8::Array::New(env->isolate, length)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_string_latin1(napi_env env, + const char* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromOneByte(isolate, + reinterpret_cast(str), + v8::NewStringType::kNormal, + length); + }); +} + +napi_status NAPI_CDECL napi_create_string_utf8(napi_env env, + const char* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromUtf8( + isolate, str, v8::NewStringType::kNormal, static_cast(length)); + }); +} + +napi_status NAPI_CDECL napi_create_string_utf16(napi_env env, + const char16_t* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromTwoByte(isolate, + reinterpret_cast(str), + v8::NewStringType::kNormal, + length); + }); +} + +napi_status NAPI_CDECL node_api_create_external_string_latin1( + napi_env env, + char* str, + size_t length, + node_api_basic_finalize basic_finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied) { + napi_finalize finalize_callback = + reinterpret_cast(basic_finalize_callback); + return v8impl::NewExternalString( + env, + str, + length, + finalize_callback, + finalize_hint, + result, + copied, + napi_create_string_latin1, + [&](v8::Isolate* isolate) { + if (length == NAPI_AUTO_LENGTH) { + length = (std::string_view(str)).length(); + } + auto resource = new v8impl::ExternalOneByteStringResource( + env, str, length, finalize_callback, finalize_hint); + return v8::String::NewExternalOneByte(isolate, resource); + }); +} + +napi_status NAPI_CDECL node_api_create_external_string_utf16( + napi_env env, + char16_t* str, + size_t length, + node_api_basic_finalize basic_finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied) { + napi_finalize finalize_callback = + reinterpret_cast(basic_finalize_callback); + return v8impl::NewExternalString( + env, + str, + length, + finalize_callback, + finalize_hint, + result, + copied, + napi_create_string_utf16, + [&](v8::Isolate* isolate) { + if (length == NAPI_AUTO_LENGTH) { + length = (std::u16string_view(str)).length(); + } + auto resource = new v8impl::ExternalStringResource( + env, str, length, finalize_callback, finalize_hint); + return v8::String::NewExternalTwoByte(isolate, resource); + }); +} + +napi_status node_api_create_property_key_latin1(napi_env env, + const char* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromOneByte(isolate, + reinterpret_cast(str), + v8::NewStringType::kInternalized, + length); + }); +} + +napi_status node_api_create_property_key_utf8(napi_env env, + const char* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromUtf8(isolate, + str, + v8::NewStringType::kInternalized, + static_cast(length)); + }); +} + +napi_status NAPI_CDECL node_api_create_property_key_utf16(napi_env env, + const char16_t* str, + size_t length, + napi_value* result) { + return v8impl::NewString(env, str, length, result, [&](v8::Isolate* isolate) { + return v8::String::NewFromTwoByte(isolate, + reinterpret_cast(str), + v8::NewStringType::kInternalized, + static_cast(length)); + }); +} + +napi_status NAPI_CDECL napi_create_double(napi_env env, + double value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = + v8impl::JsValueFromV8LocalValue(v8::Number::New(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_int32(napi_env env, + int32_t value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = + v8impl::JsValueFromV8LocalValue(v8::Integer::New(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_uint32(napi_env env, + uint32_t value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Integer::NewFromUnsigned(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_int64(napi_env env, + int64_t value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Number::New(env->isolate, static_cast(value))); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_bigint_int64(napi_env env, + int64_t value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = + v8impl::JsValueFromV8LocalValue(v8::BigInt::New(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_bigint_uint64(napi_env env, + uint64_t value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::BigInt::NewFromUnsigned(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_bigint_words(napi_env env, + int sign_bit, + size_t word_count, + const uint64_t* words, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, words); + CHECK_ARG(env, result); + + v8::Local context = env->context(); + + RETURN_STATUS_IF_FALSE(env, word_count <= INT_MAX, napi_invalid_arg); + + v8::MaybeLocal b = + v8::BigInt::NewFromWords(context, sign_bit, word_count, words); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, b, napi_generic_failure); + + *result = v8impl::JsValueFromV8LocalValue(b.ToLocalChecked()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_boolean(napi_env env, + bool value, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + + if (value) { + *result = v8impl::JsValueFromV8LocalValue(v8::True(isolate)); + } else { + *result = v8impl::JsValueFromV8LocalValue(v8::False(isolate)); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_symbol(napi_env env, + napi_value description, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + + if (description == nullptr) { + *result = v8impl::JsValueFromV8LocalValue(v8::Symbol::New(isolate)); + } else { + v8::Local desc = v8impl::V8LocalValueFromJsValue(description); + RETURN_STATUS_IF_FALSE(env, desc->IsString(), napi_string_expected); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Symbol::New(isolate, desc.As())); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL node_api_symbol_for(napi_env env, + const char* utf8description, + size_t length, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + napi_value js_description_string; + STATUS_CALL(napi_create_string_utf8( + env, utf8description, length, &js_description_string)); + v8::Local description_string = + v8impl::V8LocalValueFromJsValue(js_description_string).As(); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Symbol::For(env->isolate, description_string)); + + return napi_clear_last_error(env); +} + +static inline napi_status set_error_code(napi_env env, + v8::Local error, + napi_value code, + const char* code_cstring) { + if ((code != nullptr) || (code_cstring != nullptr)) { + v8::Local context = env->context(); + v8::Local err_object = error.As(); + + v8::Local code_value = v8impl::V8LocalValueFromJsValue(code); + if (code != nullptr) { + code_value = v8impl::V8LocalValueFromJsValue(code); + RETURN_STATUS_IF_FALSE(env, code_value->IsString(), napi_string_expected); + } else { + CHECK_NEW_FROM_UTF8(env, code_value, code_cstring); + } + + v8::Local code_key; + CHECK_NEW_FROM_UTF8(env, code_key, "code"); + + v8::Maybe set_maybe = err_object->Set(context, code_key, code_value); + RETURN_STATUS_IF_FALSE( + env, set_maybe.FromMaybe(false), napi_generic_failure); + } + return napi_ok; +} + +napi_status NAPI_CDECL napi_create_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, msg); + CHECK_ARG(env, result); + + v8::Local message_value = v8impl::V8LocalValueFromJsValue(msg); + RETURN_STATUS_IF_FALSE(env, message_value->IsString(), napi_string_expected); + + v8::Local error_obj = + v8::Exception::Error(message_value.As()); + STATUS_CALL(set_error_code(env, error_obj, code, nullptr)); + + *result = v8impl::JsValueFromV8LocalValue(error_obj); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_type_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, msg); + CHECK_ARG(env, result); + + v8::Local message_value = v8impl::V8LocalValueFromJsValue(msg); + RETURN_STATUS_IF_FALSE(env, message_value->IsString(), napi_string_expected); + + v8::Local error_obj = + v8::Exception::TypeError(message_value.As()); + STATUS_CALL(set_error_code(env, error_obj, code, nullptr)); + + *result = v8impl::JsValueFromV8LocalValue(error_obj); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_range_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, msg); + CHECK_ARG(env, result); + + v8::Local message_value = v8impl::V8LocalValueFromJsValue(msg); + RETURN_STATUS_IF_FALSE(env, message_value->IsString(), napi_string_expected); + + v8::Local error_obj = + v8::Exception::RangeError(message_value.As()); + STATUS_CALL(set_error_code(env, error_obj, code, nullptr)); + + *result = v8impl::JsValueFromV8LocalValue(error_obj); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL node_api_create_syntax_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, msg); + CHECK_ARG(env, result); + + v8::Local message_value = v8impl::V8LocalValueFromJsValue(msg); + RETURN_STATUS_IF_FALSE(env, message_value->IsString(), napi_string_expected); + + v8::Local error_obj = + v8::Exception::SyntaxError(message_value.As()); + STATUS_CALL(set_error_code(env, error_obj, code, nullptr)); + + *result = v8impl::JsValueFromV8LocalValue(error_obj); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_typeof(napi_env env, + napi_value value, + napi_valuetype* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local v = v8impl::V8LocalValueFromJsValue(value); + + if (v->IsNumber()) { + *result = napi_number; + } else if (v->IsBigInt()) { + *result = napi_bigint; + } else if (v->IsString()) { + *result = napi_string; + } else if (v->IsFunction()) { + // This test has to come before IsObject because IsFunction + // implies IsObject + *result = napi_function; + } else if (v->IsExternal()) { + // This test has to come before IsObject because IsExternal + // implies IsObject + *result = napi_external; + } else if (v->IsObject()) { + *result = napi_object; + } else if (v->IsBoolean()) { + *result = napi_boolean; + } else if (v->IsUndefined()) { + *result = napi_undefined; + } else if (v->IsSymbol()) { + *result = napi_symbol; + } else if (v->IsNull()) { + *result = napi_null; + } else { + // Should not get here unless V8 has added some new kind of value. + return napi_set_last_error(env, napi_invalid_arg); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_undefined(napi_env env, napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue(v8::Undefined(env->isolate)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_null(napi_env env, napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue(v8::Null(env->isolate)); + + return napi_clear_last_error(env); +} + +// Gets all callback info in a single call. (Ugly, but faster.) +napi_status NAPI_CDECL napi_get_cb_info( + napi_env env, // [in] Node-API environment handle + napi_callback_info cbinfo, // [in] Opaque callback-info handle + size_t* argc, // [in-out] Specifies the size of the provided argv array + // and receives the actual count of args. + napi_value* argv, // [out] Array of values + napi_value* this_arg, // [out] Receives the JS 'this' arg for the call + void** data) { // [out] Receives the data pointer for the callback. + CHECK_ENV(env); + CHECK_ARG(env, cbinfo); + + v8impl::FunctionCallbackWrapper* info = + reinterpret_cast(cbinfo); + + if (argv != nullptr) { + CHECK_ARG(env, argc); + info->Args(argv, *argc); + } + if (argc != nullptr) { + *argc = info->ArgsLength(); + } + if (this_arg != nullptr) { + *this_arg = info->This(); + } + if (data != nullptr) { + *data = info->Data(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_new_target(napi_env env, + napi_callback_info cbinfo, + napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, cbinfo); + CHECK_ARG(env, result); + + v8impl::FunctionCallbackWrapper* info = + reinterpret_cast(cbinfo); + + *result = info->GetNewTarget(); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_call_function(napi_env env, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, recv); + if (argc > 0) { + CHECK_ARG(env, argv); + } + + v8::Local context = env->context(); + + v8::Local v8recv = v8impl::V8LocalValueFromJsValue(recv); + + v8::Local v8func; + CHECK_TO_FUNCTION(env, v8func, func); + + auto maybe = v8func->Call( + context, + v8recv, + argc, + reinterpret_cast*>(const_cast(argv))); + + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe, napi_generic_failure); + if (result != nullptr) { + *result = v8impl::JsValueFromV8LocalValue(maybe.ToLocalChecked()); + } + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_global(napi_env env, napi_value* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue(env->context()->Global()); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_throw(napi_env env, napi_value error) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, error); + + v8::Isolate* isolate = env->isolate; + + isolate->ThrowException(v8impl::V8LocalValueFromJsValue(error)); + // any VM calls after this point and before returning + // to the javascript invoker will fail + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_throw_error(napi_env env, + const char* code, + const char* msg) { + NAPI_PREAMBLE(env); + + v8::Isolate* isolate = env->isolate; + v8::Local str; + CHECK_NEW_FROM_UTF8(env, str, msg); + + v8::Local error_obj = v8::Exception::Error(str); + STATUS_CALL(set_error_code(env, error_obj, nullptr, code)); + + isolate->ThrowException(error_obj); + // any VM calls after this point and before returning + // to the javascript invoker will fail + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_throw_type_error(napi_env env, + const char* code, + const char* msg) { + NAPI_PREAMBLE(env); + + v8::Isolate* isolate = env->isolate; + v8::Local str; + CHECK_NEW_FROM_UTF8(env, str, msg); + + v8::Local error_obj = v8::Exception::TypeError(str); + STATUS_CALL(set_error_code(env, error_obj, nullptr, code)); + + isolate->ThrowException(error_obj); + // any VM calls after this point and before returning + // to the javascript invoker will fail + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_throw_range_error(napi_env env, + const char* code, + const char* msg) { + NAPI_PREAMBLE(env); + + v8::Isolate* isolate = env->isolate; + v8::Local str; + CHECK_NEW_FROM_UTF8(env, str, msg); + + v8::Local error_obj = v8::Exception::RangeError(str); + STATUS_CALL(set_error_code(env, error_obj, nullptr, code)); + + isolate->ThrowException(error_obj); + // any VM calls after this point and before returning + // to the javascript invoker will fail + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL node_api_throw_syntax_error(napi_env env, + const char* code, + const char* msg) { + NAPI_PREAMBLE(env); + + v8::Isolate* isolate = env->isolate; + v8::Local str; + CHECK_NEW_FROM_UTF8(env, str, msg); + + v8::Local error_obj = v8::Exception::SyntaxError(str); + STATUS_CALL(set_error_code(env, error_obj, nullptr, code)); + + isolate->ThrowException(error_obj); + // any VM calls after this point and before returning + // to the javascript invoker will fail + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_is_error(napi_env env, + napi_value value, + bool* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot + // throw JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsNativeError(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_double(napi_env env, + napi_value value, + double* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + + *result = val.As()->Value(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_int32(napi_env env, + napi_value value, + int32_t* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + if (val->IsInt32()) { + *result = val.As()->Value(); + } else { + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; + *result = val->Int32Value(context).FromJust(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_uint32(napi_env env, + napi_value value, + uint32_t* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + if (val->IsUint32()) { + *result = val.As()->Value(); + } else { + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; + *result = val->Uint32Value(context).FromJust(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_int64(napi_env env, + napi_value value, + int64_t* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + // This is still a fast path very likely to be taken. + if (val->IsInt32()) { + *result = val.As()->Value(); + return napi_clear_last_error(env); + } + + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + + // v8::Value::IntegerValue() converts NaN, +Inf, and -Inf to INT64_MIN, + // inconsistent with v8::Value::Int32Value() which converts those values to 0. + // Special-case all non-finite values to match that behavior. + double doubleValue = val.As()->Value(); + if (std::isfinite(doubleValue)) { + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; + *result = val->IntegerValue(context).FromJust(); + } else { + *result = 0; + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_bigint_int64(napi_env env, + napi_value value, + int64_t* result, + bool* lossless) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + CHECK_ARG(env, lossless); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + RETURN_STATUS_IF_FALSE(env, val->IsBigInt(), napi_bigint_expected); + + *result = val.As()->Int64Value(lossless); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_bigint_uint64(napi_env env, + napi_value value, + uint64_t* result, + bool* lossless) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + CHECK_ARG(env, lossless); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + RETURN_STATUS_IF_FALSE(env, val->IsBigInt(), napi_bigint_expected); + + *result = val.As()->Uint64Value(lossless); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_bigint_words(napi_env env, + napi_value value, + int* sign_bit, + size_t* word_count, + uint64_t* words) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, word_count); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + + RETURN_STATUS_IF_FALSE(env, val->IsBigInt(), napi_bigint_expected); + + v8::Local big = val.As(); + + int word_count_int = *word_count; + + if (sign_bit == nullptr && words == nullptr) { + word_count_int = big->WordCount(); + } else { + CHECK_ARG(env, sign_bit); + CHECK_ARG(env, words); + big->ToWordsArray(sign_bit, &word_count_int, words); + } + + *word_count = word_count_int; + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_value_bool(napi_env env, + napi_value value, + bool* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsBoolean(), napi_boolean_expected); + + *result = val.As()->Value(); + + return napi_clear_last_error(env); +} + +// Copies a JavaScript string into a LATIN-1 string buffer. The result is the +// number of bytes (excluding the null terminator) copied into buf. +// A sufficient buffer size should be greater than the length of string, +// reserving space for null terminator. +// If bufsize is insufficient, the string will be truncated and null terminated. +// If buf is NULL, this method returns the length of the string (in bytes) +// via the result parameter. +// The result argument is optional unless buf is NULL. +napi_status NAPI_CDECL napi_get_value_string_latin1( + napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsString(), napi_string_expected); + v8::Local str = val.As(); + + if (!buf) { + CHECK_ARG(env, result); + *result = str->Length(); + } else if (bufsize != 0) { + uint32_t length = static_cast( + std::min(bufsize - 1, static_cast(str->Length()))); + str->WriteOneByteV2(env->isolate, + 0, + length, + reinterpret_cast(buf), + v8::String::WriteFlags::kNullTerminate); + if (result != nullptr) { + *result = length; + } + } else if (result != nullptr) { + *result = 0; + } + + return napi_clear_last_error(env); +} + +// Copies a JavaScript string into a UTF-8 string buffer. The result is the +// number of bytes (excluding the null terminator) copied into buf. +// A sufficient buffer size should be greater than the length of string, +// reserving space for null terminator. +// If bufsize is insufficient, the string will be truncated and null terminated. +// If buf is NULL, this method returns the length of the string (in bytes) +// via the result parameter. +// The result argument is optional unless buf is NULL. +napi_status NAPI_CDECL napi_get_value_string_utf8( + napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsString(), napi_string_expected); + v8::Local str = val.As(); + + if (!buf) { + CHECK_ARG(env, result); + *result = str->Utf8LengthV2(env->isolate); + } else if (bufsize != 0) { + size_t copied = + str->WriteUtf8V2(env->isolate, + buf, + bufsize - 1, + v8::String::WriteFlags::kReplaceInvalidUtf8); + + buf[copied] = '\0'; + if (result != nullptr) { + *result = copied; + } + } else if (result != nullptr) { + *result = 0; + } + + return napi_clear_last_error(env); +} + +// Copies a JavaScript string into a UTF-16 string buffer. The result is the +// number of 2-byte code units (excluding the null terminator) copied into buf. +// A sufficient buffer size should be greater than the length of string, +// reserving space for null terminator. +// If bufsize is insufficient, the string will be truncated and null terminated. +// If buf is NULL, this method returns the length of the string (in 2-byte +// code units) via the result parameter. +// The result argument is optional unless buf is NULL. +napi_status NAPI_CDECL napi_get_value_string_utf16(napi_env env, + napi_value value, + char16_t* buf, + size_t bufsize, + size_t* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsString(), napi_string_expected); + v8::Local str = val.As(); + + if (!buf) { + CHECK_ARG(env, result); + // V8 assumes UTF-16 length is the same as the number of characters. + *result = str->Length(); + } else if (bufsize != 0) { + uint32_t length = static_cast( + std::min(bufsize - 1, static_cast(str->Length()))); + str->WriteV2(env->isolate, + 0, + length, + reinterpret_cast(buf), + v8::String::WriteFlags::kNullTerminate); + + if (result != nullptr) { + *result = length; + } + } else if (result != nullptr) { + *result = 0; + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_coerce_to_bool(napi_env env, + napi_value value, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + v8::Local b = + v8impl::V8LocalValueFromJsValue(value)->ToBoolean(isolate); + *result = v8impl::JsValueFromV8LocalValue(b); + return GET_RETURN_STATUS(env); +} + +#define GEN_COERCE_FUNCTION(UpperCaseName, MixedCaseName, LowerCaseName) \ + napi_status NAPI_CDECL napi_coerce_to_##LowerCaseName( \ + napi_env env, napi_value value, napi_value* result) { \ + NAPI_PREAMBLE(env); \ + CHECK_ARG(env, value); \ + CHECK_ARG(env, result); \ + \ + v8::Local context = env->context(); \ + v8::Local str; \ + \ + CHECK_TO_##UpperCaseName(env, context, str, value); \ + \ + *result = v8impl::JsValueFromV8LocalValue(str); \ + return GET_RETURN_STATUS(env); \ + } + +GEN_COERCE_FUNCTION(NUMBER, Number, number) +GEN_COERCE_FUNCTION(OBJECT, Object, object) +GEN_COERCE_FUNCTION(STRING, String, string) + +#undef GEN_COERCE_FUNCTION + +napi_status NAPI_CDECL napi_wrap(napi_env env, + napi_value js_object, + void* native_object, + node_api_basic_finalize basic_finalize_cb, + void* finalize_hint, + napi_ref* result) { + napi_finalize finalize_cb = + reinterpret_cast(basic_finalize_cb); + return v8impl::Wrap( + env, js_object, native_object, finalize_cb, finalize_hint, result); +} + +napi_status NAPI_CDECL napi_unwrap(napi_env env, + napi_value obj, + void** result) { + return v8impl::Unwrap(env, obj, result, v8impl::KeepWrap); +} + +napi_status NAPI_CDECL napi_remove_wrap(napi_env env, + napi_value obj, + void** result) { + return v8impl::Unwrap(env, obj, result, v8impl::RemoveWrap); +} + +napi_status NAPI_CDECL +napi_create_external(napi_env env, + void* data, + node_api_basic_finalize basic_finalize_cb, + void* finalize_hint, + napi_value* result) { + napi_finalize finalize_cb = + reinterpret_cast(basic_finalize_cb); + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local external_value = + v8impl::ExternalWrapper::New(env, data); + + if (finalize_cb) { + // The Reference object will delete itself after invoking the finalizer + // callback. + v8impl::ReferenceWithFinalizer::New(env, + external_value, + 0, + v8impl::ReferenceOwnership::kRuntime, + finalize_cb, + data, + finalize_hint); + } + + *result = v8impl::JsValueFromV8LocalValue(external_value); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_type_tag_object(napi_env env, + napi_value object_or_external, + const napi_type_tag* type_tag) { + NAPI_PREAMBLE(env); + v8::Local context = env->context(); + + CHECK_ARG(env, object_or_external); + v8::Local val = + v8impl::V8LocalValueFromJsValue(object_or_external); + if (val->IsExternal()) { + v8impl::ExternalWrapper* wrapper = + v8impl::ExternalWrapper::From(val.As()); + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, wrapper->TypeTag(type_tag), napi_invalid_arg); + return GET_RETURN_STATUS(env); + } + + v8::Local obj; + CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object_or_external); + CHECK_ARG_WITH_PREAMBLE(env, type_tag); + + auto key = NAPI_PRIVATE_KEY(context, type_tag); + auto maybe_has = obj->HasPrivate(context, key); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_has, napi_generic_failure); + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, !maybe_has.FromJust(), napi_invalid_arg); + + auto tag = v8::BigInt::NewFromWords( + context, 0, 2, reinterpret_cast(type_tag)); + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, tag, napi_generic_failure); + + auto maybe_set = obj->SetPrivate(context, key, tag.ToLocalChecked()); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_set, napi_generic_failure); + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( + env, maybe_set.FromJust(), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_check_object_type_tag(napi_env env, + napi_value object_or_external, + const napi_type_tag* type_tag, + bool* result) { + NAPI_PREAMBLE(env); + v8::Local context = env->context(); + + CHECK_ARG(env, object_or_external); + v8::Local obj_val = + v8impl::V8LocalValueFromJsValue(object_or_external); + if (obj_val->IsExternal()) { + v8impl::ExternalWrapper* wrapper = + v8impl::ExternalWrapper::From(obj_val.As()); + *result = wrapper->CheckTypeTag(type_tag); + return GET_RETURN_STATUS(env); + } + + v8::Local obj; + CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object_or_external); + CHECK_ARG_WITH_PREAMBLE(env, type_tag); + CHECK_ARG_WITH_PREAMBLE(env, result); + + auto maybe_value = + obj->GetPrivate(context, NAPI_PRIVATE_KEY(context, type_tag)); + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe_value, napi_generic_failure); + v8::Local val = maybe_value.ToLocalChecked(); + + // We consider the type check to have failed unless we reach the line below + // where we set whether the type check succeeded or not based on the + // comparison of the two type tags. + *result = false; + if (val->IsBigInt()) { + int sign; + int size = 2; + napi_type_tag tag; + val.As()->ToWordsArray( + &sign, &size, reinterpret_cast(&tag)); + if (sign == 0) { + if (size == 2) { + *result = + (tag.lower == type_tag->lower && tag.upper == type_tag->upper); + } else if (size == 1) { + *result = (tag.lower == type_tag->lower && 0 == type_tag->upper); + } else if (size == 0) { + *result = (0 == type_tag->lower && 0 == type_tag->upper); + } + } + } + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_get_value_external(napi_env env, + napi_value value, + void** result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsExternal(), napi_invalid_arg); + + v8::Local external_value = val.As(); + *result = v8impl::ExternalWrapper::From(external_value)->Data(); + + return napi_clear_last_error(env); +} + +// Set initial_refcount to 0 for a weak reference, >0 for a strong reference. +napi_status NAPI_CDECL napi_create_reference(napi_env env, + napi_value value, + uint32_t initial_refcount, + napi_ref* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local v8_value = v8impl::V8LocalValueFromJsValue(value); + if (env->module_api_version < 10) { + if (!(v8_value->IsObject() || v8_value->IsFunction() || + v8_value->IsSymbol())) { + return napi_set_last_error(env, napi_invalid_arg); + } + } + + v8impl::Reference* reference = v8impl::Reference::New( + env, v8_value, initial_refcount, v8impl::ReferenceOwnership::kUserland); + + *result = reinterpret_cast(reference); + return napi_clear_last_error(env); +} + +// Deletes a reference. The referenced value is released, and may be GC'd unless +// there are other references to it. +// For a napi_reference returned from `napi_wrap`, this must be called in the +// finalizer. +napi_status NAPI_CDECL napi_delete_reference(node_api_basic_env env, + napi_ref ref) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV(env); + CHECK_ARG(env, ref); + + delete reinterpret_cast(ref); + + return napi_clear_last_error(env); +} + +// Increments the reference count, optionally returning the resulting count. +// After this call the reference will be a strong reference because its +// refcount is >0, and the referenced object is effectively "pinned". +// Calling this when the refcount is 0 and the object is unavailable +// results in an error. +napi_status NAPI_CDECL napi_reference_ref(napi_env env, + napi_ref ref, + uint32_t* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, ref); + + v8impl::Reference* reference = reinterpret_cast(ref); + uint32_t count = reference->Ref(); + + if (result != nullptr) { + *result = count; + } + + return napi_clear_last_error(env); +} + +// Decrements the reference count, optionally returning the resulting count. If +// the result is 0 the reference is now weak and the object may be GC'd at any +// time if there are no other references. Calling this when the refcount is +// already 0 results in an error. +napi_status NAPI_CDECL napi_reference_unref(napi_env env, + napi_ref ref, + uint32_t* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, ref); + + v8impl::Reference* reference = reinterpret_cast(ref); + + if (reference->refcount() == 0) { + return napi_set_last_error(env, napi_generic_failure); + } + + uint32_t count = reference->Unref(); + + if (result != nullptr) { + *result = count; + } + + return napi_clear_last_error(env); +} + +// Attempts to get a referenced value. If the reference is weak, the value might +// no longer be available, in that case the call is still successful but the +// result is NULL. +napi_status NAPI_CDECL napi_get_reference_value(napi_env env, + napi_ref ref, + napi_value* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, ref); + CHECK_ARG(env, result); + + v8impl::Reference* reference = reinterpret_cast(ref); + *result = v8impl::JsValueFromV8LocalValue(reference->Get(env)); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_open_handle_scope(napi_env env, + napi_handle_scope* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsHandleScopeFromV8HandleScope( + new v8impl::HandleScopeWrapper(env->isolate)); + env->open_handle_scopes++; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_close_handle_scope(napi_env env, + napi_handle_scope scope) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, scope); + if (env->open_handle_scopes == 0) { + return napi_handle_scope_mismatch; + } + + env->open_handle_scopes--; + delete v8impl::V8HandleScopeFromJsHandleScope(scope); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_open_escapable_handle_scope( + napi_env env, napi_escapable_handle_scope* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = v8impl::JsEscapableHandleScopeFromV8EscapableHandleScope( + new v8impl::EscapableHandleScopeWrapper(env->isolate)); + env->open_handle_scopes++; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_close_escapable_handle_scope( + napi_env env, napi_escapable_handle_scope scope) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, scope); + if (env->open_handle_scopes == 0) { + return napi_handle_scope_mismatch; + } + + delete v8impl::V8EscapableHandleScopeFromJsEscapableHandleScope(scope); + env->open_handle_scopes--; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_escape_handle(napi_env env, + napi_escapable_handle_scope scope, + napi_value escapee, + napi_value* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, scope); + CHECK_ARG(env, escapee); + CHECK_ARG(env, result); + + v8impl::EscapableHandleScopeWrapper* s = + v8impl::V8EscapableHandleScopeFromJsEscapableHandleScope(scope); + if (!s->escape_called()) { + *result = v8impl::JsValueFromV8LocalValue( + s->Escape(v8impl::V8LocalValueFromJsValue(escapee))); + return napi_clear_last_error(env); + } + return napi_set_last_error(env, napi_escape_called_twice); +} + +napi_status NAPI_CDECL napi_new_instance(napi_env env, + napi_value constructor, + size_t argc, + const napi_value* argv, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, constructor); + if (argc > 0) { + CHECK_ARG(env, argv); + } + CHECK_ARG(env, result); + + v8::Local context = env->context(); + + v8::Local ctor; + CHECK_TO_FUNCTION(env, ctor, constructor); + + auto maybe = ctor->NewInstance( + context, + argc, + reinterpret_cast*>(const_cast(argv))); + + CHECK_MAYBE_EMPTY(env, maybe, napi_pending_exception); + + *result = v8impl::JsValueFromV8LocalValue(maybe.ToLocalChecked()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_instanceof(napi_env env, + napi_value object, + napi_value constructor, + bool* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, object); + CHECK_ARG(env, result); + + *result = false; + + v8::Local ctor; + v8::Local context = env->context(); + + CHECK_TO_OBJECT(env, context, ctor, constructor); + + if (!ctor->IsFunction()) { + napi_throw_type_error( + env, "ERR_NAPI_CONS_FUNCTION", "Constructor must be a function"); + + return napi_set_last_error(env, napi_function_expected); + } + + napi_status status = napi_generic_failure; + + v8::Local val = v8impl::V8LocalValueFromJsValue(object); + auto maybe_result = val->InstanceOf(context, ctor); + CHECK_MAYBE_NOTHING(env, maybe_result, status); + *result = maybe_result.FromJust(); + return GET_RETURN_STATUS(env); +} + +// Methods to support catching exceptions +napi_status NAPI_CDECL napi_is_exception_pending(napi_env env, bool* result) { + // NAPI_PREAMBLE is not used here: this function must execute when there is a + // pending exception. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + *result = !env->last_exception.IsEmpty(); + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_and_clear_last_exception(napi_env env, + napi_value* result) { + // NAPI_PREAMBLE is not used here: this function must execute when there is a + // pending exception. + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, result); + + if (env->last_exception.IsEmpty()) { + return napi_get_undefined(env, result); + } else { + *result = v8impl::JsValueFromV8LocalValue( + v8::Local::New(env->isolate, env->last_exception)); + env->last_exception.Reset(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_is_arraybuffer(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsArrayBuffer(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_arraybuffer(napi_env env, + size_t byte_length, + void** data, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + v8::Local buffer = + v8::ArrayBuffer::New(isolate, byte_length); + + // Optionally return a pointer to the buffer's data, to avoid another call to + // retrieve it. + if (data != nullptr) { + *data = buffer->Data(); + } + + *result = v8impl::JsValueFromV8LocalValue(buffer); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +napi_create_external_arraybuffer(napi_env env, + void* external_data, + size_t byte_length, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_value* result) { + // The API contract here is that the cleanup function runs on the JS thread, + // and is able to use napi_env. Implementing that properly is hard, so use the + // `Buffer` variant for easier implementation. + napi_value buffer; + STATUS_CALL(napi_create_external_buffer( + env, byte_length, external_data, finalize_cb, finalize_hint, &buffer)); + return napi_get_typedarray_info( + env, buffer, nullptr, nullptr, nullptr, result, nullptr); +} + +napi_status NAPI_CDECL +node_api_create_external_sharedarraybuffer(napi_env env, + void* external_data, + size_t byte_length, + node_api_noenv_finalize finalize_cb, + void* finalize_hint, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); +#ifdef V8_ENABLE_SANDBOX + return napi_set_last_error(env, napi_no_external_buffers_allowed); +#else + struct FinalizerData { + void (*cb)(void* external_data, void* finalize_hint); + void* hint; + }; + auto deleter = [](void* external_data, size_t length, void* deleter_data) { + if (auto fd = static_cast(deleter_data)) { + fd->cb(external_data, fd->hint); + delete fd; + } + }; + FinalizerData* deleter_data = nullptr; + if (finalize_cb != nullptr) { + deleter_data = new FinalizerData{finalize_cb, finalize_hint}; + } + auto unique_backing_store = v8::SharedArrayBuffer::NewBackingStore( + external_data, + byte_length, + deleter, + reinterpret_cast(deleter_data)); + CHECK(!!unique_backing_store); // Cannot fail. + auto shared_backing_store = + std::shared_ptr(std::move(unique_backing_store)); + auto shared_array_buffer = + v8::SharedArrayBuffer::New(env->isolate, std::move(shared_backing_store)); + CHECK_MAYBE_EMPTY(env, shared_array_buffer, napi_generic_failure); + *result = v8impl::JsValueFromV8LocalValue(shared_array_buffer); + return napi_clear_last_error(env); +#endif // V8_ENABLE_SANDBOX +} + +napi_status NAPI_CDECL napi_get_arraybuffer_info(napi_env env, + napi_value arraybuffer, + void** data, + size_t* byte_length) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, arraybuffer); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + + if (value->IsArrayBuffer()) { + v8::Local ab = value.As(); + + if (data != nullptr) { + *data = ab->Data(); + } + + if (byte_length != nullptr) { + *byte_length = ab->ByteLength(); + } + } else if (value->IsSharedArrayBuffer()) { + v8::Local sab = value.As(); + + if (data != nullptr) { + *data = sab->Data(); + } + + if (byte_length != nullptr) { + *byte_length = sab->ByteLength(); + } + } else { + return napi_set_last_error(env, napi_invalid_arg); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL node_api_is_sharedarraybuffer(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsSharedArrayBuffer(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL node_api_create_sharedarraybuffer(napi_env env, + size_t byte_length, + void** data, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + v8::Local buffer = + v8::SharedArrayBuffer::New(isolate, byte_length); + + // Optionally return a pointer to the buffer's data, to avoid another call to + // retrieve it. + if (data != nullptr) { + *data = buffer->Data(); + } + + *result = v8impl::JsValueFromV8LocalValue(buffer); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_is_typedarray(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsTypedArray(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_typedarray(napi_env env, + napi_typedarray_type type, + size_t length, + napi_value arraybuffer, + size_t byte_offset, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, arraybuffer); + CHECK_ARG(env, result); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + auto create_typedarray = [&](auto buffer) -> napi_status { + v8::Local typedArray; + + switch (type) { + case napi_int8_array: + CREATE_TYPED_ARRAY( + env, Int8Array, 1, buffer, byte_offset, length, typedArray); + break; + case napi_uint8_array: + CREATE_TYPED_ARRAY( + env, Uint8Array, 1, buffer, byte_offset, length, typedArray); + break; + case napi_uint8_clamped_array: + CREATE_TYPED_ARRAY( + env, Uint8ClampedArray, 1, buffer, byte_offset, length, typedArray); + break; + case napi_int16_array: + CREATE_TYPED_ARRAY( + env, Int16Array, 2, buffer, byte_offset, length, typedArray); + break; + case napi_uint16_array: + CREATE_TYPED_ARRAY( + env, Uint16Array, 2, buffer, byte_offset, length, typedArray); + break; + case napi_int32_array: + CREATE_TYPED_ARRAY( + env, Int32Array, 4, buffer, byte_offset, length, typedArray); + break; + case napi_uint32_array: + CREATE_TYPED_ARRAY( + env, Uint32Array, 4, buffer, byte_offset, length, typedArray); + break; + case napi_float32_array: + CREATE_TYPED_ARRAY( + env, Float32Array, 4, buffer, byte_offset, length, typedArray); + break; + case napi_float64_array: + CREATE_TYPED_ARRAY( + env, Float64Array, 8, buffer, byte_offset, length, typedArray); + break; + case napi_bigint64_array: + CREATE_TYPED_ARRAY( + env, BigInt64Array, 8, buffer, byte_offset, length, typedArray); + break; + case napi_biguint64_array: + CREATE_TYPED_ARRAY( + env, BigUint64Array, 8, buffer, byte_offset, length, typedArray); + break; + case napi_float16_array: + CREATE_TYPED_ARRAY( + env, Float16Array, 2, buffer, byte_offset, length, typedArray); + break; + default: + return napi_set_last_error(env, napi_invalid_arg); + } + + *result = v8impl::JsValueFromV8LocalValue(typedArray); + return GET_RETURN_STATUS(env); + }; + + if (value->IsArrayBuffer()) { + return create_typedarray(value.As()); + } else if (value->IsSharedArrayBuffer()) { + return create_typedarray(value.As()); + } else { + return napi_set_last_error(env, napi_invalid_arg); + } +} + +napi_status NAPI_CDECL napi_get_typedarray_info(napi_env env, + napi_value typedarray, + napi_typedarray_type* type, + size_t* length, + void** data, + napi_value* arraybuffer, + size_t* byte_offset) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, typedarray); + + v8::Local value = v8impl::V8LocalValueFromJsValue(typedarray); + RETURN_STATUS_IF_FALSE(env, value->IsTypedArray(), napi_invalid_arg); + + v8::Local array = value.As(); + + if (type != nullptr) { + if (value->IsInt8Array()) { + *type = napi_int8_array; + } else if (value->IsUint8Array()) { + *type = napi_uint8_array; + } else if (value->IsUint8ClampedArray()) { + *type = napi_uint8_clamped_array; + } else if (value->IsInt16Array()) { + *type = napi_int16_array; + } else if (value->IsUint16Array()) { + *type = napi_uint16_array; + } else if (value->IsInt32Array()) { + *type = napi_int32_array; + } else if (value->IsUint32Array()) { + *type = napi_uint32_array; + } else if (value->IsFloat16Array()) { + *type = napi_float16_array; + } else if (value->IsFloat32Array()) { + *type = napi_float32_array; + } else if (value->IsFloat64Array()) { + *type = napi_float64_array; + } else if (value->IsBigInt64Array()) { + *type = napi_bigint64_array; + } else if (value->IsBigUint64Array()) { + *type = napi_biguint64_array; + } + } + + if (length != nullptr) { + *length = array->Length(); + } + + v8::Local buffer; + if (data != nullptr || arraybuffer != nullptr) { + // Calling Buffer() may have the side effect of allocating the buffer, + // so only do this when it’s needed. + buffer = array->Buffer(); + } + + if (data != nullptr) { + *data = static_cast(buffer->Data()) + array->ByteOffset(); + } + + if (arraybuffer != nullptr) { + *arraybuffer = v8impl::JsValueFromV8LocalValue(buffer); + } + + if (byte_offset != nullptr) { + *byte_offset = array->ByteOffset(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_dataview(napi_env env, + size_t byte_length, + napi_value arraybuffer, + size_t byte_offset, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, arraybuffer); + CHECK_ARG(env, result); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + + auto create_dataview = [&](auto buffer) -> napi_status { + if (byte_length + byte_offset > buffer->ByteLength()) { + napi_throw_range_error( + env, + "ERR_NAPI_INVALID_DATAVIEW_ARGS", + "byte_offset + byte_length should be less than or " + "equal to the size in bytes of the array passed in"); + return napi_set_last_error(env, napi_pending_exception); + } + + v8::Local data_view = + v8::DataView::New(buffer, byte_offset, byte_length); + *result = v8impl::JsValueFromV8LocalValue(data_view); + return GET_RETURN_STATUS(env); + }; + + if (value->IsArrayBuffer()) { + return create_dataview(value.As()); + } else if (value->IsSharedArrayBuffer()) { + return create_dataview(value.As()); + } else { + return napi_set_last_error(env, napi_invalid_arg); + } +} + +napi_status NAPI_CDECL napi_is_dataview(napi_env env, + napi_value value, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + *result = val->IsDataView(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_dataview_info(napi_env env, + napi_value dataview, + size_t* byte_length, + void** data, + napi_value* arraybuffer, + size_t* byte_offset) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, dataview); + + v8::Local value = v8impl::V8LocalValueFromJsValue(dataview); + RETURN_STATUS_IF_FALSE(env, value->IsDataView(), napi_invalid_arg); + + v8::Local array = value.As(); + + if (byte_length != nullptr) { + *byte_length = array->ByteLength(); + } + + v8::Local buffer; + if (data != nullptr || arraybuffer != nullptr) { + // Calling Buffer() may have the side effect of allocating the buffer, + // so only do this when it’s needed. + buffer = array->Buffer(); + } + + if (data != nullptr) { + *data = static_cast(buffer->Data()) + array->ByteOffset(); + } + + if (arraybuffer != nullptr) { + *arraybuffer = v8impl::JsValueFromV8LocalValue(buffer); + } + + if (byte_offset != nullptr) { + *byte_offset = array->ByteOffset(); + } + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_version(node_api_basic_env env, + uint32_t* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + *result = NODE_API_SUPPORTED_VERSION_MAX; + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, deferred); + CHECK_ARG(env, promise); + + auto maybe = v8::Promise::Resolver::New(env->context()); + CHECK_MAYBE_EMPTY(env, maybe, napi_generic_failure); + + auto v8_resolver = maybe.ToLocalChecked(); + auto v8_deferred = new v8impl::Persistent(); + v8_deferred->Reset(env->isolate, v8_resolver); + + *deferred = v8impl::JsDeferredFromNodePersistent(v8_deferred); + *promise = v8impl::JsValueFromV8LocalValue(v8_resolver->GetPromise()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution) { + return v8impl::ConcludeDeferred(env, deferred, resolution, true); +} + +napi_status NAPI_CDECL napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution) { + return v8impl::ConcludeDeferred(env, deferred, resolution, false); +} + +napi_status NAPI_CDECL napi_is_promise(napi_env env, + napi_value value, + bool* is_promise) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, is_promise); + + *is_promise = v8impl::V8LocalValueFromJsValue(value)->IsPromise(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_create_date(napi_env env, + double time, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::MaybeLocal maybe_date = v8::Date::New(env->context(), time); + CHECK_MAYBE_EMPTY(env, maybe_date, napi_generic_failure); + + *result = v8impl::JsValueFromV8LocalValue(maybe_date.ToLocalChecked()); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_is_date(napi_env env, + napi_value value, + bool* is_date) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, value); + CHECK_ARG(env, is_date); + + *is_date = v8impl::V8LocalValueFromJsValue(value)->IsDate(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_date_value(napi_env env, + napi_value value, + double* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + + v8::Local val = v8impl::V8LocalValueFromJsValue(value); + RETURN_STATUS_IF_FALSE(env, val->IsDate(), napi_date_expected); + + v8::Local date = val.As(); + *result = date->ValueOf(); + + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL napi_run_script(napi_env env, + napi_value script, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, script); + CHECK_ARG(env, result); + + v8::Local v8_script = v8impl::V8LocalValueFromJsValue(script); + + if (!v8_script->IsString()) { + return napi_set_last_error(env, napi_string_expected); + } + + v8::Local context = env->context(); + + auto maybe_script = v8::Script::Compile(context, v8_script.As()); + CHECK_MAYBE_EMPTY(env, maybe_script, napi_generic_failure); + + auto script_result = maybe_script.ToLocalChecked()->Run(context); + CHECK_MAYBE_EMPTY(env, script_result, napi_generic_failure); + + *result = v8impl::JsValueFromV8LocalValue(script_result.ToLocalChecked()); + return GET_RETURN_STATUS(env); +} + +napi_status NAPI_CDECL +napi_add_finalizer(napi_env env, + napi_value js_object, + void* finalize_data, + node_api_basic_finalize basic_finalize_cb, + void* finalize_hint, + napi_ref* result) { + // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw + // JS exceptions. + napi_finalize finalize_cb = + reinterpret_cast(basic_finalize_cb); + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, js_object); + CHECK_ARG(env, finalize_cb); + + v8::Local v8_value = v8impl::V8LocalValueFromJsValue(js_object); + RETURN_STATUS_IF_FALSE(env, v8_value->IsObject(), napi_invalid_arg); + + // Create a self-deleting reference if the optional out-param result is not + // set. + v8impl::ReferenceOwnership ownership = + result == nullptr ? v8impl::ReferenceOwnership::kRuntime + : v8impl::ReferenceOwnership::kUserland; + v8impl::Reference* reference = v8impl::ReferenceWithFinalizer::New( + env, v8_value, 0, ownership, finalize_cb, finalize_data, finalize_hint); + + if (result != nullptr) { + *result = reinterpret_cast(reference); + } + return napi_clear_last_error(env); +} + +#ifdef NAPI_EXPERIMENTAL + +napi_status NAPI_CDECL node_api_post_finalizer(node_api_basic_env basic_env, + napi_finalize finalize_cb, + void* finalize_data, + void* finalize_hint) { + napi_env env = const_cast(basic_env); + CHECK_ENV(env); + env->EnqueueFinalizer(v8impl::TrackedFinalizer::New( + env, finalize_cb, finalize_data, finalize_hint)); + return napi_clear_last_error(env); +} + +#endif + +napi_status NAPI_CDECL napi_adjust_external_memory(node_api_basic_env env, + int64_t change_in_bytes, + int64_t* adjusted_value) { + CHECK_ENV(env); + CHECK_ARG(env, adjusted_value); + + *adjusted_value = + env->isolate->AdjustAmountOfExternalAllocatedMemory(change_in_bytes); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_set_instance_data(node_api_basic_env basic_env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint) { + napi_env env = const_cast(basic_env); + CHECK_ENV(env); + + v8impl::TrackedFinalizer* old_data = + static_cast(env->instance_data); + if (old_data != nullptr) { + // Our contract so far has been to not finalize any old data there may be. + // So we simply delete it. + delete old_data; + } + + env->instance_data = + v8impl::TrackedFinalizer::New(env, finalize_cb, data, finalize_hint); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_get_instance_data(node_api_basic_env env, + void** data) { + CHECK_ENV(env); + CHECK_ARG(env, data); + + v8impl::TrackedFinalizer* idata = + static_cast(env->instance_data); + + *data = (idata == nullptr ? nullptr : idata->data()); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_detach_arraybuffer(napi_env env, + napi_value arraybuffer) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, arraybuffer); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + RETURN_STATUS_IF_FALSE( + env, value->IsArrayBuffer(), napi_arraybuffer_expected); + + v8::Local it = value.As(); + RETURN_STATUS_IF_FALSE( + env, it->IsDetachable(), napi_detachable_arraybuffer_expected); + + it->Detach(v8::Local()).Check(); + + return napi_clear_last_error(env); +} + +napi_status NAPI_CDECL napi_is_detached_arraybuffer(napi_env env, + napi_value arraybuffer, + bool* result) { + CHECK_ENV_NOT_IN_GC(env); + CHECK_ARG(env, arraybuffer); + CHECK_ARG(env, result); + + v8::Local value = v8impl::V8LocalValueFromJsValue(arraybuffer); + + *result = + value->IsArrayBuffer() && value.As()->WasDetached(); + + return napi_clear_last_error(env); +} diff --git a/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.h b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.h new file mode 100644 index 000000000..262916c09 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/js_native_api_v8.h @@ -0,0 +1,485 @@ +#ifndef SRC_JS_NATIVE_API_V8_H_ +#define SRC_JS_NATIVE_API_V8_H_ + +#include "js_native_api_types.h" +#include "js_native_api_v8_internals.h" + +inline napi_status napi_clear_last_error(node_api_basic_env env); + +namespace v8impl { + +// Base class to track references and finalizers in a doubly linked list. +class RefTracker { + public: + using RefList = RefTracker; + + RefTracker() = default; + virtual ~RefTracker() = default; + virtual void Finalize() {} + + inline void Link(RefList* list) { + prev_ = list; + next_ = list->next_; + if (next_ != nullptr) { + next_->prev_ = this; + } + list->next_ = this; + } + + inline void Unlink() { + if (prev_ != nullptr) { + prev_->next_ = next_; + } + if (next_ != nullptr) { + next_->prev_ = prev_; + } + prev_ = nullptr; + next_ = nullptr; + } + + static void FinalizeAll(RefList* list) { + while (list->next_ != nullptr) { + list->next_->Finalize(); + } + } + + private: + RefList* next_ = nullptr; + RefList* prev_ = nullptr; +}; + +} // end of namespace v8impl + +struct napi_env__ { + explicit napi_env__(v8::Local context, + int32_t module_api_version) + : isolate(v8::Isolate::GetCurrent()), + context_persistent(isolate, context), + module_api_version(module_api_version) { + napi_clear_last_error(this); + } + + inline v8::Local context() const { + return v8impl::PersistentToLocal::Strong(context_persistent); + } + + inline void Ref() { refs++; } + inline void Unref() { + if (--refs == 0) DeleteMe(); + } + + virtual bool can_call_into_js() const { return true; } + + static inline void HandleThrow(napi_env env, v8::Local value) { + if (env->terminatedOrTerminating()) { + return; + } + env->isolate->ThrowException(value); + } + + // i.e. whether v8 exited or is about to exit + inline bool terminatedOrTerminating() { + return this->isolate->IsExecutionTerminating() || !can_call_into_js(); + } + + // v8 uses a special exception to indicate termination, the + // `handle_exception` callback should identify such case using + // terminatedOrTerminating() before actually handle the exception + template + inline void CallIntoModule(T&& call, U&& handle_exception = HandleThrow) { + int open_handle_scopes_before = open_handle_scopes; + int open_callback_scopes_before = open_callback_scopes; + napi_clear_last_error(this); + call(this); + CHECK_EQ(open_handle_scopes, open_handle_scopes_before); + CHECK_EQ(open_callback_scopes, open_callback_scopes_before); + if (!last_exception.IsEmpty()) { + handle_exception(this, last_exception.Get(this->isolate)); + last_exception.Reset(); + } + } + + virtual void CallFinalizer(napi_finalize cb, void* data, void* hint) = 0; + + // Invoke finalizer from V8 garbage collector. + void InvokeFinalizerFromGC(v8impl::RefTracker* finalizer); + + // Enqueue the finalizer to the napi_env's own queue of the second pass + // weak callback. + // Implementation should drain the queue at the time it is safe to call + // into JavaScript. + virtual void EnqueueFinalizer(v8impl::RefTracker* finalizer) { + pending_finalizers.emplace(finalizer); + } + + // Remove the finalizer from the scheduled second pass weak callback queue. + // The finalizer can be deleted after this call. + virtual void DequeueFinalizer(v8impl::RefTracker* finalizer) { + pending_finalizers.erase(finalizer); + } + + virtual void DeleteMe() { + // First we must finalize those references that have `napi_finalizer` + // callbacks. The reason is that addons might store other references which + // they delete during their `napi_finalizer` callbacks. If we deleted such + // references here first, they would be doubly deleted when the + // `napi_finalizer` deleted them subsequently. + v8impl::RefTracker::FinalizeAll(&finalizing_reflist); + v8impl::RefTracker::FinalizeAll(&reflist); + delete this; + } + + void CheckGCAccess() { + if (module_api_version == NAPI_VERSION_EXPERIMENTAL && in_gc_finalizer) { + v8impl::OnFatalError( + nullptr, + "Finalizer is calling a function that may affect GC state.\n" + "The finalizers are run directly from GC and must not affect GC " + "state.\n" + "Use `node_api_post_finalizer` from inside of the finalizer to work " + "around this issue.\n" + "It schedules the call as a new task in the event loop."); + } + } + + v8::Isolate* const isolate; // Shortcut for Isolate::GetCurrent() + v8impl::Persistent context_persistent; + + v8impl::Persistent last_exception; + + // We store references in two different lists, depending on whether they have + // `napi_finalizer` callbacks, because we must first finalize the ones that + // have such a callback. See `~napi_env__()` above for details. + v8impl::RefTracker::RefList reflist; + v8impl::RefTracker::RefList finalizing_reflist; + // The invocation order of the finalizers is not determined. + std::unordered_set pending_finalizers; + napi_extended_error_info last_error; + int open_handle_scopes = 0; + int open_callback_scopes = 0; + int refs = 1; + void* instance_data = nullptr; + int32_t module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION; + bool in_gc_finalizer = false; + + protected: + // Should not be deleted directly. Delete with `napi_env__::DeleteMe()` + // instead. + virtual ~napi_env__() = default; +}; + +inline napi_status napi_clear_last_error(node_api_basic_env basic_env) { + napi_env env = const_cast(basic_env); + env->last_error.error_code = napi_ok; + env->last_error.engine_error_code = 0; + env->last_error.engine_reserved = nullptr; + env->last_error.error_message = nullptr; + return napi_ok; +} + +inline napi_status napi_set_last_error(node_api_basic_env basic_env, + napi_status error_code, + uint32_t engine_error_code = 0, + void* engine_reserved = nullptr) { + napi_env env = const_cast(basic_env); + env->last_error.error_code = error_code; + env->last_error.engine_error_code = engine_error_code; + env->last_error.engine_reserved = engine_reserved; + return error_code; +} + +#define RETURN_STATUS_IF_FALSE(env, condition, status) \ + do { \ + if (!(condition)) { \ + return napi_set_last_error((env), (status)); \ + } \ + } while (0) + +#define RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(env, condition, status) \ + do { \ + if (!(condition)) { \ + return napi_set_last_error( \ + (env), try_catch.HasCaught() ? napi_pending_exception : (status)); \ + } \ + } while (0) + +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + } while (0) + +#define CHECK_ENV_NOT_IN_GC(env) \ + do { \ + CHECK_ENV((env)); \ + (env)->CheckGCAccess(); \ + } while (0) + +#define CHECK_ARG(env, arg) \ + RETURN_STATUS_IF_FALSE((env), ((arg) != nullptr), napi_invalid_arg) + +#define CHECK_ARG_WITH_PREAMBLE(env, arg) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE( \ + (env), ((arg) != nullptr), napi_invalid_arg) + +#define CHECK_MAYBE_EMPTY(env, maybe, status) \ + RETURN_STATUS_IF_FALSE((env), !((maybe).IsEmpty()), (status)) + +#define CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe, status) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsEmpty()), (status)) + +// NAPI_PREAMBLE is not wrapped in do..while: try_catch must have function scope +#define NAPI_PREAMBLE(env) \ + CHECK_ENV_NOT_IN_GC((env)); \ + RETURN_STATUS_IF_FALSE( \ + (env), (env)->last_exception.IsEmpty(), napi_pending_exception); \ + RETURN_STATUS_IF_FALSE( \ + (env), \ + (env)->can_call_into_js(), \ + (env->module_api_version >= 10 ? napi_cannot_run_js \ + : napi_pending_exception)); \ + napi_clear_last_error((env)); \ + v8impl::TryCatch try_catch((env)) + +#define CHECK_TO_TYPE(env, type, context, result, src, status) \ + do { \ + CHECK_ARG((env), (src)); \ + auto maybe = v8impl::V8LocalValueFromJsValue((src))->To##type((context)); \ + CHECK_MAYBE_EMPTY((env), maybe, (status)); \ + (result) = maybe.ToLocalChecked(); \ + } while (0) + +#define CHECK_TO_TYPE_WITH_PREAMBLE(env, type, context, result, src, status) \ + do { \ + CHECK_ARG_WITH_PREAMBLE((env), (src)); \ + auto maybe = v8impl::V8LocalValueFromJsValue((src))->To##type((context)); \ + CHECK_MAYBE_EMPTY_WITH_PREAMBLE((env), maybe, (status)); \ + (result) = maybe.ToLocalChecked(); \ + } while (0) + +#define CHECK_TO_FUNCTION(env, result, src) \ + do { \ + CHECK_ARG((env), (src)); \ + v8::Local v8value = v8impl::V8LocalValueFromJsValue((src)); \ + RETURN_STATUS_IF_FALSE((env), v8value->IsFunction(), napi_invalid_arg); \ + (result) = v8value.As(); \ + } while (0) + +#define CHECK_TO_OBJECT(env, context, result, src) \ + CHECK_TO_TYPE((env), Object, (context), (result), (src), napi_object_expected) + +#define CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, result, src) \ + CHECK_TO_TYPE_WITH_PREAMBLE( \ + (env), Object, (context), (result), (src), napi_object_expected) + +#define CHECK_TO_STRING(env, context, result, src) \ + CHECK_TO_TYPE((env), String, (context), (result), (src), napi_string_expected) + +#define GET_RETURN_STATUS(env) \ + (!try_catch.HasCaught() \ + ? napi_ok \ + : napi_set_last_error((env), napi_pending_exception)) + +#define THROW_RANGE_ERROR_IF_FALSE(env, condition, error, message) \ + do { \ + if (!(condition)) { \ + napi_throw_range_error((env), (error), (message)); \ + return napi_set_last_error((env), napi_generic_failure); \ + } \ + } while (0) + +#define CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe, status) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsEmpty()), (status)) + +#define STATUS_CALL(call) \ + do { \ + napi_status status = (call); \ + if (status != napi_ok) return status; \ + } while (0) + +namespace v8impl { + +//=== Conversion between V8 Handles and napi_value ======================== + +// This asserts v8::Local<> will always be implemented with a single +// pointer field so that we can pass it around as a void*. +static_assert(sizeof(v8::Local) == sizeof(napi_value), + "Cannot convert between v8::Local and napi_value"); + +inline napi_value JsValueFromV8LocalValue(v8::Local local) { + return reinterpret_cast(*local); +} + +inline v8::Local V8LocalValueFromJsValue(napi_value v) { + v8::Local local; + memcpy(static_cast(&local), &v, sizeof(v)); + return local; +} + +// Adapter for napi_finalize callbacks. +class Finalizer { + public: + Finalizer(napi_env env, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint) + : env_(env), + finalize_callback_(finalize_callback), + finalize_data_(finalize_data), + finalize_hint_(finalize_hint) {} + + napi_env env() { return env_; } + void* data() { return finalize_data_; } + + void ResetEnv(); + void ResetFinalizer(); + void CallFinalizer(); + + private: + napi_env env_; + napi_finalize finalize_callback_; + void* finalize_data_; + void* finalize_hint_; +}; + +class TryCatch : public v8::TryCatch { + public: + explicit TryCatch(napi_env env) : v8::TryCatch(env->isolate), _env(env) {} + + ~TryCatch() { + if (HasCaught()) { + _env->last_exception.Reset(_env->isolate, Exception()); + } + } + + private: + napi_env _env; +}; + +// Wrapper around Finalizer that can be tracked. +class TrackedFinalizer final : public RefTracker { + public: + static TrackedFinalizer* New(napi_env env, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint); + ~TrackedFinalizer() override; + + void* data() { return finalizer_.data(); } + + private: + TrackedFinalizer(napi_env env, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint); + void Finalize() override; + + private: + Finalizer finalizer_; +}; + +// Ownership of a reference. +enum class ReferenceOwnership : uint8_t { + // The reference is owned by the runtime. No userland call is needed to + // destruct the reference. + kRuntime, + // The reference is owned by the userland. User code is responsible to delete + // the reference with appropriate node-api calls. + kUserland, +}; + +// Wrapper around v8impl::Persistent. +class Reference : public RefTracker { + public: + static Reference* New(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership); + ~Reference() override; + + uint32_t Ref(); + uint32_t Unref(); + v8::Local Get(napi_env env); + + virtual void ResetFinalizer() {} + virtual void* Data() { return nullptr; } + + uint32_t refcount() const { return refcount_; } + ReferenceOwnership ownership() { return ownership_; } + + protected: + Reference(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership); + virtual void CallUserFinalizer() {} + virtual void InvokeFinalizerFromGC(); + + private: + static void WeakCallback(const v8::WeakCallbackInfo& data); + void SetWeak(); + void Finalize() override; + + private: + v8impl::Persistent persistent_; + uint32_t refcount_; + ReferenceOwnership ownership_; + bool can_be_weak_; +}; + +// Reference that can store additional data. +class ReferenceWithData final : public Reference { + public: + static ReferenceWithData* New(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + void* data); + + void* Data() override { return data_; } + + private: + ReferenceWithData(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + void* data); + + private: + void* data_; +}; + +// Reference that has a user finalizer callback. +class ReferenceWithFinalizer final : public Reference { + public: + static ReferenceWithFinalizer* New(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint); + ~ReferenceWithFinalizer() override; + + void ResetFinalizer() override { finalizer_.ResetFinalizer(); } + void* Data() override { return finalizer_.data(); } + + private: + ReferenceWithFinalizer(napi_env env, + v8::Local value, + uint32_t initial_refcount, + ReferenceOwnership ownership, + napi_finalize finalize_callback, + void* finalize_data, + void* finalize_hint); + void CallUserFinalizer() override; + void InvokeFinalizerFromGC() override; + + private: + Finalizer finalizer_; +}; + +} // end of namespace v8impl + +#endif // SRC_JS_NATIVE_API_V8_H_ diff --git a/test-app/runtime/src/main/cpp/napi/vendor/node_api.h b/test-app/runtime/src/main/cpp/napi/vendor/node_api.h new file mode 100644 index 000000000..46dbb02b4 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/node_api.h @@ -0,0 +1,265 @@ +#ifndef SRC_NODE_API_H_ +#define SRC_NODE_API_H_ + +#if defined(BUILDING_NODE_EXTENSION) && !defined(NAPI_EXTERN) +#ifdef _WIN32 +// Building native addon against node +#define NAPI_EXTERN __declspec(dllimport) +#elif defined(__wasm__) +#define NAPI_EXTERN __attribute__((__import_module__("napi"))) +#endif +#endif +#include "js_native_api.h" +#include "node_api_types.h" + +struct uv_loop_s; // Forward declaration. + +#ifdef _WIN32 +#define NAPI_MODULE_EXPORT __declspec(dllexport) +#else +#ifdef __EMSCRIPTEN__ +#define NAPI_MODULE_EXPORT \ + __attribute__((visibility("default"))) __attribute__((used)) +#else +#define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) +#endif +#endif + +#if defined(__GNUC__) +#define NAPI_NO_RETURN __attribute__((noreturn)) +#elif defined(_WIN32) +#define NAPI_NO_RETURN __declspec(noreturn) +#else +#define NAPI_NO_RETURN +#endif + +// Used by deprecated registration method napi_module_register. +typedef struct napi_module { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + void* reserved[4]; +} napi_module; + +#define NAPI_MODULE_VERSION 1 + +#define NAPI_MODULE_INITIALIZER_X(base, version) \ + NAPI_MODULE_INITIALIZER_X_HELPER(base, version) +#define NAPI_MODULE_INITIALIZER_X_HELPER(base, version) base##version + +#ifdef __wasm__ +#define NAPI_MODULE_INITIALIZER_BASE napi_register_wasm_v +#else +#define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v +#endif + +#define NODE_API_MODULE_GET_API_VERSION_BASE node_api_module_get_api_version_v + +#define NAPI_MODULE_INITIALIZER \ + NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) + +#define NODE_API_MODULE_GET_API_VERSION \ + NAPI_MODULE_INITIALIZER_X(NODE_API_MODULE_GET_API_VERSION_BASE, \ + NAPI_MODULE_VERSION) + +#define NAPI_MODULE_INIT() \ + EXTERN_C_START \ + NAPI_MODULE_EXPORT int32_t NODE_API_MODULE_GET_API_VERSION(void) { \ + return NAPI_VERSION; \ + } \ + NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ + napi_value exports); \ + EXTERN_C_END \ + napi_value NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports) + +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_INIT() { return regfunc(env, exports); } + +// Deprecated. Use NAPI_MODULE. +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + NAPI_MODULE(modname, regfunc) + +EXTERN_C_START + +// Deprecated. Replaced by symbol-based registration defined by NAPI_MODULE +// and NAPI_MODULE_INIT macros. +NAPI_EXTERN void NAPI_CDECL +napi_module_register(napi_module* mod); + +NAPI_EXTERN NAPI_NO_RETURN void NAPI_CDECL +napi_fatal_error(const char* location, + size_t location_len, + const char* message, + size_t message_len); + +// Methods for custom handling of async operations +NAPI_EXTERN napi_status NAPI_CDECL +napi_async_init(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_context* result); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_async_destroy(napi_env env, napi_async_context async_context); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_make_callback(napi_env env, + napi_async_context async_context, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result); + +// Methods to provide node::Buffer functionality with napi types +NAPI_EXTERN napi_status NAPI_CDECL napi_create_buffer(napi_env env, + size_t length, + void** data, + napi_value* result); +#ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_external_buffer(napi_env env, + size_t length, + void* data, + node_api_basic_finalize finalize_cb, + void* finalize_hint, + napi_value* result); +#endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED + +#if NAPI_VERSION >= 10 + +NAPI_EXTERN napi_status NAPI_CDECL +node_api_create_buffer_from_arraybuffer(napi_env env, + napi_value arraybuffer, + size_t byte_offset, + size_t byte_length, + napi_value* result); +#endif // NAPI_VERSION >= 10 + +NAPI_EXTERN napi_status NAPI_CDECL napi_create_buffer_copy(napi_env env, + size_t length, + const void* data, + void** result_data, + napi_value* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_is_buffer(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_buffer_info(napi_env env, + napi_value value, + void** data, + size_t* length); + +// Methods to manage simple async operations +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result); +NAPI_EXTERN napi_status NAPI_CDECL napi_delete_async_work(napi_env env, + napi_async_work work); +NAPI_EXTERN napi_status NAPI_CDECL napi_queue_async_work(node_api_basic_env env, + napi_async_work work); +NAPI_EXTERN napi_status NAPI_CDECL +napi_cancel_async_work(node_api_basic_env env, napi_async_work work); + +// version management +NAPI_EXTERN napi_status NAPI_CDECL napi_get_node_version( + node_api_basic_env env, const napi_node_version** version); + +#if NAPI_VERSION >= 2 + +// Return the current libuv event loop for a given environment +NAPI_EXTERN napi_status NAPI_CDECL +napi_get_uv_event_loop(node_api_basic_env env, struct uv_loop_s** loop); + +#endif // NAPI_VERSION >= 2 + +#if NAPI_VERSION >= 3 + +NAPI_EXTERN napi_status NAPI_CDECL napi_fatal_exception(napi_env env, + napi_value err); + +NAPI_EXTERN napi_status NAPI_CDECL napi_add_env_cleanup_hook( + node_api_basic_env env, napi_cleanup_hook fun, void* arg); + +NAPI_EXTERN napi_status NAPI_CDECL napi_remove_env_cleanup_hook( + node_api_basic_env env, napi_cleanup_hook fun, void* arg); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_open_callback_scope(napi_env env, + napi_value resource_object, + napi_async_context context, + napi_callback_scope* result); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_close_callback_scope(napi_env env, napi_callback_scope scope); + +#endif // NAPI_VERSION >= 3 + +#if NAPI_VERSION >= 4 + +// Calling into JS from other threads +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_threadsafe_function(napi_env env, + napi_value func, + napi_value async_resource, + napi_value async_resource_name, + size_t max_queue_size, + size_t initial_thread_count, + void* thread_finalize_data, + napi_finalize thread_finalize_cb, + void* context, + napi_threadsafe_function_call_js call_js_cb, + napi_threadsafe_function* result); + +NAPI_EXTERN napi_status NAPI_CDECL napi_get_threadsafe_function_context( + napi_threadsafe_function func, void** result); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_call_threadsafe_function(napi_threadsafe_function func, + void* data, + napi_threadsafe_function_call_mode is_blocking); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_acquire_threadsafe_function(napi_threadsafe_function func); + +NAPI_EXTERN napi_status NAPI_CDECL napi_release_threadsafe_function( + napi_threadsafe_function func, napi_threadsafe_function_release_mode mode); + +NAPI_EXTERN napi_status NAPI_CDECL napi_unref_threadsafe_function( + node_api_basic_env env, napi_threadsafe_function func); + +NAPI_EXTERN napi_status NAPI_CDECL napi_ref_threadsafe_function( + node_api_basic_env env, napi_threadsafe_function func); + +#endif // NAPI_VERSION >= 4 + +#if NAPI_VERSION >= 8 + +NAPI_EXTERN napi_status NAPI_CDECL +napi_add_async_cleanup_hook(node_api_basic_env env, + napi_async_cleanup_hook hook, + void* arg, + napi_async_cleanup_hook_handle* remove_handle); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_remove_async_cleanup_hook(napi_async_cleanup_hook_handle remove_handle); + +#endif // NAPI_VERSION >= 8 + +#if NAPI_VERSION >= 9 + +NAPI_EXTERN napi_status NAPI_CDECL +node_api_get_module_file_name(node_api_basic_env env, const char** result); + +#endif // NAPI_VERSION >= 9 + +EXTERN_C_END + +#endif // SRC_NODE_API_H_ diff --git a/test-app/runtime/src/main/cpp/napi/vendor/node_api_types.h b/test-app/runtime/src/main/cpp/napi/vendor/node_api_types.h new file mode 100644 index 000000000..79123f042 --- /dev/null +++ b/test-app/runtime/src/main/cpp/napi/vendor/node_api_types.h @@ -0,0 +1,58 @@ +#ifndef SRC_NODE_API_TYPES_H_ +#define SRC_NODE_API_TYPES_H_ + +#include "js_native_api_types.h" + +typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env, + napi_value exports); +// False positive: https://github.com/cpplint/cpplint/issues/409 +// NOLINTNEXTLINE (readability/casting) +typedef int32_t(NAPI_CDECL* node_api_addon_get_api_version_func)(void); + +typedef struct napi_callback_scope__* napi_callback_scope; +typedef struct napi_async_context__* napi_async_context; +typedef struct napi_async_work__* napi_async_work; + +#if NAPI_VERSION >= 3 +typedef void(NAPI_CDECL* napi_cleanup_hook)(void* arg); +#endif // NAPI_VERSION >= 3 + +#if NAPI_VERSION >= 4 +typedef struct napi_threadsafe_function__* napi_threadsafe_function; +#endif // NAPI_VERSION >= 4 + +#if NAPI_VERSION >= 4 +typedef enum { + napi_tsfn_release, + napi_tsfn_abort +} napi_threadsafe_function_release_mode; + +typedef enum { + napi_tsfn_nonblocking, + napi_tsfn_blocking +} napi_threadsafe_function_call_mode; +#endif // NAPI_VERSION >= 4 + +typedef void(NAPI_CDECL* napi_async_execute_callback)(napi_env env, void* data); +typedef void(NAPI_CDECL* napi_async_complete_callback)(napi_env env, + napi_status status, + void* data); +#if NAPI_VERSION >= 4 +typedef void(NAPI_CDECL* napi_threadsafe_function_call_js)( + napi_env env, napi_value js_callback, void* context, void* data); +#endif // NAPI_VERSION >= 4 + +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t patch; + const char* release; +} napi_node_version; + +#if NAPI_VERSION >= 8 +typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle; +typedef void(NAPI_CDECL* napi_async_cleanup_hook)( + napi_async_cleanup_hook_handle handle, void* data); +#endif // NAPI_VERSION >= 8 + +#endif // SRC_NODE_API_TYPES_H_