diff --git a/packages/emnapi/include/node/js_native_api.h b/packages/emnapi/include/node/js_native_api.h index cd782911..681d32c6 100644 --- a/packages/emnapi/include/node/js_native_api.h +++ b/packages/emnapi/include/node/js_native_api.h @@ -55,8 +55,8 @@ EXTERN_C_START -NAPI_EXTERN napi_status NAPI_CDECL -napi_get_last_error_info(napi_env env, const napi_extended_error_info** result); +NAPI_EXTERN napi_status NAPI_CDECL napi_get_last_error_info( + node_api_nogc_env env, const napi_extended_error_info** result); // Getters for defined singletons NAPI_EXTERN napi_status NAPI_CDECL napi_get_undefined(napi_env env, @@ -104,7 +104,7 @@ NAPI_EXTERN napi_status NAPI_CDECL node_api_create_external_string_latin1(napi_env env, char* str, size_t length, - napi_finalize finalize_callback, + node_api_nogc_finalize finalize_callback, void* finalize_hint, napi_value* result, bool* copied); @@ -112,7 +112,7 @@ 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, + node_api_nogc_finalize finalize_callback, void* finalize_hint, napi_value* result, bool* copied); @@ -303,7 +303,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_instanceof(napi_env env, // 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] NAPI environment handle + 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. @@ -327,7 +327,7 @@ napi_define_class(napi_env env, NAPI_EXTERN napi_status NAPI_CDECL napi_wrap(napi_env env, napi_value js_object, void* native_object, - napi_finalize finalize_cb, + node_api_nogc_finalize finalize_cb, void* finalize_hint, napi_ref* result); NAPI_EXTERN napi_status NAPI_CDECL napi_unwrap(napi_env env, @@ -339,7 +339,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_remove_wrap(napi_env env, NAPI_EXTERN napi_status NAPI_CDECL napi_create_external(napi_env env, void* data, - napi_finalize finalize_cb, + node_api_nogc_finalize finalize_cb, void* finalize_hint, napi_value* result); NAPI_EXTERN napi_status NAPI_CDECL napi_get_value_external(napi_env env, @@ -438,7 +438,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_create_external_arraybuffer(napi_env env, void* external_data, size_t byte_length, - napi_finalize finalize_cb, + node_api_nogc_finalize finalize_cb, void* finalize_hint, napi_value* result); #endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED @@ -480,7 +480,7 @@ napi_get_dataview_info(napi_env env, size_t* byte_offset); // version management -NAPI_EXTERN napi_status NAPI_CDECL napi_get_version(napi_env env, +NAPI_EXTERN napi_status NAPI_CDECL napi_get_version(node_api_nogc_env env, uint32_t* result); // Promises @@ -504,7 +504,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_run_script(napi_env env, // Memory management NAPI_EXTERN napi_status NAPI_CDECL napi_adjust_external_memory( - napi_env env, int64_t change_in_bytes, int64_t* adjusted_value); + node_api_nogc_env env, int64_t change_in_bytes, int64_t* adjusted_value); #if NAPI_VERSION >= 5 @@ -522,12 +522,13 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_get_date_value(napi_env env, 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, - napi_finalize finalize_cb, - void* finalize_hint, - napi_ref* result); +NAPI_EXTERN napi_status NAPI_CDECL +napi_add_finalizer(napi_env env, + napi_value js_object, + void* finalize_data, + node_api_nogc_finalize finalize_cb, + void* finalize_hint, + napi_ref* result); #endif // NAPI_VERSION >= 5 @@ -535,7 +536,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_add_finalizer(napi_env env, #define NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER NAPI_EXTERN napi_status NAPI_CDECL -node_api_post_finalizer(napi_env env, +node_api_post_finalizer(node_api_nogc_env env, napi_finalize finalize_cb, void* finalize_data, void* finalize_hint); @@ -579,10 +580,13 @@ napi_get_all_property_names(napi_env env, napi_value* result); // Instance data -NAPI_EXTERN napi_status NAPI_CDECL napi_set_instance_data( - napi_env env, void* data, napi_finalize finalize_cb, void* finalize_hint); +NAPI_EXTERN napi_status NAPI_CDECL +napi_set_instance_data(node_api_nogc_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint); -NAPI_EXTERN napi_status NAPI_CDECL napi_get_instance_data(napi_env env, +NAPI_EXTERN napi_status NAPI_CDECL napi_get_instance_data(node_api_nogc_env env, void** data); #endif // NAPI_VERSION >= 6 diff --git a/packages/emnapi/include/node/js_native_api_types.h b/packages/emnapi/include/node/js_native_api_types.h index 005382f1..7cb5b080 100644 --- a/packages/emnapi/include/node/js_native_api_types.h +++ b/packages/emnapi/include/node/js_native_api_types.h @@ -22,6 +22,35 @@ typedef uint16_t char16_t; // 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_nogc_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 +// (nogc) environment into an API that expects a non-const environment, and the +// definition of nogc finalizer function pointer types below, which receive a +// nogc environment as their first parameter, and can thus only call nogc 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 (nogc) finalizer. +#if !defined(NAPI_EXPERIMENTAL) || \ + (defined(NAPI_EXPERIMENTAL) && \ + defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT)) +typedef struct napi_env__* node_api_nogc_env; +#else +typedef const struct napi_env__* node_api_nogc_env; +#endif + typedef struct napi_value__* napi_value; typedef struct napi_ref__* napi_ref; typedef struct napi_handle_scope__* napi_handle_scope; @@ -116,6 +145,16 @@ 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)) +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 struct { // One of utf8name or name should be NULL. const char* utf8name; diff --git a/packages/emnapi/include/node/node_api.h b/packages/emnapi/include/node/node_api.h index a12ad129..02987473 100644 --- a/packages/emnapi/include/node/node_api.h +++ b/packages/emnapi/include/node/node_api.h @@ -141,7 +141,7 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_create_external_buffer(napi_env env, size_t length, void* data, - napi_finalize finalize_cb, + node_api_nogc_finalize finalize_cb, void* finalize_hint, napi_value* result); #endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED @@ -169,20 +169,20 @@ napi_create_async_work(napi_env env, 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(napi_env env, +NAPI_EXTERN napi_status NAPI_CDECL napi_queue_async_work(node_api_nogc_env env, napi_async_work work); -NAPI_EXTERN napi_status NAPI_CDECL napi_cancel_async_work(napi_env env, +NAPI_EXTERN napi_status NAPI_CDECL napi_cancel_async_work(node_api_nogc_env env, napi_async_work work); // version management NAPI_EXTERN napi_status NAPI_CDECL -napi_get_node_version(napi_env env, const napi_node_version** version); +napi_get_node_version(node_api_nogc_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(napi_env env, struct uv_loop_s** loop); +napi_get_uv_event_loop(node_api_nogc_env env, struct uv_loop_s** loop); #endif // NAPI_VERSION >= 2 @@ -191,11 +191,11 @@ napi_get_uv_event_loop(napi_env env, struct uv_loop_s** loop); 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(napi_env env, napi_cleanup_hook fun, void* arg); +NAPI_EXTERN napi_status NAPI_CDECL napi_add_env_cleanup_hook( + node_api_nogc_env env, napi_cleanup_hook fun, void* arg); -NAPI_EXTERN napi_status NAPI_CDECL -napi_remove_env_cleanup_hook(napi_env env, napi_cleanup_hook fun, void* arg); +NAPI_EXTERN napi_status NAPI_CDECL napi_remove_env_cleanup_hook( + node_api_nogc_env env, napi_cleanup_hook fun, void* arg); NAPI_EXTERN napi_status NAPI_CDECL napi_open_callback_scope(napi_env env, @@ -238,18 +238,18 @@ 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(napi_env env, napi_threadsafe_function func); +NAPI_EXTERN napi_status NAPI_CDECL napi_unref_threadsafe_function( + node_api_nogc_env env, napi_threadsafe_function func); -NAPI_EXTERN napi_status NAPI_CDECL -napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func); +NAPI_EXTERN napi_status NAPI_CDECL napi_ref_threadsafe_function( + node_api_nogc_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(napi_env env, +napi_add_async_cleanup_hook(node_api_nogc_env env, napi_async_cleanup_hook hook, void* arg, napi_async_cleanup_hook_handle* remove_handle); @@ -262,7 +262,7 @@ napi_remove_async_cleanup_hook(napi_async_cleanup_hook_handle remove_handle); #if NAPI_VERSION >= 9 NAPI_EXTERN napi_status NAPI_CDECL -node_api_get_module_file_name(napi_env env, const char** result); +node_api_get_module_file_name(node_api_nogc_env env, const char** result); #endif // NAPI_VERSION >= 9 diff --git a/packages/emnapi/src/emnapi_internal.h b/packages/emnapi/src/emnapi_internal.h index cdcd4104..1901047a 100644 --- a/packages/emnapi/src/emnapi_internal.h +++ b/packages/emnapi/src/emnapi_internal.h @@ -72,11 +72,11 @@ EXTERN_C_END EXTERN_C_START -EMNAPI_INTERNAL_EXTERN napi_status napi_set_last_error(napi_env env, +EMNAPI_INTERNAL_EXTERN napi_status napi_set_last_error(node_api_nogc_env env, napi_status error_code, uint32_t engine_error_code, void* engine_reserved); -EMNAPI_INTERNAL_EXTERN napi_status napi_clear_last_error(napi_env env); +EMNAPI_INTERNAL_EXTERN napi_status napi_clear_last_error(node_api_nogc_env env); #ifdef __EMSCRIPTEN__ #if __EMSCRIPTEN_major__ * 10000 + __EMSCRIPTEN_minor__ * 100 + __EMSCRIPTEN_tiny__ >= 30114 // NOLINT diff --git a/packages/test/common.h b/packages/test/common.h index d15d53b0..ff370c6f 100644 --- a/packages/test/common.h +++ b/packages/test/common.h @@ -3,6 +3,19 @@ #include +#if !defined(__wasm__) || (defined(__EMSCRIPTEN__) || defined(__wasi__)) +#include +#include // abort() + +#define EPRINT(str) fprintf(stderr, "%s\n", (str)) + +#else +#include +void console_error(const char* fmt, const char* str); +#define abort() __builtin_trap() +#define EPRINT(str) console_error("%s", (str)) +#endif + // Empty value so that macros here are able to return NULL or void #define NODE_API_RETVAL_NOTHING // Intentionally blank #define @@ -22,6 +35,19 @@ } \ } while (0) +// The nogc version of GET_AND_THROW_LAST_ERROR. We cannot access any +// exceptions and we cannot fail by way of JS exception, so we abort. +#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + const char* err_message = error_info->error_message; \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + EPRINT(error_message); \ + abort(); \ + } while (0) + #define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ do { \ if (!(assertion)) { \ @@ -33,6 +59,15 @@ } \ } while (0) +#define NODE_API_NOGC_ASSERT_BASE(assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + EPRINT("assertion (" #assertion ") failed: " message); \ + abort(); \ + return ret_val; \ + } \ + } while (0) + // Returns NULL on failed assertion. // This is meant to be used inside napi_callback methods. #define NODE_API_ASSERT(env, assertion, message) \ @@ -43,6 +78,9 @@ #define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) +#define NODE_API_NOGC_ASSERT_RETURN_VOID(assertion, message) \ + NODE_API_NOGC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) + #define NODE_API_CALL_BASE(env, the_call, ret_val) \ do { \ if ((the_call) != napi_ok) { \ @@ -51,6 +89,14 @@ } \ } while (0) +#define NODE_API_NOGC_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + FATALLY_FAIL_WITH_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + // Returns NULL if the_call doesn't return napi_ok. #define NODE_API_CALL(env, the_call) \ NODE_API_CALL_BASE(env, the_call, NULL) @@ -59,6 +105,9 @@ #define NODE_API_CALL_RETURN_VOID(env, the_call) \ NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) +#define NODE_API_NOGC_CALL_RETURN_VOID(env, the_call) \ + NODE_API_NOGC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + #define NODE_API_CHECK_STATUS(the_call) \ do { \ napi_status status = (the_call); \ diff --git a/packages/test/finalizer/binding.c b/packages/test/finalizer/binding.c index 0034b0be..8326ccbd 100644 --- a/packages/test/finalizer/binding.c +++ b/packages/test/finalizer/binding.c @@ -18,17 +18,17 @@ typedef struct { napi_ref js_func; } FinalizerData; -static void finalizerOnlyCallback(napi_env env, +static void finalizerOnlyCallback(node_api_nogc_env env, void* finalize_data, void* finalize_hint) { FinalizerData* data = (FinalizerData*)finalize_data; int32_t count = ++data->finalize_count; // It is safe to access instance data - NODE_API_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); - NODE_API_ASSERT_RETURN_VOID(env, - count = data->finalize_count, - "Expected to be the same FinalizerData"); + NODE_API_NOGC_CALL_RETURN_VOID(env, + napi_get_instance_data(env, (void**)&data)); + NODE_API_NOGC_ASSERT_RETURN_VOID(count = data->finalize_count, + "Expected to be the same FinalizerData"); } static void finalizerCallingJSCallback(napi_env env, @@ -47,18 +47,20 @@ static void finalizerCallingJSCallback(napi_env env, } // Schedule async finalizer to run JavaScript-touching code. -static void finalizerWithJSCallback(napi_env env, +static void finalizerWithJSCallback(node_api_nogc_env env, void* finalize_data, void* finalize_hint) { - NODE_API_CALL_RETURN_VOID( + NODE_API_NOGC_CALL_RETURN_VOID( env, node_api_post_finalizer( env, finalizerCallingJSCallback, finalize_data, finalize_hint)); } -static void finalizerWithFailedJSCallback(napi_env env, +static void finalizerWithFailedJSCallback(node_api_nogc_env nogc_env, void* finalize_data, void* finalize_hint) { + // Intentionally cast to a napi_env to test the fatal failure. + napi_env env = (napi_env)nogc_env; napi_value obj; FinalizerData* data = (FinalizerData*)finalize_data; ++data->finalize_count; diff --git a/packages/test/ref_by_node_api_version/binding.c b/packages/test/ref_by_node_api_version/binding.c index 386fd86b..cd289310 100644 --- a/packages/test/ref_by_node_api_version/binding.c +++ b/packages/test/ref_by_node_api_version/binding.c @@ -9,12 +9,12 @@ void free(void* p); static uint32_t finalizeCount = 0; -static void FreeData(napi_env env, void* data, void* hint) { - NODE_API_ASSERT_RETURN_VOID(env, data != NULL, "Expects non-NULL data."); +static void FreeData(node_api_nogc_env env, void* data, void* hint) { + NODE_API_NOGC_ASSERT_RETURN_VOID(data != NULL, "Expects non-NULL data."); free(data); } -static void Finalize(napi_env env, void* data, void* hint) { +static void Finalize(node_api_nogc_env env, void* data, void* hint) { ++finalizeCount; } @@ -63,10 +63,10 @@ static napi_value ToUInt32Value(napi_env env, uint32_t value) { return result; } -static napi_status InitRefArray(napi_env env) { +static napi_status InitRefArray(node_api_nogc_env env) { // valueRefs array has one entry per napi_valuetype napi_ref* valueRefs = malloc(sizeof(napi_ref) * ((int)napi_bigint + 1)); - return napi_set_instance_data(env, valueRefs, &FreeData, NULL); + return napi_set_instance_data(env, valueRefs, (napi_finalize)&FreeData, NULL); } static napi_value CreateExternal(napi_env env, napi_callback_info info) { diff --git a/packages/test/runjs/binding.c b/packages/test/runjs/binding.c index 948ce821..ad3afdb0 100644 --- a/packages/test/runjs/binding.c +++ b/packages/test/runjs/binding.c @@ -6,9 +6,6 @@ #else void* malloc(size_t size); void free(void* p); -void abort() { - __builtin_trap(); -} #endif static void Finalize(napi_env env, void* data, void* hint) { @@ -28,6 +25,15 @@ static void Finalize(napi_env env, void* data, void* hint) { free(ref); } +static void NogcFinalize(node_api_nogc_env env, void* data, void* hint) { +#ifdef NAPI_EXPERIMENTAL + NODE_API_NOGC_CALL_RETURN_VOID( + env, node_api_post_finalizer(env, Finalize, data, hint)); +#else + Finalize(env, data, hint); +#endif +} + static napi_value CreateRef(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value cb; @@ -38,7 +44,7 @@ static napi_value CreateRef(napi_env env, napi_callback_info info) { NODE_API_CALL(env, napi_typeof(env, cb, &value_type)); NODE_API_ASSERT( env, value_type == napi_function, "argument must be function"); - NODE_API_CALL(env, napi_add_finalizer(env, cb, ref, Finalize, NULL, ref)); + NODE_API_CALL(env, napi_add_finalizer(env, cb, ref, NogcFinalize, NULL, ref)); return cb; } diff --git a/packages/test/string/binding.c b/packages/test/string/binding.c index d20a4168..31425e2a 100644 --- a/packages/test/string/binding.c +++ b/packages/test/string/binding.c @@ -23,10 +23,6 @@ size_t strlen(const char *s) for (; *s; s++); return s-a; } - -void abort() { - __builtin_trap(); -} #endif #define NAPI_EXPERIMENTAL @@ -116,7 +112,7 @@ static napi_value TestTwoByteImpl(napi_env env, return output; } -static void free_string(napi_env env, void* data, void* hint) { +static void free_string(node_api_nogc_env env, void* data, void* hint) { free(data); } diff --git a/packages/test/util.js b/packages/test/util.js index 8cf7eb73..b7903e80 100644 --- a/packages/test/util.js +++ b/packages/test/util.js @@ -111,6 +111,11 @@ function loadPath (request, options) { console.log(fmtString, ...args) return 0 } + importObject.env.console_error = function (fmt, ...args) { + const fmtString = UTF8ToString(fmt) + console.error(fmtString, ...args) + return 0 + } importObject.env.sleep = function (n) { const end = Date.now() + n * 1000 while (Date.now() < end) {