Skip to content

Commit

Permalink
node-api: implement external strings
Browse files Browse the repository at this point in the history
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: #48339
Fixes: #48198
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com>
  • Loading branch information
gabrielschulhof authored and ruyadorno committed Sep 12, 2023
1 parent a30f2fb commit feb6a54
Show file tree
Hide file tree
Showing 11 changed files with 713 additions and 176 deletions.
1 change: 1 addition & 0 deletions benchmark/napi/string/.gitignore
@@ -0,0 +1 @@
build/
56 changes: 56 additions & 0 deletions benchmark/napi/string/binding.c
@@ -0,0 +1,56 @@
#include <assert.h>
#define NAPI_EXPERIMENTAL
#include <node_api.h>

#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;
}
8 changes: 8 additions & 0 deletions benchmark/napi/string/binding.gyp
@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.c' ]
}
]
}
19 changes: 19 additions & 0 deletions 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);
}
109 changes: 108 additions & 1 deletion doc/api/n-api.md
Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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`

<!-- YAML
added: REPLACEME
-->

> 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`

<!-- YAML
Expand Down Expand Up @@ -2910,6 +2965,56 @@ 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_utf16`

<!-- YAML
added: REPLACEME
-->

> 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`

<!-- YAML
Expand Down Expand Up @@ -6472,6 +6577,8 @@ the add-on's file name during loading.
[`napi_wrap`]: #napi_wrap
[`node-addon-api`]: https://github.com/nodejs/node-addon-api
[`node_api.h`]: https://github.com/nodejs/node/blob/HEAD/src/node_api.h
[`node_api_create_external_string_latin1`]: #node_api_create_external_string_latin1
[`node_api_create_external_string_utf16`]: #node_api_create_external_string_utf16
[`node_api_create_syntax_error`]: #node_api_create_syntax_error
[`node_api_throw_syntax_error`]: #node_api_throw_syntax_error
[`process.release`]: process.md#processrelease
Expand Down
18 changes: 18 additions & 0 deletions src/js_native_api.h
Expand Up @@ -92,6 +92,24 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_create_string_utf16(napi_env env,
const char16_t* str,
size_t length,
napi_value* result);
#ifdef NAPI_EXPERIMENTAL
NAPI_EXTERN napi_status NAPI_CDECL
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);
NAPI_EXTERN napi_status NAPI_CDECL
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);
#endif // NAPI_EXPERIMENTAL
NAPI_EXTERN napi_status NAPI_CDECL napi_create_symbol(napi_env env,
napi_value description,
napi_value* result);
Expand Down

0 comments on commit feb6a54

Please sign in to comment.