From feb6a54dc358c580f42665632bb4393da6947571 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Fri, 2 Jun 2023 16:03:55 -0700 Subject: [PATCH] node-api: implement external strings Introduce APIs that allow for the creation of JavaScript strings without copying the underlying native string into the engine. The APIs fall back to regular string creation if the engine's external string APIs are unavailable. In this case, an optional boolean out-parameter indicates that the string was copied, and the optional finalizer is called if given. PR-URL: https://github.com/nodejs/node/pull/48339 Fixes: https://github.com/nodejs/node/issues/48198 Reviewed-By: Daeyeon Jeong Signed-off-by: Gabriel Schulhof --- benchmark/napi/string/.gitignore | 1 + benchmark/napi/string/binding.c | 56 +++ benchmark/napi/string/binding.gyp | 8 + benchmark/napi/string/index.js | 19 + doc/api/n-api.md | 109 +++++- src/js_native_api.h | 18 + src/js_native_api_v8.cc | 237 ++++++++++-- test/js-native-api/common.h | 11 + test/js-native-api/test_string/test.js | 46 +++ test/js-native-api/test_string/test_string.c | 363 ++++++++++++------ .../test_reference_by_node_api_version.c | 21 +- 11 files changed, 713 insertions(+), 176 deletions(-) create mode 100644 benchmark/napi/string/.gitignore create mode 100644 benchmark/napi/string/binding.c create mode 100644 benchmark/napi/string/binding.gyp create mode 100644 benchmark/napi/string/index.js diff --git a/benchmark/napi/string/.gitignore b/benchmark/napi/string/.gitignore new file mode 100644 index 00000000000000..567609b1234a9b --- /dev/null +++ b/benchmark/napi/string/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/benchmark/napi/string/binding.c b/benchmark/napi/string/binding.c new file mode 100644 index 00000000000000..9ac7de5875ef75 --- /dev/null +++ b/benchmark/napi/string/binding.c @@ -0,0 +1,56 @@ +#include +#define NAPI_EXPERIMENTAL +#include + +#define NAPI_CALL(call) \ + do { \ + napi_status status = call; \ + assert(status == napi_ok && #call " failed"); \ + } while (0); + +#define EXPORT_FUNC(env, exports, name, func) \ + do { \ + napi_value js_func; \ + NAPI_CALL(napi_create_function( \ + (env), (name), NAPI_AUTO_LENGTH, (func), NULL, &js_func)); \ + NAPI_CALL(napi_set_named_property((env), (exports), (name), js_func)); \ + } while (0); + +const char* one_byte_string = "The Quick Brown Fox Jumped Over The Lazy Dog."; +const char16_t* two_byte_string = + u"The Quick Brown Fox Jumped Over The Lazy Dog."; + +#define DECLARE_BINDING(CapName, lowercase_name, var_name) \ + static napi_value CreateString##CapName(napi_env env, \ + napi_callback_info info) { \ + size_t argc = 4; \ + napi_value argv[4]; \ + uint32_t n; \ + uint32_t index; \ + napi_handle_scope scope; \ + napi_value js_string; \ + \ + NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); \ + NAPI_CALL(napi_get_value_uint32(env, argv[0], &n)); \ + NAPI_CALL(napi_open_handle_scope(env, &scope)); \ + NAPI_CALL(napi_call_function(env, argv[1], argv[2], 0, NULL, NULL)); \ + for (index = 0; index < n; index++) { \ + NAPI_CALL(napi_create_string_##lowercase_name( \ + env, (var_name), NAPI_AUTO_LENGTH, &js_string)); \ + } \ + NAPI_CALL(napi_call_function(env, argv[1], argv[3], 1, &argv[0], NULL)); \ + NAPI_CALL(napi_close_handle_scope(env, scope)); \ + \ + return NULL; \ + } + +DECLARE_BINDING(Latin1, latin1, one_byte_string) +DECLARE_BINDING(Utf8, utf8, one_byte_string) +DECLARE_BINDING(Utf16, utf16, two_byte_string) + +NAPI_MODULE_INIT() { + EXPORT_FUNC(env, exports, "createStringLatin1", CreateStringLatin1); + EXPORT_FUNC(env, exports, "createStringUtf8", CreateStringUtf8); + EXPORT_FUNC(env, exports, "createStringUtf16", CreateStringUtf16); + return exports; +} diff --git a/benchmark/napi/string/binding.gyp b/benchmark/napi/string/binding.gyp new file mode 100644 index 00000000000000..413621ade335a1 --- /dev/null +++ b/benchmark/napi/string/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.c' ] + } + ] +} diff --git a/benchmark/napi/string/index.js b/benchmark/napi/string/index.js new file mode 100644 index 00000000000000..2d2f82b39bdcaf --- /dev/null +++ b/benchmark/napi/string/index.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common.js'); + +let binding; +try { + binding = require(`./build/${common.buildType}/binding`); +} catch { + console.error(`${__filename}: Binding failed to load`); + process.exit(0); +} + +const bench = common.createBenchmark(main, { + n: [1e5, 1e6, 1e7], + stringType: ['Latin1', 'Utf8', 'Utf16'], +}); + +function main({ n, stringType }) { + binding[`createString${stringType}`](n, bench, bench.start, bench.end); +} diff --git a/doc/api/n-api.md b/doc/api/n-api.md index ac3bd95e7c5f8c..5fc9fd6587e7ff 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -801,7 +801,7 @@ napiVersion: 1 Function pointer type for add-on provided functions that allow the user to be notified when externally-owned data is ready to be cleaned up because the -object with which it was associated with, has been garbage-collected. The user +object with which it was associated with has been garbage-collected. The user must provide a function satisfying the following signature which would get called upon the object's collection. Currently, `napi_finalize` can be used for finding out when objects that have external data are collected. @@ -819,6 +819,11 @@ Since these functions may be called while the JavaScript engine is in a state where it cannot execute JavaScript code, some Node-API calls may return `napi_pending_exception` even when there is no exception pending. +In the case of [`node_api_create_external_string_latin1`][] and +[`node_api_create_external_string_utf16`][] the `env` parameter may be null, +because external strings can be collected during the latter part of environment +shutdown. + Change History: * experimental (`NAPI_EXPERIMENTAL` is defined): @@ -2882,6 +2887,56 @@ string. The native string is copied. The JavaScript `string` type is described in [Section 6.1.4][] of the ECMAScript Language Specification. +#### `node_api_create_external_string_latin1` + + + +> Stability: 1 - Experimental + +```c +napi_status +node_api_create_external_string_latin1(napi_env env, + char* str, + size_t length, + napi_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); +``` + +* `[in] env`: The environment that the API is invoked under. +* `[in] str`: Character buffer representing an ISO-8859-1-encoded string. +* `[in] length`: The length of the string in bytes, or `NAPI_AUTO_LENGTH` if it + is null-terminated. +* `[in] finalize_callback`: The function to call when the string is being + collected. The function will be called with the following parameters: + * `[in] env`: The environment in which the add-on is running. This value + may be null if the string is being collected as part of the termination + of the worker or the main Node.js instance. + * `[in] data`: This is the value `str` as a `void*` pointer. + * `[in] finalize_hint`: This is the value `finalize_hint` that was given + to the API. + [`napi_finalize`][] provides more details. + This parameter is optional. Passing a null value means that the add-on + doesn't need to be notified when the corresponding JavaScript string is + collected. +* `[in] finalize_hint`: Optional hint to pass to the finalize callback during + collection. +* `[out] result`: A `napi_value` representing a JavaScript `string`. +* `[out] copied`: Whether the string was copied. If it was, the finalizer will + already have been invoked to destroy `str`. + +Returns `napi_ok` if the API succeeded. + +This API creates a JavaScript `string` value from an ISO-8859-1-encoded C +string. The native string may not be copied and must thus exist for the entire +life cycle of the JavaScript value. + +The JavaScript `string` type is described in +[Section 6.1.4][] of the ECMAScript Language Specification. + #### `napi_create_string_utf16` + +> Stability: 1 - Experimental + +```c +napi_status +node_api_create_external_string_utf16(napi_env env, + char16_t* str, + size_t length, + napi_finalize finalize_callback, + void* finalize_hint, + napi_value* result, + bool* copied); +``` + +* `[in] env`: The environment that the API is invoked under. +* `[in] str`: Character buffer representing a UTF16-LE-encoded string. +* `[in] length`: The length of the string in two-byte code units, or + `NAPI_AUTO_LENGTH` if it is null-terminated. +* `[in] finalize_callback`: The function to call when the string is being + collected. The function will be called with the following parameters: + * `[in] env`: The environment in which the add-on is running. This value + may be null if the string is being collected as part of the termination + of the worker or the main Node.js instance. + * `[in] data`: This is the value `str` as a `void*` pointer. + * `[in] finalize_hint`: This is the value `finalize_hint` that was given + to the API. + [`napi_finalize`][] provides more details. + This parameter is optional. Passing a null value means that the add-on + doesn't need to be notified when the corresponding JavaScript string is + collected. +* `[in] finalize_hint`: Optional hint to pass to the finalize callback during + collection. +* `[out] result`: A `napi_value` representing a JavaScript `string`. +* `[out] copied`: Whether the string was copied. If it was, the finalizer will + already have been invoked to destroy `str`. + +Returns `napi_ok` if the API succeeded. + +This API creates a JavaScript `string` value from a UTF16-LE-encoded C string. +The native string may not be copied and must thus exist for the entire life +cycle of the JavaScript value. + +The JavaScript `string` type is described in +[Section 6.1.4][] of the ECMAScript Language Specification. + #### `napi_create_string_utf8`