diff --git a/.github/workflows/es-actions.yml b/.github/workflows/es-actions.yml index 161a42375..988fa8569 100644 --- a/.github/workflows/es-actions.yml +++ b/.github/workflows/es-actions.yml @@ -660,8 +660,11 @@ jobs: submodules: true - name: Install Packages run: | + # for i386 ICU (x86 cctest needs 32-bit ICU at runtime via dlopen) + sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install -y ninja-build gcc-multilib g++-multilib + sudo apt-get install -y libicu-dev libicu-dev:i386 - name: Build x86/x64 env: BUILD_OPTIONS_X86: -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=x86 -DESCARGOT_MODE=debug -DESCARGOT_THREADING=ON -DESCARGOT_DEBUGGER=1 -DESCARGOT_USE_EXTENDED_API=ON -DESCARGOT_TEST=ON -DESCARGOT_OUTPUT=cctest -GNinja @@ -676,6 +679,44 @@ jobs: $RUNNER --arch=x86 --engine="$GITHUB_WORKSPACE/out/cctest/x86/cctest" cctest $RUNNER --arch=x86_64 --engine="$GITHUB_WORKSPACE/out/cctest/x64/cctest" cctest + # Separate from build-test-api: N-API support is an early PoC + # gated behind ESCARGOT_NAPI, and test/napi-tc is a sparse checkout of the + # nodejs/node monorepo (submodule.test/napi-tc.update=none in .gitmodules + # keeps every OTHER job's plain `submodules: true` from fetching it in full). + build-test-napi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Sparse checkout napi test corpus + run: | + NAPI_TC_SHA=$(git ls-tree HEAD test/napi-tc | awk '{print $3}') + if [ -z "$NAPI_TC_SHA" ]; then + echo "::error::test/napi-tc gitlink not found in HEAD tree." >&2 + echo "::error::Ensure the submodule gitlink is committed (git add test/napi-tc && git commit)." >&2 + exit 1 + fi + rm -rf test/napi-tc + git clone --filter=blob:none --no-checkout https://github.com/nodejs/node.git test/napi-tc + cd test/napi-tc + git sparse-checkout init --no-cone + git sparse-checkout set test/js-native-api test/node-api test/common src/js_native_api.h src/node_api.h src/js_native_api_types.h src/node_api_types.h + git checkout "$NAPI_TC_SHA" + - name: Install Packages + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libuv1-dev libicu-dev + - name: Build + env: + BUILD_OPTIONS: -DESCARGOT_MODE=debug -DESCARGOT_THREADING=1 -DESCARGOT_DEBUGGER=1 -DESCARGOT_USE_EXTENDED_API=ON -DESCARGOT_TEST=ON -DESCARGOT_OUTPUT=cctest -DESCARGOT_NAPI=ON -GNinja + run: | + cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -H. -Bout/cctest/napi $BUILD_OPTIONS + ninja -Cout/cctest/napi + - name: Run Test + run: | + $RUNNER --arch=x86_64 --engine="$GITHUB_WORKSPACE/out/cctest/napi/cctest" cctest + build-test-codecache: runs-on: ubuntu-latest steps: diff --git a/.gitmodules b/.gitmodules index 57da2c7ab..c6ab876d3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -33,3 +33,14 @@ [submodule "samples/rtos/freertos/FreeRTOS-Kernel"] path = samples/rtos/freertos/FreeRTOS-Kernel url = https://github.com/FreeRTOS/FreeRTOS-Kernel.git +[submodule "test/napi-tc"] + path = test/napi-tc + url = https://github.com/nodejs/node.git + ignore = untracked + shallow = true + # this points at the nodejs/node monorepo; a plain `submodule update` + # would check out its ENTIRE tree (large). `update = none` makes + # generic `git submodule update --init [--recursive]` (what every other + # CI job's `submodules: true` checkout runs) skip it automatically. + # Only fetch it deliberately with a sparse checkout. + update = none diff --git a/CMakeLists.txt b/CMakeLists.txt index 0737143f0..2575f179a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,4 +101,5 @@ MESSAGE(STATUS "ESCARGOT_EXPORT_ALL: " ${ESCARGOT_EXPORT_ALL}) MESSAGE(STATUS "ESCARGOT_TCO: " ${ESCARGOT_TCO}) MESSAGE(STATUS "ESCARGOT_TEMPORAL: " ${ESCARGOT_TEMPORAL}) MESSAGE(STATUS "ESCARGOT_SHADOWREALM: " ${ESCARGOT_SHADOWREALM}) +MESSAGE(STATUS "ESCARGOT_NAPI: " ${ESCARGOT_NAPI}) MESSAGE(STATUS "ESCARGOT_TEST: " ${ESCARGOT_TEST}) diff --git a/build/config.cmake b/build/config.cmake index fd13852ab..4f62dfd2f 100644 --- a/build/config.cmake +++ b/build/config.cmake @@ -71,7 +71,7 @@ SET (CFLAGS_FROM_ENV ${CFLAGS_FROM_ENV} ${ESCARGOT_CFLAGS_FROM_EXTERNAL}) SET (LDFLAGS_FROM_ENV ${LDFLAGS_FROM_ENV} ${ESCARGOT_LDFLAGS_FROM_EXTERNAL}) # ESCARGOT COMMON LDFLAGS -SET (ESCARGOT_LDFLAGS ${ESCARGOT_LDFLAGS} -fvisibility=hidden) +# Note: -fvisibility=hidden is set in target.cmake for the compiler # bdwgc IF (${ESCARGOT_MODE} STREQUAL "debug") @@ -127,6 +127,12 @@ IF (ESCARGOT_LIBICU_SUPPORT) ENDIF() ENDIF() +# napi_define_class needs FunctionTemplateRef, which is gated behind +# ENABLE_EXTENDED_API; default it on for NAPI unless the caller already chose +IF (ESCARGOT_NAPI AND NOT DEFINED ESCARGOT_USE_EXTENDED_API) + SET (ESCARGOT_USE_EXTENDED_API ON) +ENDIF() + IF (ESCARGOT_USE_EXTENDED_API) SET (ESCARGOT_DEFINITIONS ${ESCARGOT_DEFINITIONS} -DENABLE_EXTENDED_API) ENDIF() @@ -188,6 +194,33 @@ IF (ESCARGOT_SHADOWREALM) SET (ESCARGOT_DEFINITIONS ${ESCARGOT_DEFINITIONS} -DENABLE_SHADOWREALM) ENDIF() +IF (ESCARGOT_NAPI) + SET (ESCARGOT_DEFINITIONS ${ESCARGOT_DEFINITIONS} -DENABLE_NAPI) + # Target Node-API version 10: unlocks the additive v9/v10 declarations + # (node_api_create_syntax_error, property-key/external-string helpers, ...) + # in the vendored headers, which otherwise default to NAPI_VERSION 8. Also + # what napi_get_version reports. + SET (ESCARGOT_DEFINITIONS ${ESCARGOT_DEFINITIONS} -DNAPI_VERSION=10) + SET (ESCARGOT_INCDIRS ${ESCARGOT_INCDIRS} ${ESCARGOT_ROOT}/test/napi-tc/src) + SET (ESCARGOT_LIBRARIES ${ESCARGOT_LIBRARIES} dl) + # napi_* symbols live in this binary itself; addons are dlopen()'d with + # unresolved napi_* references (same as real Node, which also needs + # -rdynamic on its own executable for the same reason). Each napi_* + # definition carries its own default-visibility attribute (see + # ESCARGOT_NAPI_EXPORT in NapiTypes.h) instead of overriding + # -fvisibility=hidden for the whole binary. + SET (ESCARGOT_LDFLAGS ${ESCARGOT_LDFLAGS} -rdynamic) + + # async_work/threadsafe_function (NapiAsyncWork.cpp) and + # napi_get_uv_event_loop (NapiRuntime.cpp) are backed by a real system + # libuv event loop - link it via pkg-config, scoped to ESCARGOT_NAPI only + # so non-napi builds stay unaffected. + PKG_CHECK_MODULES (LIBUV REQUIRED libuv) + SET (ESCARGOT_INCDIRS ${ESCARGOT_INCDIRS} ${LIBUV_INCLUDE_DIRS}) + SET (ESCARGOT_LIBRARIES ${ESCARGOT_LIBRARIES} ${LIBUV_LIBRARIES}) + SET (ESCARGOT_LDFLAGS ${ESCARGOT_LDFLAGS} ${LIBUV_LDFLAGS}) +ENDIF() + IF (ESCARGOT_TLS_ACCESS_BY_ADDRESS) SET (ESCARGOT_DEFINITIONS ${ESCARGOT_DEFINITIONS} -DENABLE_TLS_ACCESS_BY_ADDRESS) ENDIF() diff --git a/build/escargot.cmake b/build/escargot.cmake index baec6e26b..2c31e47af 100644 --- a/build/escargot.cmake +++ b/build/escargot.cmake @@ -66,7 +66,7 @@ IF (${ESCARGOT_OUTPUT} STREQUAL "cctest") SET (INSTALL_GTEST OFF) ADD_COMPILE_OPTIONS(${ESCARGOT_THIRDPARTY_CFLAGS}) ADD_SUBDIRECTORY (third_party/googletest) - FILE (GLOB CCTEST_SRC ${ESCARGOT_ROOT}/test/cctest/testapi.cpp) + FILE (GLOB CCTEST_SRC ${ESCARGOT_ROOT}/test/cctest/*.cpp) ENDIF() SET (ESCARGOT_SRC_LIST @@ -303,8 +303,204 @@ ELSEIF (${ESCARGOT_OUTPUT} STREQUAL "static_lib") ELSEIF (${ESCARGOT_OUTPUT} STREQUAL "cctest") ADD_EXECUTABLE (${ESCARGOT_CCTEST_TARGET} ${ESCARGOT_SRC_LIST}) - TARGET_LINK_LIBRARIES (${ESCARGOT_CCTEST_TARGET} PRIVATE ${ESCARGOT_LIBRARIES} ${ESCARGOT_LDFLAGS} ${LDFLAGS_FROM_ENV} gtest) + TARGET_LINK_LIBRARIES (${ESCARGOT_CCTEST_TARGET} PRIVATE ${ESCARGOT_LIBRARIES} ${ESCARGOT_LDFLAGS} ${LDFLAGS_FROM_ENV} gtest gtest_main) TARGET_INCLUDE_DIRECTORIES (${ESCARGOT_CCTEST_TARGET} PRIVATE ${ESCARGOT_INCDIRS}) TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE ${ESCARGOT_DEFINITIONS}) TARGET_COMPILE_OPTIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE ${ESCARGOT_CXXFLAGS} ${CXXFLAGS_FROM_ENV}) + + IF (ESCARGOT_NAPI) + # build real Node-API TCs (vendored under test/napi-tc) into .so files the + # cctest binary dlopen()s directly. Add the TC's source file(s), relative + # to test/js-native-api/, here as more TCs get supported; join multiple + # sources for one addon with `|` (see 7_factory_wrap). The .so is named + # after the FIRST source's stem (matching its binding.gyp target_name), + # not necessarily its containing directory. + SET (NAPI_TEST_ADDON_DIR ${CMAKE_BINARY_DIR}/napi_test_addons) + SET (NAPI_TEST_TC_ENTRIES + 2_function_arguments/2_function_arguments.c + 3_callbacks/3_callbacks.c + 4_object_factory/4_object_factory.c + 5_function_factory/5_function_factory.c + 6_object_wrap/myobject.cc + 7_factory_wrap/7_factory_wrap.cc|7_factory_wrap/myobject.cc + test_handle_scope/test_handle_scope.c + # --- test/cctest/napi_harness (NapiSuite.*) additions below --- + test_number/test_number.c|test_number/test_null.c + test_string/test_string.c|test_string/test_null.c + test_object/test_exceptions.c + test_array/test_array.c + test_conversions/test_conversions.c|test_conversions/test_null.c + test_properties/test_properties.c + test_constructor/test_constructor.c|test_constructor/test_null.c + test_symbol/test_symbol.c + test_bigint/test_bigint.c + test_error/test_error.c + test_exception/test_exception.c + test_typedarray/test_typedarray.c + test_typedarray/test_typedarray_sharedarraybuffer.c + test_date/test_date.c + test_new_target/test_new_target.c + test_reference/test_reference.c + # test_reference/binding.gyp and test_finalizer/binding.gyp each + # independently declare a target_name "test_finalizer" backed by + # a *different* source file; disambiguate the one from + # test_reference/ with an explicit output name (see the `=` + # override support in the loop below) so it doesn't collide with + # test_finalizer/test_finalizer.c's own entry name. + test_reference/test_finalizer.c=test_reference_test_finalizer + test_promise/test_promise.c + test_function/test_function.c + test_instance_data/test_instance_data.c + 8_passing_wrapped/8_passing_wrapped.cc|8_passing_wrapped/myobject.cc + # A second, independently-built copy of 7_factory_wrap's own addon + # (identical source, different output .so) - NapiSuite.FactoryWrap + # (testnapi_suite.cpp) dlopen()s *this* one instead of the same + # .so Napi.FactoryWrap (testnapi.cpp) uses, so the two suites don't + # share that addon's static `finalizeCount`/`instanceCount` + # counters (see the NapiSuite report's cross-suite isolation note - + # neither suite ever dlclose()s, and test.js's own + # `assert.strictEqual(test.finalizeCount, 0)` at the top requires + # a fresh-from-zero counter every run). + 7_factory_wrap/7_factory_wrap.cc|7_factory_wrap/myobject.cc=7_factory_wrap_napisuite + # These four now compile (node_api_create_object_with_properties/ + # node_api_set_prototype/node_api_post_finalizer/ + # node_api_is_sharedarraybuffer are declared - see the + # node_api_is_sharedarraybuffer are declared in Node.js's own + # src/js_native_api.h (fetched via the test/napi-tc submodule's + # sparse checkout, not the standalone node-api-headers package + # which lags behind) - and implemented + # in src/napi/NapiExtras.cpp). test_general/testEnvCleanup.js and + # test_finalizer/test_fatal_finalize.js are now wired into + # NapiSuite.* TESTs (test/cctest/testnapi_suite.cpp) - both are + # child_process.spawnSync-based self-respawn tests, supported via + # the harness's single-test CLI mode (--napi-run, same file). + # test_general/test.js and test_finalizer/test.js themselves (as + # opposed to the specific files above) still aren't wired in + # (worker_threads-style respawn, which this harness has no shim + # for); test_object/test.js and test_dataview/test.js are simply + # not yet ported. Listed here only to keep them compiling as + # further evidence the addon-level gap is closed. + test_object/test_object.c + test_general/test_general.c + test_dataview/test_dataview.c + test_finalizer/test_finalizer.c + # Now ported to real test.js under NapiSuite.* (test/cctest/ + # testnapi_suite.cpp): test_dataview/test.js (above), + # test_sharedarraybuffer/test.js (needs node_api_create_ + # sharedarraybuffer, implemented in src/napi/NapiExtras.cpp and + # declared in Node.js's own src/js_native_api.h), and + # test_reference_double_free/test.js (standard wrap/remove_wrap + # double-free regression, no new API needed). + test_sharedarraybuffer/test_sharedarraybuffer.c + test_reference_double_free/test_reference_double_free.c + ) + # Tier 1 & 2 node-api/ addons (real test.js under NapiSuite.NodeApi*). + # Same entry grammar as NAPI_TEST_TC_ENTRIES, but sources resolve + # under test/napi-tc/test/node-api/ instead of js-native-api/. Many + # node-api addons name their single source binding.c/binding.cc, so + # the =explicitName override (derived name would collide as "binding") + # is used heavily here; test_general/test_exception also exist under + # js-native-api/, so their node-api builds get a node_api_ prefix to + # avoid the shared SO_PATH compile-def macro colliding. + SET (NAPI_TEST_TC_ENTRIES_NODE_API + test_uv_loop/test_uv_loop.cc + # Tier 1 (no worker_threads/async_hooks/child_process deps) + test_env_teardown_gc/binding.c=test_env_teardown_gc + test_fatal_exception/test_fatal_exception.c + test_init_order/test_init_order.cc + test_make_callback/binding.c=test_make_callback + test_make_callback_recurse/binding.c=test_make_callback_recurse + test_callback_scope/binding.c=test_callback_scope + # test_buffer deferred: this vendored copy's test_buffer.c calls the + # non-standard node_api_create_external_sharedarraybuffer and the + # test.js uses the global Buffer (not provided by this harness). + test_threadsafe_function_abort/binding.cc=test_threadsafe_function_abort + # Tier 2 (child_process self-respawn via the harness --napi-run shim) + test_async/test_async.c + test_cleanup_hook/binding.c=test_cleanup_hook + test_fatal/test_fatal.c + test_threadsafe_function/binding.c=test_threadsafe_function + test_threadsafe_function_shutdown/binding.cc=test_threadsafe_function_shutdown + ) + SET (NAPI_TEST_ADDON_SOS) + # process one entries list, resolving each source relative to + # test/napi-tc/test/${_subdir}/ (js-native-api or node-api). + MACRO (BUILD_NAPI_TEST_ADDONS _subdir) + FOREACH (NAPI_TEST_TC_ENTRY ${ARGN}) + # optional `=explicitName` suffix to override the derived output + # name, for the rare case where two addons in different test + # directories legitimately share a binding.gyp target_name (and + # would otherwise produce the same .so path/compile-def macro) + STRING (REPLACE "=" ";" NAPI_TEST_TC_ENTRY_PARTS ${NAPI_TEST_TC_ENTRY}) + LIST (LENGTH NAPI_TEST_TC_ENTRY_PARTS NAPI_TEST_TC_ENTRY_NPARTS) + LIST (GET NAPI_TEST_TC_ENTRY_PARTS 0 NAPI_TEST_TC_ENTRY_SRCPART) + + STRING (REPLACE "|" ";" NAPI_TEST_TC_SRC_RELS ${NAPI_TEST_TC_ENTRY_SRCPART}) + SET (NAPI_TEST_TC_SRCS) + FOREACH (NAPI_TEST_TC_SRC_REL ${NAPI_TEST_TC_SRC_RELS}) + LIST (APPEND NAPI_TEST_TC_SRCS ${ESCARGOT_ROOT}/test/napi-tc/test/${_subdir}/${NAPI_TEST_TC_SRC_REL}) + ENDFOREACH() + + LIST (GET NAPI_TEST_TC_SRC_RELS 0 NAPI_TEST_TC_FIRST_SRC_REL) + IF (${NAPI_TEST_TC_ENTRY_NPARTS} GREATER 1) + LIST (GET NAPI_TEST_TC_ENTRY_PARTS 1 NAPI_TEST_TC_NAME) + ELSE() + GET_FILENAME_COMPONENT (NAPI_TEST_TC_NAME ${NAPI_TEST_TC_FIRST_SRC_REL} NAME_WE) + ENDIF() + GET_FILENAME_COMPONENT (NAPI_TEST_TC_EXT ${NAPI_TEST_TC_FIRST_SRC_REL} EXT) + SET (NAPI_TEST_TC_SO ${NAPI_TEST_ADDON_DIR}/${NAPI_TEST_TC_NAME}.so) + + IF (${NAPI_TEST_TC_EXT} STREQUAL ".c") + SET (NAPI_TEST_TC_COMPILER ${CMAKE_C_COMPILER}) + ELSE() + SET (NAPI_TEST_TC_COMPILER ${CMAKE_CXX_COMPILER}) + ENDIF() + + ADD_CUSTOM_COMMAND ( + OUTPUT ${NAPI_TEST_TC_SO} + COMMAND ${CMAKE_COMMAND} -E make_directory ${NAPI_TEST_ADDON_DIR} + COMMAND ${NAPI_TEST_TC_COMPILER} -shared -fPIC -DNAPI_VERSION=10 -I${ESCARGOT_ROOT}/test/napi-tc/src ${NAPI_TEST_TC_SRCS} -o ${NAPI_TEST_TC_SO} + DEPENDS ${NAPI_TEST_TC_SRCS} + COMMENT "Building napi test addon ${NAPI_TEST_TC_NAME}.so" + ) + LIST (APPEND NAPI_TEST_ADDON_SOS ${NAPI_TEST_TC_SO}) + + STRING (TOUPPER ${NAPI_TEST_TC_NAME} NAPI_TEST_TC_NAME_UPPER) + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_${NAPI_TEST_TC_NAME_UPPER}_SO_PATH="${NAPI_TEST_TC_SO}") + ENDFOREACH() + ENDMACRO() + BUILD_NAPI_TEST_ADDONS (js-native-api ${NAPI_TEST_TC_ENTRIES}) + BUILD_NAPI_TEST_ADDONS (node-api ${NAPI_TEST_TC_ENTRIES_NODE_API}) + + # Custom (non-upstream) test addons live in the main repo tree under + # test/cctest/napi_custom_addons/, NOT in the test/napi-tc submodule + # (which is a sparse checkout of nodejs/node and gets re-cloned in CI). + # Build them the same way as the macro above, just from a different + # source root. + SET (NAPI_CUSTOM_ADDON_DIR ${ESCARGOT_ROOT}/test/cctest/napi_custom_addons) + SET (NAPI_CUSTOM_SYMBOL_VERIFY_SO ${NAPI_TEST_ADDON_DIR}/test_symbol_verify.so) + SET (NAPI_CUSTOM_SYMBOL_VERIFY_SRC ${NAPI_CUSTOM_ADDON_DIR}/test_symbol_verify/test_symbol_verify.c) + ADD_CUSTOM_COMMAND ( + OUTPUT ${NAPI_CUSTOM_SYMBOL_VERIFY_SO} + COMMAND ${CMAKE_COMMAND} -E make_directory ${NAPI_TEST_ADDON_DIR} + COMMAND ${CMAKE_C_COMPILER} -shared -fPIC -DNAPI_VERSION=10 -I${ESCARGOT_ROOT}/test/napi-tc/src -I${ESCARGOT_ROOT}/test/napi-tc/test/js-native-api ${NAPI_CUSTOM_SYMBOL_VERIFY_SRC} -o ${NAPI_CUSTOM_SYMBOL_VERIFY_SO} + DEPENDS ${NAPI_CUSTOM_SYMBOL_VERIFY_SRC} + COMMENT "Building napi custom test addon test_symbol_verify.so" + ) + LIST (APPEND NAPI_TEST_ADDON_SOS ${NAPI_CUSTOM_SYMBOL_VERIFY_SO}) + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_TEST_SYMBOL_VERIFY_SO_PATH="${NAPI_CUSTOM_SYMBOL_VERIFY_SO}") + + ADD_CUSTOM_TARGET (napi_test_addons ALL DEPENDS ${NAPI_TEST_ADDON_SOS}) + ADD_DEPENDENCIES (${ESCARGOT_CCTEST_TARGET} napi_test_addons) + + # test/cctest/testnapi_suite.cpp (NapiSuite.*): the JS compatibility + # harness (require()/assert/common shims) and the base directory the + # real Node js-native-api test.js files live under, so the suite can + # resolve `test_number` -> `/test_number/test.js` etc. at runtime + # without hardcoding the repo layout into the .cpp itself. + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_HARNESS_JS_PATH="${ESCARGOT_ROOT}/test/cctest/napi_harness/harness.js") + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_TC_JS_DIR="${ESCARGOT_ROOT}/test/napi-tc/test/js-native-api") + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_TC_NODE_API_JS_DIR="${ESCARGOT_ROOT}/test/napi-tc/test/node-api") + TARGET_COMPILE_DEFINITIONS (${ESCARGOT_CCTEST_TARGET} PRIVATE NAPI_CUSTOM_ADDON_JS_DIR="${ESCARGOT_ROOT}/test/cctest/napi_custom_addons") + ENDIF() ENDIF() diff --git a/src/api/EscargotPublic.cpp b/src/api/EscargotPublic.cpp index aa8095b6d..3c0a16723 100644 --- a/src/api/EscargotPublic.cpp +++ b/src/api/EscargotPublic.cpp @@ -776,6 +776,17 @@ uint32_t PersistentValueRefMap::remove(ValueRef* ptr) } else { if (iter.value().asUInt32() == 1) { self->erase(iter); + // tsl::robin_map's erase() only marks the bucket empty (it runs + // the stored pair's destructor, a no-op for a raw pointer key) - + // it does not clear the bucket's memory. Since this map's + // backing array is itself GC-managed and thus conservatively + // scanned, that leftover ValueRef* bit pattern can still look + // like a live root, keeping `ptr` alive until some unrelated + // future insert() happens to reuse that exact bucket. Force a + // fresh (GC_malloc-zeroed) backing array now instead of relying + // on that, so a fully-unrooted value becomes collectible right + // away, as callers of this "0 means unrooted" API expect. + self->rehash(0); return 0; } else { iter.value() = EncodedValue(iter.value().asUInt32() - 1); @@ -4940,6 +4951,11 @@ FunctionTemplateRef* FunctionTemplateRef::create(AtomicStringRef* name, size_t a return toRef(new FunctionTemplate(toImpl(name), argumentCount, isStrict, isConstructor, fn)); } +ObjectRef* FunctionTemplateRef::instantiate(ContextRef* ctx, bool addToContextCache) +{ + return toRef(toImpl(this)->instantiate(toImpl(ctx), addToContextCache)); +} + void FunctionTemplateRef::setName(AtomicStringRef* name) { toImpl(this)->setName(toImpl(name)); diff --git a/src/api/EscargotPublic.h b/src/api/EscargotPublic.h index 1c0f05890..dab4bf8f1 100644 --- a/src/api/EscargotPublic.h +++ b/src/api/EscargotPublic.h @@ -2165,6 +2165,15 @@ class ESCARGOT_EXPORT FunctionTemplateRef : public TemplateRef { void setName(AtomicStringRef* name); void setLength(size_t length); + // Like TemplateRef::instantiate(ctx), but when addToContextCache is false + // the resulting function is NOT retained in the context's instantiated- + // function cache. Use false for throwaway templates created dynamically and + // unboundedly (per-call), so they remain collectible instead of leaking for + // the context's lifetime. (The cache only benefits a fixed set of templates + // that need a stable per-context identity.) + using TemplateRef::instantiate; // keep the inherited instantiate(ctx) visible alongside this overload + ObjectRef* instantiate(ContextRef* ctx, bool addToContextCache); + void updateCallbackFunction(FunctionTemplateRef::NativeFunctionPointer fn); ObjectTemplateRef* prototypeTemplate(); diff --git a/src/interpreter/ByteCodeInterpreter.cpp b/src/interpreter/ByteCodeInterpreter.cpp index 5b8cdf6b3..a6ffaf190 100644 --- a/src/interpreter/ByteCodeInterpreter.cpp +++ b/src/interpreter/ByteCodeInterpreter.cpp @@ -2894,7 +2894,7 @@ NEVER_INLINE void InterpreterSlowPath::setObjectPreComputedCaseOperationCacheMis if (code->m_isLength && originalObject->isArrayObject()) { if (LIKELY(originalObject->asArrayObject()->isFastModeArray())) { if (!originalObject->asArrayObject()->setArrayLength(state, value) && state.inStrictMode()) { - ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, code->m_propertyName.toExceptionString(), false, String::emptyString(), ErrorObject::Messages::DefineProperty_NotWritable); + Object::throwCannotWriteError(state, originalObject, code->m_propertyName); } } else { originalObject->setThrowsExceptionWhenStrictMode(state, ObjectPropertyName(state, code->m_propertyName), value, willBeObject); @@ -3009,7 +3009,9 @@ NEVER_INLINE void InterpreterSlowPath::setObjectPreComputedCaseOperationCacheMis code->m_missCount = SetObjectInlineCacheData::MaxCacheMissCount + 1; if (state.inStrictMode()) { // throw exception - originalObject->throwCannotWriteError(state, code->m_propertyName); + Object* tagObject = willBeObject.isObject() ? willBeObject.asObject() : originalObject; + bool isGetterOnlyAccessor = Object::isGetterOnlyAccessorProperty(state, originalObject, ObjectPropertyName(state, code->m_propertyName)); + Object::throwCannotWriteError(state, tagObject, code->m_propertyName, isGetterOnlyAccessor); } return; } @@ -3962,12 +3964,16 @@ NEVER_INLINE void InterpreterSlowPath::complexSetObjectOperation(ExecutionState& // [...] // Let succeeded be ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)). // If succeeded is false and IsStrictReference(V) is true, throw a TypeError exception. - bool result = object.toObject(state)->set(state, ObjectPropertyName(state, registerFile[code->m_propertyNameIndex]), registerFile[code->m_loadRegisterIndex], thisValue); + Object* superBaseObject = object.toObject(state); + ObjectPropertyName superPropertyName(state, registerFile[code->m_propertyNameIndex]); + bool result = superBaseObject->set(state, superPropertyName, registerFile[code->m_loadRegisterIndex], thisValue); if (UNLIKELY(!result)) { // testing is strict mode || IsStrictReference(V) // IsStrictReference returns true if code is class method if (state.inStrictMode() || !state.resolveCallee()->codeBlock()->asInterpretedCodeBlock()->isObjectMethod()) { - ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, ObjectPropertyName(state, registerFile[code->m_propertyNameIndex]).toExceptionString(), false, String::emptyString(), ErrorObject::Messages::DefineProperty_NotWritable); + Object* tagObject = thisValue.isObject() ? thisValue.asObject() : superBaseObject; + bool isGetterOnlyAccessor = Object::isGetterOnlyAccessorProperty(state, superBaseObject, superPropertyName); + Object::throwCannotWriteError(state, tagObject, superPropertyName.toObjectStructurePropertyName(state), isGetterOnlyAccessor); } } } else { @@ -5565,7 +5571,9 @@ NEVER_INLINE void InterpreterSlowPath::setObjectOpcodeSlowCase(ExecutionState& s bool result = obj->setIndexedProperty(state, property, registerFile[code->m_loadRegisterIndex]); if (UNLIKELY(!result) && state.inStrictMode()) { - Object::throwCannotWriteError(state, ObjectStructurePropertyName(state, property.toString(state))); + String* propertyNameString = property.toString(state); + bool isGetterOnlyAccessor = Object::isGetterOnlyAccessorProperty(state, obj, ObjectPropertyName(state, propertyNameString)); + Object::throwCannotWriteError(state, obj, ObjectStructurePropertyName(state, propertyNameString), isGetterOnlyAccessor); } } diff --git a/src/napi/NapiArrayBuffer.cpp b/src/napi/NapiArrayBuffer.cpp new file mode 100644 index 000000000..c6e8e64a5 --- /dev/null +++ b/src/napi/NapiArrayBuffer.cpp @@ -0,0 +1,618 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// ArrayBuffer/TypedArray/DataView/External/type-tag/Buffer slice of +// js_native_api.h + node_api.h, following the same patterns as +// NapiFunctions.cpp (see especially napi_wrap/setExtraData/finalizer +// registration there). + +#include "NapiTypes.h" + +#include +#include +#include + +namespace Escargot { +namespace Napi { + +// shared by napi_create_external_arraybuffer and napi_create_external_buffer: +// bridges Escargot's BackingStoreRefDeleterCallback (void*, size_t, void*) +// back to the N-API node_api_basic_finalize (env, data, hint) contract, +// carrying just enough state to make that call once the backing store itself +// is torn down. +struct ExternalBackingStoreFinalizeData { + napi_env env; + node_api_basic_finalize finalizeCb; + void* finalizeHint; +}; + +static void NapiExternalBackingStoreDeleter(void* data, size_t length, void* deleterData) +{ + ExternalBackingStoreFinalizeData* finalizeData = reinterpret_cast(deleterData); + if (finalizeData->finalizeCb != nullptr) { + finalizeData->finalizeCb(finalizeData->env, data, finalizeData->finalizeHint); + } + delete finalizeData; +} + +// backs napi_create_external: an ObjectRef with the user's pointer stashed in +// the same extraData() slot napi_wrap uses (see NapiFunctions.cpp), plus a GC +// finalizer that invokes the user's finalize_cb once the object is collected +// - same registration pattern as NapiWrapFinalizer/WrapFinalizeData there. +struct ExternalObjectFinalizeData { + napi_env env; + node_api_basic_finalize finalizeCb; + void* nativeData; + void* finalizeHint; +}; + +static void NapiExternalObjectFinalizer(void* self, void* data) +{ + ExternalObjectFinalizeData* finalizeData = reinterpret_cast(data); + napi_env env = finalizeData->env; + finalizeData->finalizeCb(env, finalizeData->nativeData, finalizeData->finalizeHint); + + if (env->pendingException.hasValue()) { + ValueRef* fatalErr = env->pendingException.value(); + env->pendingException = nullptr; + napi_fatal_exception(env, ToNapi(fatalErr)); + } + + delete finalizeData; +} + +// napi_type_tag_object/napi_check_object_type_tag: Escargot has no spare +// per-object slot left for this (extraData() is already napi_wrap's/ +// napi_create_external's), so tags are kept in this file-local side table +// instead, keyed by the object's raw pointer. A GC finalizer erases the +// entry once the object is collected (registered in napi_type_tag_object +// below), so a later, unrelated object allocated at the same address can't +// spuriously read as tagged. This is still not as robust as a real engine +// slot would be: if the finalizer somehow doesn't run before the address is +// reused (it always should for Boehm-GC'd objects here, but this is a +// PoC-level assumption, not a guarantee enforced by the type system) a +// stale/incorrect tag could be observed. Fine for this PoC's scope. +static std::unordered_map> g_typeTags; + +static void NapiTypeTagFinalizer(void* self, void* data) +{ + g_typeTags.erase(reinterpret_cast(self)); +} + +static size_t NapiTypedArrayElementSize(napi_typedarray_type type) +{ + switch (type) { + case napi_int8_array: + case napi_uint8_array: + case napi_uint8_clamped_array: + return 1; + case napi_int16_array: + case napi_uint16_array: + case napi_float16_array: + return 2; + case napi_int32_array: + case napi_uint32_array: + case napi_float32_array: + return 4; + case napi_float64_array: + case napi_bigint64_array: + case napi_biguint64_array: + default: + return 8; + } +} + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_is_arraybuffer(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = FromNapi(value)->isArrayBuffer(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_arraybuffer(napi_env env, size_t byte_length, void** data, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + + ArrayBufferObjectRef* buf = ArrayBufferObjectRef::create(state); + buf->allocateBuffer(state, byte_length); + + if (data != nullptr) { + *data = buf->rawBuffer(); + } + *result = ToNapi(buf); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + ExecutionStateRef* state = env->executionState; + + ExternalBackingStoreFinalizeData* finalizeData = new ExternalBackingStoreFinalizeData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_cb; + finalizeData->finalizeHint = finalize_hint; + + BackingStoreRef* backingStore = BackingStoreRef::createNonSharedBackingStore(external_data, byte_length, NapiExternalBackingStoreDeleter, finalizeData); + + ArrayBufferObjectRef* buf = ArrayBufferObjectRef::create(state); + buf->attachBuffer(backingStore); + + *result = ToNapi(buf); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_arraybuffer_info(napi_env env, napi_value arraybuffer, void** data, size_t* byte_length) +{ + ValueRef* v = FromNapi(arraybuffer); + if (!v->isArrayBuffer()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferRef* buf = v->asArrayBuffer(); + + if (data != nullptr) { + *data = buf->rawBuffer(); + } + if (byte_length != nullptr) { + *byte_length = buf->byteLength(); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_detach_arraybuffer(napi_env env, napi_value arraybuffer) +{ + ValueRef* v = FromNapi(arraybuffer); + if (!v->isArrayBufferObject()) { + return SetLastError(env, napi_invalid_arg); + } + v->asArrayBufferObject()->detachArrayBuffer(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_detached_arraybuffer(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isArrayBufferObject()) { + *result = false; + return napi_ok; + } + + ArrayBufferObjectRef* buf = v->asArrayBufferObject(); + // A real detach (napi_detach_arraybuffer/ArrayBuffer.prototype.transfer) + // sets isDetachedBuffer() - but an ArrayBuffer whose backing store was + // never attached to real memory in the first place (e.g. + // napi_create_external_arraybuffer(env, NULL, 0, ...), test_typedarray/ + // test.js's NullArrayBuffer()) reports isDetachedBuffer() == false (it + // was never attached-then-detached; it just has a null data pointer) yet + // Node-API's own napi_is_detached_arraybuffer treats a null backing + // store's data pointer as detached too - so check rawBuffer() directly as + // well, not just the explicit-detach flag. + *result = buf->isDetachedBuffer() || buf->rawBuffer() == nullptr; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_typedarray(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = FromNapi(value)->isTypedArrayObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_typedarray(napi_env env, napi_typedarray_type type, size_t length, napi_value arraybuffer, size_t byte_offset, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + + ValueRef* bufValue = FromNapi(arraybuffer); + if (!bufValue->isArrayBuffer()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferRef* buf = bufValue->asArrayBuffer(); + + size_t elementSize = NapiTypedArrayElementSize(type); + size_t byteLength = length * elementSize; + + // Per the typed array spec (and this API's own documented contract): + // - a view may not extend past the end of its backing buffer, and + // - byte_offset must be a multiple of the view's element size. + // Neither was previously checked - view->setBuffer() below happily wires + // up an out-of-bounds or misaligned view - so callers silently got a + // view that reads/writes past the buffer's storage (or at the wrong + // offset) instead of the RangeError real Node-API raises (found via + // test_typedarray/test.js's `CreateTypedArray(template, buffer, 0, 136)` + // on a 128-byte buffer, and `CreateTypedArray(template, buffer, + // currentType.BYTES_PER_ELEMENT + 1, 1)`, both expected to throw + // RangeError). + if (byte_offset + byteLength > buf->byteLength()) { + env->pendingException = ErrorObjectRef::create(state, ErrorObjectRef::RangeError, StringRef::createFromASCII("byte_offset + length must be smaller than the size in bytes of the array passed in")); + return SetLastError(env, napi_pending_exception); + } + if (elementSize > 1 && (byte_offset % elementSize) != 0) { + env->pendingException = ErrorObjectRef::create(state, ErrorObjectRef::RangeError, StringRef::createFromASCII("start offset of typed array must be a multiple of the byte length of the element type")); + return SetLastError(env, napi_pending_exception); + } + + ArrayBufferViewRef* view; + switch (type) { + case napi_int8_array: + view = Int8ArrayObjectRef::create(state); + break; + case napi_uint8_array: + view = Uint8ArrayObjectRef::create(state); + break; + case napi_uint8_clamped_array: + view = Uint8ClampedArrayObjectRef::create(state); + break; + case napi_int16_array: + view = Int16ArrayObjectRef::create(state); + break; + case napi_uint16_array: + view = Uint16ArrayObjectRef::create(state); + break; + case napi_int32_array: + view = Int32ArrayObjectRef::create(state); + break; + case napi_uint32_array: + view = Uint32ArrayObjectRef::create(state); + break; + case napi_float32_array: + view = Float32ArrayObjectRef::create(state); + break; + case napi_float64_array: + view = Float64ArrayObjectRef::create(state); + break; + case napi_bigint64_array: + view = BigInt64ArrayObjectRef::create(state); + break; + case napi_biguint64_array: + view = BigUint64ArrayObjectRef::create(state); + break; + case napi_float16_array: + view = Float16ArrayObjectRef::create(state); + break; + default: + return SetLastError(env, napi_invalid_arg); + } + + view->setBuffer(buf, byte_offset, byteLength, length); + *result = ToNapi(view); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + ValueRef* v = FromNapi(typedarray); + if (!v->isTypedArrayObject()) { + return SetLastError(env, napi_invalid_arg); + } + + napi_typedarray_type resolvedType; + if (v->isInt8ArrayObject()) { + resolvedType = napi_int8_array; + } else if (v->isUint8ArrayObject()) { + resolvedType = napi_uint8_array; + } else if (v->isUint8ClampedArrayObject()) { + resolvedType = napi_uint8_clamped_array; + } else if (v->isInt16ArrayObject()) { + resolvedType = napi_int16_array; + } else if (v->isUint16ArrayObject()) { + resolvedType = napi_uint16_array; + } else if (v->isInt32ArrayObject()) { + resolvedType = napi_int32_array; + } else if (v->isUint32ArrayObject()) { + resolvedType = napi_uint32_array; + } else if (v->isFloat32ArrayObject()) { + resolvedType = napi_float32_array; + } else if (v->isFloat64ArrayObject()) { + resolvedType = napi_float64_array; + } else if (v->isBigInt64ArrayObject()) { + resolvedType = napi_bigint64_array; + } else if (v->isBigUint64ArrayObject()) { + resolvedType = napi_biguint64_array; + } else if (v->isFloat16ArrayObject()) { + resolvedType = napi_float16_array; + } else { + return SetLastError(env, napi_invalid_arg); + } + + ArrayBufferViewRef* view = v->asArrayBufferView(); + + if (type != nullptr) { + *type = resolvedType; + } + if (length != nullptr) { + *length = view->arrayLength(); + } + if (data != nullptr) { + *data = view->rawBuffer(); // already offset-adjusted (points at the view's own first element) + } + if (arraybuffer != nullptr) { + *arraybuffer = ToNapi(view->buffer()); + } + if (byte_offset != nullptr) { + *byte_offset = view->byteOffset(); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_dataview(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = FromNapi(value)->isDataViewObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_dataview(napi_env env, size_t length, napi_value arraybuffer, size_t byte_offset, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + + ValueRef* bufValue = FromNapi(arraybuffer); + if (!bufValue->isArrayBuffer()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferRef* buf = bufValue->asArrayBuffer(); + + // Node-API contract: byte_offset + length must fit within the buffer, + // else throw a RangeError and report napi_pending_exception (matches + // v8impl::napi_create_dataview). Guard against size_t overflow too. + if (byte_offset + length < byte_offset || byte_offset + length > buf->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 SetLastError(env, napi_pending_exception); + } + + DataViewObjectRef* view = DataViewObjectRef::create(state); + view->setBuffer(buf, byte_offset, length); + + *result = ToNapi(view); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_dataview_info(napi_env env, napi_value dataview, size_t* bytelength, void** data, napi_value* arraybuffer, size_t* byte_offset) +{ + ValueRef* v = FromNapi(dataview); + if (!v->isDataViewObject()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferViewRef* view = v->asArrayBufferView(); + + if (bytelength != nullptr) { + *bytelength = view->byteLength(); + } + if (data != nullptr) { + *data = view->rawBuffer(); + } + if (arraybuffer != nullptr) { + *arraybuffer = ToNapi(view->buffer()); + } + if (byte_offset != nullptr) { + *byte_offset = view->byteOffset(); + } + return napi_ok; +} + +// Escargot has no first-class "external" value kind, unlike V8's +// napi_external. This is approximated as a plain ObjectRef carrying the raw +// pointer in its extraData() slot (the same slot napi_wrap uses on other +// objects) plus a GC finalizer that runs the user's finalize_cb - see +// napi_wrap/WrapFinalizeData in NapiFunctions.cpp for the identical pattern. +// CAVEAT: napi_typeof on the resulting napi_value reports napi_object, not +// napi_external, since NapiFunctions.cpp's napi_typeof has no way to +// distinguish this from any other plain object (there is no dedicated +// external/opaque ValueRef kind in EscargotPublic.h to check against). +ESCARGOT_NAPI_EXPORT napi_status napi_create_external(napi_env env, void* data, node_api_basic_finalize finalize_cb, void* finalize_hint, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + + ObjectRef* obj = ObjectRef::create(state); + obj->setExtraData(data); + + if (finalize_cb != nullptr) { + ExternalObjectFinalizeData* finalizeData = new ExternalObjectFinalizeData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_cb; + finalizeData->nativeData = data; + finalizeData->finalizeHint = finalize_hint; + Memory::gcRegisterFinalizer(obj, NapiExternalObjectFinalizer, finalizeData); + } + + *result = ToNapi(obj); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_external(napi_env env, napi_value value, void** result) +{ + ValueRef* v = FromNapi(value); + if (!v->isObject()) { + return SetLastError(env, napi_invalid_arg); + } + *result = v->asObject()->extraData(); + return napi_ok; +} + +// no engine-level tracking of external memory pressure to hook into here; +// per the task's guidance this simply echoes the requested delta back as the +// "new" adjusted value instead of accumulating any real running total. +ESCARGOT_NAPI_EXPORT napi_status napi_adjust_external_memory(node_api_basic_env env, int64_t change_in_bytes, int64_t* adjusted_value) +{ + if (adjusted_value != nullptr) { + *adjusted_value = change_in_bytes; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_type_tag_object(napi_env env, napi_value value, const napi_type_tag* type_tag) +{ + ValueRef* v = FromNapi(value); + if (!v->isObject()) { + return SetLastError(env, napi_object_expected); + } + ObjectRef* obj = v->asObject(); + + // matches Node's own contract: an object may only be tagged once + if (g_typeTags.find(obj) != g_typeTags.end()) { + return SetLastError(env, napi_invalid_arg); + } + + g_typeTags[obj] = std::make_pair(type_tag->lower, type_tag->upper); + Memory::gcRegisterFinalizer(obj, NapiTypeTagFinalizer, nullptr); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_check_object_type_tag(napi_env env, napi_value value, const napi_type_tag* type_tag, bool* result) +{ + ValueRef* v = FromNapi(value); + if (!v->isObject()) { + return SetLastError(env, napi_object_expected); + } + ObjectRef* obj = v->asObject(); + + auto iter = g_typeTags.find(obj); + if (iter == g_typeTags.end()) { + *result = false; + } else { + *result = (iter->second.first == type_tag->lower && iter->second.second == type_tag->upper); + } + return napi_ok; +} + +// Node's Buffer is a Uint8Array subclass; Escargot has no dedicated Buffer +// object kind, so a plain Uint8ArrayObjectRef over the requested backing +// store stands in for it here at the engine level (napi_is_buffer therefore +// also returns true for any plain Uint8Array created via +// napi_create_typedarray, not just ones created through the functions +// below - an approximation of Node's real, distinct Buffer type). +ESCARGOT_NAPI_EXPORT napi_status napi_create_buffer(napi_env env, size_t length, void** data, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + + ArrayBufferObjectRef* buf = ArrayBufferObjectRef::create(state); + buf->allocateBuffer(state, length); + + Uint8ArrayObjectRef* view = Uint8ArrayObjectRef::create(state); + view->setBuffer(buf, 0, length, length); + + if (data != nullptr) { + *data = view->rawBuffer(); + } + *result = ToNapi(view); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_external_buffer(napi_env env, size_t length, void* data, node_api_basic_finalize finalize_cb, void* finalize_hint, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + + ExternalBackingStoreFinalizeData* finalizeData = new ExternalBackingStoreFinalizeData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_cb; + finalizeData->finalizeHint = finalize_hint; + + BackingStoreRef* backingStore = BackingStoreRef::createNonSharedBackingStore(data, length, NapiExternalBackingStoreDeleter, finalizeData); + + ArrayBufferObjectRef* buf = ArrayBufferObjectRef::create(state); + buf->attachBuffer(backingStore); + + Uint8ArrayObjectRef* view = Uint8ArrayObjectRef::create(state); + view->setBuffer(buf, 0, length, length); + + *result = ToNapi(view); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_buffer_copy(napi_env env, size_t length, const void* data, void** result_data, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + + ArrayBufferObjectRef* buf = ArrayBufferObjectRef::create(state); + buf->allocateBuffer(state, length); + if (length > 0 && data != nullptr) { + memcpy(buf->rawBuffer(), data, length); + } + + Uint8ArrayObjectRef* view = Uint8ArrayObjectRef::create(state); + view->setBuffer(buf, 0, length, length); + + if (result_data != nullptr) { + *result_data = view->rawBuffer(); + } + *result = ToNapi(view); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = FromNapi(value)->isUint8ArrayObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_buffer_info(napi_env env, napi_value value, void** data, size_t* length) +{ + ValueRef* v = FromNapi(value); + if (!v->isUint8ArrayObject()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferViewRef* view = v->asArrayBufferView(); + + if (data != nullptr) { + *data = view->rawBuffer(); + } + if (length != nullptr) { + *length = view->arrayLength(); + } + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiAsyncWork.cpp b/src/napi/NapiAsyncWork.cpp new file mode 100644 index 000000000..716697247 --- /dev/null +++ b/src/napi/NapiAsyncWork.cpp @@ -0,0 +1,636 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// napi_create_async_work/napi_queue_async_work/napi_cancel_async_work/ +// napi_delete_async_work and the napi_threadsafe_function family - the two +// slices of node_api.h that need real OS threads, deliberately left out of +// NapiRuntime.cpp (see that file's own header comment). +// +// Threading/execution model (backed by the real system libuv event loop +// owned by this env - NapiEnv::uvLoop(), NapiEnv.h/.cpp): +// - napi_queue_async_work hands `execute`/`complete` straight to +// uv_queue_work(): `execute` runs on one of libuv's own thread-pool +// threads (a small, persistent, reused pool - not one fresh std::thread +// per work item), and `complete` (the "after work" callback) runs back on +// the loop thread, i.e. whichever thread calls +// NapiEnv::drainPendingJobs() (which itself calls uv_run(loop, +// UV_RUN_NOWAIT) - see NapiEnv.cpp). That is why this file's tests can +// keep assuming `complete` only ever runs from inside drainPendingJobs. +// - Every libuv thread-pool thread that reaches this file's ExecuteWork +// calls Escargot::Globals::initializeThread() at entry and +// Globals::finalizeThread() at exit, exactly bracketing the `execute` +// callback (same convention Shell.cpp's builtin262EvalScript/ +// builtin262AgentStart use for a real VMInstance/Context - this worker +// skips that part entirely, see below). Escargot's GC (Boehm, +// conservative/stop-the-world) must know about a thread's existence to +// suspend it during a collection and scan its stack; without this +// registration a GC pass triggered from any other thread while a +// thread-pool thread is mid-`execute` would not see (or safely stop) that +// thread at all. Unlike the one-shot-std::thread predecessor of this file, +// a libuv thread-pool thread is reused across many work items over its +// lifetime - initializeThread()/finalizeThread() bracket each individual +// `execute` call (not the thread's whole lifetime), which is safe because +// both are symmetric per-call (ThreadLocal::inited, EscargotPublic.cpp, is +// reset to false by finalizeThread() before any later initializeThread() +// call on that same, reused OS thread) and Boehm's own thread registration +// (triggered transitively through GC_init(), inside +// Globals::initializeThread()'s call chain) is idempotent for a thread +// that's already known to it. +// - `execute` is native-only by N-API contract - no JS/napi_value/GC-heap +// access - so a thread-pool thread never touches this env's +// VMInstanceRef/ContextRef and never creates a JS value while running it; +// it may still call Memory::gcMalloc et al for its own native allocations, +// which is exactly why the Globals::initializeThread() registration above +// is required even though no JS ever runs there. +// - Both `complete` (async_work) and a threadsafe function's call_js_cb only +// ever run on the loop thread (reached via uv_run(..., UV_RUN_NOWAIT) from +// NapiEnv::drainPendingJobs() - the after-work callback of a queued +// uv_work_t for async_work, and a napi_threadsafe_function's own +// uv_async_t callback for threadsafe functions), wrapped in its own +// Evaluator::execute (not env->executionState, which is only valid nested +// inside an existing napi call) - same top-level-entry pattern used by +// test/cctest/testnapi_runtime.cpp's own MakeCallback/ +// BufferFromArrayBuffer tests and by ~NapiEnv()'s own teardown Evaluator +// call: neither callback is nested inside any other napi_* call's own +// ExecutionStateRef, so each must create its own SandBox to safely call +// back into JS (call_js_cb, or the default napi_call_function below, may +// throw a raw C++ exception on an uncaught JS exception). +// - Ordering between "push a call onto a threadsafe function's queue" and +// "the last release setting `closing`" is made safe purely by both +// happening under the same napi_threadsafe_function__::mutex: whichever +// happens first under that lock is what the OTHER one observes - a push +// that wins the race is already in `queue` by the time a subsequent +// release's teardown check (TsfnAsyncCallback, which always drains `queue` +// down to empty before deciding whether to tear down) can ever run, and a +// release that wins instead flips `closing` before that push's own lock +// acquisition, so the push correctly observes `closing` and is rejected - +// see TsfnAsyncCallback's own comment for why this holds regardless of how +// many uv_async_send() calls end up coalesced by libuv. +// +// Approximations (see individual functions for detail): +// - napi_cancel_async_work is best-effort: succeeds (skipping `execute` +// entirely) if `execute` hasn't actually started running yet - whether +// that's because the work item was never queued at all, or because +// uv_cancel() on an already-queued-but-not-yet-started uv_work_t +// succeeds - and fails (with `execute` left to run to completion) once +// it's already running. +// - napi_ref_threadsafe_function/napi_unref_threadsafe_function are close to +// no-ops: nothing here actually keeps a whole process alive/lets it exit +// the way Node's real event loop ref-counting does, so they just record a +// bookkeeping flag and always succeed. +// - max_queue_size is honored (napi_queue_full for a non-blocking call +// against a full bounded queue; a blocking call waits on a condition +// variable instead) but, like real Node-API, this offers no guarantee once +// a caller misuses the acquire/release contract concurrently with +// in-flight calls - see the ordering note above for exactly what *is* +// guaranteed. + +#include "NapiTypes.h" + +#include + +#include +#include +#include +#include + +// the opaque type node_api.h forward-declares for napi_create_async_work et +// al (node_api_types.h: `typedef struct napi_async_work__* napi_async_work`). +// Plain heap allocation (not GC-managed): nothing here is a JS/GC-heap value, +// and its lifetime is explicitly managed by napi_create_async_work/ +// napi_delete_async_work, same contract as napi_ref__ (NapiTypes.h). +struct napi_async_work__ { + napi_env env; + napi_async_execute_callback execute; + napi_async_complete_callback complete; + void* data; + + // Idle -> Running -> Completed is the normal path (set from + // ExecuteWork/AfterWork below, libuv's thread-pool-thread/loop-thread + // callbacks for this work item's uv_work_t). Idle -> Cancelled happens + // instead if napi_cancel_async_work runs before ExecuteWork has actually + // taken `mutex` for the first time (whether or not this work item has + // even been queued to libuv yet). + enum class State { + Idle, + Running, + Cancelled, + Completed + }; + + std::mutex mutex; // guards state/queued below (also serializes against ExecuteWork/AfterWork's own transitions) + State state = State::Idle; + bool queued = false; // true once napi_queue_async_work has actually called uv_queue_work, guards against double-queueing + uv_work_t req; // req.data == this once queued; the uv_work_t this napi_async_work is queued as +}; + +// the opaque type node_api.h forward-declares for +// napi_create_threadsafe_function et al (node_api_types.h: +// `typedef struct napi_threadsafe_function__* napi_threadsafe_function`). +// Same plain-heap-allocation rationale as napi_async_work__ above. +struct napi_threadsafe_function__ { + napi_env env; + napi_ref funcRef; // strong napi_ref to `func` (napi_create_reference w/ initial_refcount 1), so it survives for as long as this tsfn does; null if `func` was itself NULL (call_js_cb-only usage) + void* context; + napi_threadsafe_function_call_js callJsCb; + void* threadFinalizeData; + napi_finalize threadFinalizeCb; + + uv_async_t async; // async.data == this; the uv_async_t producers (napi_call_threadsafe_function, from ANY thread) wake to get delivery running on the loop thread - see TsfnAsyncCallback + + std::mutex mutex; // guards every field below (queue/maxQueueSize/threadCount/refd/closing/tornDown), including the push-vs-closing ordering (see this file's header comment) + std::condition_variable cv; // signalled whenever `queue` shrinks or `closing` becomes true, for a blocking napi_call_threadsafe_function waiting on queue space + std::deque queue; + size_t maxQueueSize; // 0 means unbounded + size_t threadCount; // acquire/release count - seeded from initial_thread_count + bool refd = true; // napi_ref_threadsafe_function/napi_unref_threadsafe_function bookkeeping only (see this file's header comment) - not read anywhere else + bool closing = false; // set once the last release (or any napi_tsfn_abort release) happens; no further calls are accepted once true + bool tornDown = false; // guards TsfnAsyncCallback's one-shot teardown (funcRef/finalizer/uv_close) against running more than once +}; + +namespace Escargot { +namespace Napi { +namespace { + +// Delivers exactly one already-popped call to `func`'s JS function, on the +// loop thread (called only from TsfnAsyncCallback below, which pops `data` +// off `func->queue` itself). +void InvokeThreadsafeFunctionCall(napi_threadsafe_function func, void* data) +{ + napi_env env = func->env; + ContextRef* ctx = env->context(); + // Evaluator::execute (not env->executionState, which is only valid + // nested inside an existing napi call) - see this file's header comment. + Evaluator::execute( + ctx, [](ExecutionStateRef* state, napi_threadsafe_function func, napi_env env, void* data) -> ValueRef* { + env->executionState = state; + + napi_value jsFunc = nullptr; + if (func->funcRef != nullptr) { + napi_get_reference_value(env, func->funcRef, &jsFunc); + } + + if (func->callJsCb != nullptr) { + func->callJsCb(env, jsFunc, func->context, data); + } else if (jsFunc != nullptr) { + // no call_js_cb: default behavior is calling `func` with no + // arguments, per napi_create_threadsafe_function's own + // contract + napi_call_function(env, ToNapi(ValueRef::createUndefined()), jsFunc, 0, nullptr, nullptr); + } + return ValueRef::createUndefined(); + }, + func, env, data); +} + +// Runs exactly once, the first time TsfnAsyncCallback (below) observes +// `func->closing` with an already-empty queue: releases the strong napi_ref +// to `func`'s JS function, runs the user's thread_finalize_cb, then closes +// `func`'s uv_async_t (asynchronously freeing `func` itself from that +// handle's close callback, once libuv actually gets around to running it - +// see this function's own uv_close call). By the time this runs, +// `func->closing` is already true and TsfnAsyncCallback always drains +// `func->queue` down to empty before ever reaching this call - see this +// file's header comment for why that ordering guarantee means no in-flight +// delivery ever observes `func` already torn down. +void TearDownThreadsafeFunction(napi_threadsafe_function func) +{ + napi_env env = func->env; + if (func->funcRef != nullptr) { + napi_delete_reference(env, func->funcRef); + func->funcRef = nullptr; + } + if (func->threadFinalizeCb != nullptr) { + // Node-API contract: the thread finalize callback is invoked as + // thread_finalize_cb(env, thread_finalize_data, context) - i.e. the + // tsfn's `context` is passed as the finalize_hint, NOT nullptr. Addons + // routinely stash their state in `context` and recover it here as + // finalize_hint (e.g. test_threadsafe_function_abort's tsfn_finalize + // does `static_cast(finalize_hint)`); passing nullptr made + // that a null-deref crash on abort teardown. + func->threadFinalizeCb(env, func->threadFinalizeData, func->context); + } + uv_close(reinterpret_cast(&func->async), [](uv_handle_t* handle) { + delete static_cast(handle->data); + }); +} + +// napi_threadsafe_function's uv_async_t callback - runs on the loop thread +// (via NapiEnv::drainPendingJobs()'s uv_run(loop, UV_RUN_NOWAIT), see +// NapiEnv.cpp), woken by uv_async_send() from napi_call_threadsafe_function +// (any thread) or napi_release_threadsafe_function (the releasing thread). +// libuv may coalesce multiple uv_async_send() calls that happen before this +// callback gets to run into a single invocation - safe here because this +// always drains `func->queue` down to empty in a loop (rather than assuming +// exactly one queued item per invocation), so no pushed call is ever skipped +// regardless of how many sends coalesced into the invocation that eventually +// observes it. +void TsfnAsyncCallback(uv_async_t* handle) +{ + napi_threadsafe_function func = static_cast(handle->data); + + for (;;) { + void* data = nullptr; + bool hasData = false; + { + std::lock_guard lock(func->mutex); + if (!func->queue.empty()) { + data = func->queue.front(); + func->queue.pop_front(); + hasData = true; + } + } + if (!hasData) { + break; + } + // wake any blocking napi_call_threadsafe_function producer waiting + // for room, now that a slot just freed up + func->cv.notify_all(); + InvokeThreadsafeFunctionCall(func, data); + } + + bool shouldTearDown = false; + { + std::lock_guard lock(func->mutex); + if (func->closing && func->queue.empty() && !func->tornDown) { + func->tornDown = true; + shouldTearDown = true; + } + } + if (shouldTearDown) { + TearDownThreadsafeFunction(func); + } +} + +// uv_queue_work's "work" callback - runs on one of libuv's thread-pool +// threads (see this file's header comment for the Boehm-GC-registration +// rationale of the initializeThread()/finalizeThread() bracket below). +void ExecuteWork(uv_work_t* req) +{ + napi_async_work work = static_cast(req->data); + + bool cancelled = false; + { + std::lock_guard lock(work->mutex); + if (work->state == napi_async_work__::State::Cancelled) { + cancelled = true; + } else { + work->state = napi_async_work__::State::Running; + } + } + + if (!cancelled) { + Globals::initializeThread(); + work->execute(work->env, work->data); + Globals::finalizeThread(); + } +} + +// uv_queue_work's "after work" callback - runs on the loop thread (i.e. +// whichever thread calls NapiEnv::drainPendingJobs(), see NapiEnv.cpp). +// `status` is UV_ECANCELED if uv_cancel() (napi_cancel_async_work below) +// actually managed to cancel this work item before libuv's thread pool ever +// started running it; ExecuteWork itself may also have separately observed +// `state == Cancelled` and skipped calling `execute` (the +// cancelled-before-ever-being-queued case) without libuv itself considering +// the uv_work_t cancelled - both are reported identically as napi_cancelled. +void AfterWork(uv_work_t* req, int status) +{ + napi_async_work work = static_cast(req->data); + + napi_status napiStatus; + { + std::lock_guard lock(work->mutex); + bool cancelled = (status == UV_ECANCELED) || (work->state == napi_async_work__::State::Cancelled); + napiStatus = cancelled ? napi_cancelled : napi_ok; + } + + // `complete` (unlike `execute`) has full napi_env access (napi_value/ + // GC-heap-creating calls included) - it needs a valid + // env->executionState to do that, same reason + // InvokeThreadsafeFunctionCall above wraps its own call_js_cb invocation + // in Evaluator::execute rather than calling straight through. + napi_env env = work->env; + ContextRef* ctx = env->context(); + Evaluator::execute( + ctx, [](ExecutionStateRef* state, napi_async_work work, napi_status status) -> ValueRef* { + work->env->executionState = state; + work->complete(work->env, status, work->data); + return ValueRef::createUndefined(); + }, + work, napiStatus); + + std::lock_guard lock(work->mutex); + work->state = napi_async_work__::State::Completed; +} + +} // namespace + +extern "C" { + +// --------------------------------------------------------------------------- +// napi_create_async_work / napi_queue_async_work / napi_cancel_async_work / +// napi_delete_async_work +// --------------------------------------------------------------------------- + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (env == nullptr || execute == nullptr || complete == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // async_resource/async_resource_name are accepted purely for API-surface + // compatibility, same as napi_async_init (NapiRuntime.cpp) - there is no + // async_hooks integration here to route them through. + + napi_async_work__* work = new napi_async_work__(); + work->env = env; + work->execute = execute; + work->complete = complete; + work->data = data; + *result = work; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work) +{ + if (env == nullptr || work == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + { + std::lock_guard lock(work->mutex); + if (work->queued) { + return SetLastError(env, napi_generic_failure); + } + work->queued = true; + } + + work->req.data = work; + // Queued unconditionally, even if napi_cancel_async_work already flipped + // `state` to Cancelled before this call (Idle-but-not-yet-queued is a + // valid state for that) - ExecuteWork/AfterWork above both check for + // Cancelled themselves and skip running/report napi_cancelled + // accordingly, so this still ends up calling `complete` exactly once, + // same as the normal path. + int rc = uv_queue_work(env->napiEnv->uvLoop(), &work->req, ExecuteWork, AfterWork); + if (rc != 0) { + std::lock_guard lock(work->mutex); + work->queued = false; + return SetLastError(env, napi_generic_failure); + } + + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_cancel_async_work(node_api_basic_env env, napi_async_work work) +{ + if (env == nullptr || work == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + std::lock_guard lock(work->mutex); + if (work->state == napi_async_work__::State::Idle) { + bool wasQueued = work->queued; + work->state = napi_async_work__::State::Cancelled; + if (wasQueued) { + // Best-effort (see this file's header comment): succeeds only if + // libuv's thread pool hasn't actually started ExecuteWork for + // this uv_work_t yet - a no-op failure otherwise, which is fine, + // since ExecuteWork's own Cancelled check (set just above, under + // this same mutex, so already visible to it) is what actually + // guarantees `execute` never runs in that case too. + uv_cancel(reinterpret_cast(&work->req)); + } + return napi_ok; + } + // Not Idle: `execute` is already running (or has already finished) - + // there is no way to interrupt it from here. + return SetLastError(env, napi_generic_failure); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_delete_async_work(napi_env env, napi_async_work work) +{ + if (env == nullptr || work == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + { + std::lock_guard lock(work->mutex); + // Safe to delete only once this work item's own `complete` has + // actually finished running (state Completed, matching real + // Node-API's contract that napi_delete_async_work runs after + // `complete` returns - typically called as the last thing `complete` + // itself does), or if it was never queued at all. Anything else + // (Running, or Idle-but-queued/Cancelled-but-not-yet-run) means + // libuv's thread pool may still touch `work->req` later - freeing it + // now would be a use-after-free once ExecuteWork/AfterWork gets + // there. + bool safeToDelete = (work->state == napi_async_work__::State::Completed) || !work->queued; + if (!safeToDelete) { + return SetLastError(env, napi_generic_failure); + } + } + + delete work; + return napi_ok; +} + +// --------------------------------------------------------------------------- +// napi_create_threadsafe_function / napi_get_threadsafe_function_context / +// napi_call_threadsafe_function / napi_acquire_threadsafe_function / +// napi_release_threadsafe_function / napi_ref_threadsafe_function / +// napi_unref_threadsafe_function +// --------------------------------------------------------------------------- + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (env == nullptr || result == nullptr || initial_thread_count == 0) { + return SetLastError(env, napi_invalid_arg); + } + if (func == nullptr && call_js_cb == nullptr) { + // nothing this tsfn could ever actually call + return SetLastError(env, napi_invalid_arg); + } + // async_resource/async_resource_name: same API-surface-only acceptance + // as napi_create_async_work above. + + napi_ref funcRef = nullptr; + if (func != nullptr) { + napi_status refStatus = napi_create_reference(env, func, 1, &funcRef); + if (refStatus != napi_ok) { + return SetLastError(env, refStatus); + } + } + + napi_threadsafe_function__* tsfn = new napi_threadsafe_function__(); + tsfn->env = env; + tsfn->funcRef = funcRef; + tsfn->context = context; + tsfn->callJsCb = call_js_cb; + tsfn->threadFinalizeData = thread_finalize_data; + tsfn->threadFinalizeCb = thread_finalize_cb; + tsfn->maxQueueSize = max_queue_size; + tsfn->threadCount = initial_thread_count; + + tsfn->async.data = tsfn; + int rc = uv_async_init(env->napiEnv->uvLoop(), &tsfn->async, TsfnAsyncCallback); + if (rc != 0) { + if (funcRef != nullptr) { + napi_delete_reference(env, funcRef); + } + delete tsfn; + return SetLastError(env, napi_generic_failure); + } + + *result = tsfn; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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; +} + +ESCARGOT_NAPI_EXPORT napi_status 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); + if (func->closing) { + return SetLastError(func->env, napi_closing); + } + if (func->maxQueueSize > 0 && func->queue.size() >= func->maxQueueSize) { + if (is_blocking == napi_tsfn_nonblocking) { + return SetLastError(func->env, napi_queue_full); + } + func->cv.wait(lock, [func]() { + return func->closing || func->queue.size() < func->maxQueueSize; + }); + if (func->closing) { + return SetLastError(func->env, napi_closing); + } + } + + func->queue.push_back(data); + lock.unlock(); + // Wakes the loop thread (see TsfnAsyncCallback) - safe to call from any + // thread, and safe with respect to a concurrent + // napi_release_threadsafe_function's own teardown check, which always + // drains `queue` down to empty before ever tearing down (see this file's + // header comment). + uv_async_send(&func->async); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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 SetLastError(func->env, napi_closing); + } + func->threadCount++; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_release_threadsafe_function(napi_threadsafe_function func, napi_threadsafe_function_release_mode mode) +{ + if (func == nullptr) { + return napi_invalid_arg; + } + + std::unique_lock lock(func->mutex); + if (func->closing) { + return SetLastError(func->env, napi_closing); + } + if (func->threadCount == 0) { + // unbalanced release - nothing left to release + return SetLastError(func->env, napi_invalid_arg); + } + + func->threadCount--; + // abort forces teardown regardless of remaining thread count; a normal + // release only tears down once the last thread has released. + bool shouldTearDown = (func->threadCount == 0 || mode == napi_tsfn_abort); + if (shouldTearDown) { + func->closing = true; + // NOTE: abort does NOT discard already-queued items. Real Node-API + // dispatches everything enqueued before the abort to the JS callback, + // then finalizes (verified against Node's own ThreadSafeFunction and + // required by node-api/test_threadsafe_function_abort, whose finalizer + // asserts its one pre-abort call actually ran). abort only (a) forces + // teardown here even if threadCount > 0, and (b) makes subsequent + // napi_call_threadsafe_function return napi_closing (via func->closing + // above). TsfnAsyncCallback drains the queue - delivering those items - + // before it runs the teardown. + } + lock.unlock(); + func->cv.notify_all(); + if (shouldTearDown) { + // Wakes the loop thread so TsfnAsyncCallback drains any remaining + // queued items (delivering them) and then runs the actual teardown - + // see this file's header comment for why this is safe regardless of + // whether a concurrent napi_call_threadsafe_function's own push+send + // is still in flight. + uv_async_send(&func->async); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_ref_threadsafe_function(node_api_basic_env env, napi_threadsafe_function func) +{ + if (env == nullptr || func == nullptr) { + return SetLastError(reinterpret_cast(env), napi_invalid_arg); + } + std::lock_guard lock(func->mutex); + if (!func->refd) { + func->refd = true; + uv_ref(reinterpret_cast(&func->async)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_unref_threadsafe_function(node_api_basic_env env, napi_threadsafe_function func) +{ + if (env == nullptr || func == nullptr) { + return SetLastError(reinterpret_cast(env), napi_invalid_arg); + } + std::lock_guard lock(func->mutex); + if (func->refd) { + func->refd = false; + uv_unref(reinterpret_cast(&func->async)); + } + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiDatePromise.cpp b/src/napi/NapiDatePromise.cpp new file mode 100644 index 000000000..582280c12 --- /dev/null +++ b/src/napi/NapiDatePromise.cpp @@ -0,0 +1,203 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Implements Date, Promise (deferred-based) and napi_run_script, on top of +// the same conventions NapiFunctions.cpp establishes (napi_value <-> ValueRef* +// punning, Evaluator::execute-wrapped exception boundaries, etc). + +#include "NapiTypes.h" + +// the opaque type node_api.h forward-declares for napi_create_promise et al. +// `promise` is a raw GC pointer, not itself rooted by this struct - unlike +// napi_value (which is fine sitting bare in a native stack local, since +// Boehm GC conservatively scans the stack), this struct is heap-allocated +// with plain `new`, so it is *not* itself a GC root and would not keep +// `promise` alive on its own. It is rooted explicitly via +// env->napiEnv->persistentValueRefMap()->add()/remove() (the same +// PersistentValueRefMap primitive napi_create_reference/napi_reference_ref +// use for strong napi_ref - see NapiFunctions.cpp), for exactly as long as +// the deferred is outstanding: one add() in napi_create_promise, matched by +// one remove() in whichever of napi_resolve_deferred/napi_reject_deferred +// settles it (both also delete the deferred itself, matching real Node-API's +// contract that a deferred may only be settled once). +struct napi_deferred__ { + Escargot::PromiseObjectRef* promise; +}; + +namespace Escargot { +namespace Napi { + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_create_date(napi_env env, double time, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + DateObjectRef* date = DateObjectRef::create(state); + // DateObjectRef only exposes an int64_t setter; `time` (already + // milliseconds since epoch, per the napi_create_date contract) is + // truncated to fit. Pure - constructing/initializing a DateObject cannot + // run user JS, so unlike napi_call_function there is nothing to wrap in + // Evaluator::execute here. + date->setTimeValue(static_cast(time)); + *result = ToNapi(date); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_date_value(napi_env env, napi_value value, double* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isDateObject()) { + return SetLastError(env, napi_date_expected); + } + *result = v->asDateObject()->primitiveValue(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_date(napi_env env, napi_value value, bool* result) +{ + *result = FromNapi(value)->isDateObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_promise(napi_env env, napi_deferred* deferred, napi_value* promise) +{ + if (deferred == nullptr || promise == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + PromiseObjectRef* promiseObj = PromiseObjectRef::create(state); + + napi_deferred__* def = new napi_deferred__(); + def->promise = promiseObj; + // root the promise for as long as `def` is outstanding - see the + // napi_deferred__ comment above. + env->napiEnv->persistentValueRefMap()->add(promiseObj); + + *deferred = def; + *promise = ToNapi(promiseObj); + return napi_ok; +} + +// shared by napi_resolve_deferred/napi_reject_deferred: settles `deferred`'s +// promise, unroots it, and deletes the deferred, regardless of outcome (a +// deferred may only be settled once, successfully or not - same as real +// Node-API). +static napi_status SettleDeferred(napi_env env, napi_deferred deferred, napi_value resolution, bool isFulfill) +{ + ExecutionStateRef* state = env->executionState; + PromiseObjectRef* promiseObj = deferred->promise; + ValueRef* value = FromNapi(resolution); + + // PromiseObjectRef::fulfill/reject only enqueue reaction jobs (see + // PromiseObject::fulfill/reject) rather than running any reaction + // synchronously, so neither can currently throw a JS exception - but we + // still route the call through Evaluator::execute, exactly like + // napi_call_function (NapiFunctions.cpp), so this boundary stays safe + // even if that internal behavior ever changes (e.g. a registered + // PromiseHook), instead of ever letting a raw C++ exception unwind past + // this function. + Evaluator::EvaluatorResult settleResult = Evaluator::execute( + state, [](ExecutionStateRef* state, PromiseObjectRef* promiseObj, ValueRef* value, bool isFulfill) -> ValueRef* { + if (isFulfill) { + promiseObj->fulfill(state, value); + } else { + promiseObj->reject(state, value); + } + return ValueRef::createUndefined(); + }, + promiseObj, value, isFulfill); + + env->napiEnv->persistentValueRefMap()->remove(promiseObj); + delete deferred; + + if (!settleResult.isSuccessful()) { + env->pendingException = settleResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_resolve_deferred(napi_env env, napi_deferred deferred, napi_value resolution) +{ + return SettleDeferred(env, deferred, resolution, true); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_reject_deferred(napi_env env, napi_deferred deferred, napi_value rejection) +{ + return SettleDeferred(env, deferred, rejection, false); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_promise(napi_env env, napi_value value, bool* result) +{ + *result = FromNapi(value)->isPromiseObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_run_script(napi_env env, napi_value script, napi_value* result) +{ + ValueRef* scriptValue = FromNapi(script); + if (!scriptValue->isString()) { + return SetLastError(env, napi_string_expected); + } + + ExecutionStateRef* state = env->executionState; + ContextRef* context = env->context(); + StringRef* source = scriptValue->asString(); + + // Both parsing (fetchScriptThrowsExceptionIfParseError) and executing the + // parsed script can throw a raw C++ exception on a JS-level error (a + // SyntaxError for the former, any uncaught exception for the latter) - + // same rationale as napi_call_function's Evaluator::execute wrapping + // (NapiFunctions.cpp ~line 201): nest both inside a single sandboxed + // Evaluator::execute call so neither can cross this function's own stack + // frame as a raw C++ exception. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ContextRef* context, StringRef* source) -> ValueRef* { + ScriptRef* parsedScript = context->scriptParser()->initializeScript(source, StringRef::createFromASCII("napi_run_script"), false).fetchScriptThrowsExceptionIfParseError(state); + return parsedScript->execute(state); + }, + context, source); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (result != nullptr) { + *result = ToNapi(evalResult.result); + } + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiEnv.cpp b/src/napi/NapiEnv.cpp new file mode 100644 index 000000000..993808487 --- /dev/null +++ b/src/napi/NapiEnv.cpp @@ -0,0 +1,342 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "NapiEnv.h" +#include "NapiPlatform.h" +#include "NapiTypes.h" // RunEnvCleanupWrapFinalizers + +#include + +namespace Escargot { +namespace Napi { + +static NapiPlatform* g_platform = nullptr; + +void NapiEnv::globalInit() +{ + if (Globals::isInitialized()) { + return; + } + g_platform = new NapiPlatform(); + Globals::initialize(g_platform); +} + +void NapiEnv::globalFinalize() +{ + if (!Globals::isInitialized()) { + return; + } + Globals::finalize(); + delete g_platform; + g_platform = nullptr; +} + +void NapiEnv::threadInit() +{ + Globals::initializeThread(); +} + +void NapiEnv::threadFinalize() +{ + Globals::finalizeThread(); +} + +NapiEnv* NapiEnv::create(const char* locale, const char* timezone) +{ + PersistentRefHolder vmInstance = VMInstanceRef::create(locale, timezone); + PersistentRefHolder context = ContextRef::create(vmInstance.get()); + return new NapiEnv(std::move(vmInstance), std::move(context)); +} + +NapiEnv* NapiEnv::create(VMInstanceRef* sharedVMInstance) +{ + PersistentRefHolder vmInstance(sharedVMInstance); + PersistentRefHolder context = ContextRef::create(sharedVMInstance); + return new NapiEnv(std::move(vmInstance), std::move(context)); +} + +NapiEnv::NapiEnv(PersistentRefHolder&& vmInstance, PersistentRefHolder&& context) + : m_vmInstance(std::move(vmInstance)) + , m_context(std::move(context)) + , m_persistentValueRefMap(PersistentValueRefMap::create()) +{ + m_env.napiEnv = this; + // napi_get_uv_event_loop (NapiRuntime.cpp) and every async_work/ + // threadsafe_function (NapiAsyncWork.cpp) created against this env queue + // work directly onto this loop - see uvLoop() and drainPendingJobs below. + int rc = uv_loop_init(&m_uvLoop); + RELEASE_ASSERT(rc == 0); +} + +NapiEnv::~NapiEnv() +{ + // napi_add_async_cleanup_hook/napi_add_env_cleanup_hook (NapiRuntime.cpp): + // every still-registered cleanup hook runs exactly once here, at + // environment teardown, most-recently-added-first - mirroring Node's own + // cleanup-hook stack. Run before RunEnvCleanupWrapFinalizers/the instance + // data finalizer below, since a cleanup hook's job (freeing native + // resources this env's addons allocated) is meant to happen at the very + // start of teardown, same as Node's RunCleanup does before an + // Environment's other teardown steps. + // + // Async cleanup hooks first: each is simply invoked and treated as done + // immediately (no real async waiting for completion - see + // napi_add_async_cleanup_hook's own comment, NapiRuntime.cpp), then + // popped/freed. A hook is popped *before* being invoked so that a hook + // which itself calls napi_remove_async_cleanup_hook on another + // still-pending handle only ever operates on the (unpopped) remainder of + // this same list. + while (!m_asyncCleanupHooks.empty()) { + napi_async_cleanup_hook_handle__* handle = m_asyncCleanupHooks.back(); + m_asyncCleanupHooks.pop_back(); + napi_async_cleanup_hook hook = handle->hook; + void* arg = handle->arg; + hook(handle, arg); + delete handle; + } + while (!m_envCleanupHooks.empty()) { + std::pair entry = m_envCleanupHooks.back(); + m_envCleanupHooks.pop_back(); + entry.first(entry.second); + } + + // Real Node-API environment-teardown semantics: every still-registered + // napi_wrap finalizer runs now, regardless of whether its wrapped object + // is even still reachable (e.g. kept alive by module.exports) - see + // RunEnvCleanupWrapFinalizers's own comment (NapiFunctions.cpp). + RunEnvCleanupWrapFinalizers(this); + + // napi_set_instance_data's finalizer runs exactly once, here at + // environment teardown - this is the only other teardown hook this PoC has. + if (m_env.instanceDataFinalizer != nullptr) { + napi_finalize finalizer = m_env.instanceDataFinalizer; + m_env.instanceDataFinalizer = nullptr; + finalizer(&m_env, m_env.instanceData, m_env.instanceDataFinalizeHint); + } + + // libuv teardown: give any still-in-flight async_work/threadsafe_function + // work a bounded chance to actually finish and deliver its completion (a + // well-behaved embedder should have already quiesced these before + // destroying its NapiEnv - this is a safety net, not the primary drain + // path), then force-close every handle this loop still owns (every + // napi_threadsafe_function's uv_async_t that was never released, plus + // libuv's own internal handles) and run the loop until those close + // callbacks have actually fired, so uv_loop_close below never sees a + // handle still open (which it would otherwise refuse to close). + for (int i = 0; i < 64; i++) { + drainPendingJobs(); + if (!uv_loop_alive(&m_uvLoop)) { + break; + } + } + uv_walk( + &m_uvLoop, [](uv_handle_t* handle, void*) { + if (!uv_is_closing(handle)) { + uv_close(handle, nullptr); + } + }, + nullptr); + // A few more passes to run the close callbacks queued by uv_walk above + // (and, transitively, any napi_threadsafe_function teardown/finalizer + // they still needed to run - see NapiAsyncWork.cpp's TsfnAsyncCallback) - + // bounded (not "while still alive"): an uncompleted uv_work_t REQUEST + // (as opposed to a handle - uv_walk above only visits handles) from a + // still-in-flight async_work would otherwise keep uv_run reporting + // "still alive" indefinitely, spinning this loop until that unrelated + // background thread-pool item happens to finish - a handle's own close + // callback, by contrast, reliably fires within the first pass or two. + for (int i = 0; i < 16 && uv_run(&m_uvLoop, UV_RUN_NOWAIT) != 0; i++) { + } + int closeRc = uv_loop_close(&m_uvLoop); + // EBUSY means some handle is still open - shouldn't happen after the walk + // above, but isn't fatal (the loop object itself just leaks its internal + // bookkeeping) so this is a soft assertion, not a RELEASE_ASSERT. + ASSERT(closeRc == 0); + (void)closeRc; + + // Best-effort: flush any napi_wrap'd/finalizer-bearing garbage created + // through this env before its Context/VMInstance actually go away below + // (member destructors run after this body, in reverse declaration + // order). Left alone, such garbage can otherwise sit uncollected and get + // opportunistically finalized far later - even mid-construction of a + // totally unrelated NapiEnv's VMInstance - which is not a safe time to + // run arbitrary addon finalizer code (this actually crashed a test once, + // see napi-notes.md). Same "clear stack + churn + gc x5" pattern used in + // test/cctest/testnapi.cpp and test/cctest/testapi.cpp's WeakPtr.*/Finalizer.Basic. + // + // Must run before NapiEnv::globalFinalize() (Globals::finalize()) tears + // down the GC itself; callers are expected to destroy every NapiEnv first. + ContextRef* ctx = m_context.get(); + Evaluator::execute(ctx, [](ExecutionStateRef* state) -> ValueRef* { + return ValueRef::create(100); + }); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); +} + +void NapiEnv::addEnvCleanupHook(napi_cleanup_hook fun, void* arg) +{ + m_envCleanupHooks.push_back({ fun, arg }); +} + +void NapiEnv::removeEnvCleanupHook(napi_cleanup_hook fun, void* arg) +{ + // removes the most-recently-added matching entry (rbegin/rend), matching + // the LIFO framing used everywhere else in this file - functionally + // equivalent to removing any single matching entry, since (fun, arg) + // pairs are otherwise indistinguishable from one another. + for (auto it = m_envCleanupHooks.rbegin(); it != m_envCleanupHooks.rend(); ++it) { + if (it->first == fun && it->second == arg) { + m_envCleanupHooks.erase(std::next(it).base()); + break; + } + } +} + +napi_async_cleanup_hook_handle__* NapiEnv::addAsyncCleanupHook(napi_async_cleanup_hook hook, void* arg) +{ + napi_async_cleanup_hook_handle__* handle = new napi_async_cleanup_hook_handle__(); + handle->napiEnv = this; + handle->hook = hook; + handle->arg = arg; + m_asyncCleanupHooks.push_back(handle); + return handle; +} + +void NapiEnv::removeAsyncCleanupHook(napi_async_cleanup_hook_handle__* handle) +{ + for (size_t i = 0; i < m_asyncCleanupHooks.size(); i++) { + if (m_asyncCleanupHooks[i] == handle) { + m_asyncCleanupHooks.erase(m_asyncCleanupHooks.begin() + i); + delete handle; + return; + } + } +} + +bool NapiEnv::drainPendingJobs() +{ + VMInstanceRef* instance = m_vmInstance.get(); + bool anyProgress = false; + // Alternates between three sources of work, each of which may feed one of + // the others: + // - this env's VMInstance's own pending-job queue (e.g. resolved Promise + // reactions); + // - the libuv loop (uv_run(..., UV_RUN_NOWAIT)) - an async_work's + // uv_queue_work completion or a threadsafe_function's uv_async_t + // wakeup, both delivered from here (see NapiAsyncWork.cpp), may + // themselves run JS (`complete`/call_js_cb) that queues further + // Promise-reaction jobs; + // - the legacy main-thread callback queue (enqueueMainThreadCallback/ + // drainMainThreadCallbacks above) - kept as a fallback path, in case + // anything still posts through it directly. + // A resolved Promise reaction may itself be what a napi_ref'd JS callback + // was waiting to observe, so this keeps alternating until a full pass + // makes no further progress, rather than draining each source exactly + // once. Bounded (not "until literally nothing is left"): a + // napi_threadsafe_function's uv_async_t handle (or any other open libuv + // handle) keeps the loop "alive" for as long as it stays open, which is + // not the same thing as there being actual work to do right now - an + // unbounded loop here would spin forever on a long-lived, otherwise-idle + // handle. + for (int i = 0; i < 32; i++) { + bool progressed = false; + + while (instance->hasPendingJob()) { + instance->executePendingJob(); + progressed = true; + } + + size_t mainThreadCallbacksBefore; + { + std::lock_guard guard(m_mainThreadCallbacksMutex); + mainThreadCallbacksBefore = m_mainThreadCallbacks.size(); + } + drainMainThreadCallbacks(); + progressed = progressed || (mainThreadCallbacksBefore > 0); + + // Runs any already-completed uv_work_t (async_work)/uv_async_t + // (threadsafe_function) callbacks without blocking; its own return + // value (whether the loop still has active handles/requests) isn't a + // reliable "did anything actually happen" signal on its own - e.g. an + // un-released threadsafe_function's uv_async_t handle alone keeps it + // non-zero - so this loop tracks progress via the VM job queue below + // instead, which is what a uv callback would actually feed into. + // + // Wrapped in Evaluator::execute so a *raw* libuv callback an addon + // registered straight on the loop (e.g. node-api/test_uv_loop's + // uv_check, which calls napi_call_function directly) has a live + // env->executionState to run against. async_work's complete and + // threadsafe_function's call_js each already establish their own state + // in a nested Evaluator::execute (NapiAsyncWork.cpp), so they're + // unaffected; this only supplies the default state raw callbacks need. + // previousState is restored after, since executionState is only ever + // meant to be valid for the duration of a single napi call boundary. + { + ExecutionStateRef* previousState = m_env.executionState; + Evaluator::execute( + context(), [](ExecutionStateRef* state, napi_env env, uv_loop_t* loop) -> ValueRef* { + env->executionState = state; + uv_run(loop, UV_RUN_NOWAIT); + return ValueRef::createUndefined(); + }, + &m_env, &m_uvLoop); + m_env.executionState = previousState; + } + + while (instance->hasPendingJob()) { + instance->executePendingJob(); + progressed = true; + } + + if (!progressed) { + break; + } + anyProgress = true; + } + return anyProgress; +} + +void NapiEnv::drainPostFinalizers() +{ + // swap out first (rather than iterating m_pendingPostFinalizers directly) + // so a finalizer that itself calls node_api_post_finalizer/enqueues more + // work during this drain gets picked up by a *later* drain call instead + // of being invoked recursively out of this same loop or invalidating the + // vector being iterated. + while (!m_pendingPostFinalizers.empty()) { + std::vector batch; + batch.swap(m_pendingPostFinalizers); + for (const PostFinalizerEntry& entry : batch) { + entry.finalizeCb(&m_env, entry.finalizeData, entry.finalizeHint); + } + } +} + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiEnv.h b/src/napi/NapiEnv.h new file mode 100644 index 000000000..f9423e01b --- /dev/null +++ b/src/napi/NapiEnv.h @@ -0,0 +1,463 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#ifndef __EscargotNapiEnv__ +#define __EscargotNapiEnv__ + +#include "Escargot.h" +#include "api/EscargotPublic.h" + +#include +#include + +#include +#include +#include +#include +#include + +// forward-declared here (full definition, alongside napi_env__/ +// napi_handle_scope__/etc, lives in NapiTypes.h, which includes this header) +// purely so NapiEnv below can hold pointers to it - see +// NapiEnv::trackWeakRefTarget/clearWeakRefTargets. +struct napi_ref__; + +// forward-declared here for the same reason as napi_ref__ above (full +// definitions live in NapiTypes.h) - napi_env__::topCallbackScope below only +// ever holds a pointer to the former, and NapiEnv only ever holds pointers to +// the latter in m_asyncCleanupHooks (NapiRuntime.cpp). +struct napi_callback_scope__; +struct napi_async_cleanup_hook_handle__; + +namespace Escargot { +namespace Napi { + +class NapiPlatform; +class NapiEnv; + +// the real napi_env__ definition (only forward-declared by node_api.h). +// Owned directly by NapiEnv (see NapiEnv::m_env below) instead of being +// stack-allocated per call/per test: a napi_callback (e.g. a napi_wrap +// finalizer) can run later than the call that created it, so the env it +// captures must stay valid for as long as NapiEnv itself does, not just for +// one Evaluator::execute invocation. +struct EnvData { + NapiEnv* napiEnv = nullptr; + ExecutionStateRef* executionState = nullptr; // only valid for the duration of the current call + OptionalRef pendingException; + std::string lastErrorMessage; + napi_extended_error_info lastErrorInfo; + // the napi_status of the most recent napi_*/node_api_* call that + // returned something other than napi_ok, tracked so + // napi_get_last_error_info (NapiFunctions.cpp) can look up its message + // from Node's own error_messages[] table (see SetLastError in + // NapiTypes.h) - mirrors js_native_api_v8.cc's env->last_error.error_code. + napi_status lastErrorCode = napi_ok; + + // napi_set_instance_data/napi_get_instance_data; the finalizer is + // invoked once from ~NapiEnv() (NapiEnv.cpp), the only environment- + // teardown hook this PoC has + void* instanceData = nullptr; + napi_finalize instanceDataFinalizer = nullptr; + void* instanceDataFinalizeHint = nullptr; + + // handle-scope bookkeeping (napi_open_handle_scope/napi_close_handle_scope/ + // napi_open_escapable_handle_scope/napi_close_escapable_handle_scope, + // NapiFunctions.cpp). napi_value doesn't need real handle buffering here - + // it's already the GC pointer itself (see ToNapi/FromNapi in + // NapiTypes.h) - so this exists purely to enforce proper open/close + // nesting order (LIFO, for napi_handle_scope_mismatch), the same way + // V8's real handle scope stack does. Full type defined in NapiTypes.h. + napi_handle_scope__* topHandleScope = nullptr; + + // same LIFO-nesting-order bookkeeping as topHandleScope above, but for + // napi_open_callback_scope/napi_close_callback_scope (NapiRuntime.cpp) - + // napi_close_callback_scope returns napi_callback_scope_mismatch if + // `scope` isn't currently the innermost open one, same contract as + // napi_close_handle_scope. Full type defined in NapiTypes.h. + napi_callback_scope__* topCallbackScope = nullptr; + int32_t callbackScopeDepth = 0; + + ContextRef* context(); // defined below, out-of-line (needs NapiEnv complete) +}; + +} // namespace Napi +} // namespace Escargot + +// the opaque type node_api.h forward-declares; napi_env is `EnvData*` in disguise +struct napi_env__ : public Escargot::Napi::EnvData { +}; + +namespace Escargot { +namespace Napi { + +// Owns the VMInstanceRef + ContextRef pair (and the napi_env__ itself) that a +// napi_env is backed by. +class NapiEnv { +public: + // process-wide setup/teardown; call once before creating any NapiEnv, and once at shutdown + static void globalInit(); + static void globalFinalize(); + + // per-thread setup/teardown; call once per thread that will create/use a NapiEnv + static void threadInit(); + static void threadFinalize(); + + // creates a fresh, independent VMInstance + Context + static NapiEnv* create(const char* locale = nullptr, const char* timezone = nullptr); + // creates a Context on top of a VMInstance shared with other NapiEnv instances + static NapiEnv* create(VMInstanceRef* sharedVMInstance); + + ~NapiEnv(); + + VMInstanceRef* vmInstance() + { + return m_vmInstance.get(); + } + + ContextRef* context() + { + return m_context.get(); + } + + // the napi_env to pass across the N-API boundary; valid for as long as + // this NapiEnv is. Callers must still set env()->executionState before + // making napi_* calls, since that part is only valid for the duration of + // the current Evaluator::execute call. + napi_env env() + { + return &m_env; + } + + // runs every job queued on this env's VMInstance (e.g. resolved Promise + // reactions), then drains the main-thread callback queue below - this is + // the single pump the whole N-API host (test harness / any embedder) is + // expected to keep calling on the main (JS) thread; wiring the + // main-thread callback queue in here means async-work completion + // callbacks and threadsafe-function calls both surface through the exact + // same pump an embedder is already required to drive for Promise + // reactions, without needing a second, separate API to remember to call. + // + // As of the libuv integration (see NapiAsyncWork.cpp/NapiRuntime.cpp), + // this is also where uvLoop() actually gets pumped + // (uv_run(..., UV_RUN_NOWAIT)): async_work's uv_queue_work completions and + // a threadsafe_function's uv_async_t wakeups are both delivered from + // there, on this same thread. The legacy main-thread callback queue above + // is drained alongside it (kept as a fallback path/for any other user of + // enqueueMainThreadCallback), and the whole thing loops, bounded, until a + // full pass makes no further progress - a uv callback or a main-thread + // callback may itself resolve a Promise (queuing a VM job), and running a + // VM job may itself be what unblocks something waiting on the JS side, so + // a single one-shot drain of each independently isn't enough. + // Returns true if any pass made progress (a VM job ran, a main-thread + // callback fired, or a uv callback fed new work) - lets an external pump + // loop (e.g. the NapiSuite test harness) fold uv-loop progress into its + // own quiescence check rather than treating uv work as invisible. + bool drainPendingJobs(); + + // the real libuv event loop backing this env - owned for its whole + // lifetime (uv_loop_init in the constructor, uv_loop_close at teardown, + // see NapiEnv.cpp). napi_get_uv_event_loop (NapiRuntime.cpp) hands this + // straight back to callers; NapiAsyncWork.cpp queues uv_work_t/uv_async_t + // on it directly. + uv_loop_t* uvLoop() + { + return &m_uvLoop; + } + + // True while the loop still has genuinely pending work: an in-flight + // uv_queue_work (async_work still running on the thread pool) or a ref'd, + // active handle. An *unref'd* idle handle (e.g. an unref'd + // threadsafe_function's uv_async_t) does NOT keep this true - matching + // libuv's own "should the loop keep the process alive" semantics. An + // external pump (the NapiSuite harness) uses this to keep pumping until + // asynchronously-completing work has actually landed, rather than settling + // the instant a single uv_run(NOWAIT) pass happens to find nothing ready. + bool uvLoopAlive() + { + return uv_loop_alive(&m_uvLoop) != 0; + } + + // Thread-safe main-thread callback queue - the single mechanism + // napi_queue_async_work's completion callback and + // napi_call_threadsafe_function use to get back onto the main (JS) + // thread (see src/napi/NapiAsyncWork.cpp). enqueueMainThreadCallback may + // be called from ANY thread (it only ever touches m_mainThreadCallbacks + // under m_mainThreadCallbacksMutex); drainMainThreadCallbacks must only + // ever be called from the main thread (it's what actually invokes the + // queued std::functions, which are free to touch JS/napi_value/ + // the GC heap). + void enqueueMainThreadCallback(std::function callback) + { + std::lock_guard guard(m_mainThreadCallbacksMutex); + m_mainThreadCallbacks.push_back(std::move(callback)); + } + + // invokes and clears every main-thread callback queued so far, in FIFO + // order. Swaps the queue out first (rather than iterating it directly) + // so a callback that itself enqueues further work (or a concurrent + // enqueueMainThreadCallback call from another thread, racing this drain) + // is picked up by a *later* drain instead of invalidating the container + // being iterated or being invoked out of this same loop. + void drainMainThreadCallbacks() + { + for (;;) { + std::deque> batch; + { + std::lock_guard guard(m_mainThreadCallbacksMutex); + if (m_mainThreadCallbacks.empty()) { + return; + } + batch.swap(m_mainThreadCallbacks); + } + for (std::function& callback : batch) { + callback(); + } + } + } + + // backs node_api_post_finalizer (NapiExtras.cpp): unlike a plain + // finalize_cb passed to napi_wrap/napi_add_finalizer (which runs + // synchronously from inside the GC's finalizer sweep, where calling back + // into JS is unsafe), a post-finalizer is only ever recorded here and + // actually invoked later, once execution has returned to a safe point - + // draining happens in drainPostFinalizers(), which the test harness + // pump loop (test/cctest/testnapi_suite.cpp) or any other safe-point + // caller invokes. Entries are queued, not invoked, from + // node_api_post_finalizer itself, exactly matching Node's own contract. + struct PostFinalizerEntry { + napi_finalize finalizeCb; + void* finalizeData; + void* finalizeHint; + }; + + void enqueuePostFinalizer(napi_finalize finalizeCb, void* finalizeData, void* finalizeHint) + { + m_pendingPostFinalizers.push_back({ finalizeCb, finalizeData, finalizeHint }); + } + + // invokes and clears every post-finalizer queued so far, in FIFO order. + // A finalizer running here is free to enqueue further post-finalizers + // (e.g. chaining) or call back into JS via env()->executionState, same as + // real Node-API - both are safe at this point, unlike from within + // node_api_post_finalizer's own registration call or a GC sweep. + void drainPostFinalizers(); + + // backs napi_ref: PersistentValueRefMap::add()/remove() are the GC-root + // primitives napi_create_reference/napi_reference_ref/unref/delete_reference + // build on for strong (refcount > 0) references + PersistentValueRefMap* persistentValueRefMap() + { + return m_persistentValueRefMap.get(); + } + + // per-object side storage for napi_wrap's WrapFinalizeData* (NapiFunctions.cpp), + // so napi_remove_wrap can find and unregister the GC finalizer napi_wrap + // registered without needing extraData() for it - that slot must stay + // exactly the caller's native_object, per napi_unwrap's contract. Cleared + // by whichever happens first: napi_remove_wrap, or the wrap finalizer + // itself once the wrapped object is actually collected. `obj` is a + // non-owning key: safe because Escargot's GC never moves objects, and + // every insertion is paired with an eventual removal along one of those + // two paths, so a collected object's address is never left stale here. + void setWrapFinalizerData(ObjectRef* obj, void* data) + { + m_wrapFinalizerData[obj] = data; + } + + void* takeWrapFinalizerData(ObjectRef* obj) + { + auto iter = m_wrapFinalizerData.find(obj); + if (iter == m_wrapFinalizerData.end()) { + return nullptr; + } + void* data = iter->second; + m_wrapFinalizerData.erase(iter); + return data; + } + + // non-mutating lookup, used by RunEnvCleanupWrapFinalizers (NapiFunctions.cpp) + // to double-check a snapshotted (obj, data) pair is still the object's + // *current* wrap-finalizer entry before invoking it (napi_remove_wrap, or + // a finalizer that itself already ran reentrantly from within another + // one's finalize_cb, may have changed it since the snapshot was taken). + void* peekWrapFinalizerData(ObjectRef* obj) const + { + auto iter = m_wrapFinalizerData.find(obj); + return iter == m_wrapFinalizerData.end() ? nullptr : iter->second; + } + + // a point-in-time copy of every still-registered napi_wrap finalizer + // entry, for env-cleanup-time forced finalization (RunEnvCleanupWrapFinalizers, + // NapiFunctions.cpp): iterating m_wrapFinalizerData directly there isn't + // safe, since invoking one entry's finalizer removes *itself* from that + // same map (NapiWrapFinalizer calls takeWrapFinalizerData up front). + std::vector> snapshotWrapFinalizerData() const + { + std::vector> result; + result.reserve(m_wrapFinalizerData.size()); + for (auto& entry : m_wrapFinalizerData) { + result.push_back(entry); + } + return result; + } + + // Tracks every still-weak napi_ref pointing at a given GC-heap target + // (`target` is that target's identity as a raw pointer - an ObjectRef* + // for napi_wrap/napi_add_finalizer's purposes below, but napi_ref targets + // in general can be any heap value a weak napi_ref can point at, e.g. a + // Symbol from napi_create_reference; same non-owning-key rationale as + // m_wrapFinalizerData above: Escargot's GC never moves objects), tracked + // independently of Escargot's own GC finalizer list + // (Memory::gcRegisterFinalizer, EscargotPublic.cpp), whose per-object + // finalizer callbacks fire in plain registration order - unlike V8, + // which clears every weak handle to a dying object *before* running any + // of that object's second-pass finalizer callbacks. Without this, an + // object that has both a napi_wrap/napi_add_finalizer finalizer *and* a + // separate weak napi_ref to the same object + // (napi_create_reference/napi_reference_unref) could have its + // wrap/add_finalizer callback observe napi_get_reference_value as + // still-live (not yet nulled) if that finalizer happened to be + // registered - i.e. napi_wrap/napi_add_finalizer called - before the weak + // napi_ref was created, which is exactly Escargot's registration order in + // that case. NapiWrapFinalizer/NapiAddFinalizerFinalizer + // (NapiFunctions.cpp/NapiExtras.cpp) call clearWeakRefTargets() up front, + // before invoking the user's own finalize_cb, to force that same + // already-nulled guarantee regardless of registration order (found via + // test_reference/test.js's validateDeleteBeforeFinalize/ + // DeleteBeforeFinalizeFinalizer, which asserts exactly this). + // NapiWeakRefFinalizer (the plain, no-other-finalizer case) still exists + // and independently nulls the same napi_ref__::value - redundantly but + // harmlessly, since by the time it runs here it's already null. + // `ref` is stored as a type-erased void* (rather than napi_ref__*) so + // this header doesn't need napi_ref__ to be a complete type (it's only + // forward-declared here) - clearWeakRefTargets casts back once it's + // defined out-of-line in NapiTypes.h, after napi_ref__'s real definition. + void trackWeakRefTarget(void* target, napi_ref__* ref) + { + m_weakRefTargets[target].push_back(ref); + } + + void untrackWeakRefTarget(void* target, napi_ref__* ref) + { + if (m_weakRefTargets.count(target) == 0) { + return; + } + std::vector& refs = m_weakRefTargets[target]; + for (size_t i = 0; i < refs.size(); i++) { + if (refs[i] == ref) { + refs.erase(refs.begin() + i); + break; + } + } + if (refs.empty()) { + m_weakRefTargets.erase(target); + } + } + + // defined out-of-line in NapiTypes.h, once napi_ref__ is a complete type + // (this dereferences ref->value, unlike the two methods above). + void clearWeakRefTargets(void* target); + + // True for the duration of a synchronous, GC-triggered napi_wrap/ + // napi_add_finalizer/napi_create_external finalize_cb (NapiWrapFinalizer/ + // NapiAddFinalizerFinalizer/NapiExternalObjectFinalizer) - i.e. NOT during + // a node_api_post_finalizer drain (NapiEnv::drainPostFinalizers), which is + // explicitly safe to call back into JS from. Real Node-API finalizers of + // this synchronous kind receive only a `node_api_basic_env` for exactly + // this reason: casting it back to a real napi_env and calling something + // that needs to allocate/run JS (e.g. napi_create_object) is a contract + // violation matching V8's own "may affect GC state" fatal error - see + // this flag's one reader, napi_create_object (NapiFunctions.cpp). + void setInGCUnsafeFinalizer(bool value) + { + m_inGCUnsafeFinalizer = value; + } + + bool isInGCUnsafeFinalizer() const + { + return m_inGCUnsafeFinalizer; + } + + // napi_add_env_cleanup_hook/napi_remove_env_cleanup_hook (NapiRuntime.cpp): + // every still-registered hook runs exactly once at environment teardown, + // most-recently-added-first (~NapiEnv(), NapiEnv.cpp) - mirroring Node's + // own env_cleanup_hooks_ stack. removeEnvCleanupHook removes the + // most-recently-added entry whose (fun, arg) pair matches exactly, same + // as Node's RemoveCleanupHook. + void addEnvCleanupHook(napi_cleanup_hook fun, void* arg); + void removeEnvCleanupHook(napi_cleanup_hook fun, void* arg); + + // napi_add_async_cleanup_hook/napi_remove_async_cleanup_hook + // (NapiRuntime.cpp): same LIFO-at-teardown contract as the env cleanup + // hooks above, but keyed by an opaque napi_async_cleanup_hook_handle__* + // (rather than a (fun, arg) pair) so a single hook can be registered more + // than once and still be individually removable. Synchronous + // approximation: at teardown each hook is simply invoked and then + // considered done immediately, with no real waiting for asynchronous + // completion (see NapiRuntime.cpp's own comment on napi_add_async_cleanup_hook). + napi_async_cleanup_hook_handle__* addAsyncCleanupHook(napi_async_cleanup_hook hook, void* arg); + void removeAsyncCleanupHook(napi_async_cleanup_hook_handle__* handle); + + // node_api_get_module_file_name (NapiRuntime.cpp): defaults to "" until a + // real module loader exists to set it via setModuleFileName. + void setModuleFileName(const std::string& name) + { + m_moduleFileName = name; + } + + const std::string& moduleFileName() const + { + return m_moduleFileName; + } + +private: + NapiEnv(PersistentRefHolder&& vmInstance, PersistentRefHolder&& context); + + PersistentRefHolder m_vmInstance; + PersistentRefHolder m_context; + PersistentRefHolder m_persistentValueRefMap; + HashMap m_wrapFinalizerData; + HashMap> m_weakRefTargets; + std::vector m_pendingPostFinalizers; + std::vector> m_envCleanupHooks; + std::vector m_asyncCleanupHooks; + std::string m_moduleFileName; + bool m_inGCUnsafeFinalizer = false; + napi_env__ m_env; + + // see enqueueMainThreadCallback/drainMainThreadCallbacks above + std::mutex m_mainThreadCallbacksMutex; + std::deque> m_mainThreadCallbacks; + + // see uvLoop() above; initialized/torn down in the constructor/destructor (NapiEnv.cpp) + uv_loop_t m_uvLoop; +}; + +} // namespace Napi +} // namespace Escargot + +inline Escargot::ContextRef* Escargot::Napi::EnvData::context() +{ + return napiEnv->context(); +} + +#endif +#endif // ENABLE_NAPI diff --git a/src/napi/NapiError.cpp b/src/napi/NapiError.cpp new file mode 100644 index 000000000..bbe0af9c3 --- /dev/null +++ b/src/napi/NapiError.cpp @@ -0,0 +1,112 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "NapiTypes.h" + +#include + +namespace Escargot { +namespace Napi { + +// shared by napi_create_error/napi_create_type_error/napi_create_range_error/ +// node_api_create_syntax_error: builds an ErrorObjectRef of the given `code` +// (ErrorObjectRef::Code, not to be confused with the napi_value `code` +// parameter below) from `msg`, then - mirroring napi_throw_error - sets the +// object's ".code" property to `codeValue`'s string contents when non-null. +// Constructing an ErrorObjectRef never throws, so no Evaluator::execute +// wrapping is needed here. +static napi_status CreateErrorWithCode(napi_env env, napi_value codeValue, napi_value msg, ErrorObjectRef::Code code, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + StringRef* message = FromNapi(msg)->asString(); + ErrorObjectRef* error = ErrorObjectRef::create(state, code, message); + if (codeValue != nullptr) { + StringRef* codeString = FromNapi(codeValue)->asString(); + error->set(state, StringRef::createFromASCII("code"), codeString); + } + *result = ToNapi(error); + return napi_ok; +} + +// shared by napi_throw_type_error/napi_throw_range_error/ +// node_api_throw_syntax_error: mirrors napi_throw_error (NapiFunctions.cpp) +// exactly, storing the newly created error into env->pendingException instead +// of throwing across the API boundary. +static napi_status ThrowErrorWithCode(napi_env env, const char* code, const char* msg, ErrorObjectRef::Code errorCode) +{ + ExecutionStateRef* state = env->executionState; + StringRef* message = StringRef::createFromUTF8(msg, strlen(msg)); + ErrorObjectRef* error = ErrorObjectRef::create(state, errorCode, message); + if (code) { + error->set(state, StringRef::createFromASCII("code"), StringRef::createFromUTF8(code, strlen(code))); + } + env->pendingException = error; + return napi_ok; +} + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_create_error(napi_env env, napi_value code, napi_value msg, napi_value* result) +{ + return CreateErrorWithCode(env, code, msg, ErrorObjectRef::Code::None, result); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_type_error(napi_env env, napi_value code, napi_value msg, napi_value* result) +{ + return CreateErrorWithCode(env, code, msg, ErrorObjectRef::Code::TypeError, result); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_range_error(napi_env env, napi_value code, napi_value msg, napi_value* result) +{ + return CreateErrorWithCode(env, code, msg, ErrorObjectRef::Code::RangeError, result); +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_syntax_error(napi_env env, napi_value code, napi_value msg, napi_value* result) +{ + return CreateErrorWithCode(env, code, msg, ErrorObjectRef::Code::SyntaxError, result); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_throw_type_error(napi_env env, const char* code, const char* msg) +{ + return ThrowErrorWithCode(env, code, msg, ErrorObjectRef::Code::TypeError); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_throw_range_error(napi_env env, const char* code, const char* msg) +{ + return ThrowErrorWithCode(env, code, msg, ErrorObjectRef::Code::RangeError); +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_throw_syntax_error(napi_env env, const char* code, const char* msg) +{ + return ThrowErrorWithCode(env, code, msg, ErrorObjectRef::Code::SyntaxError); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_error(napi_env env, napi_value value, bool* result) +{ + *result = FromNapi(value)->isErrorObject(); + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiExtras.cpp b/src/napi/NapiExtras.cpp new file mode 100644 index 000000000..111a98a00 --- /dev/null +++ b/src/napi/NapiExtras.cpp @@ -0,0 +1,553 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Functions missing from the original PoC slice, following the same +// conventions NapiFunctions.cpp establishes (napi_value <-> ValueRef* +// punning via ToNapi/FromNapi, Evaluator::execute-wrapped exception +// boundaries, SetLastError-routed error returns). Two kinds of gaps live +// here: +// - functions declared in the vendored js_native_api.h/node_api.h but never +// implemented (node_api_symbol_for, node_api_create_property_key_*, +// napi_get_all_property_names, napi_add_finalizer); +// - functions the vendored headers didn't even declare, patched in +// alongside their implementation here because a newer vendored test +// addon calls them (node_api_create_object_with_properties, +// node_api_is_sharedarraybuffer, node_api_set_prototype, +// node_api_post_finalizer - see the header comments at each declaration). + +#include "NapiTypes.h" + +#include +#include +#include +#include + +namespace Escargot { +namespace Napi { + +namespace { + +// napi_add_finalizer: a GC finalizer that just invokes the user's +// finalize_cb, deliberately NOT touching extraData()/the wrap-finalizer-data +// side table the way napi_wrap's WrapFinalizeData/NapiWrapFinalizer +// (NapiFunctions.cpp) do - napi_add_finalizer must be attachable to an +// object independently of (and without disturbing) any existing napi_wrap on +// the same object, so it gets its own, simpler finalizer plumbing instead of +// reusing napi_wrap's. +struct AddFinalizerData { + node_api_basic_env env; + node_api_basic_finalize finalizeCb; + void* nativeData; + void* finalizeHint; +}; + +static void NapiAddFinalizerFinalizer(void* self, void* data) +{ + AddFinalizerData* finalizeData = reinterpret_cast(data); + // force any other still-weak napi_ref to this same object to already + // read as cleared before this finalizer runs - see + // NapiEnv::clearWeakRefTargets's own comment (NapiEnv.h) and + // NapiWrapFinalizer's identical call (NapiFunctions.cpp). + finalizeData->env->napiEnv->clearWeakRefTargets(self); + // see NapiEnv::isInGCUnsafeFinalizer's own comment (NapiEnv.h) and + // NapiWrapFinalizer's identical bracketing (NapiFunctions.cpp): + // finalizeCb only ever received a node_api_basic_env, so calling + // anything that needs to allocate/run JS from it is a contract violation + // this project surfaces as the same fatal error real Node-API would - + // found via test_finalizer/test_fatal_finalize.js's own + // addFinalizerFailOnJS, which uses napi_add_finalizer (not napi_wrap) for + // exactly this. + finalizeData->env->napiEnv->setInGCUnsafeFinalizer(true); + finalizeData->finalizeCb(finalizeData->env, finalizeData->nativeData, finalizeData->finalizeHint); + finalizeData->env->napiEnv->setInGCUnsafeFinalizer(false); + delete finalizeData; +} + +// napi_get_all_property_names' napi_key_keep_numbers conversion: a property +// key that is a canonical array-index string (ECMA-262 6.1.7's "array +// index": no leading zero other than "0" itself, value in +// [0, 2^32-2]) is reported back as an actual Number instead of the String +// every own property key otherwise naturally is - matching V8's own +// behavior for this option (napi_key_numbers_to_strings, the other option, +// requires no special handling: it just leaves every key as the String it +// already is). +bool TryParseArrayIndexString(StringRef* str, double* outValue) +{ + size_t len = str->length(); + if (len == 0 || len > 10) { // 2^32-1 is 10 digits; anything longer can't fit + return false; + } + + std::string digits; + digits.reserve(len); + for (size_t i = 0; i < len; i++) { + char16_t c = str->charAt(i); + if (c < u'0' || c > u'9') { + return false; + } + digits.push_back(static_cast(c)); + } + if (digits.size() > 1 && digits[0] == '0') { // "01" etc is not a canonical index + return false; + } + + unsigned long long parsed = strtoull(digits.c_str(), nullptr, 10); + if (parsed > 4294967294ULL) { // max valid array index is 2^32-2 + return false; + } + *outValue = static_cast(parsed); + return true; +} + +// reads `name` off a property descriptor object (the {value, writable, +// enumerable, configurable}/{get, set, enumerable, configurable} shape +// ObjectRef::getOwnPropertyDescriptor returns) as a plain bool, defaulting +// to `defaultValue` if the descriptor doesn't have that key at all - this +// happens legitimately for "writable" on an accessor-property descriptor +// (which has no such concept), which napi_get_all_property_names then +// treats as "not writable" for its napi_key_writable filter, same as a +// getter-only property would report via any other API. +bool ReadDescriptorBool(ExecutionStateRef* state, ObjectRef* descriptor, const char* name, bool defaultValue) +{ + StringRef* key = StringRef::createFromASCII(name, strlen(name)); + if (!descriptor->hasOwnProperty(state, key)) { + return defaultValue; + } + return descriptor->get(state, key)->toBoolean(state); +} + +} // namespace + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status node_api_symbol_for(napi_env env, const char* utf8description, size_t length, napi_value* result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + size_t descLen = (length == NAPI_AUTO_LENGTH) ? (utf8description ? strlen(utf8description) : 0) : length; + StringRef* desc = StringRef::createFromUTF8(utf8description ? utf8description : "", descLen); + + // SymbolRef::fromGlobalSymbolRegistry (EscargotPublic.h) already *is* + // Symbol.for's global-registry lookup/insert, keyed process-wide off the + // owning VMInstanceRef (matching the spec: the registry is a per-realm- + // group, not per-Context, table) - no separate NapiEnv-level map needed. + *result = ToNapi(SymbolRef::fromGlobalSymbolRegistry(env->napiEnv->vmInstance(), desc)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_property_key_latin1(napi_env env, const char* str, size_t length, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // a Latin1 string is already a valid property key (ToPropertyKey on any + // String is the identity), so this is exactly napi_create_string_latin1 + // (NapiValue.cpp) under a name that documents the intended use. + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? strlen(str) : length; + *result = ToNapi(StringRef::createFromLatin1(reinterpret_cast(str), stringLength)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_property_key_utf8(napi_env env, const char* str, size_t length, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + size_t byteLength = (length == NAPI_AUTO_LENGTH) ? strlen(str) : length; + *result = ToNapi(StringRef::createFromUTF8(str, byteLength)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_property_key_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // same reasoning as node_api_create_property_key_latin1 above, mirroring + // napi_create_string_utf16 (NapiValue.cpp). + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? std::char_traits::length(str) : length; + *result = ToNapi(StringRef::createFromUTF16(str, stringLength)); + return napi_ok; +} + +// Also declared (js_native_api.h) but unimplemented - dlopen'd by +// test_string's addon right alongside node_api_create_property_key_utf16 +// above. StringRef::createExternalFrom* (EscargotPublic.h) has no deleter +// callback parameter of its own, unlike e.g. BackingStoreRef's - but since +// the returned StringRef is itself an ordinary GC-heap pointer, a finalizer +// can still be attached to *it* directly via Memory::gcRegisterFinalizer, +// exactly as if it were an object (see napi_wrap/napi_create_external's +// identical pattern - NapiFunctions.cpp/NapiArrayBuffer.cpp), invoking +// finalize_callback for the caller's original buffer once the wrapping +// string is collected. This actually avoids copying `str` (`*copied = +// false`), unlike this milestone's earlier always-copy shortcut: some +// addons - e.g. test_string/test_string.c's create_external_latin1/ +// create_external_utf16 helpers - specifically assert `copied` comes back +// false and treat a copy as a test failure. +struct ExternalStringFinalizeData { + napi_env env; + node_api_basic_finalize finalizeCb; + void* nativeData; + void* finalizeHint; +}; + +static void NapiExternalStringFinalizer(void* self, void* data) +{ + ExternalStringFinalizeData* finalizeData = reinterpret_cast(data); + finalizeData->finalizeCb(finalizeData->env, finalizeData->nativeData, finalizeData->finalizeHint); + delete finalizeData; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? strlen(str) : length; + StringRef* strRef = StringRef::createExternalFromLatin1(reinterpret_cast(str), stringLength); + *result = ToNapi(strRef); + if (copied != nullptr) { + *copied = false; + } + if (finalize_callback != nullptr) { + ExternalStringFinalizeData* finalizeData = new ExternalStringFinalizeData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_callback; + finalizeData->nativeData = str; + finalizeData->finalizeHint = finalize_hint; + Memory::gcRegisterFinalizer(strRef, NapiExternalStringFinalizer, finalizeData); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? std::char_traits::length(str) : length; + StringRef* strRef = StringRef::createExternalFromUTF16(str, stringLength); + *result = ToNapi(strRef); + if (copied != nullptr) { + *copied = false; + } + if (finalize_callback != nullptr) { + ExternalStringFinalizeData* finalizeData = new ExternalStringFinalizeData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_callback; + finalizeData->nativeData = str; + finalizeData->finalizeHint = finalize_hint; + Memory::gcRegisterFinalizer(strRef, NapiExternalStringFinalizer, finalizeData); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // Walking own properties and following the prototype chain can each + // invoke a Proxy trap (ownKeys/getOwnPropertyDescriptor/getPrototypeOf), + // any of which may throw - same rationale as napi_get_property_names' + // identical wrapping (NapiObject.cpp). Deliberately built on + // ownPropertyKeys()/getOwnPropertyDescriptor() rather than + // enumerateObjectOwnProperties(): ProxyObject::enumeration() + // (ProxyObject.cpp) is explicitly documented there as *not* invoking the + // Proxy's own ownKeys trap at all (it just walks the underlying target + // directly, unlike ownPropertyKeys()/getOwnProperty(), which are + // properly overridden to invoke the real traps) - found via + // test_object/test_exceptions.js, whose every trap deliberately throws; + // enumerateObjectOwnProperties on that Proxy silently produced zero + // properties (reading straight through to the underlying, empty `{}` + // target) instead of propagating the ownKeys trap's exception. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, napi_key_collection_mode key_mode, napi_key_filter key_filter, napi_key_conversion key_conversion) -> ValueRef* { + bool skipStrings = (key_filter & napi_key_skip_strings) != 0; + bool skipSymbols = (key_filter & napi_key_skip_symbols) != 0; + unsigned attributeFilter = key_filter & (napi_key_writable | napi_key_enumerable | napi_key_configurable); + + ValueVectorRef* names = ValueVectorRef::create(); + + OptionalRef current = obj; + while (current.hasValue()) { + ValueVectorRef* ownKeys = current.value()->ownPropertyKeys(state); + for (size_t i = 0; i < ownKeys->size(); i++) { + ValueRef* propertyName = ownKeys->at(i); + if (propertyName->isString() && skipStrings) { + continue; + } + if (propertyName->isSymbol() && skipSymbols) { + continue; + } + + if (attributeFilter != 0) { + ValueRef* descriptor = current.value()->getOwnPropertyDescriptor(state, propertyName); + if (descriptor->isUndefined()) { + // vanished (e.g. a trap reporting an + // inconsistent ownKeys/getOwnPropertyDescriptor + // pair) - nothing to filter on, so skip it. + continue; + } + ObjectRef* descriptorObj = descriptor->asObject(); + if ((attributeFilter & napi_key_writable) && !ReadDescriptorBool(state, descriptorObj, "writable", false)) { + continue; + } + if ((attributeFilter & napi_key_enumerable) && !ReadDescriptorBool(state, descriptorObj, "enumerable", false)) { + continue; + } + if ((attributeFilter & napi_key_configurable) && !ReadDescriptorBool(state, descriptorObj, "configurable", false)) { + continue; + } + } + + // a property already collected from a more-derived + // object in the chain shadows this (ancestor) one - + // matches for-in/Reflect.ownKeys-walk semantics. + bool alreadyCollected = false; + for (size_t j = 0; j < names->size(); j++) { + if (names->at(j)->equalsTo(state, propertyName)) { + alreadyCollected = true; + break; + } + } + if (alreadyCollected) { + continue; + } + + ValueRef* keyToPush = propertyName; + if (key_conversion == napi_key_keep_numbers && propertyName->isString()) { + double numericValue; + if (TryParseArrayIndexString(propertyName->asString(), &numericValue)) { + keyToPush = ValueRef::create(numericValue); + } + } + names->pushBack(keyToPush); + } + + if (key_mode == napi_key_own_only) { + break; + } + current = current.value()->getPrototypeObject(state); + } + + return ArrayObjectRef::create(state, names); + }, + obj, key_mode, key_filter, key_conversion); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return napi_pending_exception; + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + if (env == nullptr || js_object == nullptr || finalize_cb == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* obj = FromNapi(js_object); + if (!obj->isObject() && !obj->isSymbol()) { + return SetLastError(env, napi_object_expected); + } + + AddFinalizerData* finalizeData = new AddFinalizerData(); + finalizeData->env = env; + finalizeData->finalizeCb = finalize_cb; + finalizeData->nativeData = finalize_data; + finalizeData->finalizeHint = finalize_hint; + // deliberately NOT obj->setExtraData(...) - see this file's + // AddFinalizerData comment: napi_add_finalizer must not conflict with a + // napi_wrap already (or later) placed on the same object. + Memory::gcRegisterFinalizer(obj, NapiAddFinalizerFinalizer, finalizeData); + + if (result != nullptr) { + // a weak napi_ref to the object, same as napi_wrap's `result` - + // reuses NapiFunctions.cpp's own RegisterWeakRefFinalizerIfNeeded + // (rather than duplicating it with a second, distinct finalizer + // callback) precisely so this ref behaves identically to any other + // napi_ref under napi_reference_ref/napi_delete_reference, both of + // which look specifically for NapiWeakRefFinalizer via + // Memory::gcUnregisterFinalizer to manage it. + napi_ref__* ref = new napi_ref__(); + ref->value = obj; + ref->refcount = 0; + RegisterWeakRefFinalizerIfNeeded(env, ref); + *result = ref; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_object_with_properties(napi_env env, napi_value prototype, const napi_value* names, const napi_value* values, size_t property_count, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + + // setPrototype (only reached when `prototype` is non-NULL - a genuine + // napi_value, itself possibly the JS `null` value, not merely "argument + // omitted") can invoke a Proxy's setPrototypeOf trap and throw, so the + // whole thing is sandboxed the same way napi_call_function is + // (NapiFunctions.cpp). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, napi_value prototypeNapi, const napi_value* names, const napi_value* values, size_t property_count) -> ValueRef* { + ObjectRef* obj = ObjectRef::create(state); + if (prototypeNapi != nullptr) { + obj->setPrototype(state, FromNapi(prototypeNapi)); + } + for (size_t i = 0; i < property_count; i++) { + obj->defineDataProperty(state, FromNapi(names[i]), FromNapi(values[i]), true, true, true); + } + return obj; + }, + prototype, names, values, property_count); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return napi_pending_exception; + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_is_sharedarraybuffer(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + *result = FromNapi(value)->isSharedArrayBufferObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_sharedarraybuffer(napi_env env, size_t byte_length, void** data, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + + // SharedArrayBufferObjectRef::create allocates the SharedDataBlock backing + // store itself (unlike ArrayBufferObjectRef::create + allocateBuffer). + SharedArrayBufferObjectRef* buf = SharedArrayBufferObjectRef::create(state, byte_length); + + if (data != nullptr) { + *data = buf->rawBuffer(); + } + *result = ToNapi(buf); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_set_prototype(napi_env env, napi_value object, napi_value prototype) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* proto = FromNapi(prototype); + ExecutionStateRef* state = env->executionState; + + // ObjectRef::setPrototype can invoke a Proxy's setPrototypeOf trap and + // throw - same rationale as node_api_create_object_with_properties above. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* proto) -> ValueRef* { + obj->setPrototype(state, proto); + return ValueRef::createUndefined(); + }, + obj, proto); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return napi_pending_exception; + } + return napi_ok; +} + +// NAPI_NO_RETURN void, not napi_status - node_api.h's own declaration (there +// is no napi_env to report a status through in the first place: this is +// meant to be callable even where the engine/env state itself may already be +// broken). +ESCARGOT_NAPI_EXPORT NAPI_NO_RETURN void napi_fatal_error(const char* location, size_t location_len, const char* message, size_t message_len) +{ + std::string locationStr = (location == nullptr) ? std::string() : ((location_len == NAPI_AUTO_LENGTH) ? std::string(location) : std::string(location, location_len)); + std::string messageStr = (message == nullptr) ? std::string() : ((message_len == NAPI_AUTO_LENGTH) ? std::string(message) : std::string(message, message_len)); + + if (!locationStr.empty()) { + fprintf(stderr, "FATAL ERROR: %s %s\n", locationStr.c_str(), messageStr.c_str()); + } else { + fprintf(stderr, "FATAL ERROR: %s\n", messageStr.c_str()); + } + fflush(stderr); + abort(); +} + +// Minimal viable behavior per this milestone's scope: report `err` to +// stderr the way an actual uncaught exception would print, then return - +// there is no process-wide 'uncaughtException' hook wired into this PoC yet +// (a separate wave handles uncaughtException support generally) for this to +// forward into instead. +ESCARGOT_NAPI_EXPORT napi_status napi_fatal_exception(napi_env env, napi_value err) +{ + if (env == nullptr || err == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // Node-API: napi_fatal_exception triggers an 'uncaughtException'. Model + // that by making `err` the env's pending exception; the napi callback + // trampoline (NapiCallbackTrampoline, NapiFunctions.cpp) rethrows it into + // JS the instant the current native callback returns, so it propagates as + // an ordinary uncaught exception - routed to any process.on( + // 'uncaughtException') handler by the embedder/harness, or reported + // fatally if none. Supersedes the previous stderr-only stub, which never + // reached a handler (so test_fatal_exception's mustCall never fired). + env->pendingException = FromNapi(err); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_post_finalizer(node_api_basic_env basic_env, napi_finalize finalize_cb, void* finalize_data, void* finalize_hint) +{ + if (basic_env == nullptr || finalize_cb == nullptr) { + return SetLastError(basic_env, napi_invalid_arg); + } + // queued, not invoked here - see NapiEnv::drainPostFinalizers (NapiEnv.h/ + // NapiEnv.cpp) for why (a post-finalizer's whole point, unlike a plain + // napi_wrap/napi_add_finalizer finalize_cb, is that it is safe to call + // back into JS - which requires running it later, at a safe point, not + // synchronously from here or from a GC sweep). + basic_env->napiEnv->enqueuePostFinalizer(finalize_cb, finalize_data, finalize_hint); + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiFunctions.cpp b/src/napi/NapiFunctions.cpp new file mode 100644 index 000000000..911480714 --- /dev/null +++ b/src/napi/NapiFunctions.cpp @@ -0,0 +1,1047 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Implements just enough of js_native_api.h to run +// test/napi-tc/test/js-native-api/2_function_arguments. This is an early +// slice of a larger PoC function list, not the full surface. + +#include "NapiTypes.h" + +#include +#include +#include +#include + +namespace Escargot { +namespace Napi { + +static ValueRef* NapiCallbackTrampoline(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + FunctionObjectRef* callee = state->resolveCallee().value(); + CallbackData* callbackData = reinterpret_cast(callee->extraData()); + napi_env env = callbackData->env; + + ExecutionStateRef* previousState = env->executionState; + env->executionState = state; + + napi_callback_info__ cbinfo{ argc, argv, thisValue, callbackData->data, nullptr }; + napi_value result = callbackData->callback(env, reinterpret_cast(&cbinfo)); + + env->executionState = previousState; + + if (env->pendingException.hasValue()) { + ValueRef* exceptionValue = env->pendingException.value(); + env->pendingException = nullptr; + state->throwException(exceptionValue); // does not return + } + + return result ? FromNapi(result) : ValueRef::createUndefined(); +} + +static void NapiFunctionCallbackDataFinalizer(void* self, void* data) +{ + delete reinterpret_cast(data); +} + +// `this` for a constructor call is always a fresh, engine-created object here +// (see FunctionTemplateRef::NativeFunctionPointer's contract), unlike +// NapiCallbackTrampoline/FunctionObjectRef::NativeFunctionPointer above, so +// napi_get_new_target has a real value to report and napi_wrap can attach +// native data to `this` the same way a user's constructor callback expects. +static ValueRef* NapiClassConstructorTrampoline(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, OptionalRef newTarget) +{ + FunctionObjectRef* callee = state->resolveCallee().value(); + CallbackData* callbackData = reinterpret_cast(callee->extraData()); + napi_env env = callbackData->env; + + ExecutionStateRef* previousState = env->executionState; + env->executionState = state; + + napi_callback_info__ cbinfo{ argc, argv, thisValue, callbackData->data, newTarget.hasValue() ? OptionalRef(newTarget.value()) : nullptr }; + napi_value result = callbackData->callback(env, reinterpret_cast(&cbinfo)); + + env->executionState = previousState; + + if (env->pendingException.hasValue()) { + ValueRef* exceptionValue = env->pendingException.value(); + env->pendingException = nullptr; + state->throwException(exceptionValue); // does not return + } + + if (newTarget.hasValue()) { + // ES construct semantics: an explicit object return overrides the + // pre-created `this` (rare in practice; N-API constructors usually + // just `return _this`) + OptionalRef returnValue = FromNapi(result); // FromNapi(nullptr) is an empty OptionalRef + return (returnValue.hasValue() && returnValue->isObject()) ? returnValue.value() : thisValue; + } + return result ? FromNapi(result) : ValueRef::createUndefined(); +} + +struct WrapFinalizeData { + napi_env env; + node_api_basic_finalize finalizeCb; + void* nativeObject; + void* finalizeHint; +}; + +static void NapiWrapFinalizer(void* self, void* data) +{ + WrapFinalizeData* wrapData = reinterpret_cast(data); + // the object is being collected, so its napi_remove_wrap lookup entry + // would otherwise dangle once this same address is reused later + wrapData->env->napiEnv->takeWrapFinalizerData(reinterpret_cast(self)); + // force any other still-weak napi_ref to this same object to already + // read as cleared before this finalizer runs - see + // NapiEnv::clearWeakRefTargets's own comment (NapiEnv.h) for why this + // can't just be left to NapiWeakRefFinalizer/Escargot's own + // registration-ordered GC finalizer list. + wrapData->env->napiEnv->clearWeakRefTargets(self); + // see NapiEnv::isInGCUnsafeFinalizer's own comment (NapiEnv.h): this + // finalize_cb only ever received a node_api_basic_env (even though it's + // stored here as a full napi_env for convenience), so it must not call + // anything that needs to allocate/run JS - found via + // test_finalizer/test_fatal_finalize.js's finalizerWithFailedJSCallback, + // which deliberately casts basic_env back to napi_env and calls + // napi_create_object to check exactly this is caught. + wrapData->env->napiEnv->setInGCUnsafeFinalizer(true); + wrapData->finalizeCb(wrapData->env, wrapData->nativeObject, wrapData->finalizeHint); + wrapData->env->napiEnv->setInGCUnsafeFinalizer(false); + + if (wrapData->env->pendingException.hasValue()) { + ValueRef* fatalErr = wrapData->env->pendingException.value(); + wrapData->env->pendingException = nullptr; + napi_fatal_exception(wrapData->env, ToNapi(fatalErr)); + } + + delete wrapData; +} + +// Forces every still-registered (i.e. neither napi_remove_wrap'd nor already +// GC-finalized) napi_wrap finalizer in `napiEnv` to run right now, regardless +// of whether its wrapped object is still reachable - the real Node-API +// contract for environment teardown: an object kept alive by, say, a +// module.exports property (so ordinary GC would never collect it) still gets +// its finalizer invoked once the owning env/process itself goes away. Called +// from NapiEnv::~NapiEnv() (NapiEnv.cpp). Only napi_wrap/napi_remove_wrap's +// own registry (NapiEnv::m_wrapFinalizerData) is covered - napi_add_finalizer +// (NapiExtras.cpp) has no equivalent forced-run path yet, since no target +// test.js (see test_general/testEnvCleanup.js) needs it. +void RunEnvCleanupWrapFinalizers(NapiEnv* napiEnv) +{ + // snapshot first: NapiWrapFinalizer (invoked below) removes itself from + // napiEnv's registry as it runs, which would otherwise invalidate the + // live map's iterators mid-walk. + std::vector> snapshot = napiEnv->snapshotWrapFinalizerData(); + for (auto& entry : snapshot) { + ObjectRef* obj = entry.first; + void* wrapDataRaw = entry.second; + if (napiEnv->peekWrapFinalizerData(obj) != wrapDataRaw) { + // already gone (napi_remove_wrap'd, replaced by a later + // napi_wrap, or already actually GC-finalized) since the + // snapshot was taken - possible if an earlier entry's own + // finalize_cb reentrantly touched this object - skip it. + continue; + } + WrapFinalizeData* wrapData = reinterpret_cast(wrapDataRaw); + // unregister Boehm's own copy first, so a *later* real GC pass never + // also invokes (and double-frees) this same wrapData once obj + // actually becomes garbage. + Memory::gcUnregisterFinalizer(obj, NapiWrapFinalizer, wrapData); + NapiWrapFinalizer(obj, wrapDataRaw); + } +} + +// clears a weak napi_ref's target once it is collected, so +// napi_get_reference_value stops returning a dangling pointer. `ref` itself +// is a plain (non-GC) heap allocation; see napi_delete_reference's own +// comment for why the actual `delete ref` for a still-weakFinalizerRegistered +// ref is deferred to here (via ref->pendingDelete) instead of happening +// synchronously there. +static void NapiWeakRefFinalizer(void* self, void* data) +{ + napi_ref__* ref = reinterpret_cast(data); + if (ref->pendingDelete) { + delete ref; + return; + } + ref->value = nullptr; +} + +// idempotent: safe to call whenever `ref` is (or becomes) weak, regardless of +// whether NapiWeakRefFinalizer is already registered for it +void RegisterWeakRefFinalizerIfNeeded(napi_env env, napi_ref__* ref) +{ + if (!ref->weakFinalizerRegistered && ref->value.hasValue() && ref->value.value()->isStoredInHeap()) { + Memory::gcRegisterFinalizer(ref->value.value(), NapiWeakRefFinalizer, ref); + ref->weakFinalizerRegistered = true; + // see NapiEnv::trackWeakRefTarget's own comment (NapiEnv.h): tracked + // independently of the GC finalizer list itself, so a + // napi_wrap/napi_add_finalizer finalizer on the same object can + // force this ref's value cleared up front, ahead of Escargot's own + // (registration-ordered, not phase-ordered) finalizer invocation. + env->napiEnv->trackWeakRefTarget(reinterpret_cast(ref->value.value()), ref); + } +} + +// idempotent counterpart of RegisterWeakRefFinalizerIfNeeded; call before +// `ref` stops being weak (napi_reference_ref) or is deleted (napi_delete_reference) +static void UnregisterWeakRefFinalizerIfNeeded(napi_env env, napi_ref__* ref) +{ + if (ref->weakFinalizerRegistered && ref->value.hasValue()) { + env->napiEnv->untrackWeakRefTarget(reinterpret_cast(ref->value.value()), ref); + Memory::gcUnregisterFinalizer(ref->value.value(), NapiWeakRefFinalizer, ref); + ref->weakFinalizerRegistered = false; + } +} + +// Node's own js_native_api_v8.cc error_messages[], indexed by napi_status +// (js_native_api_types.h's napi_status enum order) - see SetLastError +// (NapiTypes.h)/env->lastErrorCode (NapiEnv.h). A null entry (napi_ok) means +// "no message" per napi_get_last_error_info's own contract. +static const char* const kNapiErrorMessages[] = { + nullptr, // napi_ok + "Invalid argument", // napi_invalid_arg + "An object was expected", // napi_object_expected + "A string was expected", // napi_string_expected + "A string or symbol was expected", // napi_name_expected + "A function was expected", // napi_function_expected + "A number was expected", // napi_number_expected + "A boolean was expected", // napi_boolean_expected + "An array was expected", // napi_array_expected + "Unknown failure", // napi_generic_failure + "An exception is pending", // napi_pending_exception + "The async work item was cancelled", // napi_cancelled + "napi_escape_handle already called on scope", // napi_escape_called_twice + "Invalid handle scope usage", // napi_handle_scope_mismatch + "Invalid callback scope usage", // napi_callback_scope_mismatch + "Thread-safe function queue is full", // napi_queue_full + "Thread-safe function handle is closing", // napi_closing + "A bigint was expected", // napi_bigint_expected + "A date was expected", // napi_date_expected + "An arraybuffer was expected", // napi_arraybuffer_expected + "A detachable arraybuffer was expected", // napi_detachable_arraybuffer_expected + "Main thread would deadlock", // napi_would_deadlock + "External buffers are not allowed", // napi_no_external_buffers_allowed + "Cannot run JS trigger by StopIfNecessary", // napi_cannot_run_js +}; + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_get_last_error_info(node_api_basic_env env, const napi_extended_error_info** result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + size_t index = static_cast(env->lastErrorCode); + size_t tableSize = sizeof(kNapiErrorMessages) / sizeof(kNapiErrorMessages[0]); + env->lastErrorInfo.error_message = (index < tableSize) ? kNapiErrorMessages[index] : "Unknown error code"; + env->lastErrorInfo.engine_reserved = nullptr; + env->lastErrorInfo.engine_error_code = 0; + env->lastErrorInfo.error_code = env->lastErrorCode; + *result = &env->lastErrorInfo; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_undefined(napi_env env, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::createUndefined()); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_double(napi_env env, double value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_uint32(napi_env env, uint32_t value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_global(napi_env env, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(env->context()->globalObject()); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_object(napi_env env, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // See NapiEnv::isInGCUnsafeFinalizer's own comment (NapiEnv.h): a + // synchronous GC-triggered finalizer (napi_wrap/napi_add_finalizer/ + // napi_create_external's finalize_cb) is only ever handed a + // node_api_basic_env - allocating a new JS object from inside one anyway + // (by casting it back to a real napi_env, as + // test_finalizer/test_fatal_finalize.js's finalizerWithFailedJSCallback + // deliberately does) is exactly the "calling a function that may affect + // GC state" contract violation real Node-API fatally aborts on. + if (env != nullptr && env->napiEnv->isInGCUnsafeFinalizer()) { + napi_fatal_error(nullptr, 0, "Finalizer is calling a function that may affect GC state.", NAPI_AUTO_LENGTH); + } + + *result = ToNapi(ObjectRef::create(env->executionState)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_string_utf8(napi_env env, const char* str, size_t length, napi_value* result) +{ + if (str == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // matches real Node-API's own guard (js_native_api_v8.cc): an explicit + // (not NAPI_AUTO_LENGTH) length beyond INT_MAX is rejected outright, + // rather than actually reading that many bytes from `str` - found via + // test_string/test.js's TestLargeUtf8, which deliberately passes + // INT_MAX+1 alongside a 1-byte (empty) `str` specifically to check this + // is rejected instead of reading far out of bounds (this previously + // SIGBUS'd instead). + if (length != NAPI_AUTO_LENGTH && length > static_cast(INT_MAX)) { + return SetLastError(env, napi_invalid_arg); + } + + size_t byteLength = (length == NAPI_AUTO_LENGTH) ? strlen(str) : length; + // Transparent compressible-string routing: a large string handed in + // through a perfectly standard napi_create_string_utf8 call is, above + // kCompressibleStringThreshold bytes, allocated as one of Escargot's + // compressible strings instead of a plain one - functionally identical + // (it decompresses on any access) but eligible to be compressed back down + // at idle (VMInstanceRef::enterIdleMode) or during GC + // (CompressCompressibleStringsWhileGC), which is how an unmodified N-API + // addon that simply holds onto lots of document-sized text ends up using + // substantially less resident memory on Escargot with no source changes + // of its own. Small strings (below the threshold - every existing test + // string) keep using the plain path unchanged. + if (byteLength >= kCompressibleStringThreshold && StringRef::isCompressibleStringEnabled() && env != nullptr && env->napiEnv->vmInstance() != nullptr) { + *result = ToNapi(StringRef::createFromUTF8ToCompressibleString(env->napiEnv->vmInstance(), str, byteLength)); + } else { + *result = ToNapi(StringRef::createFromUTF8(str, byteLength)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_set_named_property(napi_env env, napi_value object, const char* utf8name, napi_value value) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + StringRef* propertyName = StringRef::createFromUTF8(utf8name, strlen(utf8name)); + ValueRef* propertyValue = FromNapi(value); + ExecutionStateRef* state = env->executionState; + + // ObjectRef::set can invoke a user setter (or a Proxy `set` trap), either + // of which may throw a raw C++ exception - this was previously left + // unsandboxed here (unlike napi_set_property's identical-in-spirit + // Evaluator::execute wrapping, NapiObject.cpp), so a throwing setter's + // exception would escape all the way past this function's own napi_status + // return contract, past the calling native addon function entirely, and + // surface as a raw uncaught top-level exception instead of + // napi_pending_exception (found via test_object/test_exceptions.js, whose + // Proxy's `set` trap always throws). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, StringRef* name, ValueRef* value) -> ValueRef* { + obj->set(state, name, value); + return ValueRef::createUndefined(); + }, + obj, propertyName, propertyValue); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return napi_pending_exception; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_call_function(napi_env env, napi_value recv, napi_value func, size_t argc, const napi_value* argv, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + ValueRef* fn = FromNapi(func); + ValueRef* thisArg = FromNapi(recv); + + std::vector args(argc); + for (size_t i = 0; i < argc; i++) { + args[i] = FromNapi(argv[i]); + } + + // ValueRef::call() throws a raw C++ exception (Escargot::Value, by + // value) on an uncaught JS exception, with nothing in between catching + // it - it must not be allowed to cross this function's own stack frame, + // or N-API's "exceptions never cross an API call boundary" contract + // breaks. Evaluator::execute's ExecutionStateRef* overload runs the call + // in a nested SandBox that catches it for us. + Evaluator::EvaluatorResult callResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* fn, ValueRef* thisArg, size_t argc, ValueRef** argv) -> ValueRef* { + return fn->call(state, thisArg, argc, argv); + }, + fn, thisArg, argc, args.data()); + + if (!callResult.isSuccessful()) { + env->pendingException = callResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (result != nullptr) { + *result = ToNapi(callResult.result); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result) +{ + ValueRef* v = FromNapi(value); + if (v->isUndefined()) { + *result = napi_undefined; + } else if (v->isNull()) { + *result = napi_null; + } else if (v->isBoolean()) { + *result = napi_boolean; + } else if (v->isNumber()) { + *result = napi_number; + } else if (v->isString()) { + *result = napi_string; + } else if (v->isSymbol()) { + *result = napi_symbol; + } else if (v->isCallable()) { + *result = napi_function; + } else if (v->isBigInt()) { + *result = napi_bigint; + } else { + *result = napi_object; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_double(napi_env env, napi_value value, double* result) +{ + // env/value/result checked (and *before* the isNumber() type check) - + // see NapiValue.cpp's napi_get_value_bool for the identical reasoning + // and originating test (test_conversions/test_null.c's + // GEN_NULL_CHECK_BINDING(..., napi_get_value_double)). + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isNumber()) { + return SetLastError(env, napi_number_expected); + } + *result = v->asNumber(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_uint32(napi_env env, napi_value value, uint32_t* result) +{ + // see napi_get_value_double's identical reasoning (above). + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isNumber()) { + return SetLastError(env, napi_number_expected); + } + *result = v->toUint32(env->executionState); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_function(napi_env env, const char* utf8name, size_t length, napi_callback cb, void* data, napi_value* result) +{ + // env/cb/result are all mandatory (utf8name is not - an anonymous + // function is valid Node-API); env==nullptr in particular must be + // checked *before* anything below dereferences it. This was previously + // entirely unchecked, unconditionally crashing (SIGSEGV on + // `env->context()`) instead of returning napi_invalid_arg - found via + // test_function/test.js's TestCreateFunctionParameters, which - like + // test_number/test_null.js's similar pattern - deliberately calls this + // with each of env/cb/result null in turn to verify graceful rejection. + if (env == nullptr) { + return napi_invalid_arg; + } + if (cb == nullptr || result == nullptr) { + // env is non-null here, so (unlike the env==nullptr case above) + // napi_get_last_error_info can and should report a real message for + // this - see SetLastError (NapiTypes.h)/kNapiErrorMessages above - + // and this addon's test (like several others) checks that message, + // not just the returned napi_status. + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + ContextRef* context = env->context(); + + size_t nameLen = (utf8name == nullptr) ? 0 : ((length == NAPI_AUTO_LENGTH) ? strlen(utf8name) : length); + AtomicStringRef* name = AtomicStringRef::create(context, utf8name ? utf8name : "", nameLen); + + // Route through FunctionTemplateRef + NapiClassConstructorTrampoline - + // exactly what napi_define_class (below) already does - instead of the + // plain FunctionObjectRef::create(..., NapiCallbackTrampoline) this used + // previously. Per the Node-API contract, a function created by + // napi_create_function may be invoked with `new` (and, transitively, + // used as an ES6 `class ... extends` target) exactly like one created by + // napi_define_class does; it's only napi_define_class's *properties* + // (static vs instance, via napi_static) that differ, not constructibility + // of the plain function case. The previous NapiCallbackTrampoline path + // hardcoded isConstructor=false *and* always reported new.target as NULL + // (napi_callback_info__'s newTarget field was a hardcoded `nullptr`), + // silently breaking `new`/`extends`/napi_get_new_target on any function + // returned from napi_create_function. Found via test_new_target/test.js: + // `class Class extends binding.BaseClass` (BaseClass created via + // napi_create_function, per Node.js's own test) threw "Class extends + // value is not object nor null", and even after allowing constructibility + // alone, the addon's own new.target-non-null assertion (invoked through + // `super()`) would still have failed. + FunctionTemplateRef* tpl = FunctionTemplateRef::create(name, 0, true, true, NapiClassConstructorTrampoline); + // addToContextCache=false: napi_create_function is called dynamically and + // unboundedly, so the throwaway template must not be pinned in the context + // cache (which would leak it and keep the function permanently rooted). + ObjectRef* fnObj = tpl->instantiate(context, false); + FunctionObjectRef* fn = fnObj->asFunctionObject(); + + CallbackData* callbackData = new CallbackData(); + callbackData->env = env; + callbackData->callback = cb; + callbackData->data = data; + fn->setExtraData(callbackData); + // free callbackData once the FunctionObjectRef itself is collected, instead of leaking it + Memory::gcRegisterFinalizer(fn, NapiFunctionCallbackDataFinalizer, callbackData); + + *result = ToNapi(fn); + return napi_ok; +} + +// shared by napi_define_properties (applies to the target object itself) and +// napi_define_class (applies to the constructor's .prototype, or to the +// constructor itself for napi_static members) +static void ApplyPropertyDescriptor(napi_env env, ObjectRef* target, const napi_property_descriptor& p) +{ + ExecutionStateRef* state = env->executionState; + + ValueRef* propertyName = p.utf8name ? static_cast(StringRef::createFromUTF8(p.utf8name, strlen(p.utf8name))) : FromNapi(p.name); + size_t nameLength = p.utf8name ? strlen(p.utf8name) : NAPI_AUTO_LENGTH; + + bool isWritable = (p.attributes & napi_writable) != 0; + bool isEnumerable = (p.attributes & napi_enumerable) != 0; + bool isConfigurable = (p.attributes & napi_configurable) != 0; + + if (p.getter != nullptr || p.setter != nullptr) { + ValueRef* getter = ValueRef::createUndefined(); + if (p.getter != nullptr) { + napi_value fn; + napi_create_function(env, p.utf8name, nameLength, p.getter, p.data, &fn); + getter = FromNapi(fn); + } + OptionalRef setter; + if (p.setter != nullptr) { + napi_value fn; + napi_create_function(env, p.utf8name, nameLength, p.setter, p.data, &fn); + setter = FromNapi(fn); + } + ObjectRef::PresentAttribute attr = static_cast( + (isEnumerable ? ObjectRef::EnumerablePresent : ObjectRef::NonEnumerablePresent) | (isConfigurable ? ObjectRef::ConfigurablePresent : ObjectRef::NonConfigurablePresent)); + target->defineAccessorProperty(state, propertyName, ObjectRef::AccessorPropertyDescriptor(getter, setter, attr)); + return; + } + + ValueRef* value; + if (p.method) { + napi_value fn; + napi_create_function(env, p.utf8name, nameLength, p.method, p.data, &fn); + value = FromNapi(fn); + } else if (p.value) { + value = FromNapi(p.value); + } else { + return; + } + + target->defineDataProperty(state, propertyName, value, isWritable, isEnumerable, isConfigurable); +} + +ESCARGOT_NAPI_EXPORT napi_status napi_define_properties(napi_env env, napi_value object, size_t property_count, const napi_property_descriptor* properties) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // ApplyPropertyDescriptor's own defineDataProperty/defineAccessorProperty + // calls can invoke a Proxy's defineProperty trap and throw - previously + // unsandboxed here, unlike every other property mutator in this file/ + // NapiObject.cpp (found via test_object/test_exceptions.js, whose Proxy's + // `defineProperty` trap always throws). Note that the lambda below still + // calls ApplyPropertyDescriptor(env, ...) - which internally keeps + // reading env->executionState, not this lambda's own `state` parameter - + // rather than threading `state` through; that's fine (not stale/wrong): + // Evaluator::execute's exception-catching sandbox is a plain C++ + // try/catch around this whole closure invocation, so it catches a throw + // regardless of which ExecutionStateRef the throwing call happened to + // use, and env->executionState itself is left untouched (so, unlike a + // temporary env->executionState swap would be, never at risk of being + // left dangling on the object should an exception actually unwind through here). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, napi_env env, ObjectRef* obj, size_t property_count, const napi_property_descriptor* properties) -> ValueRef* { + for (size_t i = 0; i < property_count; i++) { + ApplyPropertyDescriptor(env, obj, properties[i]); + } + return ValueRef::createUndefined(); + }, + env, obj, property_count, properties); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return napi_pending_exception; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + // env==nullptr must be checked *before* anything below dereferences it - + // same pattern/rationale as napi_create_function's own env==nullptr + // check above. Found via test_constructor/test.js's TestDefineClass, + // which - like test_number/test_null.js's similar pattern - deliberately + // calls this with each of env/utf8name/constructor/properties/result null + // in turn (data==nullptr alone is valid - it's an optional user pointer) + // to verify graceful rejection instead of a SIGSEGV. + if (env == nullptr) { + return napi_invalid_arg; + } + // matches real Node-API's NAPI_PREAMBLE(env), which clears the env's + // last-error status before doing any work, so a subsequent successful + // call reports napi_ok (not whatever an earlier failed sibling call left + // behind) - found via TestDefineClass's cbDataIsNull case, which expects + // exactly that after a preceding cbIsNull failure. + SetLastError(env, napi_ok); + if (utf8name == nullptr || constructor == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + if (property_count > 0 && properties == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ExecutionStateRef* state = env->executionState; + ContextRef* context = env->context(); + + size_t nameLen = (utf8name == nullptr) ? 0 : ((length == NAPI_AUTO_LENGTH) ? strlen(utf8name) : length); + AtomicStringRef* name = AtomicStringRef::create(context, utf8name ? utf8name : "", nameLen); + + FunctionTemplateRef* tpl = FunctionTemplateRef::create(name, 0, true, true, NapiClassConstructorTrampoline); + // addToContextCache=false: same rationale as napi_create_function - each + // napi_define_class builds a fresh throwaway template, so caching it would + // leak the class constructor for the context's lifetime. + ObjectRef* consObj = tpl->instantiate(context, false); + FunctionObjectRef* cons = consObj->asFunctionObject(); + + CallbackData* callbackData = new CallbackData(); + callbackData->env = env; + callbackData->callback = constructor; + callbackData->data = data; + cons->setExtraData(callbackData); + Memory::gcRegisterFinalizer(cons, NapiFunctionCallbackDataFinalizer, callbackData); + + ObjectRef* proto = cons->getFunctionPrototype(state)->asObject(); + + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor& p = properties[i]; + ObjectRef* target = (p.attributes & napi_static) ? static_cast(cons) : proto; + ApplyPropertyDescriptor(env, target, p); + } + + *result = ToNapi(cons); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, node_api_basic_finalize finalize_cb, void* finalize_hint, napi_ref* result) +{ + ObjectRef* obj = FromNapi(js_object)->asObject(); + obj->setExtraData(native_object); + + if (finalize_cb != nullptr) { + WrapFinalizeData* wrapData = new WrapFinalizeData(); + wrapData->env = env; + wrapData->finalizeCb = finalize_cb; + wrapData->nativeObject = native_object; + wrapData->finalizeHint = finalize_hint; + Memory::gcRegisterFinalizer(obj, NapiWrapFinalizer, wrapData); + env->napiEnv->setWrapFinalizerData(obj, wrapData); + } + + if (result != nullptr) { + napi_ref__* ref = new napi_ref__(); + ref->value = obj; + ref->refcount = 0; + RegisterWeakRefFinalizerIfNeeded(env, ref); + *result = ref; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) +{ + *result = FromNapi(js_object)->asObject()->extraData(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) +{ + ObjectRef* obj = FromNapi(js_object)->asObject(); + if (result != nullptr) { + *result = obj->extraData(); + } + obj->setExtraData(nullptr); + + // suppress the napi_wrap finalizer: a real Node-API remove_wrap must not + // invoke it, neither now nor when obj is eventually collected + void* wrapDataRaw = env->napiEnv->takeWrapFinalizerData(obj); + if (wrapDataRaw != nullptr) { + WrapFinalizeData* wrapData = reinterpret_cast(wrapDataRaw); + Memory::gcUnregisterFinalizer(obj, NapiWrapFinalizer, wrapData); + delete wrapData; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_reference(napi_env env, napi_value value, uint32_t initial_refcount, napi_ref* result) +{ + napi_ref__* ref = new napi_ref__(); + ref->value = FromNapi(value); + ref->refcount = initial_refcount; + for (uint32_t i = 0; i < initial_refcount; i++) { + env->napiEnv->persistentValueRefMap()->add(ref->value.value()); + } + // weak (refcount == 0) refs are not rooted by the map above, so track + // collection of the target directly; RegisterWeakRefFinalizerIfNeeded + // skips non-heap values (undefined/number/boolean/...) on its own, since + // those can never be collected + if (initial_refcount == 0) { + RegisterWeakRefFinalizerIfNeeded(env, ref); + } + *result = ref; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_delete_reference(node_api_basic_env env, napi_ref ref) +{ + if (ref->value.hasValue()) { + for (uint32_t i = 0; i < ref->refcount; i++) { + env->napiEnv->persistentValueRefMap()->remove(ref->value.value()); + } + } + + if (ref->weakFinalizerRegistered) { + // Do NOT free `ref` here (and don't bother with + // UnregisterWeakRefFinalizerIfNeeded's Memory::gcUnregisterFinalizer + // call either, for the same reason): NapiWeakRefFinalizer may + // already be irrevocably armed for the target's *current* GC pass. + // Boehm's per-object finalizer list, once a collection decides to + // finalize an object, is a fixed snapshot taken up front - + // unregistering from *within* another finalizer callback for that + // same object (as this reentrant call does when + // napi_delete_reference is invoked, directly or indirectly, from + // inside a napi_wrap/napi_add_finalizer finalizer sharing the same + // target - test_reference/test.js's validateDeleteBeforeFinalize) is + // a silent no-op against Boehm, not a real removal, with no way to + // detect that from here. Freeing `ref` immediately would then leave + // that still-armed callback writing into freed memory once it runs + // later in the very same sweep - a real, previously-crashing (heap + // corruption: "malloc(): unaligned tcache chunk detected") + // use-after-free. Instead, mark it and let NapiWeakRefFinalizer + // itself perform the actual `delete ref` whenever it does end up + // running (immediately afterward, in the reentrant case above; at + // the target's own eventual collection, in the ordinary case) - see + // its updated definition. This does mean a `ref` deleted this way + // isn't reclaimed until its target is next collected (which may + // never happen before process exit, if the target itself outlives + // this NapiEnv) - an acceptable, bounded leak for this PoC. + if (ref->value.hasValue()) { + env->napiEnv->untrackWeakRefTarget(reinterpret_cast(ref->value.value()), ref); + } + ref->pendingDelete = true; + return napi_ok; + } + + delete ref; + return napi_ok; +} + +// napi_reference_ref/napi_reference_unref can move `ref` between weak +// (refcount == 0) and strong (refcount > 0) any number of times over its +// life. Each call adds/removes exactly one PersistentValueRefMap rooting +// unit, same as napi_create_reference's initial add() loop - so ref->refcount +// stays equal to how many add() calls this ref has contributed for its +// value, which is what napi_delete_reference's own remove() loop assumes. +// Only the 0<->non-zero crossing also flips the weak-staleness finalizer. +ESCARGOT_NAPI_EXPORT napi_status napi_reference_ref(napi_env env, napi_ref ref, uint32_t* result) +{ + if (ref->refcount == 0 && !ref->value.hasValue()) { + // weak ref whose target is already gone - nothing to strengthen + return SetLastError(env, napi_generic_failure); + } + + bool wasWeak = (ref->refcount == 0); + ref->refcount++; + env->napiEnv->persistentValueRefMap()->add(ref->value.value()); + if (wasWeak) { + UnregisterWeakRefFinalizerIfNeeded(env, ref); + } + if (result != nullptr) { + *result = ref->refcount; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_reference_unref(napi_env env, napi_ref ref, uint32_t* result) +{ + if (ref->refcount == 0) { + return SetLastError(env, napi_generic_failure); + } + + ref->refcount--; + if (ref->value.hasValue()) { + env->napiEnv->persistentValueRefMap()->remove(ref->value.value()); + } + if (ref->refcount == 0) { + RegisterWeakRefFinalizerIfNeeded(env, ref); + } + if (result != nullptr) { + *result = ref->refcount; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_reference_value(napi_env env, napi_ref ref, napi_value* result) +{ + // a weak (refcount == 0) ref's value is cleared by NapiWeakRefFinalizer + // once its target is collected, so this no longer dangles + *result = ref->value.hasValue() ? ToNapi(ref->value.value()) : nullptr; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_open_handle_scope(napi_env env, napi_handle_scope* result) +{ + napi_handle_scope__* scope = new napi_handle_scope__(); + scope->parent = env->topHandleScope; + env->topHandleScope = scope; + *result = scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_close_handle_scope(napi_env env, napi_handle_scope scope) +{ + if (scope == nullptr || scope != env->topHandleScope) { + return SetLastError(env, napi_handle_scope_mismatch); + } + env->topHandleScope = scope->parent; + delete scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_open_escapable_handle_scope(napi_env env, napi_escapable_handle_scope* result) +{ + napi_escapable_handle_scope__* scope = new napi_escapable_handle_scope__(); + scope->parent = env->topHandleScope; + env->topHandleScope = scope; + *result = scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_close_escapable_handle_scope(napi_env env, napi_escapable_handle_scope scope) +{ + if (scope == nullptr || scope != env->topHandleScope) { + return SetLastError(env, napi_handle_scope_mismatch); + } + env->topHandleScope = scope->parent; + delete scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_escape_handle(napi_env env, napi_escapable_handle_scope scope, napi_value escapee, napi_value* result) +{ + if (scope->escapeCalled) { + return SetLastError(env, napi_escape_called_twice); + } + scope->escapeCalled = true; + // napi_value is already the GC pointer itself (see ToNapi/FromNapi in + // NapiTypes.h), so there is nothing to actually copy into an outer + // scope's buffer - `escapee` is already reachable however the caller is + // holding it. + *result = escapee; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_new_target(napi_env env, napi_callback_info cbinfo, napi_value* result) +{ + napi_callback_info__* info = reinterpret_cast(cbinfo); + *result = info->newTarget.hasValue() ? ToNapi(info->newTarget.value()) : nullptr; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_new_instance(napi_env env, napi_value constructor, size_t argc, const napi_value* argv, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + + std::vector args(argc); + for (size_t i = 0; i < argc; i++) { + args[i] = FromNapi(argv[i]); + } + + // ValueRef::construct(), like ValueRef::call() (see napi_call_function + // above), throws a raw C++ exception (Escargot::Value, by value) on an + // uncaught JS exception (e.g. the constructor itself throwing) - this + // was previously left unsandboxed here, so a throwing constructor's + // exception would escape all the way past this function's own napi_status + // return contract instead of becoming napi_pending_exception, breaking + // any addon that (correctly, per the Node-API contract) checks the + // returned status instead of expecting a raw throw across the API + // boundary (found via test_exception/test.js's constructReturnException/ + // constructAllowException, which do exactly that). + Evaluator::EvaluatorResult constructResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* ctor, size_t argc, ValueRef** argv) -> ValueRef* { + return ctor->construct(state, argc, argv); + }, + FromNapi(constructor), argc, args.data()); + + if (!constructResult.isSuccessful()) { + env->pendingException = constructResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(constructResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_int32(napi_env env, int32_t value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_boolean(napi_env env, bool value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_set_instance_data(node_api_basic_env env, void* data, napi_finalize finalize_cb, void* finalize_hint) +{ + env->instanceData = data; + env->instanceDataFinalizer = finalize_cb; + env->instanceDataFinalizeHint = finalize_hint; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_instance_data(node_api_basic_env env, void** data) +{ + *data = env->instanceData; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_cb_info(napi_env env, napi_callback_info cbinfo, size_t* argc, napi_value* argv, napi_value* this_arg, void** data) +{ + napi_callback_info__* info = reinterpret_cast(cbinfo); + + if (argv != nullptr && argc != nullptr) { + size_t capacity = *argc; + size_t count = std::min(capacity, info->argc); + for (size_t i = 0; i < count; i++) { + argv[i] = ToNapi(info->argv[i]); + } + for (size_t i = count; i < capacity; i++) { + argv[i] = ToNapi(ValueRef::createUndefined()); + } + } + if (argc != nullptr) { + *argc = info->argc; + } + if (this_arg != nullptr) { + *this_arg = ToNapi(info->thisValue); + } + if (data != nullptr) { + *data = info->data; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_throw(napi_env env, napi_value error) +{ + env->pendingException = FromNapi(error); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_throw_error(napi_env env, const char* code, const char* msg) +{ + ExecutionStateRef* state = env->executionState; + StringRef* message = StringRef::createFromUTF8(msg, strlen(msg)); + ErrorObjectRef* error = ErrorObjectRef::create(state, ErrorObjectRef::Code::None, message); + if (code) { + error->set(state, StringRef::createFromASCII("code"), StringRef::createFromUTF8(code, strlen(code))); + } + env->pendingException = error; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_exception_pending(napi_env env, bool* result) +{ + *result = env->pendingException.hasValue(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_and_clear_last_exception(napi_env env, napi_value* result) +{ + if (env->pendingException.hasValue()) { + *result = ToNapi(env->pendingException.value()); + env->pendingException = nullptr; + } else { + *result = ToNapi(ValueRef::createUndefined()); + } + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiObject.cpp b/src/napi/NapiObject.cpp new file mode 100644 index 000000000..ec7957ed5 --- /dev/null +++ b/src/napi/NapiObject.cpp @@ -0,0 +1,507 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Object/property/array-related js_native_api.h surface: property +// get/set/has/delete (both keyed and indexed variants), own-property-name +// enumeration, prototype access, freeze/seal, array creation/inspection, and +// instanceof. + +#include "NapiTypes.h" + +#include + +namespace Escargot { +namespace Napi { + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_get_property(napi_env env, napi_value object, napi_value key, napi_value* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyKey = FromNapi(key); + ExecutionStateRef* state = env->executionState; + + // ObjectRef::get can invoke a user getter (or a Proxy `get` trap), either + // of which may throw a raw C++ exception that must not cross this + // function's own stack frame - see napi_call_function's comment on the + // same Evaluator::execute pattern. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* key) -> ValueRef* { + return obj->get(state, key); + }, + obj, propertyKey); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_set_property(napi_env env, napi_value object, napi_value key, napi_value value) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyKey = FromNapi(key); + ValueRef* propertyValue = FromNapi(value); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* key, ValueRef* value) -> ValueRef* { + obj->set(state, key, value); + return ValueRef::createUndefined(); + }, + obj, propertyKey, propertyValue); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_has_property(napi_env env, napi_value object, napi_value key, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyKey = FromNapi(key); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* key) -> ValueRef* { + return ValueRef::create(obj->hasProperty(state, key)); + }, + obj, propertyKey); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = evalResult.result->asBoolean(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_delete_property(napi_env env, napi_value object, napi_value key, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyKey = FromNapi(key); + ExecutionStateRef* state = env->executionState; + + // deleteOwnProperty (ECMA-262's [[Delete]]), not deleteProperty: the + // latter (ObjectRef::deleteProperty, see its own deletePropertyOperation + // helper in EscargotPublic.cpp) additionally pre-checks + // hasOwnProperty(), then falls back to walking the prototype chain if + // the key isn't found - neither of which the JS `delete` operator / + // real Node-API's napi_delete_property ever do (delete is always an + // own-property-only [[Delete]] call, no prototype walk, and no separate + // existence pre-check trap on an exotic object). That pre-check trap + // firing (and, in this test, throwing) before the *actual* delete ever + // happens meant a Proxy's own `deleteProperty` trap - which + // ProxyObject::deleteOwnProperty correctly invokes - was never reached + // at all (found via test_object/test_exceptions.js, whose Proxy's + // `deleteProperty` trap always throws but was never observed to be + // called). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* key) -> ValueRef* { + return ValueRef::create(obj->deleteOwnProperty(state, key)); + }, + obj, propertyKey); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (result != nullptr) { + *result = evalResult.result->asBoolean(); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_has_own_property(napi_env env, napi_value object, napi_value key, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyKey = FromNapi(key); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, ValueRef* key) -> ValueRef* { + return ValueRef::create(obj->hasOwnProperty(state, key)); + }, + obj, propertyKey); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = evalResult.result->asBoolean(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_named_property(napi_env env, napi_value object, const char* utf8name, napi_value* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + StringRef* propertyName = StringRef::createFromUTF8(utf8name, strlen(utf8name)); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, StringRef* name) -> ValueRef* { + return obj->get(state, name); + }, + obj, propertyName); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_has_named_property(napi_env env, napi_value object, const char* utf8name, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + StringRef* propertyName = StringRef::createFromUTF8(utf8name, strlen(utf8name)); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, StringRef* name) -> ValueRef* { + return ValueRef::create(obj->hasProperty(state, name)); + }, + obj, propertyName); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = evalResult.result->asBoolean(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_element(napi_env env, napi_value object, uint32_t index, napi_value* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, uint32_t index) -> ValueRef* { + return obj->getIndexedProperty(state, ValueRef::create(index)); + }, + obj, index); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_set_element(napi_env env, napi_value object, uint32_t index, napi_value value) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ValueRef* propertyValue = FromNapi(value); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, uint32_t index, ValueRef* value) -> ValueRef* { + obj->setIndexedProperty(state, ValueRef::create(index), value); + return ValueRef::createUndefined(); + }, + obj, index, propertyValue); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_has_element(napi_env env, napi_value object, uint32_t index, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, uint32_t index) -> ValueRef* { + return ValueRef::create(obj->hasProperty(state, ValueRef::create(index))); + }, + obj, index); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = evalResult.result->asBoolean(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_delete_element(napi_env env, napi_value object, uint32_t index, bool* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // see napi_delete_property's identical deleteOwnProperty-not- + // deleteProperty reasoning just above. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj, uint32_t index) -> ValueRef* { + return ValueRef::create(obj->deleteOwnProperty(state, ValueRef::create(index))); + }, + obj, index); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (result != nullptr) { + *result = evalResult.result->asBoolean(); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_property_names(napi_env env, napi_value object, napi_value* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // Object.keys()-like: own, enumerable, string-keyed property names only. + // Deliberately built on ownPropertyKeys()/getOwnPropertyDescriptor() + // rather than enumerateObjectOwnProperties(): ProxyObject::enumeration() + // (ProxyObject.cpp) is explicitly documented there as *not* invoking the + // Proxy's own ownKeys trap at all (it just walks the underlying target + // directly), unlike ownPropertyKeys()/getOwnProperty(), which are + // properly overridden to invoke the real traps - see + // napi_get_all_property_names's identical rationale (NapiExtras.cpp), + // found via the same test (test_object/test_exceptions.js). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj) -> ValueRef* { + ValueVectorRef* names = ValueVectorRef::create(); + ValueVectorRef* ownKeys = obj->ownPropertyKeys(state); + for (size_t i = 0; i < ownKeys->size(); i++) { + ValueRef* propertyName = ownKeys->at(i); + if (!propertyName->isString()) { + continue; + } + ValueRef* descriptor = obj->getOwnPropertyDescriptor(state, propertyName); + if (descriptor->isUndefined()) { + continue; + } + StringRef* enumerableKey = StringRef::createFromASCII("enumerable"); + if (descriptor->asObject()->get(state, enumerableKey)->toBoolean(state)) { + names->pushBack(propertyName); + } + } + return ArrayObjectRef::create(state, names); + }, + obj); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_prototype(napi_env env, napi_value object, napi_value* result) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // getPrototype can invoke a Proxy's getPrototypeOf trap. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj) -> ValueRef* { + return obj->getPrototype(state); + }, + obj); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(evalResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_object_freeze(napi_env env, napi_value object) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + // ObjectRef::setIntegrityLevel(state, false) matches + // Object::setIntegrityLevel(state, O, false), which is exactly what + // Object.freeze's builtin uses (BuiltinObject.cpp's builtinObjectFreeze) - + // isSealed=false there means "frozen", not "not sealed". + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj) -> ValueRef* { + return ValueRef::create(obj->setIntegrityLevel(state, false)); + }, + obj); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (!evalResult.result->asBoolean()) { + return SetLastError(env, napi_generic_failure); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_object_seal(napi_env env, napi_value object) +{ + ObjectRef* obj = FromNapi(object)->asObject(); + ExecutionStateRef* state = env->executionState; + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj) -> ValueRef* { + return ValueRef::create(obj->setIntegrityLevel(state, true)); + }, + obj); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + if (!evalResult.result->asBoolean()) { + return SetLastError(env, napi_generic_failure); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_array(napi_env env, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ArrayObjectRef::create(env->executionState)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_array_with_length(napi_env env, size_t length, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // A JS array's length is always a uint32 (ECMA-262), so truncate to + // that range first instead of widening `length` (size_t) directly to + // uint64_t. Without this, a caller that derives `length` from + // napi_get_value_int32() and passes the maximum valid array length + // (2^32-1) - which truncates to int32_t -1, then widens back to a huge + // size_t/uint64_t when passed here - would spuriously hit + // ArrayObjectRef::create's `size > 2^32-1` check and throw a RangeError, + // instead of producing the (perfectly valid) 2^32-1-length array the + // caller asked for (see test_array/test.js's `NewWithLength(4294967295)`). + *result = ToNapi(ArrayObjectRef::create(env->executionState, static_cast(static_cast(length)))); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_array_length(napi_env env, napi_value value, uint32_t* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ObjectRef* obj = FromNapi(value)->asObject(); + ExecutionStateRef* state = env->executionState; + + // ObjectRef::length() reads the "length" property (ToLength(Get(obj, + // "length"))), which - like any other property get - could run through a + // user-defined accessor. + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ObjectRef* obj) -> ValueRef* { + return ValueRef::create(static_cast(obj->length(state))); + }, + obj); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = static_cast(evalResult.result->asNumber()); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_is_array(napi_env env, napi_value value, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // Escargot's isArrayObject() is a native type check on the value itself + // (never runs user code), unlike the full ECMA-262 IsArray abstract + // operation which additionally recurses through a Proxy's target chain - + // there is no public API exposing that recursive form, so a + // Proxy-wrapped array is not detected here. Every other value in this + // file's API can throw crossing the C ABI; this one structurally cannot, + // so it does not need the Evaluator::execute wrapper. + *result = FromNapi(value)->isArrayObject(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_instanceof(napi_env env, napi_value object, napi_value constructor, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* obj = FromNapi(object); + ValueRef* ctor = FromNapi(constructor); + ExecutionStateRef* state = env->executionState; + + // instanceOf can throw (e.g. `constructor` is not callable, or a custom + // Symbol.hasInstance implementation throws). + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* obj, ValueRef* ctor) -> ValueRef* { + return ValueRef::create(obj->instanceOf(state, ctor)); + }, + obj, ctor); + + if (!evalResult.isSuccessful()) { + env->pendingException = evalResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = evalResult.result->asBoolean(); + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiPlatform.cpp b/src/napi/NapiPlatform.cpp new file mode 100644 index 000000000..6dd7a64ac --- /dev/null +++ b/src/napi/NapiPlatform.cpp @@ -0,0 +1,48 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "NapiPlatform.h" + +namespace Escargot { +namespace Napi { + +void NapiPlatform::markJSJobEnqueued(ContextRef* relatedContext) +{ + // no embedder event loop yet; NapiEnv::drainPendingJobs() is called + // explicitly by callers instead of being driven from here +} + +void NapiPlatform::markJSJobFromAnotherThreadExists(ContextRef* relatedContext) +{ +} + +PlatformRef::LoadModuleResult NapiPlatform::onLoadModule(ContextRef* relatedContext, ScriptRef* whereRequestFrom, StringRef* moduleSrc, ModuleType type) +{ + return PlatformRef::LoadModuleResult(ErrorObjectRef::Code::TypeError, StringRef::createFromASCII("module loading is not supported yet")); +} + +void NapiPlatform::didLoadModule(ContextRef* relatedContext, OptionalRef whereRequestFrom, ScriptRef* loadedModule) +{ +} + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiPlatform.h b/src/napi/NapiPlatform.h new file mode 100644 index 000000000..8f101c227 --- /dev/null +++ b/src/napi/NapiPlatform.h @@ -0,0 +1,46 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#ifndef __EscargotNapiPlatform__ +#define __EscargotNapiPlatform__ + +#include "api/EscargotPublic.h" + +namespace Escargot { +namespace Napi { + +// Minimal PlatformRef for the N-API host. +// There is no embedder event loop yet (that is node_api.h territory, deferred +// for later), so job draining is done explicitly by NapiEnv +// instead of being driven by markJSJobEnqueued. +class NapiPlatform : public PlatformRef { +public: + void markJSJobEnqueued(ContextRef* relatedContext) override; + void markJSJobFromAnotherThreadExists(ContextRef* relatedContext) override; + + LoadModuleResult onLoadModule(ContextRef* relatedContext, ScriptRef* whereRequestFrom, StringRef* moduleSrc, ModuleType type) override; + void didLoadModule(ContextRef* relatedContext, OptionalRef whereRequestFrom, ScriptRef* loadedModule) override; +}; + +} // namespace Napi +} // namespace Escargot + +#endif +#endif // ENABLE_NAPI diff --git a/src/napi/NapiRuntime.cpp b/src/napi/NapiRuntime.cpp new file mode 100644 index 000000000..9f0fc9821 --- /dev/null +++ b/src/napi/NapiRuntime.cpp @@ -0,0 +1,332 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// The slice of node_api.h that doesn't require an event loop / thread pool - +// i.e. everything in node_api.h except napi_create_async_work/ +// napi_queue_async_work/napi_cancel_async_work (need a thread pool) and the +// napi_threadsafe_function family (needs a real libuv loop to marshal calls +// across threads). Following the same conventions as NapiFunctions.cpp/ +// NapiExtras.cpp (napi_value <-> ValueRef* punning via ToNapi/FromNapi, +// Evaluator::execute-wrapped exception boundaries, SetLastError-routed error +// returns). +// +// What's a full implementation here vs an approximation: +// - napi_add_env_cleanup_hook/napi_remove_env_cleanup_hook, +// napi_open_callback_scope/napi_close_callback_scope, +// napi_async_init/napi_async_destroy, napi_module_register, +// node_api_create_buffer_from_arraybuffer: full, matching real Node-API +// semantics as closely as this PoC's engine integration allows. +// - napi_add_async_cleanup_hook/napi_remove_async_cleanup_hook: a +// synchronous approximation - the hook is invoked and immediately treated +// as done, with no real asynchronous waiting (this PoC has no event loop +// to wait on in the first place). +// - napi_get_node_version: synthetic - this engine is not Node.js, so +// {major, minor, patch, release} is a reasonable made-up stand-in, not a +// real Node version. +// - node_api_get_module_file_name: defaults to "" (NapiEnv::m_moduleFileName +// has no setter call anywhere yet) until a real module loader exists to +// set it via NapiEnv::setModuleFileName. +// - napi_make_callback: behaves like napi_call_function plus draining +// pending jobs; async_context is accepted but ignored (no async_hooks +// integration to route it through). +// - napi_get_uv_event_loop: returns the real libuv event loop owned by this +// call's NapiEnv (NapiEnv::uvLoop(), NapiEnv.h/.cpp) - the same loop +// async_work/threadsafe_function (NapiAsyncWork.cpp) queue work onto and +// NapiEnv::drainPendingJobs() pumps. + +#include "NapiTypes.h" + +#include +#include + +namespace Escargot { +namespace Napi { + +namespace { + +// process-wide, like Node's own module registry - napi_module_register +// itself takes no napi_env (see node_api.h's own signature: this predates +// per-addon-instance module registration entirely). This PoC has no module +// loader to actually invoke nm_register_func, so this is purely a recording +// point - see GetLastRegisteredNapiModule's own comment (NapiTypes.h). +napi_module* g_lastRegisteredModule = nullptr; + +} // namespace + +napi_module* GetLastRegisteredNapiModule() +{ + return g_lastRegisteredModule; +} + +extern "C" { + +// Deprecated in real Node-API (superseded by NAPI_MODULE/NAPI_MODULE_INIT), +// but still the mechanism those macros' constructor-time registration +// ultimately calls into - see node_api.h's own "Used by deprecated +// registration method napi_module_register" comment on napi_module. Returns +// void (not napi_status), matching node_api.h's declaration exactly: there is +// no napi_env available at module-registration time to report a status +// through. +ESCARGOT_NAPI_EXPORT void napi_module_register(napi_module* mod) +{ + g_lastRegisteredModule = mod; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_add_env_cleanup_hook(node_api_basic_env env, napi_cleanup_hook fun, void* arg) +{ + if (env == nullptr || fun == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + env->napiEnv->addEnvCleanupHook(fun, arg); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_remove_env_cleanup_hook(node_api_basic_env env, napi_cleanup_hook fun, void* arg) +{ + if (env == nullptr || fun == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + env->napiEnv->removeEnvCleanupHook(fun, arg); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_add_async_cleanup_hook(node_api_basic_env env, napi_async_cleanup_hook hook, void* arg, napi_async_cleanup_hook_handle* remove_handle) +{ + if (env == nullptr || hook == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // Synchronous approximation (see this file's header comment): the real + // contract lets `hook` do asynchronous work and only actually finish + // once it later calls back through `handle` - there is no event loop + // here for it to do that work on, so teardown (~NapiEnv(), NapiEnv.cpp) + // just invokes it and moves on immediately. + napi_async_cleanup_hook_handle__* handle = env->napiEnv->addAsyncCleanupHook(hook, arg); + if (remove_handle != nullptr) { + *remove_handle = handle; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_remove_async_cleanup_hook(napi_async_cleanup_hook_handle remove_handle) +{ + // No napi_env parameter - matches node_api.h's own signature exactly: + // the handle itself remembers which NapiEnv registered it (napiEnv, + // NapiTypes.h), so removal doesn't need one. + if (remove_handle == nullptr) { + return napi_invalid_arg; + } + remove_handle->napiEnv->removeAsyncCleanupHook(remove_handle); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_node_version(node_api_basic_env env, const napi_node_version** version) +{ + if (env == nullptr || version == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // Synthetic: this engine is not Node.js, so there is no real Node + // version to report here - a reasonable, clearly-fake stand-in (`major`/ + // `minor` land on a recent-ish Node release for addons that + // feature-switch on them; `release` unambiguously identifies the real + // source). Static so the returned pointer stays valid indefinitely, per + // this function's own contract. + static const napi_node_version kSyntheticNodeVersion = { 20, 0, 0, "escargot" }; + *version = &kSyntheticNodeVersion; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_uv_event_loop(node_api_basic_env env, struct uv_loop_s** result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // A real libuv loop, owned by this env's NapiEnv for as long as it lives + // (uv_loop_init in NapiEnv's constructor; uv_loop_close at teardown - see + // NapiEnv.h/.cpp) - the same loop napi_queue_async_work/ + // napi_call_threadsafe_function queue work onto and NapiEnv:: + // drainPendingJobs() pumps (NapiAsyncWork.cpp). + *result = env->napiEnv->uvLoop(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_get_module_file_name(node_api_basic_env env, const char** result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // Defaults to "" (NapiEnv::m_moduleFileName) until a real module loader + // exists to set it via NapiEnv::setModuleFileName - see this file's + // header comment. + *result = env->napiEnv->moduleFileName().c_str(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_async_init(napi_env env, napi_value async_resource, napi_value async_resource_name, napi_async_context* result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // Minimal: no async_hooks integration exists to route this through (see + // napi_async_context__'s own comment, NapiTypes.h) - just remembers the + // two resource values for the caller's own napi_make_callback/ + // napi_open_callback_scope use. + napi_async_context__* context = new napi_async_context__(); + if (async_resource != nullptr) { + context->resource = FromNapi(async_resource); + } + if (async_resource_name != nullptr) { + context->resourceName = FromNapi(async_resource_name); + } + *result = context; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_async_destroy(napi_env env, napi_async_context async_context) +{ + if (env == nullptr || async_context == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + delete async_context; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_open_callback_scope(napi_env env, napi_value resource_object, napi_async_context async_context, napi_callback_scope* result) +{ + CHECK_ENV(env); + CHECK_ARG(env, result); + // Minimal LIFO-nesting bookkeeping, same rationale as + // napi_open_handle_scope (NapiFunctions.cpp): `resource_object`/ + // `async_context` aren't otherwise used since there is no async_hooks + // integration to feed them into. + napi_callback_scope__* scope = new napi_callback_scope__(); + scope->parent = env->topCallbackScope; + env->topCallbackScope = scope; + env->callbackScopeDepth++; + *result = scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_close_callback_scope(napi_env env, napi_callback_scope scope) +{ + CHECK_ENV(env); + CHECK_ARG(env, scope); + if (scope != env->topCallbackScope) { + return SetLastError(env, napi_callback_scope_mismatch); + } + env->topCallbackScope = scope->parent; + env->callbackScopeDepth--; + + // Node-API Spec: Drain microtasks and nextTicks only when the outermost callback scope closes. + if (env->callbackScopeDepth == 0) { + env->napiEnv->drainPendingJobs(); + } + + delete scope; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status 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) +{ + CHECK_ENV(env); + CHECK_ARG(env, func); + // async_context is ignored - accepted purely for API-surface + // compatibility (see napi_async_context__'s own comment, NapiTypes.h). + + env->callbackScopeDepth++; + + ExecutionStateRef* state = env->executionState; + ValueRef* fn = FromNapi(func); + ValueRef* thisArg = FromNapi(recv); + + std::vector args(argc); + for (size_t i = 0; i < argc; i++) { + args[i] = FromNapi(argv[i]); + } + + // Sandboxed exactly like napi_call_function (NapiFunctions.cpp): ->call() + // throws a raw C++ exception on an uncaught JS exception, which must not + // be allowed to cross this function's own stack frame. + Evaluator::EvaluatorResult callResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* fn, ValueRef* thisArg, size_t argc, ValueRef** argv) -> ValueRef* { + return fn->call(state, thisArg, argc, argv); + }, + fn, thisArg, argc, args.data()); + + env->callbackScopeDepth--; + + if (!callResult.isSuccessful()) { + env->pendingException = callResult.error.value(); + // Still drain pending jobs when the outermost scope finishes even on failure. + if (env->callbackScopeDepth == 0) { + env->napiEnv->drainPendingJobs(); + } + return SetLastError(env, napi_pending_exception); + } + + // Node-API Spec: Drain microtasks and nextTicks only when the outermost callback scope finishes. + if (env->callbackScopeDepth == 0) { + env->napiEnv->drainPendingJobs(); + } + + if (result != nullptr) { + *result = ToNapi(callResult.result); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status node_api_create_buffer_from_arraybuffer(napi_env env, napi_value arraybuffer, size_t byte_offset, size_t byte_length, napi_value* result) +{ + if (env == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* bufValue = FromNapi(arraybuffer); + if (!bufValue->isArrayBuffer()) { + return SetLastError(env, napi_invalid_arg); + } + ArrayBufferRef* buf = bufValue->asArrayBuffer(); + ExecutionStateRef* state = env->executionState; + + // Same out-of-bounds check (and RangeError, matching real Node-API) as + // napi_create_typedarray's identical one (NapiArrayBuffer.cpp) - a + // Buffer view may not extend past the end of its backing ArrayBuffer. + // No alignment check is needed here (unlike a multi-byte-element typed + // array): Buffer is a Uint8Array, whose element size is 1. + if (byte_offset + byte_length > buf->byteLength()) { + env->pendingException = ErrorObjectRef::create(state, ErrorObjectRef::RangeError, StringRef::createFromASCII("byte_offset + byte_length must be smaller than the size in bytes of the buffer passed in")); + return SetLastError(env, napi_pending_exception); + } + + // Node's Buffer is a Uint8Array subclass - see napi_create_buffer's own + // comment (NapiArrayBuffer.cpp) for why a plain Uint8ArrayObjectRef + // stands in for it here. + Uint8ArrayObjectRef* view = Uint8ArrayObjectRef::create(state); + view->setBuffer(buf, byte_offset, byte_length, byte_length); + + *result = ToNapi(view); + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiSymbolBigInt.cpp b/src/napi/NapiSymbolBigInt.cpp new file mode 100644 index 000000000..7bc1f5ef9 --- /dev/null +++ b/src/napi/NapiSymbolBigInt.cpp @@ -0,0 +1,259 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Implements the napi_create_symbol and BigInt (js_native_api.h) slice of +// N-API: napi_create_bigint_int64/uint64/words and +// napi_get_value_bigint_int64/uint64/words. + +#include "NapiTypes.h" + +#include +#include +#include +#include +#include + +namespace Escargot { +namespace Napi { + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_create_symbol(napi_env env, napi_value description, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + OptionalRef desc; + if (description != nullptr) { + ValueRef* descValue = FromNapi(description); + if (!descValue->isString()) { + return SetLastError(env, napi_string_expected); + } + desc = descValue->asString(); + } + + *result = ToNapi(SymbolRef::create(desc)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_bigint_int64(napi_env env, int64_t value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(BigIntRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(BigIntRef::create(value)); + return napi_ok; +} + +// EscargotPublic.h has no words-based BigInt factory/decomposer, so both this +// function and napi_get_value_bigint_words below round-trip through a hex +// string instead: BigIntRef::create(StringRef*, 16) already parses an +// arbitrary-precision hex literal (see BigIntData::init in BigInt.cpp) into +// exactly the magnitude a little-endian base-2^64 words[] array encodes, for +// any word_count - the sign (js_native_api.h's separate sign_bit) is then +// applied on top via BigIntRef::negativeValue, see the comment below. +ESCARGOT_NAPI_EXPORT napi_status napi_create_bigint_words(napi_env env, int sign_bit, size_t word_count, const uint64_t* words, napi_value* result) +{ + if (word_count == 0) { + *result = ToNapi(BigIntRef::create(static_cast(0))); + return napi_ok; + } + + // A word_count so large it would overflow this function's own hex-buffer + // size_t bookkeeping below (e.g. SIZE_MAX, as in test_bigint/test.js's + // CreateTooBigBigInt) can't even be attempted - reject outright as + // napi_invalid_arg (matching real Node-API, which also rejects a + // word_count that implausible before ever reaching V8's BigInt + // allocator). + static const size_t kOverflowGuardWords = (SIZE_MAX - 16) / 16; + if (word_count > kOverflowGuardWords) { + return SetLastError(env, napi_invalid_arg); + } + + // A merely very-large-but-computable word_count (e.g. INT_MAX, as in + // that same test's MakeBigIntWordsThrow) is instead the kind that reaches + // real BigInt construction in Node-API and fails there with a + // RangeError - Escargot's BigInt has no exposed max-length query to check + // against directly here, so kMaxBigIntWords stands in for it (chosen + // well below any word_count this implementation could plausibly build a + // hex string for) and this reports the same RangeError/message real + // Node-API does for hitting its own limit, as a pending exception rather + // than a bare napi_invalid_arg status. + static const size_t kMaxBigIntWords = 1 << 20; + if (word_count > kMaxBigIntWords) { + env->pendingException = ErrorObjectRef::create(env->executionState, ErrorObjectRef::RangeError, StringRef::createFromASCII("Maximum BigInt size exceeded")); + return SetLastError(env, napi_pending_exception); + } + + // Build a "0x"-prefixed, *unsigned* magnitude string, then apply the sign + // afterwards via BigIntRef::negativeValue instead of folding it in here. + // Two independent guards in BigIntData::init (BigInt.cpp), both meant for + // plain (non-N-API) BigInt literal parsing, would otherwise misfire on a + // bare (no "0x") hex digit string like the one this function would + // naturally produce: + // - any 'e'/'E' digit is rejected unless the string starts with "0x" + // (guards against decimal-exponent ambiguity, but 'e' is also a valid + // hex digit); + // - a leading '-' combined with any 'b'/'o'/'x' character anywhere in + // the string is rejected outright, and 'b' is itself a valid hex + // digit. + // Always prefixing with "0x" satisfies the first guard and (since the + // sign is applied separately, never producing a leading '-' here at all) + // sidesteps the second entirely. + std::string hex = "0x"; + hex.reserve(hex.size() + word_count * 16); + + char buf[24]; + for (size_t i = word_count; i > 0; i--) { + size_t idx = i - 1; + // the most significant word is written without zero-padding (still + // correct - and required - if it happens to be 0, i.e. leading + // zeros); every other word is padded to its full 16 hex digits so + // its bit position within the concatenated string stays correct + const char* format = (idx == word_count - 1) ? "%llx" : "%016llx"; + snprintf(buf, sizeof(buf), format, static_cast(words[idx])); + hex += buf; + } + + StringRef* hexString = StringRef::createFromASCII(hex.data(), hex.length()); + BigIntRef* magnitude = BigIntRef::create(hexString, 16); + *result = ToNapi((sign_bit != 0) ? magnitude->negativeValue(env->executionState) : magnitude); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_bigint_int64(napi_env env, napi_value value, int64_t* result, bool* lossless) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isBigInt()) { + return SetLastError(env, napi_bigint_expected); + } + + BigIntRef* bigint = v->asBigInt(); + int64_t converted = bigint->toInt64(); + *result = converted; + + if (lossless != nullptr) { + // toInt64() reduces modulo 2^64 (BF_GET_INT_MOD), so it never fails + // outright - losslessness instead means "converting `converted` back + // to a BigInt reproduces the original value" + *lossless = bigint->equals(BigIntRef::create(converted)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_bigint_uint64(napi_env env, napi_value value, uint64_t* result, bool* lossless) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isBigInt()) { + return SetLastError(env, napi_bigint_expected); + } + + BigIntRef* bigint = v->asBigInt(); + uint64_t converted = bigint->toUint64(); + *result = converted; + + if (lossless != nullptr) { + *lossless = bigint->equals(BigIntRef::create(converted)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_bigint_words(napi_env env, napi_value value, int* sign_bit, size_t* word_count, uint64_t* words) +{ + ValueRef* v = FromNapi(value); + if (!v->isBigInt()) { + return SetLastError(env, napi_bigint_expected); + } + if (word_count == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // sign_bit == nullptr && words == nullptr is the "how many words do you + // need" query mode js_native_api.h documents for this function; otherwise + // both must be provided, matching Node's own V8 implementation + bool isSizeQuery = (sign_bit == nullptr && words == nullptr); + if (!isSizeQuery && (sign_bit == nullptr || words == nullptr)) { + return SetLastError(env, napi_invalid_arg); + } + + BigIntRef* bigint = v->asBigInt(); + std::string hexString = bigint->toString(16)->toStdUTF8String(); + + bool negative = false; + size_t start = 0; + if (!hexString.empty() && hexString[0] == '-') { + negative = true; + start = 1; + } + std::string digits = hexString.substr(start); + // BigInt(0)'s toString(16) always yields "0" here (no '-' prefix, see + // BigInt::toString's zero-sign normalization), so this alone identifies + // the zero-word case + bool isZero = (digits == "0"); + size_t neededWordCount = isZero ? 0 : ((digits.length() + 15) / 16); + + if (isSizeQuery) { + *word_count = neededWordCount; + return napi_ok; + } + + size_t capacity = *word_count; + size_t wordsToWrite = std::min(capacity, neededWordCount); + + // fill little-endian: words[0] is the least-significant 64 bits, i.e. the + // rightmost (up to) 16 hex digits of `digits` + for (size_t i = 0; i < wordsToWrite; i++) { + size_t end = digits.length() - i * 16; + size_t begin = (end >= 16) ? end - 16 : 0; + std::string chunk = digits.substr(begin, end - begin); + words[i] = static_cast(strtoull(chunk.c_str(), nullptr, 16)); + } + + *sign_bit = negative ? 1 : 0; + *word_count = wordsToWrite; + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/napi/NapiTypes.h b/src/napi/NapiTypes.h new file mode 100644 index 000000000..6c966c13a --- /dev/null +++ b/src/napi/NapiTypes.h @@ -0,0 +1,252 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#ifndef __EscargotNapiTypes__ +#define __EscargotNapiTypes__ + +#include "api/EscargotPublic.h" +#include "NapiEnv.h" + +#include + +#include + +// exports each napi_* definition individually with default visibility, +// so the binary can be compiled with -fvisibility=hidden (the project default, +// see target.cmake) while still letting dlopen()'d addons resolve these +// specific symbols against this process (see ESCARGOT_EXPORT for the +// equivalent convention used by the rest of the public API). +#if !defined(ESCARGOT_NAPI_EXPORT) +#if defined(_MSC_VER) +#define ESCARGOT_NAPI_EXPORT __declspec(dllexport) +#else +#define ESCARGOT_NAPI_EXPORT __attribute__((visibility("default"))) +#endif +#endif + +namespace Escargot { +namespace Napi { + +// napi_value/napi_ref/etc are opaque pointer types for ABI stability +// (`struct napi_value__*` and friends); nothing ever dereferences the +// pointee, so we punn them directly onto Escargot's own GC pointers instead +// of allocating a wrapper per value. This is safe because Escargot's GC +// (Boehm) never moves objects, and Boehm conservatively scans the native +// stack/registers, so a napi_value sitting in a native local variable is +// already a GC root on its own - no V8-style handle buffer is needed to keep +// it alive. napi_open_handle_scope/napi_close_handle_scope and friends +// (NapiFunctions.cpp) exist purely to satisfy the js_native_api.h API +// contract (nesting order, escape-once semantics), not to root anything. +inline napi_value ToNapi(ValueRef* value) +{ + return reinterpret_cast(value); +} + +inline ValueRef* FromNapi(napi_value value) +{ + return reinterpret_cast(value); +} + +// records `status` as the env's most recent non-ok status, then returns it +// unchanged - so every napi_*/node_api_* function can simply route its +// `return napi_whatever;` through `return SetLastError(env, napi_whatever);` +// without otherwise changing its control flow. napi_get_last_error_info +// (NapiFunctions.cpp) looks the stashed code up in Node's own +// error_messages[] table to answer with a real message instead of always +// reporting an empty one (env->lastErrorCode, NapiEnv.h). A null `env` is +// tolerated (some callers, e.g. napi_create_function's own env==nullptr +// check, have no env to record against) and simply skips the recording. +inline napi_status SetLastError(napi_env env, napi_status status) +{ + if (env != nullptr) { + env->lastErrorCode = status; + } + return status; +} + +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) \ + return napi_invalid_arg; \ + } while (0) + +#define CHECK_ARG(env, arg) \ + do { \ + if ((arg) == nullptr) \ + return SetLastError((env), napi_invalid_arg); \ + } while (0) + +// Minimum byte/char16_t/latin1-byte length above which napi_create_string_utf8/ +// napi_create_string_latin1/napi_create_string_utf16 (NapiFunctions.cpp/ +// NapiValue.cpp) transparently route string creation through Escargot's +// compressible-string feature instead of a plain string - see those +// functions' own comments. Chosen well above every existing small test +// string (so none of them shift onto the compressible path) but small enough +// that a real addon's document/JSON-sized strings still benefit. +static const size_t kCompressibleStringThreshold = 1024; + +// data stashed on a native FunctionObjectRef via setExtraData(), so the +// callback trampoline can find the user's napi_callback + data pointer +struct CallbackData { + napi_env env; + napi_callback callback; + void* data; +}; + +} // namespace Napi +} // namespace Escargot + +// the opaque type node_api.h forward-declares for napi_get_cb_info et al. +struct napi_callback_info__ { + size_t argc; + Escargot::ValueRef** argv; + Escargot::ValueRef* thisValue; + void* data; + Escargot::OptionalRef newTarget; // present only for a `new`-invoked constructor call +}; + +// the opaque type node_api.h forward-declares for napi_create_reference et al. +// `value` is a raw GC pointer, not itself rooted by this struct; refcount > 0 +// means it has been added to the env's PersistentValueRefMap that many times +// (which is what actually roots it), matching napi_ref's own weak(0)/strong(>0) +// semantics - napi_reference_ref/napi_reference_unref (NapiFunctions.cpp) can +// move a ref between the two any number of times over its life. A weak ref's +// `value` is cleared to empty once its target is collected, via a finalizer +// registered on the target (NapiWeakRefFinalizer in NapiFunctions.cpp) - see +// napi_create_reference/napi_wrap/napi_delete_reference/napi_reference_unref. +// `weakFinalizerRegistered` tracks whether that finalizer is currently +// registered, so the weak<->strong transitions above don't register it twice +// (which the underlying finalizer list does not itself deduplicate). +struct napi_ref__ { + Escargot::OptionalRef value; + uint32_t refcount; + bool weakFinalizerRegistered = false; + // set by napi_delete_reference (NapiFunctions.cpp) instead of freeing + // `this` immediately, whenever weakFinalizerRegistered is true at that + // point: NapiWeakRefFinalizer may already be irrevocably armed for this + // same GC pass (see napi_delete_reference's own comment for the full + // reentrancy hazard - napi_wrap+napi_create_reference on the same + // target, test_reference/test.js's validateDeleteBeforeFinalize), so the + // actual `delete this` is instead deferred to whenever + // NapiWeakRefFinalizer does end up running for this ref. + bool pendingDelete = false; +}; + +namespace Escargot { +namespace Napi { + +// Defined (non-static) in NapiFunctions.cpp, alongside NapiWeakRefFinalizer/ +// UnregisterWeakRefFinalizerIfNeeded/napi_wrap's identical usage - declared +// here so napi_add_finalizer (NapiExtras.cpp) can register a weak napi_ref +// exactly the same way napi_wrap's does, instead of duplicating this with a +// second, incompatible finalizer callback pointer (which would silently +// break a later napi_reference_ref/napi_delete_reference on that same ref: +// both specifically Memory::gcUnregisterFinalizer() against this one +// NapiWeakRefFinalizer callback to find it). +void RegisterWeakRefFinalizerIfNeeded(napi_env env, napi_ref__* ref); + +// Defined (non-static) in NapiFunctions.cpp, alongside napi_wrap/ +// NapiWrapFinalizer, whose registry (NapiEnv::m_wrapFinalizerData) this walks +// and forces every still-live entry's finalize_cb to run - called from +// NapiEnv::~NapiEnv() (NapiEnv.cpp) to implement real Node-API environment- +// teardown finalization semantics (test_general/testEnvCleanup.js). +void RunEnvCleanupWrapFinalizers(NapiEnv* napiEnv); + +// Defined (non-static) in NapiRuntime.cpp, alongside napi_module_register - +// this PoC has no module loader to actually invoke a registered module's +// nm_register_func, so this exists purely as a tiny internal accessor a +// caller (or a TC) can use to confirm napi_module_register recorded the +// descriptor it was given. Process-wide (like Node's own module registry), +// not per-NapiEnv: napi_module_register itself takes no napi_env. +napi_module* GetLastRegisteredNapiModule(); + +} // namespace Napi +} // namespace Escargot + +// the opaque types node_api.h forward-declares for napi_open_handle_scope/ +// napi_open_escapable_handle_scope et al (NapiFunctions.cpp). `parent` forms +// an intrusive singly-linked stack via napi_env__::topHandleScope +// (NapiEnv.h), enforcing LIFO close order the same way V8's real handle +// scope stack does - napi_close_handle_scope/napi_close_escapable_handle_scope +// return napi_handle_scope_mismatch if `scope` isn't currently the innermost +// open one. No handle buffer is needed here: napi_value is already the GC +// pointer itself (see ToNapi/FromNapi above), so unlike V8 there's nothing +// for napi_escape_handle to copy between scopes - it just enforces the +// "at most once per scope" rule (napi_escape_called_twice) that +// js_native_api.h's contract requires. +struct napi_handle_scope__ { + napi_handle_scope__* parent = nullptr; +}; + +struct napi_escapable_handle_scope__ : public napi_handle_scope__ { + bool escapeCalled = false; +}; + +// the opaque type node_api.h forward-declares for napi_open_callback_scope/ +// napi_close_callback_scope (NapiRuntime.cpp). Same intrusive-stack shape and +// rationale as napi_handle_scope__ above, via napi_env__::topCallbackScope +// (NapiEnv.h): enforces LIFO close order, returning +// napi_callback_scope_mismatch if `scope` isn't currently the innermost open +// one. +struct napi_callback_scope__ { + napi_callback_scope__* parent = nullptr; +}; + +// the opaque type node_api.h forward-declares for napi_async_init/ +// napi_async_destroy/napi_make_callback/napi_open_callback_scope +// (NapiRuntime.cpp). This PoC has no async_hooks integration to route +// through, so an async context is just inert bookkeeping: it remembers the +// two resource values napi_async_init was given, in case a future +// async_hooks implementation wants them, and exists solely so +// napi_async_init/napi_async_destroy have something to allocate/free that +// satisfies the API's opaque-handle contract. +struct napi_async_context__ { + Escargot::OptionalRef resource; + Escargot::OptionalRef resourceName; +}; + +// the opaque type node_api.h forward-declares for napi_add_async_cleanup_hook/ +// napi_remove_async_cleanup_hook (NapiRuntime.cpp). Tracked on the +// registering NapiEnv (NapiEnv::m_asyncCleanupHooks) so every still-live one +// can be invoked at environment teardown (~NapiEnv(), NapiEnv.cpp); `napiEnv` +// lets napi_remove_async_cleanup_hook find (and remove itself from) that +// owner's list without an explicit napi_env parameter of its own - matching +// node_api.h's actual signature, which has none. +struct napi_async_cleanup_hook_handle__ { + Escargot::Napi::NapiEnv* napiEnv; + napi_async_cleanup_hook hook; + void* arg; +}; + +// out-of-line (needs napi_ref__ complete, unlike NapiEnv::trackWeakRefTarget/ +// untrackWeakRefTarget - see the declaration's own comment, NapiEnv.h). +inline void Escargot::Napi::NapiEnv::clearWeakRefTargets(void* target) +{ + if (m_weakRefTargets.count(target) == 0) { + return; + } + for (void* refVoid : m_weakRefTargets[target]) { + reinterpret_cast(refVoid)->value = nullptr; + } + m_weakRefTargets.erase(target); +} + +#endif +#endif // ENABLE_NAPI diff --git a/src/napi/NapiValue.cpp b/src/napi/NapiValue.cpp new file mode 100644 index 000000000..680ab8185 --- /dev/null +++ b/src/napi/NapiValue.cpp @@ -0,0 +1,478 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Value creation/read-out/coercion slice of js_native_api.h: singletons, +// int64/string creation, get_value_* readers, and the napi_coerce_to_*/ +// napi_strict_equals family. See NapiFunctions.cpp for the rest of the +// implemented surface and the conventions this file follows. + +#include "NapiTypes.h" + +#include +#include +#include +#include +#include + +namespace Escargot { +namespace Napi { + +// napi_get_value_string_utf8's truncation contract (js_native_api.h) is +// "Returns as many bytes as possible from the string ... into the buffer" - +// implicitly, without ever cutting a multi-byte UTF-8 sequence in half. A +// naive `std::min(utf8.size(), bufsize - 1)` byte-count truncation (this +// file's earlier implementation) doesn't respect that: a buffer that's one +// byte too small to fit an *additional whole* character can still land +// exactly mid-sequence, producing an invalid trailing byte that decodes back +// (e.g. via a later napi_create_string_utf8 on the truncated buffer, as +// test_string/test_string.c's own TestUtf8Insufficient does) as a stray +// U+FFFD replacement character instead of simply omitting that character - +// found via test_string/test.js's latin1Cases, whose 2-byte-per-character +// UTF-8 encoding (U+00A1..U+00BF) makes a 3-byte budget split a character +// right down the middle. Walks the UTF-8 byte string counting whole +// characters (via each character's leading byte) until the *next* one +// wouldn't fit within `maxBytes`, returning the byte length of the largest +// valid whole-character prefix - never more than maxBytes, but sometimes +// less (unlike plain byte-count truncation). +static size_t Utf8PrefixByteLengthWithinBudget(const std::string& utf8, size_t maxBytes) +{ + size_t i = 0; + while (i < utf8.size()) { + unsigned char leadByte = static_cast(utf8[i]); + size_t charLen; + if ((leadByte & 0x80) == 0x00) { + charLen = 1; + } else if ((leadByte & 0xE0) == 0xC0) { + charLen = 2; + } else if ((leadByte & 0xF0) == 0xE0) { + charLen = 3; + } else if ((leadByte & 0xF8) == 0xF0) { + charLen = 4; + } else { + // not a valid UTF-8 leading byte (shouldn't happen for a + // well-formed toStdUTF8String() result) - treat as a single + // byte so this still makes forward progress instead of looping + // forever. + charLen = 1; + } + if (i + charLen > utf8.size() || i + charLen > maxBytes) { + break; + } + i += charLen; + } + return i; +} + +extern "C" { + +ESCARGOT_NAPI_EXPORT napi_status napi_get_null(napi_env env, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::createNull()); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_version(node_api_basic_env env, uint32_t* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = NAPI_VERSION; + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_int64(napi_env env, int64_t value, napi_value* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + *result = ToNapi(ValueRef::create(value)); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_string_latin1(napi_env env, const char* str, size_t length, napi_value* result) +{ + if (str == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // see napi_create_string_utf8's identical guard (NapiFunctions.cpp) - + // same test_string/test.js TestLargeLatin1 rationale. + if (length != NAPI_AUTO_LENGTH && length > static_cast(INT_MAX)) { + return SetLastError(env, napi_invalid_arg); + } + + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? strlen(str) : length; + // Transparent compressible-string routing - see napi_create_string_utf8's + // identical comment (NapiFunctions.cpp). Below kCompressibleStringThreshold, + // the plain path is kept so small strings (every existing test string) + // are unaffected. + if (stringLength >= kCompressibleStringThreshold && StringRef::isCompressibleStringEnabled() && env != nullptr && env->napiEnv->vmInstance() != nullptr) { + *result = ToNapi(StringRef::createFromLatin1ToCompressibleString(env->napiEnv->vmInstance(), reinterpret_cast(str), stringLength)); + } else { + *result = ToNapi(StringRef::createFromLatin1(reinterpret_cast(str), stringLength)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_create_string_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result) +{ + if (str == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // see napi_create_string_utf8's identical guard (NapiFunctions.cpp) - + // same test_string/test.js TestLargeUtf16 rationale. + if (length != NAPI_AUTO_LENGTH && length > static_cast(INT_MAX)) { + return SetLastError(env, napi_invalid_arg); + } + + size_t stringLength = (length == NAPI_AUTO_LENGTH) ? std::char_traits::length(str) : length; + // Transparent compressible-string routing - see napi_create_string_utf8's + // identical comment (NapiFunctions.cpp). Below kCompressibleStringThreshold, + // the plain path is kept so small strings (every existing test string) + // are unaffected. + if (stringLength >= kCompressibleStringThreshold && StringRef::isCompressibleStringEnabled() && env != nullptr && env->napiEnv->vmInstance() != nullptr) { + *result = ToNapi(StringRef::createFromUTF16ToCompressibleString(env->napiEnv->vmInstance(), str, stringLength)); + } else { + *result = ToNapi(StringRef::createFromUTF16(str, stringLength)); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_bool(napi_env env, napi_value value, bool* result) +{ + // env/value/result are all checked - and *before* any type inspection of + // `value` - matching real Node-API's own CHECK_ARG ordering: a null + // `value` (a null napi_value pointer, not the JS `null`) must report + // napi_invalid_arg, not napi_boolean_expected (which napi_get_value_bool + // previously fell into "by accident", relying on FromNapi(nullptr)- + // >isBoolean() happening not to crash rather than ever explicitly + // checking) - found via test_conversions/test.js's testNull.getValueBool + // (test_conversions/test_null.c's GEN_NULL_CHECK_BINDING). + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isBoolean()) { + return SetLastError(env, napi_boolean_expected); + } + *result = v->asBoolean(); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_int32(napi_env env, napi_value value, int32_t* result) +{ + // see napi_get_value_bool's identical env/value/result-first reasoning. + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isNumber()) { + return SetLastError(env, napi_number_expected); + } + *result = v->toInt32(env->executionState); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_int64(napi_env env, napi_value value, int64_t* result) +{ + // see napi_get_value_bool's identical env/value/result-first reasoning. + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + ValueRef* v = FromNapi(value); + if (!v->isNumber()) { + return SetLastError(env, napi_number_expected); + } + + double doubleValue = v->asNumber(); + if (!std::isfinite(doubleValue)) { + // matches Node's own napi_get_value_int64: NaN/+-Infinity map to 0, + // rather than to the ToInteger-then-clamp result those would + // otherwise produce + *result = 0; + return napi_ok; + } + + // ToInteger on an already-numeric value never runs user code / throws, + // so this can be called directly without Evaluator::execute wrapping + double truncated = v->toInteger(env->executionState); + if (truncated >= 9223372036854775808.0) { // 2^63, first double >= INT64_MAX+1 + *result = INT64_MAX; + } else if (truncated < -9223372036854775808.0) { // -2^63 == INT64_MIN exactly + *result = INT64_MIN; + } else { + *result = static_cast(truncated); + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_string_utf8(napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result) +{ + // env/value checked (and *before* the isString() type check) - see + // napi_get_value_bool's identical reasoning (above). buf==nullptr is + // legitimately optional (query-length mode) - but only if `result` is + // then non-null to report that length back through; both null at once + // is otherwise unreportable, hence invalid_arg (found via the same + // test_null.c GEN_NULL_CHECK_STRING_BINDING macro's + // "bufAndOutLengthIsNull" case). + if (env == nullptr || value == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + ValueRef* v = FromNapi(value); + if (!v->isString()) { + return SetLastError(env, napi_string_expected); + } + std::string utf8 = v->asString()->toStdUTF8String(); + + if (buf == nullptr) { + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + *result = utf8.size(); + return napi_ok; + } + + if (bufsize == 0) { + if (result != nullptr) { + *result = 0; + } + return napi_ok; + } + + size_t copied = Utf8PrefixByteLengthWithinBudget(utf8, bufsize - 1); + memcpy(buf, utf8.data(), copied); + buf[copied] = '\0'; + if (result != nullptr) { + *result = copied; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_string_latin1(napi_env env, napi_value value, char* buf, size_t bufsize, size_t* result) +{ + // see napi_get_value_string_utf8's identical reasoning (above). + if (env == nullptr || value == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + ValueRef* v = FromNapi(value); + if (!v->isString()) { + return SetLastError(env, napi_string_expected); + } + StringRef* str = v->asString(); + size_t length = str->length(); + + if (buf == nullptr) { + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + *result = length; + return napi_ok; + } + + if (bufsize == 0) { + if (result != nullptr) { + *result = 0; + } + return napi_ok; + } + + size_t copied = std::min(length, bufsize - 1); + for (size_t i = 0; i < copied; i++) { + buf[i] = static_cast(str->charAt(i) & 0xFF); + } + buf[copied] = '\0'; + if (result != nullptr) { + *result = copied; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_get_value_string_utf16(napi_env env, napi_value value, char16_t* buf, size_t bufsize, size_t* result) +{ + // see napi_get_value_string_utf8's identical reasoning (above). + if (env == nullptr || value == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + ValueRef* v = FromNapi(value); + if (!v->isString()) { + return SetLastError(env, napi_string_expected); + } + StringRef* str = v->asString(); + size_t length = str->length(); + + if (buf == nullptr) { + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + *result = length; + return napi_ok; + } + + if (bufsize == 0) { + if (result != nullptr) { + *result = 0; + } + return napi_ok; + } + + size_t copied = std::min(length, bufsize - 1); + for (size_t i = 0; i < copied; i++) { + buf[i] = str->charAt(i); + } + buf[copied] = u'\0'; + if (result != nullptr) { + *result = copied; + } + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_coerce_to_bool(napi_env env, napi_value value, napi_value* result) +{ + // see napi_get_value_bool's identical env/value/result-first reasoning + // (above) - found via the same test_conversions/test_null.c + // GEN_NULL_CHECK_BINDING macro (CoerceToBool). + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + // clear a stale error code left over from an *earlier*, unrelated failed + // call on this same env (SetLastError only ever runs on an error return, + // never a success one, so without this a still-succeeding call right + // after a failed one would otherwise have napi_get_last_error_info keep + // reporting that earlier failure's message - found via this same test's + // "inputTypeCheck" case, which deliberately runs right after a + // resultIsNull-triggered napi_invalid_arg and expects to see a clean + // napi_ok afterward). + SetLastError(env, napi_ok); + + // ToBoolean never runs user code / never throws, unlike its + // to_number/to_object/to_string siblings below, so this can be called + // directly without Evaluator::execute wrapping + *result = ToNapi(ValueRef::create(FromNapi(value)->toBoolean(env->executionState))); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_coerce_to_number(napi_env env, napi_value value, napi_value* result) +{ + ExecutionStateRef* state = env->executionState; + ValueRef* v = FromNapi(value); + + // ToNumber can invoke a user-defined valueOf/Symbol.toPrimitive/toString, + // or throw (e.g. for a Symbol) - must not let that C++ exception cross + // this function's own stack frame (see napi_call_function above) + Evaluator::EvaluatorResult coerceResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* v) -> ValueRef* { + return ValueRef::create(v->toNumber(state)); + }, + v); + + if (!coerceResult.isSuccessful()) { + env->pendingException = coerceResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(coerceResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_coerce_to_object(napi_env env, napi_value value, napi_value* result) +{ + // see napi_coerce_to_bool's identical reasoning (above), including the + // stale-last-error clear. + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + SetLastError(env, napi_ok); + + ExecutionStateRef* state = env->executionState; + ValueRef* v = FromNapi(value); + + // ToObject throws for null/undefined, so this needs the same + // exception-catching wrapper as the other coercions here even though it + // never runs arbitrary user code itself + Evaluator::EvaluatorResult coerceResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* v) -> ValueRef* { + return v->toObject(state); + }, + v); + + if (!coerceResult.isSuccessful()) { + env->pendingException = coerceResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(coerceResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_coerce_to_string(napi_env env, napi_value value, napi_value* result) +{ + // see napi_coerce_to_bool's identical reasoning (above), including the + // stale-last-error clear. + if (env == nullptr || value == nullptr || result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + SetLastError(env, napi_ok); + + ExecutionStateRef* state = env->executionState; + ValueRef* v = FromNapi(value); + + // ToString can invoke a user-defined toString/Symbol.toPrimitive, or + // throw (e.g. for a Symbol) - same reasoning as napi_coerce_to_number + Evaluator::EvaluatorResult coerceResult = Evaluator::execute( + state, [](ExecutionStateRef* state, ValueRef* v) -> ValueRef* { + return v->toString(state); + }, + v); + + if (!coerceResult.isSuccessful()) { + env->pendingException = coerceResult.error.value(); + return SetLastError(env, napi_pending_exception); + } + + *result = ToNapi(coerceResult.result); + return napi_ok; +} + +ESCARGOT_NAPI_EXPORT napi_status napi_strict_equals(napi_env env, napi_value lhs, napi_value rhs, bool* result) +{ + if (result == nullptr) { + return SetLastError(env, napi_invalid_arg); + } + + // === never runs user code / never throws, so this can be called + // directly without Evaluator::execute wrapping + *result = FromNapi(lhs)->equalsTo(env->executionState, FromNapi(rhs)); + return napi_ok; +} + +} // extern "C" + +} // namespace Napi +} // namespace Escargot + +#endif // ENABLE_NAPI diff --git a/src/runtime/ErrorObject.cpp b/src/runtime/ErrorObject.cpp index 613343834..1823ad7e9 100644 --- a/src/runtime/ErrorObject.cpp +++ b/src/runtime/ErrorObject.cpp @@ -137,6 +137,31 @@ void ErrorObject::throwBuiltinError(ExecutionState& state, ErrorCode code, Strin state.throwException(Value(ErrorObject::createError(state, code, errorMessage, false))); } +void ErrorObject::throwBuiltinError(ExecutionState& state, ErrorCode code, const char* templateString, String* templateDataString1, String* templateDataString2) +{ + size_t len = strlen(templateString); + std::basic_string buf; + buf.resize(len); + for (size_t i = 0; i < len; i++) { + buf[i] = templateString[i]; + } + UTF16StringDataNonGCStd str(buf.data(), len); + + size_t idx; + if ((idx = str.find(u"%s")) != SIZE_MAX) { + auto replacer1 = templateDataString1->toUTF16StringData(); + str.replace(str.begin() + idx, str.begin() + idx + 2, replacer1.data()); + idx += replacer1.length(); + if ((idx = str.find(u"%s", idx)) != SIZE_MAX) { + auto replacer2 = templateDataString2->toUTF16StringData(); + str.replace(str.begin() + idx, str.begin() + idx + 2, replacer2.data()); + } + } + + String* errorMessage = new UTF16String(str.data(), str.length()); + state.throwException(Value(ErrorObject::createError(state, code, errorMessage, false))); +} + static Value builtinErrorObjectStackInfoGet(ExecutionState& state, Value thisValue, size_t argc, Value* argv, Optional newTarget) { if (!(LIKELY(thisValue.isPointerValue() && thisValue.asPointerValue()->isErrorObject()))) { diff --git a/src/runtime/ErrorObject.h b/src/runtime/ErrorObject.h index 5298e982f..cff9a785c 100644 --- a/src/runtime/ErrorObject.h +++ b/src/runtime/ErrorObject.h @@ -58,6 +58,11 @@ class ErrorObject : public DerivedObject { static constexpr const char* DefineProperty_LengthNotWritable = "Cannot modify property '%s': 'length' is not writable"; static constexpr const char* DefineProperty_NotWritable = "Cannot modify non-writable property '%s'"; static constexpr const char* DefineProperty_RedefineNotConfigurable = "Cannot redefine non-configurable property '%s'"; + // strict-mode assignment to a non-writable data property (V8 wording, distinct from + // DefineProperty_NotWritable which is reserved for Object.defineProperty/TypedArray index sites) + static constexpr const char* Assign_ToReadOnlyProperty = "Cannot assign to read only property '%s' of object '%s'"; + // strict-mode assignment to an accessor property that has a getter but no setter (V8 wording) + static constexpr const char* Assign_ToGetterOnlyProperty = "Cannot set property %s of %s which has only a getter"; static constexpr const char* DefineProperty_NotExtensible = "Cannot define property '%s': object is not extensible"; static constexpr const char* DefineProperty_NotConfigurable = "Cannot delete property '%s': property is not configurable"; static constexpr const char* ObjectToPrimitiveValue = "Cannot convert object to primitive value"; @@ -162,6 +167,10 @@ class ErrorObject : public DerivedObject { throwBuiltinError(state, code, templateDataString, false, String::emptyString(), templateString); } + // like throwBuiltinError above, but for templateStrings containing two '%s' placeholders, + // substituted in order by templateDataString1 and templateDataString2 respectively. + static void throwBuiltinError(ExecutionState& state, ErrorCode code, const char* templateString, String* templateDataString1, String* templateDataString2); + static ErrorObject* createBuiltinError(ExecutionState& state, ErrorCode code, const char* templateString, bool fillStackInfo = true) { return createBuiltinError(state, code, String::emptyString(), false, String::emptyString(), templateString, fillStackInfo); diff --git a/src/runtime/FunctionTemplate.cpp b/src/runtime/FunctionTemplate.cpp index 474cc648a..467540f68 100644 --- a/src/runtime/FunctionTemplate.cpp +++ b/src/runtime/FunctionTemplate.cpp @@ -138,11 +138,18 @@ void FunctionTemplate::setLength(size_t length) } Object* FunctionTemplate::instantiate(Context* ctx) +{ + return instantiate(ctx, /* addToContextCache */ true); +} + +Object* FunctionTemplate::instantiate(Context* ctx, bool addToContextCache) { auto& instantiatedFunctionObjects = ctx->instantiatedFunctionObjects(); - for (size_t i = 0; i < instantiatedFunctionObjects.size(); i++) { - if (instantiatedFunctionObjects[i].first == this) { - return instantiatedFunctionObjects[i].second; + if (addToContextCache) { + for (size_t i = 0; i < instantiatedFunctionObjects.size(); i++) { + if (instantiatedFunctionObjects[i].first == this) { + return instantiatedFunctionObjects[i].second; + } } } @@ -230,7 +237,9 @@ Object* FunctionTemplate::instantiate(Context* ctx) &d); } - instantiatedFunctionObjects.pushBack(std::make_pair(this, result)); + if (addToContextCache) { + instantiatedFunctionObjects.pushBack(std::make_pair(this, result)); + } postProcessing(result); return result; } diff --git a/src/runtime/FunctionTemplate.h b/src/runtime/FunctionTemplate.h index 2cf2e6b00..2fd49070f 100644 --- a/src/runtime/FunctionTemplate.h +++ b/src/runtime/FunctionTemplate.h @@ -44,6 +44,16 @@ class FunctionTemplate : public Template { // returns the unique function instance in context. virtual Object* instantiate(Context* ctx) override; + // Same as instantiate(ctx), but when addToContextCache is false the result + // is NOT stored in Context::instantiatedFunctionObjects(). The cache exists + // to give a fixed set of templates a stable per-context identity; callers + // that create throwaway templates dynamically and unboundedly (e.g. N-API's + // napi_create_function / napi_define_class, which build a fresh template per + // call) must pass false, otherwise every instantiated function is rooted for + // the Context's whole lifetime - an unbounded leak that also prevents the + // function from ever being collected. + Object* instantiate(Context* ctx, bool addToContextCache); + ObjectTemplate* prototypeTemplate() const { return m_prototypeTemplate; diff --git a/src/runtime/Object.cpp b/src/runtime/Object.cpp index 0a98dac9e..c12956a12 100644 --- a/src/runtime/Object.cpp +++ b/src/runtime/Object.cpp @@ -655,6 +655,15 @@ String* Object::constructorName(ExecutionState& state) } } +String* Object::classTag(ExecutionState& state) +{ + StringBuilder builder; + builder.appendString("#<", &state); + builder.appendString(constructorName(state), &state); + builder.appendChar('>', &state); + return builder.finalize(&state); +} + bool Object::setPrototype(ExecutionState& state, const Value& proto) { // https://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-setprototypeof-v @@ -1570,8 +1579,32 @@ void Object::setThrowsException(ExecutionState& state, const ObjectPropertyName& void Object::setThrowsExceptionWhenStrictMode(ExecutionState& state, const ObjectPropertyName& P, const Value& v, const Value& receiver) { if (UNLIKELY(!set(state, P, v, receiver)) && state.inStrictMode()) { - ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, P.toExceptionString(), false, String::emptyString(), ErrorObject::Messages::DefineProperty_NotWritable); + Object* tagObject = receiver.isObject() ? receiver.asObject() : this; + bool isGetterOnlyAccessor = Object::isGetterOnlyAccessorProperty(state, this, P); + String* tag = tagObject->classTag(state); + if (isGetterOnlyAccessor) { + ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, ErrorObject::Messages::Assign_ToGetterOnlyProperty, P.toExceptionString(), tag); + } else { + ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, ErrorObject::Messages::Assign_ToReadOnlyProperty, P.toExceptionString(), tag); + } + } +} + +bool Object::isGetterOnlyAccessorProperty(ExecutionState& state, Object* object, const ObjectPropertyName& P) +{ + Object* o = object; + while (o) { + ObjectGetResult desc = o->getOwnProperty(state, P); + if (desc.hasValue()) { + return !desc.isDataProperty(); + } + Value proto = o->getPrototype(state); + if (!proto.isObject()) { + break; + } + o = proto.asObject(); } + return false; } void Object::throwCannotDefineError(ExecutionState& state, const ObjectStructurePropertyName& P) @@ -1579,9 +1612,14 @@ void Object::throwCannotDefineError(ExecutionState& state, const ObjectStructure ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, P.toExceptionString(), false, String::emptyString(), ErrorObject::Messages::DefineProperty_RedefineNotConfigurable); } -void Object::throwCannotWriteError(ExecutionState& state, const ObjectStructurePropertyName& P) +void Object::throwCannotWriteError(ExecutionState& state, Object* object, const ObjectStructurePropertyName& P, bool isGetterOnlyAccessor) { - ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, P.toExceptionString(), false, String::emptyString(), ErrorObject::Messages::DefineProperty_NotWritable); + String* tag = object->classTag(state); + if (isGetterOnlyAccessor) { + ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, ErrorObject::Messages::Assign_ToGetterOnlyProperty, P.toExceptionString(), tag); + } else { + ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, ErrorObject::Messages::Assign_ToReadOnlyProperty, P.toExceptionString(), tag); + } } void Object::throwCannotDeleteError(ExecutionState& state, const ObjectStructurePropertyName& P) diff --git a/src/runtime/Object.h b/src/runtime/Object.h index 10e9a64bc..7df78f324 100644 --- a/src/runtime/Object.h +++ b/src/runtime/Object.h @@ -865,6 +865,9 @@ class Object : public PointerValue { } String* constructorName(ExecutionState& state); + // Builds V8-style object tag used in error messages, e.g. "#"/"#", + // from constructorName(state). + String* classTag(ExecutionState& state); // internal [[prototype]] virtual bool setPrototype(ExecutionState& state, const Value& proto); @@ -1120,8 +1123,18 @@ class Object : public PointerValue { static bool isCompatiblePropertyDescriptor(ExecutionState& state, bool extensible, const ObjectPropertyDescriptor& desc, const ObjectGetResult& current); static void throwCannotDefineError(ExecutionState& state, const ObjectStructurePropertyName& P); - static void throwCannotWriteError(ExecutionState& state, const ObjectStructurePropertyName& P); + // `object` is used only to build the V8-style "of object '#'"/"of #" tag in the + // thrown message; `isGetterOnlyAccessor` selects between the "read only property" wording + // (non-writable data property) and the "which has only a getter" wording (accessor with a + // getter but no setter) - both are strict-mode ASSIGNMENT failures, never + // Object.defineProperty/TypedArray-index failures (those keep DefineProperty_NotWritable). + static void throwCannotWriteError(ExecutionState& state, Object* object, const ObjectStructurePropertyName& P, bool isGetterOnlyAccessor = false); static void throwCannotDeleteError(ExecutionState& state, const ObjectStructurePropertyName& P); + // Walks `object`'s prototype chain looking for the first found descriptor of P, and reports + // whether it is an accessor (as opposed to a non-writable data property or "not found"/ + // extensibility failure) - used to pick the right wording above after a plain Object::set() + // failure, where the caller no longer has the resolved descriptor at hand. + static bool isGetterOnlyAccessorProperty(ExecutionState& state, Object* object, const ObjectPropertyName& P); static ArrayObject* createArrayFromList(ExecutionState& state, const uint64_t& size, const Value* buffer); static ArrayObject* createArrayFromList(ExecutionState& state, const ValueVector& elements); static ValueVector createListFromArrayLike(ExecutionState& state, Value obj, uint8_t types = static_cast(ElementTypes::ALL)); @@ -1349,7 +1362,9 @@ class Object : public PointerValue { ALWAYS_INLINE void setOwnPropertyThrowsExceptionWhenStrictMode(ExecutionState& state, size_t idx, const Value& newValue, const Value& receiver) { if (UNLIKELY(!setOwnPropertyUtilForObject(state, idx, newValue, receiver) && state.inStrictMode())) { - throwCannotWriteError(state, m_structure->readProperty(idx).m_propertyName); + const ObjectStructureItem& item = m_structure->readProperty(idx); + Object* tagObject = receiver.isObject() ? receiver.asObject() : this; + throwCannotWriteError(state, tagObject, item.m_propertyName, !item.m_descriptor.isDataProperty()); } } diff --git a/src/runtime/RegExpObject.h b/src/runtime/RegExpObject.h index 44c7b1877..ffa2d0ed2 100644 --- a/src/runtime/RegExpObject.h +++ b/src/runtime/RegExpObject.h @@ -155,7 +155,7 @@ class RegExpObject : public DerivedObject { void setLastIndex(ExecutionState& state, const Value& v) { if (UNLIKELY(m_hasNonWritableLastIndexRegExpObject && (option() & (Option::Sticky | Option::Global)))) { - Object::throwCannotWriteError(state, ObjectStructurePropertyName(state, String::fromASCII("lastIndex"))); + Object::throwCannotWriteError(state, this, ObjectStructurePropertyName(state, String::fromASCII("lastIndex"))); } m_lastIndex = v; } diff --git a/test/cctest/debugger_stubs.cpp b/test/cctest/debugger_stubs.cpp new file mode 100644 index 000000000..430f07d64 --- /dev/null +++ b/test/cctest/debugger_stubs.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// testapi.cpp references DebuggerOperationsRef methods whose real definitions +// (in src/api/EscargotPublic.cpp) are compiled only when ESCARGOT_DEBUGGER is +// enabled. The N-API cctest build dir is configured without the debugger, so +// those symbols are absent and testapi.cpp fails to link. These stubs satisfy +// the linker so the rest of the test binary (including the Napi.* suite) can be +// built; the debugger tests themselves are not exercised in this configuration. +// Guarded off when ESCARGOT_DEBUGGER is set, so a debugger-enabled build uses +// the real definitions and these do not clash. + +#if !defined(ESCARGOT_DEBUGGER) + +#include "api/EscargotPublic.h" + +namespace Escargot { + +StringRef* DebuggerOperationsRef::BreakpointOperations::eval(StringRef* sourceCode, bool& isError, size_t& objectIndex) +{ + isError = false; + objectIndex = 0; + return nullptr; +} + +void DebuggerOperationsRef::BreakpointOperations::getStackTrace(DebuggerStackTraceDataVector& outStackTrace) +{ +} + +void DebuggerOperationsRef::BreakpointOperations::getLexicalScopeChain(uint32_t stateIndex, LexicalScopeChainVector& outLexicalScopeChain) +{ +} + +DebuggerOperationsRef::PropertyKeyValueVector DebuggerOperationsRef::BreakpointOperations::getLexicalScopeChainProperties(uint32_t stateIndex, uint32_t scopeIndex) +{ + return PropertyKeyValueVector(); +} + +StringRef* DebuggerOperationsRef::getFunctionName(WeakCodeRef* weakCodeRef) +{ + return nullptr; +} + +bool DebuggerOperationsRef::updateBreakpoint(WeakCodeRef* weakCodeRef, uint32_t offset, bool enable) +{ + return false; +} + +} // namespace Escargot + +#endif // !ESCARGOT_DEBUGGER diff --git a/test/cctest/napi_custom_addons/test_symbol_verify/test.js b/test/cctest/napi_custom_addons/test_symbol_verify/test.js new file mode 100644 index 000000000..6f532d474 --- /dev/null +++ b/test/cctest/napi_custom_addons/test_symbol_verify/test.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../../common'); +const binding = require(`./build/${common.buildType}/test_symbol_verify`); +const assert = require('assert'); + +// Debt #11 Verification: Check if node_api_symbol_for shares the exact same +// global symbol registry as JS Symbol.for() +const c_symbol = binding.createSymbolFor("escargot_napi_verify"); +const js_symbol = Symbol.for("escargot_napi_verify"); +assert.strictEqual(typeof c_symbol, 'symbol'); +assert.strictEqual(c_symbol, js_symbol, "Debt #11: node_api_symbol_for does not share JS Symbol.for registry!"); +assert.strictEqual(Symbol.keyFor(c_symbol), "escargot_napi_verify"); + +// Debt #12 Verification: Check if a weak reference on a Symbol correctly triggers +// its GC finalizer without memory leak / unregister failure. +(async function() { + { + // Create a local, non-global symbol (no Symbol.for) so it can be GC'd. + const local_symbol = binding.createLocalSymbol("garbage_collect_me"); + binding.attachWeakFinalizer(local_symbol, () => { + // If this runs, Debt #12 is successfully verified! + console.log("Symbol Weak-Ref Finalizer successfully ran!"); + }); + } + + // No manual global.gc() here due to conservative GC stale pointers. + // The C++ harness will naturally sweep this symbol during environment teardown! +})(); diff --git a/test/cctest/napi_custom_addons/test_symbol_verify/test_symbol_verify.c b/test/cctest/napi_custom_addons/test_symbol_verify/test_symbol_verify.c new file mode 100644 index 000000000..37a9e3972 --- /dev/null +++ b/test/cctest/napi_custom_addons/test_symbol_verify/test_symbol_verify.c @@ -0,0 +1,73 @@ +#include +#include +#include "common.h" +#include "entry_point.h" + +static napi_value CreateSymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char str[256]; + size_t length; + NODE_API_CALL(env, napi_get_value_string_utf8(env, args[0], str, sizeof(str), &length)); + + napi_value result; + // Use node_api_symbol_for to resolve against the global registry + NODE_API_CALL(env, node_api_symbol_for(env, str, length, &result)); + return result; +} + +static napi_value CreateLocalSymbol(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + napi_value desc; + NODE_API_CALL(env, napi_create_string_utf8(env, "local", NAPI_AUTO_LENGTH, &desc)); + NODE_API_CALL(env, napi_create_symbol(env, desc, &result)); + return result; +} + +static void FinalizeCallback(napi_env env, void* finalize_data, void* finalize_hint) { + napi_ref cb_ref = (napi_ref)finalize_hint; + napi_value cb; + napi_value global; + + if (napi_get_reference_value(env, cb_ref, &cb) != napi_ok) return; + if (napi_get_global(env, &global) != napi_ok) return; + + napi_call_function(env, global, cb, 0, NULL, NULL); + napi_delete_reference(env, cb_ref); +} + +static napi_value AttachWeakFinalizer(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value target_symbol = args[0]; + napi_value js_cb = args[1]; + + napi_ref cb_ref; + NODE_API_CALL(env, napi_create_reference(env, js_cb, 1, &cb_ref)); + + // Attach finalizer onto the symbol itself + NODE_API_CALL(env, napi_add_finalizer(env, target_symbol, NULL, FinalizeCallback, cb_ref, NULL)); + + return NULL; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor), + DECLARE_NODE_API_PROPERTY("createLocalSymbol", CreateLocalSymbol), + DECLARE_NODE_API_PROPERTY("attachWeakFinalizer", AttachWeakFinalizer), + }; + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + return exports; +} + + diff --git a/test/cctest/napi_harness/harness.js b/test/cctest/napi_harness/harness.js new file mode 100644 index 000000000..3706c3a03 --- /dev/null +++ b/test/cctest/napi_harness/harness.js @@ -0,0 +1,878 @@ +// Minimal, in-process CommonJS + `assert`/`common` compatibility layer used +// to run the real, unmodified Node.js test/js-native-api/*/test.js files +// against Escargot's N-API implementation (see test/cctest/testnapi_suite.cpp). +// +// This file is evaluated once per NapiEnv, as a plain top-level script (via +// ScriptParserRef::initializeScript), directly in the global scope - so +// everything declared here with `var`/`function` becomes a global, which is +// intentional: it's how `gc`, `queueMicrotask`, `setImmediate`, `process` +// etc. get installed for the test file to see, and how the C++ driver finds +// its entrypoints (`__runTest`, `__finishTest`, `__pumpOnce`). +// +// Three native hooks are expected to already be installed as globals by the +// C++ side before this file runs: __gc(), __read_file(absPath) and +// __napi_load_addon(name). See installNativeHooks() in testnapi_suite.cpp. + +(function () { + 'use strict'; + + // ------------------------------------------------------------------ + // path helpers (no `path` module available - these are the only two + // operations the require() resolver below needs) + // ------------------------------------------------------------------ + + function dirnameOf(p) { + var idx = p.lastIndexOf('/'); + return idx >= 0 ? p.slice(0, idx) : '.'; + } + + // resolves `rel` (which may contain '.'/'..' segments) against `baseDir`, + // both using '/' separators; always returns an absolute path + function resolvePath(baseDir, rel) { + var combined = rel.charAt(0) === '/' ? rel : baseDir + '/' + rel; + var parts = combined.split('/'); + var out = []; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (part === '' || part === '.') { + continue; + } + if (part === '..') { + out.pop(); + } else { + out.push(part); + } + } + return '/' + out.join('/'); + } + + // ------------------------------------------------------------------ + // assert shim (subset used by the target test dirs: strictEqual, + // notStrictEqual, deepStrictEqual, throws, ok, match, bare assert()) + // ------------------------------------------------------------------ + + function AssertionError(message) { + var err = new Error(message); + err.name = 'AssertionError'; + return err; + } + + function inspect(v) { + try { + if (typeof v === 'string') { + return JSON.stringify(v); + } + if (typeof v === 'bigint') { + return String(v) + 'n'; + } + if (typeof v === 'symbol' || typeof v === 'function') { + return String(v); + } + return JSON.stringify(v); + } catch (e) { + return String(v); + } + } + + function assert(value, message) { + if (!value) { + throw AssertionError(message || ('The expression evaluated to a falsy value:\n\n' + inspect(value))); + } + } + + function strictEqual(actual, expected, message) { + if (!Object.is(actual, expected)) { + throw AssertionError(message || + ('Expected values to be strictly equal:\n' + inspect(actual) + ' !== ' + inspect(expected))); + } + } + + function notStrictEqual(actual, expected, message) { + if (Object.is(actual, expected)) { + throw AssertionError(message || + ('Expected "actual" to be strictly unequal to:\n\n' + inspect(actual))); + } + } + + function isTypedArray(v) { + return ArrayBuffer.isView(v) && !(v instanceof DataView); + } + + function deepStrictEqualInner(a, b, seen) { + if (Object.is(a, b)) { + return true; + } + if (typeof a !== typeof b) { + return false; + } + if (a === null || b === null) { + return a === b; + } + if (typeof a !== 'object') { + // primitives not handled by Object.is above (e.g. 1 vs 1 already + // caught) really are unequal at this point + return false; + } + + // both non-null objects + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) { + return false; + } + + if (Array.isArray(a)) { + if (!Array.isArray(b) || a.length !== b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (!deepStrictEqualInner(a[i], b[i], seen)) { + return false; + } + } + return true; + } + + if (isTypedArray(a) || isTypedArray(b)) { + if (!isTypedArray(a) || !isTypedArray(b)) { + return false; + } + if (a.constructor !== b.constructor || a.length !== b.length) { + return false; + } + for (var j = 0; j < a.length; j++) { + if (!Object.is(a[j], b[j])) { + return false; + } + } + return true; + } + + if (a instanceof DataView) { + if (!(b instanceof DataView) || a.byteLength !== b.byteLength) { + return false; + } + for (var k = 0; k < a.byteLength; k++) { + if (a.getUint8(k) !== b.getUint8(k)) { + return false; + } + } + return true; + } + + if (a instanceof RegExp) { + return b instanceof RegExp && a.source === b.source && a.flags === b.flags; + } + + if (a instanceof Date) { + return b instanceof Date && Object.is(a.getTime(), b.getTime()); + } + + // plain-object-ish fallback: compare own enumerable string keys + var aKeys = Object.keys(a).sort(); + var bKeys = Object.keys(b).sort(); + if (aKeys.length !== bKeys.length) { + return false; + } + for (var m = 0; m < aKeys.length; m++) { + if (aKeys[m] !== bKeys[m]) { + return false; + } + } + for (var n = 0; n < aKeys.length; n++) { + if (!deepStrictEqualInner(a[aKeys[n]], b[aKeys[n]], seen)) { + return false; + } + } + return true; + } + + function deepStrictEqual(actual, expected, message) { + if (!deepStrictEqualInner(actual, expected, null)) { + throw AssertionError(message || + ('Expected values to be strictly deep-equal:\n' + inspect(actual) + '\nvs\n' + inspect(expected))); + } + } + + function isErrorConstructorLike(fn) { + if (typeof fn !== 'function') { + return false; + } + if (fn === Error) { + return true; + } + var proto = fn.prototype; + while (proto) { + if (proto === Error.prototype) { + return true; + } + proto = Object.getPrototypeOf(proto); + } + return false; + } + + function matchErrorAgainstObject(err, expected) { + for (var key in expected) { + if (!Object.prototype.hasOwnProperty.call(expected, key)) { + continue; + } + var expectedVal = expected[key]; + var actualVal = err ? err[key] : undefined; + if (expectedVal instanceof RegExp) { + if (!expectedVal.test(String(actualVal))) { + throw AssertionError('Expected property "' + key + '" (' + inspect(actualVal) + + ') to match ' + expectedVal); + } + } else if (!Object.is(actualVal, expectedVal)) { + throw AssertionError('Expected property "' + key + '" to strictly equal ' + + inspect(expectedVal) + ' but got ' + inspect(actualVal)); + } + } + } + + function throwsImpl(shouldThrow, fn, expected, message) { + var threw = false; + var error; + try { + fn(); + } catch (e) { + threw = true; + error = e; + } + + if (!shouldThrow) { + if (threw) { + throw AssertionError(message || ('Got unwanted exception: ' + inspect(error && error.message))); + } + return; + } + + if (!threw) { + throw AssertionError(message || 'Missing expected exception'); + } + + if (expected === undefined || expected === null) { + return; + } + + if (expected instanceof RegExp) { + // Node's assert.throws tests a RegExp `expected` against + // `String(error)` (i.e. Error.prototype.toString(), "Name: message"), + // not against `error.message` alone. + var stringified = String(error); + if (!expected.test(stringified)) { + throw AssertionError(message || + ('Expected error to match ' + expected + ' but got ' + inspect(stringified))); + } + return; + } + + if (typeof expected === 'function') { + if (isErrorConstructorLike(expected)) { + if (!(error instanceof expected)) { + throw AssertionError(message || + ('Expected error to be instance of ' + (expected.name || expected) + + ' but got ' + inspect(error))); + } + return; + } + // validation predicate + var predicateResult = expected(error); + if (predicateResult === false) { + throw AssertionError(message || 'Validation function on thrown error returned false'); + } + return; + } + + if (typeof expected === 'object') { + matchErrorAgainstObject(error, expected); + return; + } + + throw new TypeError('Unsupported "expected" argument to assert.throws/doesNotThrow'); + } + + function throwsFn(fn, expected, message) { + throwsImpl(true, fn, expected, message); + } + + function doesNotThrow(fn, expected, message) { + throwsImpl(false, fn, expected, message); + } + + function ok(value, message) { + assert(value, message); + } + + function match(str, regex, message) { + if (!(regex instanceof RegExp)) { + throw new TypeError('"regex" must be a RegExp'); + } + if (!regex.test(str)) { + throw AssertionError(message || + ('The input did not match the regular expression ' + regex + '. Input:\n\n' + inspect(str))); + } + } + + function fail(message) { + throw AssertionError(message || 'Failed'); + } + + assert.strictEqual = strictEqual; + assert.notStrictEqual = notStrictEqual; + assert.deepStrictEqual = deepStrictEqual; + assert.throws = throwsFn; + assert.doesNotThrow = doesNotThrow; + assert.ok = ok; + assert.match = match; + assert.fail = fail; + // easy self-loop, some tests do `assert.equal`/`assert.notEqual` too; + // not in the documented "used" list but cheap and harmless to alias. + assert.equal = strictEqual; + assert.notEqual = notStrictEqual; + // assert.ifError(value): passes for null/undefined, otherwise rethrows the + // value (an Error) or fails - used by node-api child-process tests + // (test_async/test_fatal) to assert spawnSync produced no launch error. + assert.ifError = function (value) { + if (value !== null && value !== undefined) { + if (value instanceof Error) { + throw value; + } + throw AssertionError('ifError got unwanted exception: ' + inspect(value)); + } + }; + + // ------------------------------------------------------------------ + // common shim (buildType, mustCall, mustCallAtLeast, mustNotCall, + // nodeProcessAborted) + // ------------------------------------------------------------------ + + var mustCallChecks = []; + + function noop() {} + + function mustCallInner(fn, criteria, field) { + if (typeof fn === 'number') { + criteria = fn; + fn = undefined; + } + if (criteria === undefined) { + criteria = 1; + } + if (fn === undefined) { + fn = noop; + } + if (typeof criteria !== 'number') { + throw new TypeError('Invalid ' + field + ' value: ' + criteria); + } + + var context = { actual: 0, name: fn.name || '' }; + context[field] = criteria; + mustCallChecks.push(context); + + var wrapper = function () { + context.actual++; + return fn.apply(this, arguments); + }; + return wrapper; + } + + function mustCall(fn, exact) { + return mustCallInner(fn, exact, 'exact'); + } + + function mustCallAtLeast(fn, minimum) { + return mustCallInner(fn, minimum, 'minimum'); + } + + function mustNotCall(msg) { + return function () { + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(inspect(arguments[i])); + } + var argsInfo = args.length > 0 ? ('\ncalled with arguments: ' + args.join(', ')) : ''; + throw AssertionError((msg || 'function should not have been called') + argsInfo); + }; + } + + function checkMustCalls() { + var failed = []; + for (var i = 0; i < mustCallChecks.length; i++) { + var c = mustCallChecks[i]; + if ('minimum' in c) { + if (c.actual < c.minimum) { + failed.push('Mismatched ' + c.name + ' function calls. Expected at least ' + + c.minimum + ', actual ' + c.actual + '.'); + } + } else if (c.actual !== c.exact) { + failed.push('Mismatched ' + c.name + ' function calls. Expected exactly ' + + c.exact + ', actual ' + c.actual + '.'); + } + } + if (failed.length) { + throw new Error(failed.join('\n')); + } + } + + // matches Node's own test/common/index.js nodeProcessAborted (Linux/non- + // Windows/non-SunOS branch): a signal-killed child (spawnSync sets + // `status: null, signal: 'SIGxxx'` in that case) is "aborted" iff the + // signal is one of these three; otherwise fall back to checking the + // (non-signal) exit code against the "aborted" range a compiler's + // abort()/V8 fatal-error exit can produce. + function nodeProcessAborted(exitCode, signal) { + var expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT']; + var expectedExitCodes = [132, 133, 134]; + if (signal !== null && signal !== undefined) { + return expectedSignals.indexOf(signal) !== -1; + } + return expectedExitCodes.indexOf(exitCode) !== -1; + } + + var common = { + buildType: 'Release', + mustCall: mustCall, + mustCallAtLeast: mustCallAtLeast, + mustNotCall: mustNotCall, + nodeProcessAborted: nodeProcessAborted + }; + + // ------------------------------------------------------------------ + // common/gc shim ({ gcUntil }) + // ------------------------------------------------------------------ + + function gcUntil(name, condition, maxCount) { + if (maxCount === undefined) { + maxCount = 10; + } + return new Promise(function (resolve, reject) { + var count = 0; + function step() { + if (condition()) { + resolve(); + return; + } + count++; + if (count >= maxCount) { + reject(new Error('Test ' + name + ' failed')); + return; + } + setImmediate(function () { + __gc(); + step(); + }); + } + step(); + }); + } + + var commonGc = { gcUntil: gcUntil }; + + // ------------------------------------------------------------------ + // globals: gc, queueMicrotask, setImmediate/setTimeout family, process + // ------------------------------------------------------------------ + + globalThis.gc = function () { + __gc(); + }; + + // Node exposes both `global` and `globalThis` pointing at the same object + if (typeof globalThis.global === 'undefined') { + globalThis.global = globalThis; + } + + if (typeof globalThis.queueMicrotask !== 'function') { + globalThis.queueMicrotask = function (cb) { + Promise.resolve().then(function () { + cb(); + }); + }; + } + + globalThis.setImmediate = function (cb) { + var args = Array.prototype.slice.call(arguments, 1); + __uv_timer_start(cb, 0, ...args); + return 0; // dummy id + }; + globalThis.clearImmediate = function (id) {}; + globalThis.setTimeout = function (cb) { + var args = Array.prototype.slice.call(arguments, 2); + var delayMs = arguments[1] || 0; + __uv_timer_start(cb, delayMs, ...args); + return 0; // dummy id + }; + globalThis.clearTimeout = function (id) {}; + globalThis.setInterval = function () { return 0; }; + globalThis.clearInterval = function () {}; + + // OS-backed libuv timers natively handle setImmediate/setTimeout via __uv_timer_start. + // __pumpOnce no longer needs to artificially dispatch JS-queued immediates. + globalThis.__pumpOnce = function () { + return false; + }; + + if (typeof globalThis.console !== 'object' || globalThis.console === null) { + // map console to globalThis.print to surface outputs in --napi-run mode. + var p = typeof globalThis.print === 'function' ? globalThis.print : function() {}; + globalThis.console = { + log: p, + info: p, + warn: p, + error: p + }; + } + + // ------------------------------------------------------------------ + // process.on('uncaughtException', ...): a minimal EventEmitter-ish + // registry (only the 'uncaughtException' event is actually needed by any + // target test.js). See __routeUncaughtException below, called from the + // C++ driver (testnapi_suite.cpp) whenever a job/immediate/top-level + // script throws, or a native finalizer left a pending exception that + // never surfaced as a normal JS throw (its own napi_call_function + // already reported it via a returned status, e.g. + // test_reference/test_finalizer.js's createExternalWithJsFinalize). + // ------------------------------------------------------------------ + + var uncaughtExceptionHandlers = []; + + function processOn(event, handler) { + if (event === 'uncaughtException') { + uncaughtExceptionHandlers.push(handler); + } + return process; + } + + // process.once('uncaughtException', ...): like processOn but the handler + // removes itself right before running, so it fires at most once (used by + // test_callback_scope's throw-from-callback-scope case). + function processOnce(event, handler) { + if (event === 'uncaughtException') { + var wrapper = function (err) { + var idx = uncaughtExceptionHandlers.indexOf(wrapper); + if (idx >= 0) { + uncaughtExceptionHandlers.splice(idx, 1); + } + return handler(err); + }; + uncaughtExceptionHandlers.push(wrapper); + } + return process; + } + + // Routes a thrown value to every registered 'uncaughtException' handler, + // in registration order, same as Node itself invoking them all for one + // exception. If none are registered, rethrows `err` so the caller (the + // C++ driver) sees it and fails the test exactly as an actually-unhandled + // exception should. A handler that itself throws is likewise left to + // propagate (real Node also treats that as fatal, not silently retried). + globalThis.__routeUncaughtException = function (err) { + if (uncaughtExceptionHandlers.length === 0) { + throw err; + } + var handlers = uncaughtExceptionHandlers.slice(); + for (var i = 0; i < handlers.length; i++) { + handlers[i](err); + } + }; + + // The absolute path to the running cctest binary (captured at process + // startup - testapi.cpp's argv[0]/readlink("/proc/self/exe"), see + // testnapi_suite.cpp's ResolveCctestBinaryPath), and any extra CLI + // argv (role/arg strings) this particular run was given (empty for a + // normal, gtest-driven NapiSuite.* TEST; non-empty when this same binary + // re-invoked itself in single-test CLI mode - see __spawn_sync below). + var execPathValue = (typeof __napi_exec_path === 'function') ? __napi_exec_path() : 'escargot'; + var cliExtraArgv = (typeof __napi_cli_extra_argv === 'function') ? __napi_cli_extra_argv() : []; + + globalThis.process = { + // argv[1] (the running script's own path) is filled in by __runTest + // below, once the target test.js's absolute path is known. + argv: [execPathValue, ''].concat(cliExtraArgv), + execPath: execPathValue, + platform: 'linux', + arch: 'x64', + version: 'v20.0.0', + versions: {}, + env: {}, + on: processOn, + once: processOnce, + exit: function () {}, + cwd: function () { + return '.'; + }, + nextTick: (function() { + var nextTickQueue = []; + var nextTickScheduled = false; + + function drainNextTicks() { + nextTickScheduled = false; + // Drain everything in the queue synchronously, mirroring Node's exact nextTick behavior + var batch = nextTickQueue.slice(); + nextTickQueue = []; + for (var i = 0; i < batch.length; i++) { + var item = batch[i]; + item.cb.apply(undefined, item.args); + } + // If new nextTicks were queued recursively during the drain, schedule them again + if (nextTickQueue.length > 0 && !nextTickScheduled) { + nextTickScheduled = true; + queueMicrotask(drainNextTicks); + } + } + + return function(cb) { + var args = Array.prototype.slice.call(arguments, 1); + nextTickQueue.push({ cb: cb, args: args }); + if (!nextTickScheduled) { + nextTickScheduled = true; + // Enqueue at the very front of the microtask queue + queueMicrotask(drainNextTicks); + } + }; + })(), + // identity-only placeholders: spawnSync's `options.stdio` (below) + // only ever compares these by reference/checks for the literal + // string 'pipe', never actually reads/writes through them. + stdin: {}, + stdout: {}, + stderr: {} + }; + + // ------------------------------------------------------------------ + // child_process.spawnSync: re-invokes *this same* cctest binary in the + // single-test CLI mode implemented in testnapi_suite.cpp + // (RunNapiSingleTestCli)/testapi.cpp (`--napi-run [args...]`), + // via the native __spawn_sync(command, execArgv, options) hook + // (fork()+execvp()+waitpid(), testnapi_suite.cpp). Supports exactly the + // shape the target test.js files actually use: + // `spawnSync(process.execPath, ['--expose-gc'?, __filename, role, ...])`. + // ------------------------------------------------------------------ + + function stripNodeFlags(args) { + // this harness has no flags to consume (--expose-gc is a no-op here: + // global.gc() is always available) - only the __filename + role/args + // that follow matter to --napi-run. + var out = []; + for (var i = 0; i < args.length; i++) { + if (String(args[i]).charAt(0) !== '-') { + out.push(args[i]); + } + } + return out; + } + + function resolveStdioCapture(options) { + var stdio = options && options.stdio; + if (!stdio) { + return { stdout: true, stderr: true }; // Node's own spawnSync default: pipe both + } + return { + stdout: stdio[1] === 'pipe', + stderr: stdio[2] === 'pipe' + }; + } + + function spawnSync(command, args, options) { + var positional = stripNodeFlags(args || []); + // positional[0] is the target test.js's own __filename; anything + // after it (role, extra args) is threaded straight through. + var execArgv = ['--napi-run'].concat(positional); + var capture = resolveStdioCapture(options); + var result = __spawn_sync(command, execArgv, capture); + return { + pid: result.pid, + status: result.status, + signal: result.signal, + stdout: result.stdout, + stderr: result.stderr, + error: result.error || null + }; + } + + // child_process.fork(modulePath[, args][, options]): Node runs modulePath + // as a new Node child with an IPC channel. Here the child is the same + // cctest binary re-invoked in --napi-run mode (identical bridge to + // spawnSync), run to completion synchronously by __spawn_sync; we then + // surface a minimal ChildProcess whose 'exit'/'close' events fire on the + // next microtask (after the caller has attached its handlers). The target + // tests only observe the child's exit code via child.on('close', ...), so + // IPC/streaming is stubbed. Signature tolerates fork(path), fork(path, + // args), fork(path, options), fork(path, args, options). + function fork(modulePath, args, options) { + if (args && !Array.isArray(args)) { + options = args; + args = []; + } + var childArgs = [modulePath].concat(args || []); + var execArgv = ['--napi-run'].concat(childArgs); + var result = __spawn_sync(process.execPath, execArgv, { stdout: false, stderr: false }); + + var handlers = { exit: [], close: [], error: [], message: [], disconnect: [] }; + function on(event, cb) { + if (handlers[event]) { + handlers[event].push(cb); + } + return child; + } + var child = { + pid: result.pid, + connected: true, + on: on, + once: on, // fired at most once here anyway (single exit) + send: function () { return true; }, + disconnect: function () { child.connected = false; }, + kill: function () {}, + unref: function () {}, + ref: function () {} + }; + if (result.error) { + queueMicrotask(function () { + for (var i = 0; i < handlers.error.length; i++) { + handlers.error[i](result.error); + } + }); + return child; + } + var code = result.signal ? null : result.status; + var signal = result.signal || null; + queueMicrotask(function () { + child.connected = false; + var i; + for (i = 0; i < handlers.exit.length; i++) { + handlers.exit[i](code, signal); + } + for (i = 0; i < handlers.close.length; i++) { + handlers.close[i](code, signal); + } + }); + return child; + } + + var childProcessModule = { spawnSync: spawnSync, fork: fork }; + + // Minimal `vm` module: runInNewContext(code) evaluates `code` in a fresh + // JS context with its own global (backed by the native + // __vm_run_in_new_context hook - testnapi_suite.cpp). The optional + // sandbox/options args real Node supports aren't used by the target tests + // (test_make_callback), so they're accepted and ignored. + var vmModule = { + runInNewContext: function (code) { + return __vm_run_in_new_context(String(code)); + } + }; + + // ------------------------------------------------------------------ + // CommonJS require()/module wrapper + // ------------------------------------------------------------------ + + var moduleCache = Object.create(null); + // sentinel "main module" - always distinct from every test module object, + // so `if (module !== require.main)` (used by e.g. test_instance_data) always + // takes the "required as a module" branch instead of a + // worker_threads-style self-respawn branch (this harness has no + // worker_threads shim). child_process-based self-respawn (spawnSync + + // `process.argv[2] === 'child'`, e.g. test_finalizer/test_fatal_finalize.js) + // *is* supported - see child_process/spawnSync below. + var requireMainSentinel = { exports: {}, __isHarnessMainSentinel: true }; + + function isAddonRequest(id) { + return id.indexOf('/build/') !== -1 || (id.charAt(0) !== '.' && id.charAt(0) !== '/'); + } + + function addonBasename(id) { + var parts = id.split('/'); + return parts[parts.length - 1]; + } + + // "/" - lets the C++ side + // (NativeLoadAddon, testnapi_suite.cpp) disambiguate two different test + // directories that happen to `require()` a same-named addon backed by a + // *different* .so (e.g. test_reference/test_finalizer.js vs + // test_finalizer/test_fatal_finalize.js, both requiring "test_finalizer"), + // while staying a no-op for every other, unambiguous addon (NativeLoadAddon + // falls back to the bare basename if no qualified entry matches). + function qualifiedAddonName(requiringDirname, id) { + var dirParts = requiringDirname.split('/'); + var dirBase = dirParts[dirParts.length - 1]; + return dirBase + '/' + addonBasename(id); + } + + function loadJsModule(absPath) { + var cached = moduleCache[absPath]; + if (cached) { + return cached.exports; + } + + var source = __read_file(absPath); + var dirname = dirnameOf(absPath); + var module = { exports: {}, id: absPath, filename: absPath, loaded: false }; + moduleCache[absPath] = module; + + var req = makeRequire(dirname); + var wrapper = new Function('exports', 'require', 'module', '__filename', '__dirname', source); + wrapper.call(module.exports, module.exports, req, module, absPath, dirname); + + module.loaded = true; + return module.exports; + } + + function makeRequire(dirname) { + function req(id) { + if (id === '../../common' || id === '../common' || /(^|\/)common$/.test(id)) { + return common; + } + if (/(^|\/)common\/gc$/.test(id)) { + return commonGc; + } + if (id === 'assert') { + return assert; + } + if (id === 'child_process') { + return childProcessModule; + } + if (id === 'process') { + return globalThis.process; + } + if (id === 'vm') { + return vmModule; + } + if (isAddonRequest(id)) { + return __napi_load_addon(qualifiedAddonName(dirname, id)); + } + // relative .js (or extension-less) file, resolved against the + // requiring module's directory + var resolved = resolvePath(dirname, id); + if (!/\.js$/.test(resolved)) { + resolved += '.js'; + } + return loadJsModule(resolved); + } + req.main = requireMainSentinel; + req.resolve = function (id) { + return id; + }; + return req; + } + + // ------------------------------------------------------------------ + // entrypoints called from C++ (testnapi_suite.cpp) + // ------------------------------------------------------------------ + + // runs one target test file end-to-end (require wiring, module wrapper); + // mirrors loadJsModule but never cache-hits (a fresh call per gtest TEST, + // on top of a fresh NapiEnv/global scope, so caching across calls never + // actually matters - kept separate mainly for clarity at the call site). + globalThis.__runTest = function (absPath) { + mustCallChecks.length = 0; + // process.argv[1] is conventionally the running script's own path + // (Node); required for e.g. spawnSync(process.execPath, [__filename, ...]) + // self-respawn to pass __filename straight through. + process.argv[1] = absPath; + return loadJsModule(absPath); + }; + + // checks the common.mustCall()/mustCallAtLeast() registry accumulated by + // the just-run test file; throws (failing the gtest) if unmet + globalThis.__finishTest = function () { + checkMustCalls(); + }; +}()); diff --git a/test/cctest/testapi.cpp b/test/cctest/testapi.cpp index 8826b744d..dc9eb8b08 100644 --- a/test/cctest/testapi.cpp +++ b/test/cctest/testapi.cpp @@ -372,8 +372,34 @@ PersistentRefHolder createEscargotContext(VMInstanceRef* instance) return context; } +#if defined(ENABLE_NAPI) +// Defined in test/cctest/testnapi_suite.cpp: single-test CLI mode (task 2 of +// the Node-integration milestone's uncaughtException/child_process wave). +// Returns -1 if argv doesn't request it (`--napi-run [role] +// [arg...]` not found), meaning the caller should fall through to the normal +// gtest run below; otherwise it has already run exactly that one test.js and +// this is the process exit code to use. Declared here (rather than via a +// shared header) purely to keep this file buildable the same way regardless +// of ENABLE_NAPI - the only thing this translation unit needs from it is this +// one entrypoint. +namespace Escargot { +namespace Napi { +int RunNapiSingleTestCli(int argc, char** argv); +} // namespace Napi +} // namespace Escargot +#endif + int main(int argc, char* argv[]) { + setvbuf(stderr, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); +#if defined(ENABLE_NAPI) + int napiCliExitCode = Escargot::Napi::RunNapiSingleTestCli(argc, argv); + if (napiCliExitCode >= 0) { + return napiCliExitCode; + } +#endif + testing::InitGoogleTest(&argc, argv); Globals::initialize(new ShellPlatform()); diff --git a/test/cctest/testnapi.cpp b/test/cctest/testnapi.cpp new file mode 100644 index 000000000..1e0eb20ba --- /dev/null +++ b/test/cctest/testnapi.cpp @@ -0,0 +1,859 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Drives the real, unmodified Node-API TCs vendored at +// test/napi-tc/test/js-native-api/*. +// There is no require()/module-loader integration yet, so this dlopen()s each +// compiled addon directly and calls its exported napi_register_module_v1, +// reproducing the same assertions each addon's own test.js makes. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include + +typedef napi_value (*NapiRegisterModuleFn)(napi_env, napi_value); + +TEST(Napi, TwoFunctionArguments) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_2_FUNCTION_ARGUMENTS_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + + ObjectRef* exportsResult = FromNapi(returnedExports)->asObject(); + ValueRef* addFn = exportsResult->get(state, StringRef::createFromASCII("add")); + + ValueRef* args[2] = { ValueRef::create(3), ValueRef::create(5) }; + return addFn->call(state, ValueRef::createUndefined(), 2, args); + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_EQ(result.result->asNumber(), 8); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +// captures what a JS-side callback was invoked with, so the C++ test body can +// inspect it afterward; plain file-static globals since NativeFunctionInfo +// only accepts capture-less function pointers (no per-instance user data +// short of the extraData() mechanism napi_create_function itself uses) +static ValueRef* g_callbackArg = nullptr; +static ValueRef* g_callbackThis = nullptr; + +static ValueRef* RecordCallbackArgAndThis(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + g_callbackArg = argc > 0 ? argv[0] : ValueRef::createUndefined(); + g_callbackThis = thisValue; + return ValueRef::createUndefined(); +} + +TEST(Napi, Callbacks) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_3_CALLBACKS_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + g_callbackArg = nullptr; + g_callbackThis = nullptr; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ObjectRef* exportsResult = FromNapi(returnedExports)->asObject(); + + AtomicStringRef* cbName = AtomicStringRef::create(state->context(), "cb", 2); + FunctionObjectRef* cb = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(cbName, RecordCallbackArgAndThis, 1, true, false)); + + // RunCallback(cb) should call cb.call(global, 'hello world') + ValueRef* runCallback = exportsResult->get(state, StringRef::createFromASCII("RunCallback")); + ValueRef* runCallbackArgs[1] = { cb }; + runCallback->call(state, ValueRef::createUndefined(), 1, runCallbackArgs); + + return ValueRef::createUndefined(); + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + ASSERT_NE(g_callbackArg, nullptr); + ASSERT_TRUE(g_callbackArg->isString()); + EXPECT_TRUE(g_callbackArg->asString()->equalsWithASCIIString("hello world", strlen("hello world"))); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +TEST(Napi, CallbackRecv) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_3_CALLBACKS_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + g_callbackArg = nullptr; + g_callbackThis = nullptr; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ObjectRef* exportsResult = FromNapi(returnedExports)->asObject(); + + AtomicStringRef* cbName = AtomicStringRef::create(state->context(), "cb", 2); + FunctionObjectRef* cb = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(cbName, RecordCallbackArgAndThis, 0, true, false)); + + ObjectRef* desiredRecv = ObjectRef::create(state); + + // RunCallbackWithRecv(cb, desiredRecv) should call cb.call(desiredRecv) + ValueRef* runCallbackWithRecv = exportsResult->get(state, StringRef::createFromASCII("RunCallbackWithRecv")); + ValueRef* args[2] = { cb, desiredRecv }; + runCallbackWithRecv->call(state, ValueRef::createUndefined(), 2, args); + + return desiredRecv; + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + ASSERT_NE(g_callbackThis, nullptr); + // Escargot's GC (Boehm) never moves objects, so comparing the raw pointers + // captured by the callback against the desiredRecv returned from the lambda + // is a valid identity check + EXPECT_EQ(g_callbackThis, result.result); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +TEST(Napi, ObjectFactory) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_4_OBJECT_FACTORY_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ValueRef* factory = FromNapi(returnedExports); + + ValueRef* helloArgs[1] = { StringRef::createFromASCII("hello") }; + ValueRef* obj1 = factory->call(state, ValueRef::createUndefined(), 1, helloArgs); + ValueRef* worldArgs[1] = { StringRef::createFromASCII("world") }; + ValueRef* obj2 = factory->call(state, ValueRef::createUndefined(), 1, worldArgs); + + ValueRef* msg1 = obj1->asObject()->get(state, StringRef::createFromASCII("msg")); + ValueRef* msg2 = obj2->asObject()->get(state, StringRef::createFromASCII("msg")); + + bool ok = msg1->isString() && msg2->isString() + && msg1->asString()->equalsWithASCIIString("hello", strlen("hello")) + && msg2->asString()->equalsWithASCIIString("world", strlen("world")); + return ValueRef::create(ok); + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +TEST(Napi, FunctionFactory) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_5_FUNCTION_FACTORY_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ValueRef* factory = FromNapi(returnedExports); + + ValueRef* fn = factory->call(state, ValueRef::createUndefined(), 0, nullptr); + return fn->call(state, ValueRef::createUndefined(), 0, nullptr); + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + ASSERT_TRUE(result.result->isString()); + EXPECT_TRUE(result.result->asString()->equalsWithASCIIString("hello world", strlen("hello world"))); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +TEST(Napi, ObjectWrap) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_MYOBJECT_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ObjectRef* exportsResult = FromNapi(returnedExports)->asObject(); + + ValueRef* cons = exportsResult->get(state, StringRef::createFromASCII("MyObject")); + + ValueRef* ctorArgs[1] = { ValueRef::create(9) }; + ValueRef* obj = cons->construct(state, 1, ctorArgs); + ObjectRef* objRef = obj->asObject(); + + bool ok = true; + ok = ok && objRef->get(state, StringRef::createFromASCII("value"))->asNumber() == 9; + + objRef->set(state, StringRef::createFromASCII("value"), ValueRef::create(10)); + ok = ok && objRef->get(state, StringRef::createFromASCII("value"))->asNumber() == 10; + ok = ok && objRef->get(state, StringRef::createFromASCII("valueReadonly"))->asNumber() == 10; + + // valueReadonly has no setter (napi_define_class's `value` + // descriptor's setter field is null), so this assignment must fail + bool setSucceeded = objRef->set(state, StringRef::createFromASCII("valueReadonly"), ValueRef::create(14)); + ok = ok && !setSucceeded; + ok = ok && objRef->get(state, StringRef::createFromASCII("valueReadonly"))->asNumber() == 10; + + ValueRef* plusOne = objRef->get(state, StringRef::createFromASCII("plusOne")); + ok = ok && plusOne->call(state, objRef, 0, nullptr)->asNumber() == 11; + ok = ok && plusOne->call(state, objRef, 0, nullptr)->asNumber() == 12; + ok = ok && plusOne->call(state, objRef, 0, nullptr)->asNumber() == 13; + + ValueRef* multiply = objRef->get(state, StringRef::createFromASCII("multiply")); + + ValueRef* noArgResult = multiply->call(state, objRef, 0, nullptr); + ok = ok && noArgResult->asObject()->get(state, StringRef::createFromASCII("value"))->asNumber() == 13; + + ValueRef* tenArgs[1] = { ValueRef::create(10) }; + ValueRef* tenResult = multiply->call(state, objRef, 1, tenArgs); + ok = ok && tenResult->asObject()->get(state, StringRef::createFromASCII("value"))->asNumber() == 130; + + ValueRef* negArgs[1] = { ValueRef::create(-1) }; + ValueRef* newObj = multiply->call(state, objRef, 1, negArgs); + ok = ok && newObj->asObject()->get(state, StringRef::createFromASCII("value"))->asNumber() == -13; + ok = ok && newObj->asObject()->get(state, StringRef::createFromASCII("valueReadonly"))->asNumber() == -13; + ok = ok && (newObj != obj); + + return ValueRef::create(ok); + }, + napiEnv->env(), registerModule); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); + + // Flush out the wrapped MyObject instances created above before this test + // ends. This is not about env safety anymore (env now lives as long as + // NapiEnv itself, see NapiEnv::env()) - it is about not leaving + // unreachable-but-uncollected napi_wrap'd garbage sitting in the shared + // Boehm heap. Left alone, it can get opportunistically finalized at an + // arbitrary later point (e.g. mid-construction of a *different* test's + // brand-new VMInstance, which is an unsafe time to run addon code) - + // this actually crashed FactoryWrap when this cleanup was missing. Same + // "clear stack + churn + gc x5" pattern as + // test/cctest/testapi.cpp's WeakPtr.*/Finalizer.Basic. + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +// Overwrites a large region of the native stack with non-pointer bytes, so +// Boehm's conservative scan stops mistaking a stale MyObject* left in a +// callee-saved register / spilled stack slot by a previous Evaluator::execute +// call for a live root. Recurses to reach deeper than the frames those calls +// used. `volatile` + the sink check keep the compiler from eliding it. +__attribute__((noinline)) static void ClobberNativeStack(int depth, volatile char* sink) +{ + volatile char scratch[1024]; + for (size_t i = 0; i < sizeof(scratch); i++) { + scratch[i] = static_cast((i * 31 + depth) & 0x7f); + } + if (depth > 0) { + ClobberNativeStack(depth - 1, scratch); + } + *sink = scratch[(depth * 7) % sizeof(scratch)]; +} + +TEST(Napi, FactoryWrap) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_7_FACTORY_WRAP_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + ObjectRef* exports = nullptr; + + Evaluator::EvaluatorResult r1 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule, ObjectRef** exportsOut) -> ValueRef* { + env->executionState = state; + ObjectRef* exportsObj = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exportsObj)); + *exportsOut = FromNapi(returnedExports)->asObject(); + + ValueRef* finalizeCount = (*exportsOut)->get(state, StringRef::createFromASCII("finalizeCount")); + EXPECT_EQ(finalizeCount->asNumber(), 0); + return ValueRef::createUndefined(); + }, + napiEnv->env(), registerModule, &exports); + ASSERT_TRUE(r1.isSuccessful()) << r1.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + // napi_wrap creates a weak napi_ref (refcount 0, see NapiFunctions.cpp), + // so nothing artificially roots the wrapped MyObject instance created + // below - it becomes collectible the moment the JS side drops it. + for (int round = 0; round < 2; round++) { + int base = (round == 0) ? 10 : 20; + + // Create one wrapped MyObject and exercise it inside its own + // Evaluator::execute call, so no local variable in FactoryWrap's own + // stack frame keeps referencing it once this call returns. + Evaluator::EvaluatorResult r2 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, ObjectRef* exports, int base) -> ValueRef* { + env->executionState = state; + ValueRef* createObject = exports->get(state, StringRef::createFromASCII("createObject")); + ValueRef* args[1] = { ValueRef::create(base) }; + ValueRef* obj = createObject->call(state, ValueRef::createUndefined(), 1, args); + ObjectRef* objRef = obj->asObject(); + ValueRef* plusOne = objRef->get(state, StringRef::createFromASCII("plusOne")); + EXPECT_EQ(plusOne->call(state, objRef, 0, nullptr)->asNumber(), base + 1); + EXPECT_EQ(plusOne->call(state, objRef, 0, nullptr)->asNumber(), base + 2); + EXPECT_EQ(plusOne->call(state, objRef, 0, nullptr)->asNumber(), base + 3); + return ValueRef::createUndefined(); + }, + napiEnv->env(), exports, base); + ASSERT_TRUE(r2.isSuccessful()) << r2.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + // "clear stack": an unrelated Evaluator::execute call reuses the + // native stack frame the previous call's locals (obj/objRef/plusOne) + // sat in, so Boehm's conservative stack scan doesn't keep seeing + // those stale bit patterns as live roots. Same pattern as + // test/cctest/testapi.cpp's WeakPtr.*/Finalizer.Basic tests. + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + + volatile char stackSink = 0; + ClobberNativeStack(48, &stackSink); + + for (size_t i = 0; i < 2048; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + Evaluator::EvaluatorResult r3 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, ObjectRef* exports, int expectedCount) -> ValueRef* { + env->executionState = state; + ValueRef* finalizeCount = exports->get(state, StringRef::createFromASCII("finalizeCount")); + EXPECT_EQ(finalizeCount->asNumber(), expectedCount); + return ValueRef::createUndefined(); + }, + napiEnv->env(), exports, round + 1); + ASSERT_TRUE(r3.isSuccessful()) << r3.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + } + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +TEST(Napi, WeakReferenceStaleAfterGC) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + napi_ref ref = nullptr; + + // Create the target object and the weak (refcount 0) napi_ref to it + // inside their own Evaluator::execute call, so no local variable in this + // test's own stack frame keeps referencing the object once this call + // returns (same "own call frame" discipline as Napi.FactoryWrap above). + Evaluator::EvaluatorResult r1 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, napi_ref* refOut) -> ValueRef* { + env->executionState = state; + ObjectRef* obj = ObjectRef::create(state); + napi_create_reference(env, ToNapi(obj), 0, refOut); + return ValueRef::createUndefined(); + }, + napiEnv->env(), &ref); + ASSERT_TRUE(r1.isSuccessful()) << r1.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + napi_value value = reinterpret_cast(static_cast(1)); + napi_get_reference_value(napiEnv->env(), ref, &value); + EXPECT_NE(value, nullptr); + + // Overwrite the stale pointer bit pattern this test's own stack slot is + // still holding before collecting, so Boehm's conservative stack scan + // doesn't keep the object alive through it (the nested-call trick below + // only clears the *inner* lambda's frame, not this function's own). + value = reinterpret_cast(static_cast(1)); + + // "clear stack" + churn + gc x5, same pattern as Napi.FactoryWrap: nothing + // else roots the object created above, so this must collect it. + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + napi_get_reference_value(napiEnv->env(), ref, &value); + EXPECT_EQ(value, nullptr); + + napi_delete_reference(napiEnv->env(), ref); +} + +static int g_removeWrapFinalizeCount = 0; + +static void RemoveWrapFinalizeCallback(node_api_basic_env env, void* data, void* hint) +{ + g_removeWrapFinalizeCount++; +} + +TEST(Napi, RemoveWrapSuppressesFinalizer) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_removeWrapFinalizeCount = 0; + + // Wrap an object with a finalizer, then immediately napi_remove_wrap it, + // inside its own Evaluator::execute call so no local variable in this + // test's own stack frame keeps referencing the object once this call + // returns (same "own call frame" discipline as Napi.FactoryWrap above). + Evaluator::EvaluatorResult r1 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + ObjectRef* obj = ObjectRef::create(state); + napi_wrap(env, ToNapi(obj), nullptr, RemoveWrapFinalizeCallback, nullptr, nullptr); + + void* removed = nullptr; + napi_remove_wrap(env, ToNapi(obj), &removed); + return ValueRef::createUndefined(); + }, + napiEnv->env()); + ASSERT_TRUE(r1.isSuccessful()) << r1.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + // napi_remove_wrap itself must not invoke the finalizer + EXPECT_EQ(g_removeWrapFinalizeCount, 0); + + // "clear stack" + churn + gc x5, same pattern as Napi.FactoryWrap: nothing + // else roots the object created above, so this must collect it - and the + // finalizer must stay suppressed even once that actually happens. + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + EXPECT_EQ(g_removeWrapFinalizeCount, 0); +} + +TEST(Napi, ReferenceRefUnref) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + napi_ref ref = nullptr; + + // Create the target object and a strong (refcount 1) napi_ref to it + // inside its own Evaluator::execute call, so no local variable in this + // test's own stack frame keeps referencing the object once this call + // returns (same "own call frame" discipline as Napi.FactoryWrap above). + Evaluator::EvaluatorResult r1 = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, napi_ref* refOut) -> ValueRef* { + env->executionState = state; + ObjectRef* obj = ObjectRef::create(state); + napi_create_reference(env, ToNapi(obj), 1, refOut); + return ValueRef::createUndefined(); + }, + napiEnv->env(), &ref); + ASSERT_TRUE(r1.isSuccessful()) << r1.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + uint32_t count = 0; + ASSERT_EQ(napi_reference_ref(napiEnv->env(), ref, &count), napi_ok); + EXPECT_EQ(count, 2u); + + ASSERT_EQ(napi_reference_unref(napiEnv->env(), ref, &count), napi_ok); + EXPECT_EQ(count, 1u); + + // Still strong (count 1): must survive a collection pass. "clear stack" + + // churn + gc x5, same pattern as Napi.FactoryWrap above. + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + napi_value value = reinterpret_cast(static_cast(1)); + napi_get_reference_value(napiEnv->env(), ref, &value); + EXPECT_NE(value, nullptr); + + ASSERT_EQ(napi_reference_unref(napiEnv->env(), ref, &count), napi_ok); + EXPECT_EQ(count, 0u); + + // Overwrite the stale pointer bit pattern this test's own stack slot is + // still holding before collecting, so Boehm's conservative stack scan + // doesn't keep the object alive through it (same reasoning as + // Napi.WeakReferenceStaleAfterGC above). Now weak (count 0): must + // actually be collected this time. + value = reinterpret_cast(static_cast(1)); + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, StringRef* s) -> ValueRef* { + return ValueRef::create(100); + }, + StringRef::createFromUTF8("qwer")); + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + napi_get_reference_value(napiEnv->env(), ref, &value); + EXPECT_EQ(value, nullptr); + + // Decrementing an already-weak (count 0) ref must error, not underflow. + EXPECT_EQ(napi_reference_unref(napiEnv->env(), ref, &count), napi_generic_failure); + + // Strengthening a weak ref whose target is already gone must error too. + EXPECT_EQ(napi_reference_ref(napiEnv->env(), ref, &count), napi_generic_failure); + + napi_delete_reference(napiEnv->env(), ref); +} + +static int g_instanceDataFinalizeCount = 0; +static void* g_instanceDataFinalizeSeenData = nullptr; + +static void InstanceDataFinalizeCallback(napi_env env, void* data, void* hint) +{ + g_instanceDataFinalizeCount++; + g_instanceDataFinalizeSeenData = data; +} + +TEST(Napi, SetInstanceDataFinalizerRunsOnEnvDestruction) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_instanceDataFinalizeCount = 0; + g_instanceDataFinalizeSeenData = nullptr; + + int instanceData = 42; + ASSERT_EQ(napi_set_instance_data(napiEnv->env(), &instanceData, InstanceDataFinalizeCallback, nullptr), napi_ok); + + void* got = nullptr; + ASSERT_EQ(napi_get_instance_data(napiEnv->env(), &got), napi_ok); + EXPECT_EQ(got, &instanceData); + + // must not fire before teardown + EXPECT_EQ(g_instanceDataFinalizeCount, 0); + + // Unlike every other Napi.* test above, this NapiEnv must actually be + // destroyed (not leaked) - the whole point here is exercising + // ~NapiEnv()'s teardown hook. Safe because nothing in this test suite + // calls NapiEnv::globalFinalize() afterward (see NapiEnv.h's ordering + // requirement: every NapiEnv must be destroyed before that call). + delete napiEnv; + + EXPECT_EQ(g_instanceDataFinalizeCount, 1); + EXPECT_EQ(g_instanceDataFinalizeSeenData, &instanceData); +} + +TEST(Napi, HandleScopeOpenCloseAndMismatch) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_handle_scope outer = nullptr; + napi_handle_scope inner = nullptr; + ASSERT_EQ(napi_open_handle_scope(env, &outer), napi_ok); + ASSERT_EQ(napi_open_handle_scope(env, &inner), napi_ok); + + // Closing out of LIFO order must fail and must not actually pop anything. + EXPECT_EQ(napi_close_handle_scope(env, outer), napi_handle_scope_mismatch); + + ASSERT_EQ(napi_close_handle_scope(env, inner), napi_ok); + ASSERT_EQ(napi_close_handle_scope(env, outer), napi_ok); + + napi_escapable_handle_scope escapable = nullptr; + ASSERT_EQ(napi_open_escapable_handle_scope(env, &escapable), napi_ok); + + napi_value escapee = ToNapi(ValueRef::create(7)); + napi_value escaped = nullptr; + ASSERT_EQ(napi_escape_handle(env, escapable, escapee, &escaped), napi_ok); + EXPECT_EQ(escaped, escapee); + + // A second escape from the same scope must be rejected. + napi_value escapedAgain = nullptr; + EXPECT_EQ(napi_escape_handle(env, escapable, escapee, &escapedAgain), napi_escape_called_twice); + + ASSERT_EQ(napi_close_escapable_handle_scope(env, escapable), napi_ok); +} + +// thrown from a native FunctionObjectRef passed into NewScopeWithException +// (test_handle_scope.c), standing in for the TC's own `() => { throw new +// RangeError(); }` +static ValueRef* ThrowRangeError(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + state->throwException(RangeErrorObjectRef::create(state, StringRef::createFromASCII("boom"))); + return ValueRef::createUndefined(); +} + +static int g_markerAfterCallFunction = 0; + +TEST(Napi, CallFunctionReportsExceptionAsPendingStatus) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_markerAfterCallFunction = 0; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + AtomicStringRef* throwName = AtomicStringRef::create(state->context(), "throwRangeError", strlen("throwRangeError")); + FunctionObjectRef* throwFn = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(throwName, ThrowRangeError, 0, true, false)); + + napi_value callResult = nullptr; + napi_status status = napi_call_function(env, ToNapi(ValueRef::createUndefined()), ToNapi(throwFn), 0, nullptr, &callResult); + + // Proves control actually returned here instead of a raw C++ + // exception unwinding straight past napi_call_function's caller. + g_markerAfterCallFunction++; + + if (status != napi_pending_exception) { + return ValueRef::create(1); + } + + bool isPending = false; + napi_is_exception_pending(env, &isPending); + if (!isPending) { + return ValueRef::create(2); + } + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + if (!FromNapi(exception)->isObject()) { + return ValueRef::create(3); + } + + bool stillPending = true; + napi_is_exception_pending(env, &stillPending); + if (stillPending) { + return ValueRef::create(4); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_EQ(g_markerAfterCallFunction, 1); + EXPECT_TRUE(result.result->isUndefined()); +} + +TEST(Napi, HandleScope) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + void* handle = dlopen(NAPI_TEST_HANDLE_SCOPE_SO_PATH, RTLD_NOW); + ASSERT_NE(handle, nullptr) << dlerror(); + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(handle, "napi_register_module_v1")); + ASSERT_NE(registerModule, nullptr) << dlerror(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, NapiRegisterModuleFn registerModule) -> ValueRef* { + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + ObjectRef* exportsResult = FromNapi(returnedExports)->asObject(); + + // NewScope: open a handle scope, create an object in it, close it. + ValueRef* newScope = exportsResult->get(state, StringRef::createFromASCII("NewScope")); + newScope->call(state, ValueRef::createUndefined(), 0, nullptr); + + // NewScopeEscape: object created inside an escapable scope must + // still be usable after the scope closes. + ValueRef* newScopeEscape = exportsResult->get(state, StringRef::createFromASCII("NewScopeEscape")); + ValueRef* escapeResult = newScopeEscape->call(state, ValueRef::createUndefined(), 0, nullptr); + if (!escapeResult->isObject()) { + state->throwException(StringRef::createFromASCII("NewScopeEscape did not return an object")); + } + + // NewScopeEscapeTwice: the TC's own NODE_API_ASSERT aborts the + // process if napi_escape_handle doesn't reject the second call, + // so simply returning here is the pass condition. + ValueRef* newScopeEscapeTwice = exportsResult->get(state, StringRef::createFromASCII("NewScopeEscapeTwice")); + newScopeEscapeTwice->call(state, ValueRef::createUndefined(), 0, nullptr); + + // NewScopeWithException: the callback passed in throws a + // RangeError; the TC's own NODE_API_ASSERT aborts the process if + // napi_call_function doesn't report napi_pending_exception for + // it. That RangeError is left pending on `env` on purpose (the + // TC never clears it), so NapiCallbackTrampoline rethrows it + // once this native function returns - propagating out as a real + // JS exception, same as test.js's assert.throws(..., RangeError). + AtomicStringRef* throwName = AtomicStringRef::create(state->context(), "throwRangeError", strlen("throwRangeError")); + FunctionObjectRef* throwFn = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(throwName, ThrowRangeError, 0, true, false)); + ValueRef* newScopeWithException = exportsResult->get(state, StringRef::createFromASCII("NewScopeWithException")); + ValueRef* excArgs[1] = { throwFn }; + newScopeWithException->call(state, ValueRef::createUndefined(), 1, excArgs); + + return ValueRef::createUndefined(); + }, + napiEnv->env(), registerModule); + + ASSERT_FALSE(result.isSuccessful()); + ASSERT_TRUE(result.error.value()->isObject()); + + // Intentionally NOT dlclose(handle): a wrapped object created by this addon + // can outlive the test on the GC heap and have its finalizer - whose code + // lives inside this .so - invoked later (e.g. by a subsequent test's GC). + // Unloading the .so would turn that into a call into unmapped memory + // (SIGSEGV). Addon handles are process-lifetime in this test binary. +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_arraybuffer.cpp b/test/cctest/testnapi_arraybuffer.cpp new file mode 100644 index 000000000..fc503442c --- /dev/null +++ b/test/cctest/testnapi_arraybuffer.cpp @@ -0,0 +1,324 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises NapiArrayBuffer.cpp's slice of js_native_api.h/node_api.h +// (ArrayBuffer/TypedArray/DataView/External/type-tag/Buffer), self-contained +// (no dlopen'd addon involved) - same NapiEnv/Evaluator::execute setup as +// test/cctest/testnapi.cpp. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include + +TEST(Napi, ArrayBuffer) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + void* data = nullptr; + napi_value buf = nullptr; + napi_status status = napi_create_arraybuffer(env, 16, &data, &buf); + if (status != napi_ok || data == nullptr) { + return ValueRef::create(false); + } + + // write through the returned data pointer, then read the same bytes + // back via napi_get_arraybuffer_info + static_cast(data)[0] = 0x42; + static_cast(data)[15] = 0x7f; + + bool isAB = false; + napi_is_arraybuffer(env, buf, &isAB); + if (!isAB) { + return ValueRef::create(false); + } + + bool nonBufIsAB = true; + napi_is_arraybuffer(env, ToNapi(ValueRef::create(5)), &nonBufIsAB); + if (nonBufIsAB) { + return ValueRef::create(false); + } + + void* readData = nullptr; + size_t byteLength = 0; + napi_get_arraybuffer_info(env, buf, &readData, &byteLength); + if (byteLength != 16 || readData != data) { + return ValueRef::create(false); + } + if (static_cast(readData)[0] != 0x42 || static_cast(readData)[15] != 0x7f) { + return ValueRef::create(false); + } + + bool detachedBefore = true; + napi_is_detached_arraybuffer(env, buf, &detachedBefore); + if (detachedBefore) { + return ValueRef::create(false); + } + + napi_detach_arraybuffer(env, buf); + bool detachedAfter = false; + napi_is_detached_arraybuffer(env, buf, &detachedAfter); + if (!detachedAfter) { + return ValueRef::create(false); + } + + return ValueRef::create(true); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, TypedArray) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + void* data = nullptr; + napi_value buf = nullptr; + napi_create_arraybuffer(env, 16, &data, &buf); + + // 8-element Uint8Array starting 4 bytes into the backing arraybuffer + napi_value ta = nullptr; + napi_status status = napi_create_typedarray(env, napi_uint8_array, 8, buf, 4, &ta); + if (status != napi_ok) { + return ValueRef::create(false); + } + + bool isTA = false; + napi_is_typedarray(env, ta, &isTA); + if (!isTA) { + return ValueRef::create(false); + } + + napi_typedarray_type type; + size_t length = 0; + void* taData = nullptr; + napi_value taBuf = nullptr; + size_t byteOffset = 0; + napi_status infoStatus = napi_get_typedarray_info(env, ta, &type, &length, &taData, &taBuf, &byteOffset); + if (infoStatus != napi_ok) { + return ValueRef::create(false); + } + + bool ok = (type == napi_uint8_array) && (length == 8) && (byteOffset == 4) && (taData == static_cast(data) + 4) && (FromNapi(taBuf) == FromNapi(buf)); + return ValueRef::create(ok); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, DataView) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + void* data = nullptr; + napi_value buf = nullptr; + napi_create_arraybuffer(env, 16, &data, &buf); + + napi_value dv = nullptr; + napi_status status = napi_create_dataview(env, 8, buf, 2, &dv); + if (status != napi_ok) { + return ValueRef::create(false); + } + + bool isDV = false; + napi_is_dataview(env, dv, &isDV); + if (!isDV) { + return ValueRef::create(false); + } + + // a DataView must not also report as a TypedArray + bool isTA = true; + napi_is_typedarray(env, dv, &isTA); + if (isTA) { + return ValueRef::create(false); + } + + size_t byteLength = 0; + void* dvData = nullptr; + napi_value dvBuf = nullptr; + size_t byteOffset = 0; + napi_status infoStatus = napi_get_dataview_info(env, dv, &byteLength, &dvData, &dvBuf, &byteOffset); + if (infoStatus != napi_ok) { + return ValueRef::create(false); + } + + bool ok = (byteLength == 8) && (byteOffset == 2) && (dvData == static_cast(data) + 2) && (FromNapi(dvBuf) == FromNapi(buf)); + return ValueRef::create(ok); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, External) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + int payload = 1234; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, int* payload) -> ValueRef* { + env->executionState = state; + + napi_value ext = nullptr; + napi_status status = napi_create_external(env, payload, nullptr, nullptr, &ext); + if (status != napi_ok) { + return ValueRef::create(false); + } + + void* roundTripped = nullptr; + napi_get_value_external(env, ext, &roundTripped); + bool pointerMatches = (roundTripped == static_cast(payload)); + + // documented limitation: napi_typeof cannot distinguish this from + // a plain object, so it reports napi_object rather than + // napi_external (see NapiArrayBuffer.cpp's napi_create_external) + napi_valuetype type; + napi_typeof(env, ext, &type); + bool typeofIsObject = (type == napi_object); + + return ValueRef::create(pointerMatches && typeofIsObject); + }, + napiEnv->env(), &payload); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, TypeTag) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + ObjectRef* obj = ObjectRef::create(state); + napi_value objValue = ToNapi(obj); + + napi_type_tag tag = { 0x1111111111111111ULL, 0x2222222222222222ULL }; + napi_status tagStatus = napi_type_tag_object(env, objValue, &tag); + if (tagStatus != napi_ok) { + return ValueRef::create(false); + } + + bool matches = false; + napi_check_object_type_tag(env, objValue, &tag, &matches); + if (!matches) { + return ValueRef::create(false); + } + + napi_type_tag otherTag = { 0x3333333333333333ULL, 0x4444444444444444ULL }; + bool mismatches = true; + napi_check_object_type_tag(env, objValue, &otherTag, &mismatches); + if (mismatches) { + return ValueRef::create(false); + } + + // an untagged object must not match any tag + ObjectRef* untaggedObj = ObjectRef::create(state); + bool untaggedMatches = true; + napi_check_object_type_tag(env, ToNapi(untaggedObj), &tag, &untaggedMatches); + if (untaggedMatches) { + return ValueRef::create(false); + } + + // re-tagging an already-tagged object must be rejected + napi_status retagStatus = napi_type_tag_object(env, objValue, &tag); + if (retagStatus == napi_ok) { + return ValueRef::create(false); + } + + return ValueRef::create(true); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +// Buffer is implemented as a plain Uint8Array over a backing store (see +// NapiArrayBuffer.cpp's napi_create_buffer) - this exercises that +// approximation end-to-end. +TEST(Napi, Buffer) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + void* data = nullptr; + napi_value buf = nullptr; + napi_status status = napi_create_buffer(env, 4, &data, &buf); + if (status != napi_ok || data == nullptr) { + return ValueRef::create(false); + } + static_cast(data)[0] = 9; + + bool isBuf = false; + napi_is_buffer(env, buf, &isBuf); + if (!isBuf) { + return ValueRef::create(false); + } + + void* readData = nullptr; + size_t length = 0; + napi_get_buffer_info(env, buf, &readData, &length); + + bool ok = (length == 4) && (static_cast(readData)[0] == 9); + return ValueRef::create(ok); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_asyncwork.cpp b/test/cctest/testnapi_asyncwork.cpp new file mode 100644 index 000000000..e5bf74a55 --- /dev/null +++ b/test/cctest/testnapi_asyncwork.cpp @@ -0,0 +1,689 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises src/napi/NapiAsyncWork.cpp - napi_create_async_work/ +// napi_queue_async_work/napi_cancel_async_work/napi_delete_async_work and the +// napi_threadsafe_function family. Unlike every other testnapi_*.cpp, these +// TESTs involve real worker std::threads racing NapiEnv's own main-thread +// callback queue - each test spawns work/calls a threadsafe function from +// another thread, then repeatedly calls napiEnv->drainPendingJobs() (via the +// DrainUntil helper below), with short bounded sleeps in between, until the +// expected callback(s) have actually run - never an unbounded wait, so a +// regression here fails the test instead of hanging it. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// shared test helpers +// --------------------------------------------------------------------------- + +// Repeatedly drains napiEnv's pending jobs (which, per NapiEnv::drainPendingJobs, +// also drains its main-thread callback queue - see NapiEnv.h) until `done()` +// reports true or `maxIterations` short sleeps have elapsed. Every test below +// bounds its own wait through this, so a stuck/never-firing callback fails the +// assertion that follows instead of hanging the test binary. +template +static bool DrainUntil(NapiEnv* napiEnv, Predicate done, int maxIterations = 400, int sleepMs = 5) +{ + for (int i = 0; i < maxIterations; i++) { + napiEnv->drainPendingJobs(); + if (done()) { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); + } + return done(); +} + +// --------------------------------------------------------------------------- +// napi_create_async_work / napi_queue_async_work +// --------------------------------------------------------------------------- + +static std::atomic g_basicExecuteRan{ false }; +static std::atomic g_basicCompleteRan{ false }; +static std::thread::id g_basicExecuteThreadId; +static bool g_basicExecuteRanBeforeComplete = false; +static napi_status g_basicCompleteStatus = napi_generic_failure; +static bool g_basicCompleteCouldCreateValue = false; + +static void BasicAsyncExecute(napi_env env, void* data) +{ + // native-only, per N-API contract - no env/napi_value touched here, just + // native side effects a test can observe afterward + g_basicExecuteThreadId = std::this_thread::get_id(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + g_basicExecuteRan.store(true); +} + +static void BasicAsyncComplete(napi_env env, napi_status status, void* data) +{ + g_basicExecuteRanBeforeComplete = g_basicExecuteRan.load(); + g_basicCompleteStatus = status; + + // `complete` (unlike `execute`) is allowed to touch napi_value/the GC + // heap - confirm this actually works from here + napi_value obj = nullptr; + napi_status createStatus = napi_create_object(env, &obj); + g_basicCompleteCouldCreateValue = (createStatus == napi_ok && obj != nullptr); + + g_basicCompleteRan.store(true); +} + +TEST(NapiAsyncWork, ExecuteRunsOnWorkerCompleteRunsOnMainThread) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_basicExecuteRan = false; + g_basicCompleteRan = false; + g_basicExecuteRanBeforeComplete = false; + g_basicCompleteStatus = napi_generic_failure; + g_basicCompleteCouldCreateValue = false; + std::thread::id mainThreadId = std::this_thread::get_id(); + + napi_async_work work = nullptr; + ASSERT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, &work), napi_ok); + ASSERT_NE(work, nullptr); + + ASSERT_EQ(napi_queue_async_work(env, work), napi_ok); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_basicCompleteRan.load(); })); + + EXPECT_TRUE(g_basicExecuteRan.load()); + EXPECT_NE(g_basicExecuteThreadId, mainThreadId); + EXPECT_TRUE(g_basicExecuteRanBeforeComplete); + EXPECT_EQ(g_basicCompleteStatus, napi_ok); + EXPECT_TRUE(g_basicCompleteCouldCreateValue); + + EXPECT_EQ(napi_delete_async_work(env, work), napi_ok); +} + +TEST(NapiAsyncWork, QueueTwiceRejectsSecondCall) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_basicCompleteRan = false; + + napi_async_work work = nullptr; + ASSERT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, &work), napi_ok); + + ASSERT_EQ(napi_queue_async_work(env, work), napi_ok); + // a second napi_queue_async_work on the same (already-queued) work item + // must be rejected, deterministically regardless of scheduling - `queued` + // is set synchronously, under `work->mutex`, before the first call even + // returns + EXPECT_EQ(napi_queue_async_work(env, work), napi_generic_failure); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_basicCompleteRan.load(); })); + EXPECT_EQ(napi_delete_async_work(env, work), napi_ok); +} + +TEST(NapiAsyncWork, DeleteWhileRunningIsRejectedThenSucceedsAfterCompletion) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_basicCompleteRan = false; + + napi_async_work work = nullptr; + ASSERT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, &work), napi_ok); + ASSERT_EQ(napi_queue_async_work(env, work), napi_ok); + + // Deterministic regardless of scheduling: `complete` can only run from + // inside drainPendingJobs, which this test has not called yet - so `work` + // cannot possibly be in the Completed state at this point. + EXPECT_EQ(napi_delete_async_work(env, work), napi_generic_failure); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_basicCompleteRan.load(); })); + + // now safe + EXPECT_EQ(napi_delete_async_work(env, work), napi_ok); +} + +// --------------------------------------------------------------------------- +// napi_cancel_async_work +// --------------------------------------------------------------------------- + +static std::atomic g_cancelExecuteRan{ false }; +static std::atomic g_cancelCompleteRan{ false }; +static napi_status g_cancelCompleteStatus = napi_ok; + +static void CancelAsyncExecute(napi_env env, void* data) +{ + g_cancelExecuteRan.store(true); +} + +static void CancelAsyncComplete(napi_env env, napi_status status, void* data) +{ + g_cancelCompleteStatus = status; + g_cancelCompleteRan.store(true); +} + +TEST(NapiAsyncWork, CancelBeforeQueueSkipsExecuteAndReportsCancelled) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_cancelExecuteRan = false; + g_cancelCompleteRan = false; + g_cancelCompleteStatus = napi_ok; + + napi_async_work work = nullptr; + ASSERT_EQ(napi_create_async_work(env, nullptr, nullptr, CancelAsyncExecute, CancelAsyncComplete, nullptr, &work), napi_ok); + + // best-effort cancel (see NapiAsyncWork.cpp's own comment): succeeds here + // because `work` is still Idle - napi_queue_async_work hasn't run yet + ASSERT_EQ(napi_cancel_async_work(env, work), napi_ok); + + ASSERT_EQ(napi_queue_async_work(env, work), napi_ok); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_cancelCompleteRan.load(); })); + + EXPECT_FALSE(g_cancelExecuteRan.load()); + EXPECT_EQ(g_cancelCompleteStatus, napi_cancelled); + + // cancelling an already-completed work item must fail (nothing left to cancel) + EXPECT_EQ(napi_cancel_async_work(env, work), napi_generic_failure); + + EXPECT_EQ(napi_delete_async_work(env, work), napi_ok); +} + +TEST(NapiAsyncWork, CancelAfterCompletionFails) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_basicCompleteRan = false; + + napi_async_work work = nullptr; + ASSERT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, &work), napi_ok); + ASSERT_EQ(napi_queue_async_work(env, work), napi_ok); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_basicCompleteRan.load(); })); + + EXPECT_EQ(napi_cancel_async_work(env, work), napi_generic_failure); + EXPECT_EQ(napi_delete_async_work(env, work), napi_ok); +} + +// --------------------------------------------------------------------------- +// NULL-arg guards (async_work) +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, AsyncWorkNullArgGuards) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_async_work work = nullptr; + EXPECT_EQ(napi_create_async_work(nullptr, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, &work), napi_invalid_arg); + EXPECT_EQ(napi_create_async_work(env, nullptr, nullptr, nullptr, BasicAsyncComplete, nullptr, &work), napi_invalid_arg); + EXPECT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, nullptr, nullptr, &work), napi_invalid_arg); + EXPECT_EQ(napi_create_async_work(env, nullptr, nullptr, BasicAsyncExecute, BasicAsyncComplete, nullptr, nullptr), napi_invalid_arg); + + EXPECT_EQ(napi_queue_async_work(nullptr, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_cancel_async_work(nullptr, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_delete_async_work(nullptr, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_delete_async_work(env, nullptr), napi_invalid_arg); +} + +// --------------------------------------------------------------------------- +// napi_threadsafe_function shared helpers +// --------------------------------------------------------------------------- + +// Creates `(function(){ globalThis. = []; return function(x){ +// globalThis..push(x); }; })()` in napiEnv's context and returns the +// inner pusher function, so a threadsafe function's call_js_cb has a real JS +// function (not just a native one) to actually call. +static napi_value CreateArrayPusherFunction(NapiEnv* napiEnv, const char* globalArrayName) +{ + napi_value result = nullptr; + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, const char* globalArrayName, napi_value* outFunc) -> ValueRef* { + env->executionState = state; + std::string src = std::string("(function(){ globalThis.") + globalArrayName + " = []; return function(x){ globalThis." + globalArrayName + ".push(x); }; })()"; + ScriptRef* script = state->context()->scriptParser()->initializeScript(StringRef::createFromUTF8(src.data(), src.size()), StringRef::createFromASCII("testnapi_asyncwork"), false).fetchScriptThrowsExceptionIfParseError(state); + ValueRef* fn = script->execute(state); + *outFunc = ToNapi(fn); + return ValueRef::createUndefined(); + }, + napiEnv->env(), globalArrayName, &result); + return result; +} + +static void ReadArrayLengthAndSum(NapiEnv* napiEnv, const char* globalArrayName, uint32_t* outLen, double* outSum) +{ + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, const char* globalArrayName, uint32_t* outLen, double* outSum) -> ValueRef* { + env->executionState = state; + napi_value global = nullptr; + napi_get_global(env, &global); + napi_value arr = nullptr; + napi_get_named_property(env, global, globalArrayName, &arr); + uint32_t len = 0; + napi_get_array_length(env, arr, &len); + double sum = 0; + for (uint32_t i = 0; i < len; i++) { + napi_value elem = nullptr; + napi_get_element(env, arr, i, &elem); + double v = 0; + napi_get_value_double(env, elem, &v); + sum += v; + } + *outLen = len; + *outSum = sum; + return ValueRef::createUndefined(); + }, + napiEnv->env(), globalArrayName, outLen, outSum); +} + +// Standalone array-length read, wrapped in its own Evaluator::execute (same +// reason as ReadArrayLengthAndSum above: env->executionState is only valid +// nested inside an active call, and is NOT still valid just because some +// earlier, already-returned Evaluator::execute call happened to set it) - +// for use directly inside a DrainUntil predicate. +static uint32_t GetArrayLength(NapiEnv* napiEnv, const char* globalArrayName) +{ + uint32_t len = 0; + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, const char* globalArrayName, uint32_t* outLen) -> ValueRef* { + env->executionState = state; + napi_value global = nullptr; + napi_get_global(env, &global); + napi_value arr = nullptr; + napi_get_named_property(env, global, globalArrayName, &arr); + napi_get_array_length(env, arr, outLen); + return ValueRef::createUndefined(); + }, + napiEnv->env(), globalArrayName, &len); + return len; +} + +// call_js_cb: converts `data` (a small int smuggled through the void* payload +// via reinterpret_cast, same convention Node's own tests use) into a real +// napi_value and calls `js_callback` with it as its one argument. +static void PushIntCallJs(napi_env env, napi_value js_callback, void* context, void* data) +{ + if (js_callback == nullptr) { + return; + } + napi_value arg = nullptr; + napi_create_int32(env, static_cast(reinterpret_cast(data)), &arg); + napi_value argv[1] = { arg }; + napi_call_function(env, ToNapi(ValueRef::createUndefined()), js_callback, 1, argv, nullptr); +} + +static void* IntData(int value) +{ + return reinterpret_cast(static_cast(value)); +} + +static std::atomic g_tsfnFinalizeRan{ false }; +static void* g_tsfnFinalizeSeenData = nullptr; + +static void RecordThreadFinalize(napi_env env, void* data, void* hint) +{ + g_tsfnFinalizeSeenData = data; + g_tsfnFinalizeRan.store(true); +} + +// --------------------------------------------------------------------------- +// napi_create_threadsafe_function / napi_call_threadsafe_function - +// multi-producer delivery +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, TsfnDeliversCallsFromMultipleThreads) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnMultiResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + g_tsfnFinalizeSeenData = nullptr; + int finalizeMarker = 0xBEEF; + + constexpr int kThreadCount = 3; + constexpr int kCallsPerThread = 5; + constexpr int kTotalCalls = kThreadCount * kCallsPerThread; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, /*max_queue_size*/ 0, /*initial_thread_count*/ kThreadCount, &finalizeMarker, RecordThreadFinalize, /*context*/ &finalizeMarker, PushIntCallJs, &tsfn), napi_ok); + ASSERT_NE(tsfn, nullptr); + + std::atomic nextValue{ 0 }; + std::vector producers; + for (int t = 0; t < kThreadCount; t++) { + producers.emplace_back([tsfn, &nextValue]() { + for (int c = 0; c < kCallsPerThread; c++) { + int value = nextValue.fetch_add(1); + napi_status status = napi_call_threadsafe_function(tsfn, IntData(value), napi_tsfn_blocking); + EXPECT_EQ(status, napi_ok); + } + EXPECT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + }); + } + for (std::thread& t : producers) { + t.join(); + } + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); + EXPECT_EQ(g_tsfnFinalizeSeenData, &finalizeMarker); + + uint32_t len = 0; + double sum = 0; + ReadArrayLengthAndSum(napiEnv, "__tsfnMultiResults", &len, &sum); + EXPECT_EQ(len, static_cast(kTotalCalls)); + // values delivered are exactly {0, ..., kTotalCalls-1} in some order (one + // per fetch_add), so their sum is fully determined regardless of delivery + // order across the 3 producer threads + double expectedSum = (kTotalCalls - 1) * kTotalCalls / 2.0; + EXPECT_DOUBLE_EQ(sum, expectedSum); +} + +// --------------------------------------------------------------------------- +// napi_acquire_threadsafe_function / napi_release_threadsafe_function lifecycle +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, TsfnAcquireReleaseLifecycle) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnLifecycleResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, 0, /*initial_thread_count*/ 1, nullptr, RecordThreadFinalize, nullptr, PushIntCallJs, &tsfn), napi_ok); + + // acquire bumps the count to 2; one matching release must NOT tear it down yet + ASSERT_EQ(napi_acquire_threadsafe_function(tsfn), napi_ok); + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + EXPECT_FALSE(g_tsfnFinalizeRan.load()); + + // still usable - one acquisition remains + ASSERT_EQ(napi_call_threadsafe_function(tsfn, IntData(7), napi_tsfn_blocking), napi_ok); + ASSERT_TRUE(DrainUntil(napiEnv, [napiEnv]() { + return GetArrayLength(napiEnv, "__tsfnLifecycleResults") == 1; + })); + + // final release tears it down - `closing` flips synchronously, inside + // this very call, before teardown (freeing `tsfn`) is actually enqueued + // to run later on the main thread - so an extra release attempted right + // here (and only right here, before draining lets teardown actually run) + // can safely observe `napi_closing` without touching already-freed memory. + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + EXPECT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_closing); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); + // `tsfn` is freed as of the drain above - must not be touched again +} + +// --------------------------------------------------------------------------- +// max_queue_size: non-blocking napi_queue_full + blocking wait-for-room +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, TsfnMaxQueueSizeNonBlockingReturnsQueueFull) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnQueueFullResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, /*max_queue_size*/ 1, /*initial_thread_count*/ 1, nullptr, RecordThreadFinalize, nullptr, PushIntCallJs, &tsfn), napi_ok); + + // fills the single slot - deterministic, no drain has happened yet + ASSERT_EQ(napi_call_threadsafe_function(tsfn, IntData(1), napi_tsfn_nonblocking), napi_ok); + // queue is now full; a second non-blocking call must fail immediately + EXPECT_EQ(napi_call_threadsafe_function(tsfn, IntData(2), napi_tsfn_nonblocking), napi_queue_full); + + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); +} + +TEST(NapiAsyncWork, TsfnMaxQueueSizeBlockingWaitsForRoom) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnBlockingResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, /*max_queue_size*/ 1, /*initial_thread_count*/ 1, nullptr, RecordThreadFinalize, nullptr, PushIntCallJs, &tsfn), napi_ok); + + // fills the single slot + ASSERT_EQ(napi_call_threadsafe_function(tsfn, IntData(1), napi_tsfn_nonblocking), napi_ok); + + std::atomic blockingCallReturned{ false }; + std::thread blocker([tsfn, &blockingCallReturned]() { + napi_status status = napi_call_threadsafe_function(tsfn, IntData(2), napi_tsfn_blocking); + EXPECT_EQ(status, napi_ok); + blockingCallReturned.store(true); + }); + + // give the blocking call a moment to actually start waiting on the full queue + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + EXPECT_FALSE(blockingCallReturned.load()); + + // draining delivers the first queued item, freeing a slot and waking the + // blocked producer + ASSERT_TRUE(DrainUntil(napiEnv, [&blockingCallReturned]() { return blockingCallReturned.load(); })); + blocker.join(); + EXPECT_TRUE(blockingCallReturned.load()); + + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); + + uint32_t len = 0; + double sum = 0; + ReadArrayLengthAndSum(napiEnv, "__tsfnBlockingResults", &len, &sum); + EXPECT_EQ(len, 2u); + EXPECT_DOUBLE_EQ(sum, 3.0); // 1 + 2 +} + +// --------------------------------------------------------------------------- +// napi_tsfn_abort: already-queued calls are still delivered, THEN the +// finalizer runs. abort does not discard pre-abort items (matches real +// Node-API; see napi_release_threadsafe_function's note and +// node-api/test_threadsafe_function_abort). It only forces teardown and +// rejects *new* calls with napi_closing. +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, TsfnAbortDeliversQueuedCallsThenRunsFinalizer) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnAbortResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, /*max_queue_size*/ 0, /*initial_thread_count*/ 1, nullptr, RecordThreadFinalize, nullptr, PushIntCallJs, &tsfn), napi_ok); + + // queue a few calls but never drain before aborting + for (int i = 0; i < 3; i++) { + ASSERT_EQ(napi_call_threadsafe_function(tsfn, IntData(i), napi_tsfn_nonblocking), napi_ok); + } + + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_abort), napi_ok); + + // further calls must be rejected once closing + EXPECT_EQ(napi_call_threadsafe_function(tsfn, IntData(99), napi_tsfn_nonblocking), napi_closing); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); + + // the 3 pre-abort calls ARE delivered before teardown (0 + 1 + 2 = 3); + // the post-abort call (99) was rejected with napi_closing, so it is not + // included. + uint32_t len = 0; + double sum = 0; + ReadArrayLengthAndSum(napiEnv, "__tsfnAbortResults", &len, &sum); + EXPECT_EQ(len, 3u); + EXPECT_EQ(sum, 3.0); +} + +// --------------------------------------------------------------------------- +// default call_js_cb (NULL): calls `func` with no arguments +// --------------------------------------------------------------------------- + +static std::atomic g_defaultCallJsInvocations{ 0 }; + +static napi_value CountingNativeFunction(napi_env env, napi_callback_info info) +{ + g_defaultCallJsInvocations.fetch_add(1); + napi_value undef = nullptr; + napi_get_undefined(env, &undef); + return undef; +} + +TEST(NapiAsyncWork, TsfnDefaultCallJsCbInvokesFuncWithNoArgs) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + g_defaultCallJsInvocations = 0; + g_tsfnFinalizeRan = false; + + napi_value fn = nullptr; + Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, napi_value* outFn) -> ValueRef* { + env->executionState = state; + napi_create_function(env, "counting", NAPI_AUTO_LENGTH, CountingNativeFunction, nullptr, outFn); + return ValueRef::createUndefined(); + }, + env, &fn); + ASSERT_NE(fn, nullptr); + + napi_threadsafe_function tsfn = nullptr; + // call_js_cb == nullptr: default behavior is calling `func` with no args + ASSERT_EQ(napi_create_threadsafe_function(env, fn, nullptr, nullptr, 0, 1, nullptr, RecordThreadFinalize, nullptr, nullptr, &tsfn), napi_ok); + + ASSERT_EQ(napi_call_threadsafe_function(tsfn, nullptr, napi_tsfn_blocking), napi_ok); + ASSERT_EQ(napi_call_threadsafe_function(tsfn, nullptr, napi_tsfn_blocking), napi_ok); + + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_defaultCallJsInvocations.load() == 2; })); + + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); +} + +// --------------------------------------------------------------------------- +// napi_get_threadsafe_function_context / napi_ref_threadsafe_function / +// napi_unref_threadsafe_function +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, TsfnGetContextAndRefUnref) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnContextResults"); + ASSERT_NE(jsFunc, nullptr); + + g_tsfnFinalizeRan = false; + int contextMarker = 123; + + napi_threadsafe_function tsfn = nullptr; + ASSERT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, 0, 1, nullptr, RecordThreadFinalize, &contextMarker, PushIntCallJs, &tsfn), napi_ok); + + void* contextOut = nullptr; + ASSERT_EQ(napi_get_threadsafe_function_context(tsfn, &contextOut), napi_ok); + EXPECT_EQ(contextOut, &contextMarker); + + // minimal keepalive bookkeeping (see NapiAsyncWork.cpp's own comment) - + // must not crash and must always succeed + EXPECT_EQ(napi_ref_threadsafe_function(env, tsfn), napi_ok); + EXPECT_EQ(napi_unref_threadsafe_function(env, tsfn), napi_ok); + EXPECT_EQ(napi_ref_threadsafe_function(env, tsfn), napi_ok); + + ASSERT_EQ(napi_release_threadsafe_function(tsfn, napi_tsfn_release), napi_ok); + ASSERT_TRUE(DrainUntil(napiEnv, []() { return g_tsfnFinalizeRan.load(); })); +} + +// --------------------------------------------------------------------------- +// NULL-arg guards (threadsafe function) +// --------------------------------------------------------------------------- + +TEST(NapiAsyncWork, ThreadsafeFunctionNullArgGuards) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value jsFunc = CreateArrayPusherFunction(napiEnv, "__tsfnNullArgResults"); + ASSERT_NE(jsFunc, nullptr); + + napi_threadsafe_function tsfn = nullptr; + EXPECT_EQ(napi_create_threadsafe_function(nullptr, jsFunc, nullptr, nullptr, 0, 1, nullptr, nullptr, nullptr, PushIntCallJs, &tsfn), napi_invalid_arg); + EXPECT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, 0, 1, nullptr, nullptr, nullptr, PushIntCallJs, nullptr), napi_invalid_arg); + // initial_thread_count == 0 is rejected + EXPECT_EQ(napi_create_threadsafe_function(env, jsFunc, nullptr, nullptr, 0, 0, nullptr, nullptr, nullptr, PushIntCallJs, &tsfn), napi_invalid_arg); + // func == NULL and call_js_cb == NULL together leave nothing to ever call + EXPECT_EQ(napi_create_threadsafe_function(env, nullptr, nullptr, nullptr, 0, 1, nullptr, nullptr, nullptr, nullptr, &tsfn), napi_invalid_arg); + + EXPECT_EQ(napi_get_threadsafe_function_context(nullptr, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_call_threadsafe_function(nullptr, nullptr, napi_tsfn_nonblocking), napi_invalid_arg); + EXPECT_EQ(napi_acquire_threadsafe_function(nullptr), napi_invalid_arg); + EXPECT_EQ(napi_release_threadsafe_function(nullptr, napi_tsfn_release), napi_invalid_arg); + EXPECT_EQ(napi_ref_threadsafe_function(nullptr, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_unref_threadsafe_function(env, nullptr), napi_invalid_arg); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_datepromise.cpp b/test/cctest/testnapi_datepromise.cpp new file mode 100644 index 000000000..4e6ce9dde --- /dev/null +++ b/test/cctest/testnapi_datepromise.cpp @@ -0,0 +1,288 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises napi_create_date/napi_get_date_value/napi_is_date, +// napi_create_promise/napi_resolve_deferred/napi_reject_deferred/napi_is_promise +// and napi_run_script (NapiDatePromise.cpp), self-contained (no dlopen'd +// addon needed - unlike test/cctest/testnapi.cpp's TCs). + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include + +TEST(Napi, Date) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + const double milliseconds = 1700000000123.0; + + napi_value date = nullptr; + napi_status status = napi_create_date(env, milliseconds, &date); + if (status != napi_ok) { + return ValueRef::create(1); + } + + bool isDate = false; + napi_is_date(env, date, &isDate); + if (!isDate) { + return ValueRef::create(2); + } + + napi_value notADate = ToNapi(ValueRef::create(1)); + bool notADateIsDate = true; + napi_is_date(env, notADate, ¬ADateIsDate); + if (notADateIsDate) { + return ValueRef::create(3); + } + + double roundTripped = 0; + status = napi_get_date_value(env, date, &roundTripped); + if (status != napi_ok) { + return ValueRef::create(4); + } + if (roundTripped != milliseconds) { + return ValueRef::create(5); + } + + double unusedResult = 0; + status = napi_get_date_value(env, notADate, &unusedResult); + if (status != napi_date_expected) { + return ValueRef::create(6); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +// captures what a promise's .then callback (attached via a direct property +// get + call, since this test has no require()/module loader to install a +// real thenable chain through) observed once napi_resolve_deferred settles +// the deferred and env->napiEnv->drainPendingJobs() runs the reaction job; +// plain file-static globals for the same reason testnapi.cpp's +// g_callbackArg/g_callbackThis are (NativeFunctionInfo only accepts +// capture-less function pointers). +static int g_promiseThenCallCount = 0; +static ValueRef* g_promiseThenArg = nullptr; + +static ValueRef* RecordPromiseResolution(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + g_promiseThenCallCount++; + g_promiseThenArg = argc > 0 ? argv[0] : ValueRef::createUndefined(); + return ValueRef::createUndefined(); +} + +TEST(Napi, Promise) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_promiseThenCallCount = 0; + g_promiseThenArg = nullptr; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value notAPromise = ToNapi(ValueRef::create(42)); + bool notAPromiseIsPromise = true; + napi_is_promise(env, notAPromise, ¬APromiseIsPromise); + if (notAPromiseIsPromise) { + return ValueRef::create(1); + } + + napi_deferred deferred = nullptr; + napi_value promise = nullptr; + napi_status status = napi_create_promise(env, &deferred, &promise); + if (status != napi_ok) { + return ValueRef::create(2); + } + + bool isPromise = false; + napi_is_promise(env, promise, &isPromise); + if (!isPromise) { + return ValueRef::create(3); + } + + // attach a .then handler via a direct property get + call + // (napi_call_function would work equally well here; this avoids + // needing napi_get_named_property, which this slice doesn't add) + AtomicStringRef* thenCallbackName = AtomicStringRef::create(state->context(), "recordThen", strlen("recordThen")); + FunctionObjectRef* thenCallback = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(thenCallbackName, RecordPromiseResolution, 1, true, false)); + + ObjectRef* promiseObj = FromNapi(promise)->asObject(); + ValueRef* thenFn = promiseObj->get(state, StringRef::createFromASCII("then")); + ValueRef* thenArgs[1] = { thenCallback }; + thenFn->call(state, promiseObj, 1, thenArgs); + + napi_value resolution = ToNapi(ValueRef::create(42)); + status = napi_resolve_deferred(env, deferred, resolution); + if (status != napi_ok) { + return ValueRef::create(4); + } + + // fulfill()/reject() only enqueue reaction jobs rather than + // running them synchronously, so the .then callback above has + // not run yet at this point. + if (g_promiseThenCallCount != 0) { + return ValueRef::create(5); + } + + env->napiEnv->drainPendingJobs(); + + if (g_promiseThenCallCount != 1) { + return ValueRef::create(6); + } + if (g_promiseThenArg == nullptr || !g_promiseThenArg->isNumber() || g_promiseThenArg->asNumber() != 42) { + return ValueRef::create(7); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +TEST(Napi, PromiseReject) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_promiseThenCallCount = 0; + g_promiseThenArg = nullptr; + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_deferred deferred = nullptr; + napi_value promise = nullptr; + napi_status status = napi_create_promise(env, &deferred, &promise); + if (status != napi_ok) { + return ValueRef::create(1); + } + + AtomicStringRef* catchCallbackName = AtomicStringRef::create(state->context(), "recordCatch", strlen("recordCatch")); + FunctionObjectRef* catchCallback = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(catchCallbackName, RecordPromiseResolution, 1, true, false)); + + ObjectRef* promiseObj = FromNapi(promise)->asObject(); + ValueRef* catchFn = promiseObj->get(state, StringRef::createFromASCII("catch")); + ValueRef* catchArgs[1] = { catchCallback }; + catchFn->call(state, promiseObj, 1, catchArgs); + + napi_value rejection = ToNapi(StringRef::createFromASCII("nope")); + status = napi_reject_deferred(env, deferred, rejection); + if (status != napi_ok) { + return ValueRef::create(2); + } + + env->napiEnv->drainPendingJobs(); + + if (g_promiseThenCallCount != 1) { + return ValueRef::create(3); + } + if (g_promiseThenArg == nullptr || !g_promiseThenArg->isString()) { + return ValueRef::create(4); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +TEST(Napi, RunScript) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value script = ToNapi(StringRef::createFromASCII("1 + 2")); + napi_value scriptResult = nullptr; + napi_status status = napi_run_script(env, script, &scriptResult); + if (status != napi_ok) { + return ValueRef::create(1); + } + if (!FromNapi(scriptResult)->isNumber() || FromNapi(scriptResult)->asNumber() != 3) { + return ValueRef::create(2); + } + + // a non-string napi_value must be rejected up front rather than + // crashing/misbehaving. + napi_value notAString = ToNapi(ValueRef::create(5)); + napi_value unusedResult = nullptr; + status = napi_run_script(env, notAString, &unusedResult); + if (status != napi_string_expected) { + return ValueRef::create(3); + } + + // a throwing script must report napi_pending_exception instead of + // letting a raw C++ exception unwind past napi_run_script - + // mirrors testnapi.cpp's CallFunctionReportsExceptionAsPendingStatus. + napi_value throwingScript = ToNapi(StringRef::createFromASCII("throw new RangeError('boom')")); + napi_value throwResult = nullptr; + status = napi_run_script(env, throwingScript, &throwResult); + if (status != napi_pending_exception) { + return ValueRef::create(4); + } + + bool isPending = false; + napi_is_exception_pending(env, &isPending); + if (!isPending) { + return ValueRef::create(5); + } + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + if (!FromNapi(exception)->isObject()) { + return ValueRef::create(6); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_error.cpp b/test/cctest/testnapi_error.cpp new file mode 100644 index 000000000..2bc54dc51 --- /dev/null +++ b/test/cctest/testnapi_error.cpp @@ -0,0 +1,204 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include + +// Every assertion below is performed inside the Evaluator::execute closure +// (property reads like ObjectRef::get need a live ExecutionStateRef*, which +// only exists for the duration of that call - see CallFunctionReportsExceptionAsPendingStatus +// in testnapi.cpp for the same pattern). Failures are reported by returning a +// human-readable StringRef describing what went wrong; StringRef::asString()/ +// toStdUTF8String() need no ExecutionStateRef, so the outer test body can +// inspect that string once Evaluator::execute has returned. +static ValueRef* MakeOkString() +{ + return StringRef::createFromASCII("OK"); +} + +static ValueRef* MakeFailString(const std::string& msg) +{ + return StringRef::createFromUTF8(msg.c_str(), msg.length()); +} + +TEST(Napi, ErrorCreateAndIsError) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value codeValue; + napi_create_string_utf8(env, "ERR_CODE", NAPI_AUTO_LENGTH, &codeValue); + napi_value msgValue; + napi_create_string_utf8(env, "something failed", NAPI_AUTO_LENGTH, &msgValue); + + struct Kind { + const char* name; + napi_status (*create)(napi_env, napi_value, napi_value, napi_value*); + }; + Kind kinds[] = { + { "Error", napi_create_error }, + { "TypeError", napi_create_type_error }, + { "RangeError", napi_create_range_error }, + { "SyntaxError", node_api_create_syntax_error }, + }; + + for (const Kind& kind : kinds) { + napi_value errorValue = nullptr; + napi_status status = kind.create(env, codeValue, msgValue, &errorValue); + if (status != napi_ok) { + return MakeFailString(std::string(kind.name) + ": create failed"); + } + + bool isError = false; + napi_is_error(env, errorValue, &isError); + if (!isError) { + return MakeFailString(std::string(kind.name) + ": napi_is_error was false"); + } + + napi_valuetype type; + napi_typeof(env, errorValue, &type); + if (type != napi_object) { + return MakeFailString(std::string(kind.name) + ": typeof was not object"); + } + + ObjectRef* obj = FromNapi(errorValue)->asObject(); + ValueRef* message = obj->get(state, StringRef::createFromASCII("message")); + if (!message->isString() || !message->asString()->equalsWithASCIIString("something failed", strlen("something failed"))) { + return MakeFailString(std::string(kind.name) + ": message mismatch"); + } + + ValueRef* code = obj->get(state, StringRef::createFromASCII("code")); + if (!code->isString() || !code->asString()->equalsWithASCIIString("ERR_CODE", strlen("ERR_CODE"))) { + return MakeFailString(std::string(kind.name) + ": code mismatch"); + } + } + + // a napi_value that isn't an Error must be reported as such. + bool isErrorForPlainObject = true; + napi_value plainObject; + napi_create_object(env, &plainObject); + napi_is_error(env, plainObject, &isErrorForPlainObject); + if (isErrorForPlainObject) { + return StringRef::createFromASCII("plain object: napi_is_error was true"); + } + + // omitting the optional `code` napi_value must not set ".code". + napi_value errorNoCode = nullptr; + napi_create_error(env, nullptr, msgValue, &errorNoCode); + bool hasCode = FromNapi(errorNoCode)->asObject()->hasOwnProperty(state, StringRef::createFromASCII("code")); + if (hasCode) { + return StringRef::createFromASCII("errorNoCode: code should be absent"); + } + + return MakeOkString(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + ASSERT_TRUE(result.result->isString()); + EXPECT_EQ(result.result->asString()->toStdUTF8String(), "OK"); +} + +TEST(Napi, ErrorThrowSetsPendingException) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + struct Kind { + const char* name; + napi_status (*throwFn)(napi_env, const char*, const char*); + }; + Kind kinds[] = { + { "TypeError", napi_throw_type_error }, + { "RangeError", napi_throw_range_error }, + { "SyntaxError", node_api_throw_syntax_error }, + }; + + for (const Kind& kind : kinds) { + bool isPendingBefore = true; + napi_is_exception_pending(env, &isPendingBefore); + if (isPendingBefore) { + return MakeFailString(std::string(kind.name) + ": exception already pending"); + } + + napi_status status = kind.throwFn(env, "ERR_CODE", "boom"); + if (status != napi_ok) { + return MakeFailString(std::string(kind.name) + ": throw call failed"); + } + + bool isPendingAfter = false; + napi_is_exception_pending(env, &isPendingAfter); + if (!isPendingAfter) { + return MakeFailString(std::string(kind.name) + ": exception not pending"); + } + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + + bool isError = false; + napi_is_error(env, exception, &isError); + if (!isError) { + return MakeFailString(std::string(kind.name) + ": thrown value is not an Error"); + } + + ObjectRef* obj = FromNapi(exception)->asObject(); + ValueRef* message = obj->get(state, StringRef::createFromASCII("message")); + if (!message->isString() || !message->asString()->equalsWithASCIIString("boom", strlen("boom"))) { + return MakeFailString(std::string(kind.name) + ": message mismatch"); + } + + ValueRef* code = obj->get(state, StringRef::createFromASCII("code")); + if (!code->isString() || !code->asString()->equalsWithASCIIString("ERR_CODE", strlen("ERR_CODE"))) { + return MakeFailString(std::string(kind.name) + ": code mismatch"); + } + + bool isPendingAfterClear = true; + napi_is_exception_pending(env, &isPendingAfterClear); + if (isPendingAfterClear) { + return MakeFailString(std::string(kind.name) + ": still pending after clear"); + } + } + + return MakeOkString(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + ASSERT_TRUE(result.result->isString()); + EXPECT_EQ(result.result->asString()->toStdUTF8String(), "OK"); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_memorydemo.cpp b/test/cctest/testnapi_memorydemo.cpp new file mode 100644 index 000000000..29cc22426 --- /dev/null +++ b/test/cctest/testnapi_memorydemo.cpp @@ -0,0 +1,197 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Demonstrates that a completely standard, unmodified N-API string workload - +// an addon that simply calls napi_create_string_utf8 to hold onto a lot of +// document-sized text - ends up using substantially less resident memory +// (RSS) on Escargot than it otherwise would, because large strings created +// through napi_create_string_utf8/latin1/utf16 (see those functions' +// transparent-compressible-string-routing comments, NapiFunctions.cpp/ +// NapiValue.cpp) are eligible for Escargot's compressible-string feature: the +// underlying bytes can be compressed back down once the engine is idle +// (VMInstanceRef::enterIdleMode), and transparently decompressed again on any +// access, with zero visible behavior change to the addon. No napi_* call +// here is anything other than the exact same call a real, pre-existing +// addon would already be making. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace { + +// Resident set size of the current process, in bytes, read straight from +// /proc/self/statm (field 2, resident pages) - the same kernel-reported +// number `ps`/`top` show, rather than anything Escargot's own allocator +// merely believes it has requested. +size_t CurrentRSSBytes() +{ + FILE* fp = fopen("/proc/self/statm", "r"); + if (fp == nullptr) { + return 0; + } + long totalPages = 0; + long residentPages = 0; + int scanned = fscanf(fp, "%ld %ld", &totalPages, &residentPages); + fclose(fp); + if (scanned != 2) { + return 0; + } + return static_cast(residentPages) * static_cast(sysconf(_SC_PAGESIZE)); +} + +// Builds `targetBytes` of genuinely compressible ASCII text - a repeated +// lorem-ipsum-like paragraph, the same shape of redundancy a batch of +// similar real-world documents/log records/JSON blobs would have - not +// random bytes (which wouldn't compress at all) and not one single repeated +// byte (which would compress unrealistically well). A short per-document +// header keeps every string distinct, like a real corpus would be, while +// leaving the bulk of the content identical/repetitive. +std::string MakeDocumentText(size_t index, size_t targetBytes) +{ + static const char* const kParagraph = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + "mollit anim id est laborum. "; + + std::string text; + text.reserve(targetBytes + 64); + + char header[64]; + int headerLen = snprintf(header, sizeof(header), "Document #%06zu: ", index); + text.append(header, static_cast(headerLen)); + + while (text.size() < targetBytes) { + text += kParagraph; + } + text.resize(targetBytes); + return text; +} + +} // namespace + +TEST(NapiMemoryDemo, CompressibleStringRSS) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + // ~40MB of logical text, split across many document-sized strings - well + // above napi_create_string_utf8's compressible-string threshold + // (kCompressibleStringThreshold, NapiTypes.h) per string, and large + // enough in aggregate for the RSS difference to be clearly visible over + // background noise. + const size_t kStringCount = 20000; + const size_t kStringBytes = 2048; + const size_t kLogicalBytes = kStringCount * kStringBytes; + + size_t baselineRSS = CurrentRSSBytes(); + + napi_ref arrayRef = nullptr; + Evaluator::EvaluatorResult buildResult = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env, size_t stringCount, size_t stringBytes, napi_ref* arrayRefOut) -> ValueRef* { + env->executionState = state; + + // A real addon parsing/holding a batch of document-sized strings + // would build exactly this shape: an array (or any other + // container) rooted for the life of the addon, populated purely + // via napi_create_string_utf8 - nothing addon-visible changes + // here versus any other N-API host. + napi_value array = nullptr; + EXPECT_EQ(napi_create_array(env, &array), napi_ok); + + for (size_t i = 0; i < stringCount; i++) { + std::string text = MakeDocumentText(i, stringBytes); + napi_value str = nullptr; + EXPECT_EQ(napi_create_string_utf8(env, text.c_str(), text.size(), &str), napi_ok); + EXPECT_EQ(napi_set_element(env, array, static_cast(i), str), napi_ok); + } + + // Keep the array (and therefore every string in it) rooted for + // the rest of this test, exactly as a long-lived addon-owned + // napi_ref would - so nothing measured below is an artifact of + // the array/strings themselves getting collected. + napi_ref ref = nullptr; + EXPECT_EQ(napi_create_reference(env, array, 1, &ref), napi_ok); + *arrayRefOut = ref; + + return ValueRef::createUndefined(); + }, + napiEnv->env(), kStringCount, kStringBytes, &arrayRef); + ASSERT_TRUE(buildResult.isSuccessful()) << buildResult.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + + size_t preCompressionRSS = CurrentRSSBytes(); + + // Nothing addon-visible happens here either: an embedder (not the addon) + // calling VMInstanceRef::enterIdleMode - e.g. from an idle-time hook, the + // same way Escargot's own shell/other hosts do - is what triggers + // compression of every still-live compressible string, forcing a GC pass + // and unmapping freed pages along the way. + VMInstanceRef* vmInstance = napiEnv->vmInstance(); + vmInstance->setConfig(vmInstance->config() | static_cast(VMInstanceRef::ConfigFlag::CompressCompressibleStringsEnterIdle)); + vmInstance->enterIdleMode(); + + size_t postCompressionRSS = CurrentRSSBytes(); + + long long preVsBaseline = static_cast(preCompressionRSS) - static_cast(baselineRSS); + long long saved = static_cast(preCompressionRSS) - static_cast(postCompressionRSS); + double percentSaved = preCompressionRSS > 0 ? (100.0 * static_cast(saved) / static_cast(preCompressionRSS)) : 0.0; + + printf("\n"); + printf("=== Escargot N-API compressible-string RSS demo ===\n"); + printf("workload: %zu strings x %zu bytes = %.2f MB logical text\n", + kStringCount, kStringBytes, static_cast(kLogicalBytes) / (1024.0 * 1024.0)); + printf("string content: repeated lorem-ipsum-like ASCII text (genuinely compressible,\n"); + printf(" not random bytes) with a short per-document header\n"); + printf("baseline RSS (before creating any strings): %10.2f MB\n", static_cast(baselineRSS) / (1024.0 * 1024.0)); + printf("pre-compression RSS (all strings created): %10.2f MB (+%.2f MB over baseline)\n", + static_cast(preCompressionRSS) / (1024.0 * 1024.0), static_cast(preVsBaseline) / (1024.0 * 1024.0)); + printf("post-compression RSS (after enterIdleMode): %10.2f MB\n", static_cast(postCompressionRSS) / (1024.0 * 1024.0)); + printf("RSS saved by compression: %10.2f MB (%.1f%% of pre-compression RSS)\n", + static_cast(saved) / (1024.0 * 1024.0), percentSaved); + printf("====================================================\n"); + + // A real, reproducible regression guard - not a faked number. The exact + // percentage depends on how compressible the content is and on + // conservative-GC/allocator timing, so this threshold is intentionally + // well below what a healthy run should show, to avoid a flaky test + // while still catching a real regression (e.g. compression silently not + // happening at all, which would show ~0% here). + EXPECT_GT(saved, 0); + EXPECT_GT(percentSaved, 15.0); + + // keep the reference alive (and therefore silence "unused" concerns) + // until the very end of the test, exactly as a long-lived addon would. + ASSERT_NE(arrayRef, nullptr); +} +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_object.cpp b/test/cctest/testnapi_object.cpp new file mode 100644 index 000000000..b5fc6d7f4 --- /dev/null +++ b/test/cctest/testnapi_object.cpp @@ -0,0 +1,332 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Self-contained coverage for the object/property/array N-API surface +// implemented in src/napi/NapiObject.cpp - unlike testnapi.cpp's dlopen()-based +// tests, these drive napi_* entry points directly against ExecutionStateRef +// objects created in-process, the same way +// Napi.CallFunctionReportsExceptionAsPendingStatus does in testnapi.cpp. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include + +TEST(Napi, ObjectProperties) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value object; + napi_create_object(env, &object); + + napi_value key = ToNapi(StringRef::createFromASCII("foo")); + napi_value value = ToNapi(ValueRef::create(42)); + + // set/get via generic (napi_value key) entry points + EXPECT_EQ(napi_set_property(env, object, key, value), napi_ok); + + napi_value gotten = nullptr; + EXPECT_EQ(napi_get_property(env, object, key, &gotten), napi_ok); + EXPECT_EQ(FromNapi(gotten)->asNumber(), 42); + + bool hasIt = false; + EXPECT_EQ(napi_has_property(env, object, key, &hasIt), napi_ok); + EXPECT_TRUE(hasIt); + + bool hasOwnIt = false; + EXPECT_EQ(napi_has_own_property(env, object, key, &hasOwnIt), napi_ok); + EXPECT_TRUE(hasOwnIt); + + // named-property entry points (utf8name instead of napi_value key) + bool hasNamed = false; + EXPECT_EQ(napi_has_named_property(env, object, "foo", &hasNamed), napi_ok); + EXPECT_TRUE(hasNamed); + + napi_value gottenNamed = nullptr; + EXPECT_EQ(napi_get_named_property(env, object, "foo", &gottenNamed), napi_ok); + EXPECT_EQ(FromNapi(gottenNamed)->asNumber(), 42); + + napi_value gottenMissingNamed = nullptr; + EXPECT_EQ(napi_get_named_property(env, object, "missing", &gottenMissingNamed), napi_ok); + EXPECT_TRUE(FromNapi(gottenMissingNamed)->isUndefined()); + + bool hasMissingNamed = true; + EXPECT_EQ(napi_has_named_property(env, object, "missing", &hasMissingNamed), napi_ok); + EXPECT_FALSE(hasMissingNamed); + + // delete + bool deleted = false; + EXPECT_EQ(napi_delete_property(env, object, key, &deleted), napi_ok); + EXPECT_TRUE(deleted); + + bool hasAfterDelete = true; + EXPECT_EQ(napi_has_property(env, object, key, &hasAfterDelete), napi_ok); + EXPECT_FALSE(hasAfterDelete); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ObjectIndexedProperties) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value object; + napi_create_object(env, &object); + + napi_value value = ToNapi(ValueRef::create(7)); + EXPECT_EQ(napi_set_element(env, object, 3, value), napi_ok); + + bool hasIt = false; + EXPECT_EQ(napi_has_element(env, object, 3, &hasIt), napi_ok); + EXPECT_TRUE(hasIt); + + napi_value gotten = nullptr; + EXPECT_EQ(napi_get_element(env, object, 3, &gotten), napi_ok); + EXPECT_EQ(FromNapi(gotten)->asNumber(), 7); + + bool deleted = false; + EXPECT_EQ(napi_delete_element(env, object, 3, &deleted), napi_ok); + EXPECT_TRUE(deleted); + + bool hasAfterDelete = true; + EXPECT_EQ(napi_has_element(env, object, 3, &hasAfterDelete), napi_ok); + EXPECT_FALSE(hasAfterDelete); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ObjectPropertyNamesAndPrototype) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value object; + napi_create_object(env, &object); + napi_set_named_property(env, object, "a", ToNapi(ValueRef::create(1))); + napi_set_named_property(env, object, "b", ToNapi(ValueRef::create(2))); + + napi_value names = nullptr; + EXPECT_EQ(napi_get_property_names(env, object, &names), napi_ok); + ObjectRef* namesArray = FromNapi(names)->asObject(); + EXPECT_TRUE(FromNapi(names)->isArrayObject()); + EXPECT_EQ(namesArray->length(state), 2u); + + // prototype of a plain object literal is Object.prototype, itself + // an object (not null) + napi_value proto = nullptr; + EXPECT_EQ(napi_get_prototype(env, object, &proto), napi_ok); + EXPECT_TRUE(FromNapi(proto)->isObject()); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ObjectFreeze) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value object; + napi_create_object(env, &object); + napi_set_named_property(env, object, "a", ToNapi(ValueRef::create(1))); + + EXPECT_EQ(napi_object_freeze(env, object), napi_ok); + + // a set on a frozen object is silently rejected (non-strict Set + // semantics) rather than thrown + napi_value key = ToNapi(StringRef::createFromASCII("a")); + napi_value newValue = ToNapi(ValueRef::create(99)); + EXPECT_EQ(napi_set_property(env, object, key, newValue), napi_ok); + + napi_value stillOld = nullptr; + EXPECT_EQ(napi_get_property(env, object, key, &stillOld), napi_ok); + EXPECT_EQ(FromNapi(stillOld)->asNumber(), 1); + + // adding a brand-new property to a frozen object is likewise rejected + napi_value addedKey = ToNapi(StringRef::createFromASCII("b")); + EXPECT_EQ(napi_set_property(env, object, addedKey, newValue), napi_ok); + bool hasAdded = true; + EXPECT_EQ(napi_has_own_property(env, object, addedKey, &hasAdded), napi_ok); + EXPECT_FALSE(hasAdded); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ObjectSeal) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value object; + napi_create_object(env, &object); + napi_set_named_property(env, object, "a", ToNapi(ValueRef::create(1))); + + EXPECT_EQ(napi_object_seal(env, object), napi_ok); + + // existing writable properties can still be updated after seal ... + napi_value key = ToNapi(StringRef::createFromASCII("a")); + napi_value newValue = ToNapi(ValueRef::create(99)); + EXPECT_EQ(napi_set_property(env, object, key, newValue), napi_ok); + + napi_value updated = nullptr; + EXPECT_EQ(napi_get_property(env, object, key, &updated), napi_ok); + EXPECT_EQ(FromNapi(updated)->asNumber(), 99); + + // ... but a sealed object still rejects new properties + napi_value addedKey = ToNapi(StringRef::createFromASCII("b")); + EXPECT_EQ(napi_set_property(env, object, addedKey, newValue), napi_ok); + bool hasAdded = true; + EXPECT_EQ(napi_has_own_property(env, object, addedKey, &hasAdded), napi_ok); + EXPECT_FALSE(hasAdded); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ObjectArray) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value plainObject; + napi_create_object(env, &plainObject); + bool isPlainObjectArray = true; + EXPECT_EQ(napi_is_array(env, plainObject, &isPlainObjectArray), napi_ok); + EXPECT_FALSE(isPlainObjectArray); + + napi_value array; + EXPECT_EQ(napi_create_array(env, &array), napi_ok); + bool isArray = false; + EXPECT_EQ(napi_is_array(env, array, &isArray), napi_ok); + EXPECT_TRUE(isArray); + + uint32_t length = 123; + EXPECT_EQ(napi_get_array_length(env, array, &length), napi_ok); + EXPECT_EQ(length, 0u); + + napi_value arrayWithLength; + EXPECT_EQ(napi_create_array_with_length(env, 5, &arrayWithLength), napi_ok); + bool isArrayWithLength = false; + EXPECT_EQ(napi_is_array(env, arrayWithLength, &isArrayWithLength), napi_ok); + EXPECT_TRUE(isArrayWithLength); + + uint32_t lengthWithLength = 0; + EXPECT_EQ(napi_get_array_length(env, arrayWithLength, &lengthWithLength), napi_ok); + EXPECT_EQ(lengthWithLength, 5u); + + EXPECT_EQ(napi_set_element(env, arrayWithLength, 10, ToNapi(ValueRef::create(1))), napi_ok); + uint32_t lengthAfterSet = 0; + EXPECT_EQ(napi_get_array_length(env, arrayWithLength, &lengthAfterSet), napi_ok); + EXPECT_EQ(lengthAfterSet, 11u); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, InstanceOf) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + // Exercise napi_instanceof against a real constructor (the built-in + // Array). A plain no-op native FunctionObjectRef cannot model ES + // prototype-chain construction in Escargot: a NativeFunctionInfo + // constructor must itself return an object, and that object would + // not carry the constructor's .prototype - so napi_define_class / + // built-ins are the constructors that produce instanceof-able + // instances (see napi-notes.md). + ValueRef* arrayCtor = state->context()->globalObject()->get(state, StringRef::createFromASCII("Array")); + + napi_value arrayInstance = ToNapi(static_cast(ArrayObjectRef::create(state))); + bool isInstance = false; + EXPECT_EQ(napi_instanceof(env, arrayInstance, ToNapi(arrayCtor), &isInstance), napi_ok); + EXPECT_TRUE(isInstance); + + napi_value plainObject; + napi_create_object(env, &plainObject); + bool isNotInstance = true; + EXPECT_EQ(napi_instanceof(env, plainObject, ToNapi(arrayCtor), &isNotInstance), napi_ok); + EXPECT_FALSE(isNotInstance); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_runtime.cpp b/test/cctest/testnapi_runtime.cpp new file mode 100644 index 000000000..d1e68384b --- /dev/null +++ b/test/cctest/testnapi_runtime.cpp @@ -0,0 +1,441 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises NapiRuntime.cpp's slice of node_api.h - the runtime-integration +// functions that don't need an event loop / thread pool (env/async cleanup +// hooks, node version/module-file-name queries, napi_make_callback, callback +// scopes, async contexts, module registration, buffer-from-arraybuffer, and +// the uv event loop accessor). Self-contained (no dlopen'd addon involved), +// same NapiEnv/Evaluator::execute setup as test/cctest/testnapi.cpp and +// test/cctest/testnapi_arraybuffer.cpp. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include + +// --------------------------------------------------------------------------- +// napi_add_env_cleanup_hook / napi_remove_env_cleanup_hook +// --------------------------------------------------------------------------- + +static int g_envCleanupCallCount = 0; +static void* g_envCleanupSeenArg = nullptr; + +static void RecordEnvCleanup(void* arg) +{ + g_envCleanupCallCount++; + g_envCleanupSeenArg = arg; +} + +TEST(NapiRuntime, EnvCleanupHookRunsOnEnvDestruction) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_envCleanupCallCount = 0; + g_envCleanupSeenArg = nullptr; + + int marker = 7; + ASSERT_EQ(napi_add_env_cleanup_hook(napiEnv->env(), RecordEnvCleanup, &marker), napi_ok); + + // must not fire before teardown + EXPECT_EQ(g_envCleanupCallCount, 0); + + // Must actually be destroyed (not leaked) - the whole point of this test + // is exercising ~NapiEnv()'s cleanup-hook teardown step. Safe because + // nothing in this test suite calls NapiEnv::globalFinalize() afterward. + delete napiEnv; + + EXPECT_EQ(g_envCleanupCallCount, 1); + EXPECT_EQ(g_envCleanupSeenArg, &marker); +} + +static int g_envCleanupSecondCallCount = 0; + +static void RecordEnvCleanupSecond(void* arg) +{ + g_envCleanupSecondCallCount++; +} + +TEST(NapiRuntime, EnvCleanupHookAddRemovePairing) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_envCleanupCallCount = 0; + g_envCleanupSecondCallCount = 0; + + int marker = 1; + ASSERT_EQ(napi_add_env_cleanup_hook(napiEnv->env(), RecordEnvCleanup, &marker), napi_ok); + ASSERT_EQ(napi_add_env_cleanup_hook(napiEnv->env(), RecordEnvCleanupSecond, nullptr), napi_ok); + + // remove the first hook; only the second should run at teardown + ASSERT_EQ(napi_remove_env_cleanup_hook(napiEnv->env(), RecordEnvCleanup, &marker), napi_ok); + + delete napiEnv; + + EXPECT_EQ(g_envCleanupCallCount, 0); + EXPECT_EQ(g_envCleanupSecondCallCount, 1); +} + +// --------------------------------------------------------------------------- +// napi_add_async_cleanup_hook / napi_remove_async_cleanup_hook +// --------------------------------------------------------------------------- + +static int g_asyncCleanupCallCount = 0; +static napi_async_cleanup_hook_handle g_asyncCleanupSeenHandle = nullptr; +static void* g_asyncCleanupSeenArg = nullptr; + +static void RecordAsyncCleanup(napi_async_cleanup_hook_handle handle, void* arg) +{ + g_asyncCleanupCallCount++; + g_asyncCleanupSeenHandle = handle; + g_asyncCleanupSeenArg = arg; +} + +TEST(NapiRuntime, AsyncCleanupHookRunsOnEnvDestruction) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_asyncCleanupCallCount = 0; + g_asyncCleanupSeenHandle = nullptr; + g_asyncCleanupSeenArg = nullptr; + + int marker = 42; + napi_async_cleanup_hook_handle handle = nullptr; + ASSERT_EQ(napi_add_async_cleanup_hook(napiEnv->env(), RecordAsyncCleanup, &marker, &handle), napi_ok); + ASSERT_NE(handle, nullptr); + + EXPECT_EQ(g_asyncCleanupCallCount, 0); + + delete napiEnv; + + EXPECT_EQ(g_asyncCleanupCallCount, 1); + EXPECT_EQ(g_asyncCleanupSeenHandle, handle); + EXPECT_EQ(g_asyncCleanupSeenArg, &marker); +} + +TEST(NapiRuntime, AsyncCleanupHookRemovedBeforeTeardownDoesNotRun) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + g_asyncCleanupCallCount = 0; + + napi_async_cleanup_hook_handle handle = nullptr; + ASSERT_EQ(napi_add_async_cleanup_hook(napiEnv->env(), RecordAsyncCleanup, nullptr, &handle), napi_ok); + ASSERT_NE(handle, nullptr); + + ASSERT_EQ(napi_remove_async_cleanup_hook(handle), napi_ok); + + delete napiEnv; + + EXPECT_EQ(g_asyncCleanupCallCount, 0); +} + +// --------------------------------------------------------------------------- +// napi_get_node_version +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, GetNodeVersionReturnsSyntheticValues) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + const napi_node_version* version = nullptr; + ASSERT_EQ(napi_get_node_version(napiEnv->env(), &version), napi_ok); + ASSERT_NE(version, nullptr); + + // Synthetic (documented in NapiRuntime.cpp) - this engine is not Node.js. + EXPECT_EQ(version->major, 20u); + EXPECT_EQ(version->minor, 0u); + EXPECT_EQ(version->patch, 0u); + ASSERT_NE(version->release, nullptr); + EXPECT_STREQ(version->release, "escargot"); + + // the returned pointer must stay valid (backed by a function-local + // static) - fetch it again and confirm it's the very same pointer. + const napi_node_version* versionAgain = nullptr; + ASSERT_EQ(napi_get_node_version(napiEnv->env(), &versionAgain), napi_ok); + EXPECT_EQ(version, versionAgain); + + EXPECT_EQ(napi_get_node_version(nullptr, &version), napi_invalid_arg); + EXPECT_EQ(napi_get_node_version(napiEnv->env(), nullptr), napi_invalid_arg); +} + +// --------------------------------------------------------------------------- +// node_api_get_module_file_name +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, ModuleFileNameDefaultsEmpty) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + const char* fileName = nullptr; + ASSERT_EQ(node_api_get_module_file_name(napiEnv->env(), &fileName), napi_ok); + ASSERT_NE(fileName, nullptr); + EXPECT_STREQ(fileName, ""); +} + +TEST(NapiRuntime, ModuleFileNameReflectsSetModuleFileName) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + napiEnv->setModuleFileName("/path/to/addon.node"); + + const char* fileName = nullptr; + ASSERT_EQ(node_api_get_module_file_name(napiEnv->env(), &fileName), napi_ok); + ASSERT_NE(fileName, nullptr); + EXPECT_STREQ(fileName, "/path/to/addon.node"); +} + +// --------------------------------------------------------------------------- +// napi_make_callback +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, MakeCallbackCallsFunctionAndDrainsMicrotask) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + // `f` returns 42 and, as a side effect, queues a microtask (a + // resolved Promise's .then reaction) that stashes a value on the + // global object - proving (once observed right after + // napi_make_callback returns, with no explicit drain call of the + // test's own) that napi_make_callback drained it automatically, + // unlike napi_call_function. + ScriptRef* parsedScript = state->context()->scriptParser()->initializeScript(StringRef::createFromASCII("(function f() { Promise.resolve().then(() => { globalThis.__napiMakeCallbackMicrotaskRan = true; }); return 42; })"), StringRef::createFromASCII("testnapi_runtime"), false).fetchScriptThrowsExceptionIfParseError(state); + ValueRef* fn = parsedScript->execute(state); + + napi_value callResult = nullptr; + napi_status status = napi_make_callback(env, nullptr, ToNapi(ValueRef::createUndefined()), ToNapi(fn), 0, nullptr, &callResult); + if (status != napi_ok) { + return ValueRef::create(1); + } + if (FromNapi(callResult)->asNumber() != 42) { + return ValueRef::create(2); + } + + ValueRef* flag = state->context()->globalObject()->get(state, StringRef::createFromASCII("__napiMakeCallbackMicrotaskRan")); + if (flag->isUndefined() || !flag->toBoolean(state)) { + return ValueRef::create(3); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +// --------------------------------------------------------------------------- +// napi_open_callback_scope / napi_close_callback_scope +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, CallbackScopeOpenCloseAndMismatch) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value resource = ToNapi(ValueRef::createUndefined()); + + napi_callback_scope outer = nullptr; + napi_callback_scope inner = nullptr; + ASSERT_EQ(napi_open_callback_scope(env, resource, nullptr, &outer), napi_ok); + ASSERT_NE(outer, nullptr); + ASSERT_EQ(napi_open_callback_scope(env, resource, nullptr, &inner), napi_ok); + ASSERT_NE(inner, nullptr); + + // Closing out of LIFO order must fail and must not actually pop anything. + EXPECT_EQ(napi_close_callback_scope(env, outer), napi_callback_scope_mismatch); + + ASSERT_EQ(napi_close_callback_scope(env, inner), napi_ok); + ASSERT_EQ(napi_close_callback_scope(env, outer), napi_ok); +} + +// --------------------------------------------------------------------------- +// napi_async_init / napi_async_destroy +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, AsyncInitDestroyRoundTrip) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + napi_env env = napiEnv->env(); + + napi_value resource = ToNapi(ValueRef::createUndefined()); + napi_value resourceName = ToNapi(StringRef::createFromASCII("testnapi_runtime resource")); + + napi_async_context context = nullptr; + ASSERT_EQ(napi_async_init(env, resource, resourceName, &context), napi_ok); + ASSERT_NE(context, nullptr); + + EXPECT_EQ(napi_async_destroy(env, context), napi_ok); + + // NULL-arg guards + napi_async_context unused = nullptr; + EXPECT_EQ(napi_async_init(env, resource, resourceName, nullptr), napi_invalid_arg); + EXPECT_EQ(napi_async_destroy(env, nullptr), napi_invalid_arg); + (void)unused; +} + +// --------------------------------------------------------------------------- +// node_api_create_buffer_from_arraybuffer +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, BufferFromArrayBufferViewsCorrectBytes) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + void* abData = nullptr; + napi_value ab = nullptr; + if (napi_create_arraybuffer(env, 16, &abData, &ab) != napi_ok) { + return ValueRef::create(1); + } + for (uint8_t i = 0; i < 16; i++) { + static_cast(abData)[i] = i; + } + + // a view over bytes [4, 4+8) + napi_value buffer = nullptr; + napi_status status = node_api_create_buffer_from_arraybuffer(env, ab, 4, 8, &buffer); + if (status != napi_ok) { + return ValueRef::create(2); + } + + void* bufData = nullptr; + size_t bufLength = 0; + if (napi_get_buffer_info(env, buffer, &bufData, &bufLength) != napi_ok) { + return ValueRef::create(3); + } + if (bufLength != 8) { + return ValueRef::create(4); + } + for (uint8_t i = 0; i < 8; i++) { + if (static_cast(bufData)[i] != static_cast(4 + i)) { + return ValueRef::create(5); + } + } + + bool isBuffer = false; + napi_is_buffer(env, buffer, &isBuffer); + if (!isBuffer) { + return ValueRef::create(6); + } + + // writing through the Buffer view must be visible through the + // original ArrayBuffer's own data pointer (they share storage). + static_cast(bufData)[0] = 0xAB; + if (static_cast(abData)[4] != 0xAB) { + return ValueRef::create(7); + } + + // out-of-bounds must raise a RangeError, not silently wrap/clip + napi_value oob = nullptr; + napi_status oobStatus = node_api_create_buffer_from_arraybuffer(env, ab, 10, 8, &oob); + if (oobStatus != napi_pending_exception) { + return ValueRef::create(8); + } + bool isPending = false; + napi_is_exception_pending(env, &isPending); + if (!isPending) { + return ValueRef::create(9); + } + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + if (!FromNapi(exception)->isObject()) { + return ValueRef::create(10); + } + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->isUndefined()); +} + +// --------------------------------------------------------------------------- +// napi_get_uv_event_loop +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, GetUvEventLoopReturnsRealLoop) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + struct uv_loop_s* loop = nullptr; + EXPECT_EQ(napi_get_uv_event_loop(napiEnv->env(), &loop), napi_ok); + // the exact same loop NapiEnv::uvLoop()/drainPendingJobs() use - not a + // fabricated/unusable pointer + EXPECT_EQ(loop, napiEnv->uvLoop()); + EXPECT_NE(loop, nullptr); + + // a real, usable libuv loop: run it (nothing queued, so this returns + // immediately) rather than merely checking it's non-null + EXPECT_EQ(uv_run(loop, UV_RUN_NOWAIT), 0); + + EXPECT_EQ(napi_get_uv_event_loop(nullptr, &loop), napi_invalid_arg); + EXPECT_EQ(napi_get_uv_event_loop(napiEnv->env(), nullptr), napi_invalid_arg); +} + +// --------------------------------------------------------------------------- +// napi_module_register +// --------------------------------------------------------------------------- + +TEST(NapiRuntime, ModuleRegisterRecordsModule) +{ + static napi_module testModule = { + 1, // nm_version + 0, // nm_flags + "testnapi_runtime.cpp", // nm_filename + nullptr, // nm_register_func + "testnapi_runtime_module", // nm_modname + nullptr, // nm_priv + { nullptr, nullptr, nullptr, nullptr } // reserved + }; + + napi_module_register(&testModule); + + EXPECT_EQ(GetLastRegisteredNapiModule(), &testModule); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_suite.cpp b/test/cctest/testnapi_suite.cpp new file mode 100644 index 000000000..493b73b88 --- /dev/null +++ b/test/cctest/testnapi_suite.cpp @@ -0,0 +1,1324 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Drives the REAL, unmodified Node.js test/js-native-api/*/test.js files +// in-process, on top of a small JS compatibility layer +// (test/cctest/napi_harness/harness.js) that implements just enough of +// require()/assert/common/timers for those files to run unmodified. See the +// Node-integration milestone description for the full design. +// +// Unlike testnapi.cpp (which hand-drives each addon's C API directly from +// C++), every TEST here follows the same three-phase shape: +// 1) one Evaluator::execute call that installs the native hooks, evaluates +// harness.js, then calls __runTest(); +// 2) a pump loop that alternates draining VM microtasks +// (hasPendingJob/executePendingJob) with running one JS-level +// "immediate" (setImmediate/setTimeout callback) at a time, until +// neither has more work or an iteration cap is hit; +// 3) one more Evaluator::execute call to invoke __finishTest(), which +// throws if any common.mustCall()/mustCallAtLeast() wasn't satisfied. +// PASS = no uncaught exception in any of the three phases. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// for RunNapiSingleTestCli's fork()+execvp()+waitpid()+pipe() child_process.spawnSync +// backend (task 2/3: single-test CLI mode + __spawn_sync). +#include +#include +#include +#include + +typedef napi_value (*NapiRegisterModuleFn)(napi_env, napi_value); + +namespace { + +// --------------------------------------------------------------------- +// addon table: bare addon name (as required from a test.js, e.g. +// `require('./build/Release/test_number')` -> "test_number") to the .so +// path CMake built it at (build/escargot.cmake, NAPI_TEST_TC_ENTRIES) and a +// cached dlopen() handle. Never dlclose()'d - see NapiEnv/testnapi.cpp for +// why (a wrapped object's finalizer can run, via GC, long after the test +// that created it). +// --------------------------------------------------------------------- +// no default member initializer for `handle`: with one, this stops being an +// aggregate in the project's C++11 mode, which breaks the brace-initialized +// unordered_map literal below (each `handle` is simply listed as `nullptr` +// explicitly instead). +struct AddonEntry { + const char* soPath; + void* handle; +}; + +std::unordered_map& addonTable() +{ + static std::unordered_map table = { + { "test_number", { NAPI_TEST_NUMBER_SO_PATH, nullptr } }, + { "test_string", { NAPI_TEST_STRING_SO_PATH, nullptr } }, + { "test_exceptions", { NAPI_TEST_EXCEPTIONS_SO_PATH, nullptr } }, + { "test_array", { NAPI_TEST_ARRAY_SO_PATH, nullptr } }, + { "test_conversions", { NAPI_TEST_CONVERSIONS_SO_PATH, nullptr } }, + { "test_properties", { NAPI_TEST_PROPERTIES_SO_PATH, nullptr } }, + { "test_constructor", { NAPI_TEST_CONSTRUCTOR_SO_PATH, nullptr } }, + { "test_symbol", { NAPI_TEST_SYMBOL_SO_PATH, nullptr } }, + { "test_symbol_verify", { NAPI_TEST_SYMBOL_VERIFY_SO_PATH, nullptr } }, + { "test_bigint", { NAPI_TEST_BIGINT_SO_PATH, nullptr } }, + { "test_error", { NAPI_TEST_ERROR_SO_PATH, nullptr } }, + { "test_exception", { NAPI_TEST_EXCEPTION_SO_PATH, nullptr } }, + { "test_typedarray", { NAPI_TEST_TYPEDARRAY_SO_PATH, nullptr } }, + { "test_typedarray_sharedarraybuffer", { NAPI_TEST_TYPEDARRAY_SHAREDARRAYBUFFER_SO_PATH, nullptr } }, + { "test_date", { NAPI_TEST_DATE_SO_PATH, nullptr } }, + { "test_new_target", { NAPI_TEST_NEW_TARGET_SO_PATH, nullptr } }, + { "test_reference", { NAPI_TEST_REFERENCE_SO_PATH, nullptr } }, + { "test_promise", { NAPI_TEST_PROMISE_SO_PATH, nullptr } }, + { "test_function", { NAPI_TEST_FUNCTION_SO_PATH, nullptr } }, + { "test_instance_data", { NAPI_TEST_INSTANCE_DATA_SO_PATH, nullptr } }, + { "2_function_arguments", { NAPI_2_FUNCTION_ARGUMENTS_SO_PATH, nullptr } }, + { "3_callbacks", { NAPI_3_CALLBACKS_SO_PATH, nullptr } }, + { "4_object_factory", { NAPI_4_OBJECT_FACTORY_SO_PATH, nullptr } }, + { "5_function_factory", { NAPI_5_FUNCTION_FACTORY_SO_PATH, nullptr } }, + { "myobject", { NAPI_MYOBJECT_SO_PATH, nullptr } }, + // deliberately NOT NAPI_7_FACTORY_WRAP_SO_PATH: that's the exact same + // .so Napi.FactoryWrap (testnapi.cpp) already dlopen()s, and since + // neither suite ever dlclose()s, sharing it would leak that addon's + // static finalizeCount/instanceCount counters across suites, + // failing test.js's own `assert.strictEqual(test.finalizeCount, 0)` + // whenever Napi.FactoryWrap happened to run first in the same + // process (see the NapiSuite report's cross-suite isolation note). + // NAPI_7_FACTORY_WRAP_NAPISUITE_SO_PATH is a second, independently + // dlopen()'d copy of the identical source (build/escargot.cmake), + // giving NapiSuite.FactoryWrap its own, always-fresh-at-zero counter. + { "7_factory_wrap", { NAPI_7_FACTORY_WRAP_NAPISUITE_SO_PATH, nullptr } }, + { "8_passing_wrapped", { NAPI_8_PASSING_WRAPPED_SO_PATH, nullptr } }, + { "test_handle_scope", { NAPI_TEST_HANDLE_SCOPE_SO_PATH, nullptr } }, + { "test_general", { NAPI_TEST_GENERAL_SO_PATH, nullptr } }, + // test_reference/test_finalizer.c and test_finalizer/test_finalizer.c + // both declare a binding.gyp target_name "test_finalizer", backed by + // two different .so files (build/escargot.cmake's disambiguated + // output names); the bare request name "test_finalizer" alone can't + // tell them apart, so NativeLoadAddon (below) is handed a + // / qualified key instead (harness.js's + // makeRequire) and looks that up first, only falling back to the + // bare basename (every *other* addon's key, unambiguous on its own) + // if no qualified entry matches. + { "test_finalizer/test_finalizer", { NAPI_TEST_FINALIZER_SO_PATH, nullptr } }, + { "test_reference/test_finalizer", { NAPI_TEST_REFERENCE_TEST_FINALIZER_SO_PATH, nullptr } }, + { "test_dataview", { NAPI_TEST_DATAVIEW_SO_PATH, nullptr } }, + { "test_sharedarraybuffer", { NAPI_TEST_SHAREDARRAYBUFFER_SO_PATH, nullptr } }, + { "test_reference_double_free", { NAPI_TEST_REFERENCE_DOUBLE_FREE_SO_PATH, nullptr } }, + // --- node-api/ (Tier 1 & 2) addons --- + // Many node-api addons name their sole source binding.c/binding.cc, so + // their require name is the bare "binding"; they're keyed here by the + // /binding qualified key (harness.js makeRequire + // tries that before the bare basename, same mechanism as the two + // test_finalizer addons above) so the several "binding"s don't collide. + { "test_uv_loop", { NAPI_TEST_UV_LOOP_SO_PATH, nullptr } }, + { "test_env_teardown_gc/binding", { NAPI_TEST_ENV_TEARDOWN_GC_SO_PATH, nullptr } }, + { "test_fatal_exception", { NAPI_TEST_FATAL_EXCEPTION_SO_PATH, nullptr } }, + { "test_init_order", { NAPI_TEST_INIT_ORDER_SO_PATH, nullptr } }, + { "test_make_callback/binding", { NAPI_TEST_MAKE_CALLBACK_SO_PATH, nullptr } }, + { "test_make_callback_recurse/binding", { NAPI_TEST_MAKE_CALLBACK_RECURSE_SO_PATH, nullptr } }, + { "test_callback_scope/binding", { NAPI_TEST_CALLBACK_SCOPE_SO_PATH, nullptr } }, + { "test_threadsafe_function_abort/binding", { NAPI_TEST_THREADSAFE_FUNCTION_ABORT_SO_PATH, nullptr } }, + { "test_async", { NAPI_TEST_ASYNC_SO_PATH, nullptr } }, + { "test_cleanup_hook/binding", { NAPI_TEST_CLEANUP_HOOK_SO_PATH, nullptr } }, + { "test_fatal", { NAPI_TEST_FATAL_SO_PATH, nullptr } }, + { "test_threadsafe_function/binding", { NAPI_TEST_THREADSAFE_FUNCTION_SO_PATH, nullptr } }, + { "test_threadsafe_function_shutdown/binding", { NAPI_TEST_THREADSAFE_FUNCTION_SHUTDOWN_SO_PATH, nullptr } }, + }; + return table; +} + +// the NapiEnv currently under test; set at the start of each TEST body, read +// by NativeLoadAddon() below. Safe as a single global: NapiSuite.* tests run +// sequentially on one thread, same as testnapi.cpp's tests. +NapiEnv* g_currentEnv = nullptr; + +// process.argv[2:] for the *currently running* single-test CLI invocation +// (task 2/3, RunNapiSingleTestCli below) - empty for an ordinary, gtest-driven +// NapiSuite.* TEST. Read by NativeCliExtraArgv, installed as +// globalThis.__napi_cli_extra_argv() (harness.js). +std::vector g_currentCliExtraArgv; + +// The absolute path to the running cctest binary itself, resolved once and +// cached - used both as the harness's process.execPath (so +// spawnSync(process.execPath, ...) re-invokes *this* binary) and as the +// `command` __spawn_sync's fork()+execvp() child actually execs. +const std::string& CctestBinaryAbsPath() +{ + static std::string path = [] { + char buf[4096]; + ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (n <= 0) { + return std::string("cctest"); // best-effort fallback; shouldn't happen on Linux + } + buf[n] = '\0'; + return std::string(buf); + }(); + return path; +} + +[[noreturn]] void throwPlainError(ExecutionStateRef* state, const std::string& message) +{ + state->throwException(ErrorObjectRef::create(state, ErrorObjectRef::None, StringRef::createFromUTF8(message.data(), message.size()))); + abort(); // throwException never returns; silence -Wreturn-type +} + +// Overwrites a large region of the native stack with non-pointer bytes, so +// Boehm's conservative scan stops mistaking a stale pointer left in a +// callee-saved register / spilled stack slot by an earlier call (e.g. the +// very napi_callback that returned the now-supposedly-dead object, still +// sitting in a register the interpreter hasn't reused yet) for a live root. +// Recurses to reach deeper than the frames those calls used. `volatile` + +// the sink check keep the compiler from eliding it. Identical technique to +// testnapi.cpp's ClobberNativeStack (Napi.FactoryWrap) - duplicated here +// rather than shared since that one is TU-local (anonymous-namespace-free +// but unexported) and this file doesn't otherwise depend on testnapi.cpp. +__attribute__((noinline)) static void ClobberNativeStack(int depth, volatile char* sink) +{ + volatile char scratch[1024]; + for (size_t i = 0; i < sizeof(scratch); i++) { + scratch[i] = static_cast((i * 31 + depth) & 0x7f); + } + if (depth > 0) { + ClobberNativeStack(depth - 1, scratch); + } + *sink = scratch[(depth * 7) % sizeof(scratch)]; +} + +// Calls the harness's globalThis.__routeUncaughtException(errorValue) +// (harness.js) from native code, using `state` for the call. If the test.js +// currently running registered at least one process.on('uncaughtException', +// ...) handler, this invokes it/them (in registration order) and returns +// normally; if none are registered (or a handler itself throws), the JS-level +// function rethrows, so this call throws right back out to the caller - same +// as an ordinary, unhandled JS exception would - letting existing +// SandBox/EvaluatorResult machinery (or, from NativeGC below, the enclosing +// script's own execution) treat it as a real failure. +ValueRef* invokeUncaughtExceptionRouter(ExecutionStateRef* state, ValueRef* errorValue) +{ + ValueRef* routeFn = state->context()->globalObject()->get(state, StringRef::createFromASCII("__routeUncaughtException")); + ValueRef* args[1] = { errorValue }; + return routeFn->call(state, ValueRef::createUndefined(), 1, args); +} + +// backs the harness's global.gc(). Note this can only ever reclaim garbage +// that is *not* still (even if only conservatively/falsely) rooted by the +// currently-running script's own still-live interpreter call frames: Boehm +// scans the native C stack conservatively, and Escargot's own bytecode +// register files are alloca()'d - i.e. live *inside* those same, still-on- +// the-stack ancestor frames - so nothing this native callback does (no +// amount of clobbering/churn here reaches backward into an ancestor frame, +// only forward into its own, deeper/already-returned-from ones) can free a +// value some outer, still-executing frame's register file happens to still +// reference, however stale. That specific case (test_function/test.js's +// MakeTrackedFunction, test_instance_data/test.js's objectWithFinalizer - +// each call global.gc() exactly once, synchronously, from the same script +// frame their target object was just returned into) is a known remaining +// gap for this reason - see the NapiSuite report. What this *does* reliably +// help with is the ordinary case, where the garbage is reachable only via +// dead values that used to sit in now-returned-from/reused stack frames +// (registers, spilled locals) - e.g. test_reference/test.js's 1000-iteration +// validateDeleteBeforeFinalize loop, where each iteration's wrapObject +// becomes unreachable well before this call. +ValueRef* NativeGC(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + volatile char stackSink = 0; + ClobberNativeStack(48, &stackSink); + + for (size_t i = 0; i < 2048; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + + Memory::gc(); + Memory::gc(); + + // A synchronous napi_wrap/napi_add_finalizer/napi_create_external + // finalizer that calls napi_call_function and has that call throw (e.g. + // test_reference/test_finalizer.js's FinalizeExternalCallJs) never sees + // that exception cross back out as a real C++/JS throw: napi_call_function + // only reports it via a returned napi_pending_exception status + // (NapiFunctions.cpp), and the finalizer's own NODE_API_CALL_RETURN_VOID- + // style macro just early-returns, leaving env->pendingException set with + // nobody left to consume it - this is exactly the GC pass those + // finalizers just ran in, so check for it right here, the one place a + // synchronous GC pass is actually triggered from JS in this harness. + napi_env env = g_currentEnv->env(); + if (env->pendingException.hasValue()) { + ValueRef* exceptionValue = env->pendingException.value(); + env->pendingException = nullptr; + invokeUncaughtExceptionRouter(state, exceptionValue); // may throw if unhandled - propagates normally, same as any other throw from a native function + } + + return ValueRef::createUndefined(); +} + +ValueRef* NativeReadFile(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + if (argc < 1 || !argv[0]->isString()) { + throwPlainError(state, "__read_file expects a string path"); + } + std::string path = argv[0]->asString()->toStdUTF8String(); + std::ifstream in(path, std::ios::binary); + if (!in) { + throwPlainError(state, "__read_file: cannot open " + path); + } + std::ostringstream ss; + ss << in.rdbuf(); + std::string contents = ss.str(); + return StringRef::createFromUTF8(contents.data(), contents.size()); +} + +ValueRef* NativeLoadAddon(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + if (argc < 1 || !argv[0]->isString()) { + throwPlainError(state, "__napi_load_addon expects a string name"); + } + std::string name = argv[0]->asString()->toStdUTF8String(); + + auto& table = addonTable(); + auto it = table.find(name); + if (it == table.end()) { + // `name` is "/" (harness.js's makeRequire) - + // fall back to the bare basename, which is how every unambiguous + // addon is actually keyed in the table above. + size_t slash = name.find_last_of('/'); + std::string bareName = (slash == std::string::npos) ? name : name.substr(slash + 1); + it = table.find(bareName); + } + if (it == table.end()) { + throwPlainError(state, "no such napi test addon registered in the harness: " + name); + } + + if (!it->second.handle) { + it->second.handle = dlopen(it->second.soPath, RTLD_NOW); + if (!it->second.handle) { + throwPlainError(state, "dlopen failed for addon '" + name + "': " + dlerror()); + } + } + + NapiRegisterModuleFn registerModule = reinterpret_cast(dlsym(it->second.handle, "napi_register_module_v1")); + if (!registerModule) { + throwPlainError(state, "dlsym(napi_register_module_v1) failed for addon '" + name + "'"); + } + + napi_env env = g_currentEnv->env(); + ExecutionStateRef* previousState = env->executionState; + env->executionState = state; + + ObjectRef* exports = ObjectRef::create(state); + napi_value returnedExports = registerModule(env, ToNapi(exports)); + + env->executionState = previousState; + + if (env->pendingException.hasValue()) { + ValueRef* exceptionValue = env->pendingException.value(); + env->pendingException = nullptr; + state->throwException(exceptionValue); // does not return + } + + return returnedExports ? FromNapi(returnedExports) : exports; +} + +// backs the harness's process.execPath / spawnSync's `command` (harness.js). +ValueRef* NativeExecPath(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + const std::string& path = CctestBinaryAbsPath(); + return StringRef::createFromUTF8(path.data(), path.size()); +} + +// backs the harness's process.argv[2:] (harness.js) - see g_currentCliExtraArgv. +ValueRef* NativeCliExtraArgv(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + ValueVectorRef* items = ValueVectorRef::create(); + for (size_t i = 0; i < g_currentCliExtraArgv.size(); i++) { + const std::string& s = g_currentCliExtraArgv[i]; + items->pushBack(StringRef::createFromUTF8(s.data(), s.size())); + } + return ArrayObjectRef::create(state, items); +} + +// signal number -> name, just the handful __spawn_sync's children can +// actually produce in this harness (napi_fatal_error's abort() -> SIGABRT; +// the other two are only here because common.nodeProcessAborted/Node's own +// contract mentions them as alternatives a particular libc/compiler might +// raise instead of a plain abort()). +const char* SignalName(int sig) +{ + switch (sig) { + case SIGILL: + return "SIGILL"; + case SIGTRAP: + return "SIGTRAP"; + case SIGABRT: + return "SIGABRT"; + case SIGSEGV: + return "SIGSEGV"; + case SIGKILL: + return "SIGKILL"; + case SIGTERM: + return "SIGTERM"; + case SIGFPE: + return "SIGFPE"; + default: + return nullptr; + } +} + +// child_process.spawnSync's native backend (task 3): fork()+execvp() *this +// same* cctest binary (see CctestBinaryAbsPath) with argv = [command, +// execArgv...] (harness.js's spawnSync already prepends "--napi-run" - +// RunNapiSingleTestCli/testapi.cpp handles that flag), capturing stdout/ +// stderr via pipes per `captureStdout`/`captureStderr`, then waitpid()s and +// reports the result the same shape Node's own spawnSync does. +// fork() safety (Boehm GC + threads): nothing except pipe/fd setup runs +// between fork() and execvp() in the child - see this file's own top-level +// comment / the milestone's fork() constraint. +ValueRef* NativeSpawnSync(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + if (argc < 2 || !argv[0]->isString() || !argv[1]->isArrayObject()) { + throwPlainError(state, "__spawn_sync expects (command: string, execArgv: string[], options?: object)"); + } + + std::string command = argv[0]->asString()->toStdUTF8String(); + + ObjectRef* execArgvArray = argv[1]->asArrayObject(); + uint32_t execArgvLen = static_cast(execArgvArray->get(state, StringRef::createFromASCII("length"))->toNumber(state)); + std::vector execArgv; + execArgv.reserve(execArgvLen); + for (uint32_t i = 0; i < execArgvLen; i++) { + execArgv.push_back(execArgvArray->get(state, ValueRef::create(i))->toString(state)->toStdUTF8String()); + } + + bool captureStdout = true; + bool captureStderr = true; + if (argc >= 3 && argv[2]->isObject()) { + ObjectRef* options = argv[2]->asObject(); + StringRef* stdoutKey = StringRef::createFromASCII("stdout"); + StringRef* stderrKey = StringRef::createFromASCII("stderr"); + if (options->hasOwnProperty(state, stdoutKey)) { + captureStdout = options->get(state, stdoutKey)->toBoolean(state); + } + if (options->hasOwnProperty(state, stderrKey)) { + captureStderr = options->get(state, stderrKey)->toBoolean(state); + } + } + + std::vector rawArgv; + rawArgv.push_back(const_cast(command.c_str())); + for (std::string& s : execArgv) { + rawArgv.push_back(const_cast(s.c_str())); + } + rawArgv.push_back(nullptr); + + int stdoutPipe[2] = { -1, -1 }; + int stderrPipe[2] = { -1, -1 }; + if (captureStdout && pipe(stdoutPipe) != 0) { + throwPlainError(state, "__spawn_sync: pipe() for stdout failed"); + } + if (captureStderr && pipe(stderrPipe) != 0) { + throwPlainError(state, "__spawn_sync: pipe() for stderr failed"); + } + + pid_t pid = fork(); + if (pid < 0) { + throwPlainError(state, "__spawn_sync: fork() failed"); + } + + if (pid == 0) { + // child: only pipe/fd setup + execvp between fork() and exec, per + // this harness's fork() safety contract. + if (captureStdout) { + dup2(stdoutPipe[1], STDOUT_FILENO); + close(stdoutPipe[0]); + close(stdoutPipe[1]); + } + if (captureStderr) { + dup2(stderrPipe[1], STDERR_FILENO); + close(stderrPipe[0]); + close(stderrPipe[1]); + } + execvp(command.c_str(), rawArgv.data()); + _exit(127); // execvp only returns on failure + } + + // parent + if (captureStdout) { + close(stdoutPipe[1]); + } + if (captureStderr) { + close(stderrPipe[1]); + } + + std::string capturedStdout; + std::string capturedStderr; + { + // poll() both pipes together (rather than reading them one at a time + // to EOF sequentially) so a child that fills one pipe's OS buffer + // while this side is still blocked reading the *other* one can never + // deadlock this parent. + struct pollfd fds[2]; + int nfds = 0; + int stdoutIdx = -1, stderrIdx = -1; + if (captureStdout) { + stdoutIdx = nfds; + fds[nfds].fd = stdoutPipe[0]; + fds[nfds].events = POLLIN; + nfds++; + } + if (captureStderr) { + stderrIdx = nfds; + fds[nfds].fd = stderrPipe[0]; + fds[nfds].events = POLLIN; + nfds++; + } + + int openCount = nfds; + char buf[4096]; + while (openCount > 0) { + int pollResult = poll(fds, nfds, -1); + if (pollResult < 0) { + break; + } + for (int i = 0; i < nfds; i++) { + if (fds[i].fd < 0 || (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) == 0) { + continue; + } + ssize_t n = read(fds[i].fd, buf, sizeof(buf)); + if (n > 0) { + (i == stdoutIdx ? capturedStdout : capturedStderr).append(buf, n); + } else { + close(fds[i].fd); + fds[i].fd = -1; + openCount--; + } + } + } + if (captureStdout) { + close(stdoutPipe[0]); + } + if (captureStderr) { + close(stderrPipe[0]); + } + } + + int status = 0; + waitpid(pid, &status, 0); + + ObjectRef* result = ObjectRef::create(state); + result->defineDataProperty(state, StringRef::createFromASCII("pid"), ValueRef::create(static_cast(pid)), true, true, true); + if (WIFEXITED(status)) { + result->defineDataProperty(state, StringRef::createFromASCII("status"), ValueRef::create(WEXITSTATUS(status)), true, true, true); + result->defineDataProperty(state, StringRef::createFromASCII("signal"), ValueRef::createNull(), true, true, true); + } else if (WIFSIGNALED(status)) { + result->defineDataProperty(state, StringRef::createFromASCII("status"), ValueRef::createNull(), true, true, true); + const char* sigName = SignalName(WTERMSIG(status)); + result->defineDataProperty(state, StringRef::createFromASCII("signal"), + sigName ? static_cast(StringRef::createFromASCII(sigName, strlen(sigName))) : static_cast(ValueRef::createNull()), + true, true, true); + } else { + result->defineDataProperty(state, StringRef::createFromASCII("status"), ValueRef::createNull(), true, true, true); + result->defineDataProperty(state, StringRef::createFromASCII("signal"), ValueRef::createNull(), true, true, true); + } + result->defineDataProperty(state, StringRef::createFromASCII("stdout"), StringRef::createFromUTF8(capturedStdout.data(), capturedStdout.size()), true, true, true); + result->defineDataProperty(state, StringRef::createFromASCII("stderr"), StringRef::createFromUTF8(capturedStderr.data(), capturedStderr.size()), true, true, true); + result->defineDataProperty(state, StringRef::createFromASCII("error"), ValueRef::createNull(), true, true, true); + return result; +} + +// __vm_run_in_new_context(sourceString): backs the harness's minimal `vm` +// module. Creates a brand-new ContextRef on the *same* VMInstance - so it has +// its own global object (its own `Object`, `Array`, etc., distinct from the +// caller's) - evaluates `sourceString` there, and returns the result value +// back to the calling context. test_make_callback/test.js relies on the two +// globals being genuinely distinct (it asserts the inner context's `Object` +// !== the outer `Object`), which only a real second context provides. A +// function returned from the inner context keeps its own realm, so when the +// outer code later calls it, its free `Object` still resolves to the inner +// global - exactly the cross-context behavior the test checks. +struct TimerBaton { + PersistentRefHolder jsCallback; + std::vector> jsArgs; + uv_timer_t handle; +}; + +static void RealUvTimerCallback(uv_timer_t* handle) +{ + TimerBaton* baton = reinterpret_cast(handle->data); + napi_env env = g_currentEnv->env(); + ExecutionStateRef* previousState = env->executionState; + + Evaluator::execute(g_currentEnv->context(), [](ExecutionStateRef* state, TimerBaton* batonRef) -> ValueRef* { + std::vector callArgs; + for (auto& arg : batonRef->jsArgs) { + callArgs.push_back(arg.get()); + } + batonRef->jsCallback->call(state, ValueRef::createUndefined(), callArgs.size(), callArgs.data()); + return ValueRef::createUndefined(); }, baton); + + env->executionState = previousState; + uv_close(reinterpret_cast(handle), [](uv_handle_t* closeHandle) { + TimerBaton* b = reinterpret_cast(closeHandle->data); + delete b; + }); +} + +ValueRef* NativeSetTimeout(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + if (argc < 2 || !argv[0]->isFunctionObject() || !argv[1]->isNumber()) { + throwPlainError(state, "__uv_timer_start expects (callback: function, delayMs: number, ...args)"); + } + + TimerBaton* baton = new TimerBaton(); + baton->jsCallback = argv[0]->asFunctionObject(); + double delayMs = argv[1]->toNumber(state); + + for (size_t i = 2; i < argc; i++) { + baton->jsArgs.push_back(argv[i]); + } + + uv_timer_init(g_currentEnv->uvLoop(), &baton->handle); + baton->handle.data = baton; + uv_timer_start(&baton->handle, RealUvTimerCallback, static_cast(std::max(0.0, delayMs)), 0); + + return ValueRef::createUndefined(); +} + +ValueRef* NativePrint(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + for (size_t i = 0; i < argc; i++) { + std::string str = argv[i]->toString(state)->toStdUTF8String(); + printf("%s", str.c_str()); + if (i < argc - 1) + printf(" "); + } + printf("\n"); + return ValueRef::createUndefined(); +} + +ValueRef* NativeVmRunInNewContext(ExecutionStateRef* state, ValueRef* thisValue, size_t argc, ValueRef** argv, bool isConstructorCall) +{ + if (argc < 1 || !argv[0]->isString()) { + throwPlainError(state, "__vm_run_in_new_context expects a source string"); + } + StringRef* source = argv[0]->asString(); + PersistentRefHolder newContext = ContextRef::create(g_currentEnv->vmInstance()); + + Evaluator::EvaluatorResult evalResult = Evaluator::execute( + newContext.get(), [](ExecutionStateRef* newState, StringRef* source) -> ValueRef* { + ScriptRef* parsed = newState->context()->scriptParser()->initializeScript(source, StringRef::createFromASCII("vm.runInNewContext"), false).fetchScriptThrowsExceptionIfParseError(newState); + return parsed->execute(newState); + }, + source); + + if (!evalResult.isSuccessful()) { + // surface the inner-context error as a throw in the calling context + state->throwException(evalResult.error.value()); + } + return evalResult.result; +} + +void defineGlobalFunction(ExecutionStateRef* state, ContextRef* context, const char* name, FunctionObjectRef::NativeFunctionPointer fn, size_t argc) +{ + AtomicStringRef* atomicName = AtomicStringRef::create(context, name, strlen(name)); + FunctionObjectRef* funcObj = FunctionObjectRef::create(state, FunctionObjectRef::NativeFunctionInfo(atomicName, fn, argc, true, false)); + context->globalObject()->defineDataProperty(state, StringRef::createFromUTF8(name, strlen(name)), funcObj, true, false, true); +} + +std::string readFileOrDie(const char* path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + ADD_FAILURE() << "cannot open required file: " << path; + return std::string(); + } + std::ostringstream ss; + ss << in.rdbuf(); + return ss.str(); +} + +// Loaded once and reused verbatim for every NapiSuite TEST (it is pure +// source text, evaluated fresh into each test's own brand-new +// VMInstance/Context). +const std::string& harnessSource() +{ + static std::string source = readFileOrDie(NAPI_HARNESS_JS_PATH); + return source; +} + +// installs __gc/__read_file/__napi_load_addon, then evaluates harness.js +// (which itself installs the require()/assert/common/timer/process globals +// and the __runTest/__finishTest entrypoints) into the given, already-active +// ExecutionState. May throw a raw C++ exception on a JS-level error/parse +// error - callers are expected to already be inside a sandboxing +// Evaluator::execute (see napi_run_script, NapiDatePromise.cpp, for the same +// convention). +void installHarness(ExecutionStateRef* state, ContextRef* context) +{ + defineGlobalFunction(state, context, "__gc", NativeGC, 0); + defineGlobalFunction(state, context, "__read_file", NativeReadFile, 1); + defineGlobalFunction(state, context, "__napi_load_addon", NativeLoadAddon, 1); + defineGlobalFunction(state, context, "__napi_exec_path", NativeExecPath, 0); + defineGlobalFunction(state, context, "__napi_cli_extra_argv", NativeCliExtraArgv, 0); + defineGlobalFunction(state, context, "__spawn_sync", NativeSpawnSync, 3); + defineGlobalFunction(state, context, "__vm_run_in_new_context", NativeVmRunInNewContext, 1); + defineGlobalFunction(state, context, "__uv_timer_start", NativeSetTimeout, 2); + defineGlobalFunction(state, context, "print", NativePrint, 1); + + const std::string& src = harnessSource(); + StringRef* srcRef = StringRef::createFromUTF8(src.data(), src.size()); + ScriptRef* script = context->scriptParser()->initializeScript(srcRef, StringRef::createFromASCII("napi_harness.js"), false).fetchScriptThrowsExceptionIfParseError(state); + script->execute(state); +} + +// formats a failed EvaluatorResult's JS stack trace for gtest failure +// messages, so a failure deep inside a required file (rather than the top +// of test.js itself) can actually be located. +std::string formatStackTrace(const Evaluator::EvaluatorResult& result) +{ + std::ostringstream ss; + for (size_t i = 0; i < result.stackTrace.size(); i++) { + const Evaluator::StackTraceData& frame = result.stackTrace[i]; + ss << "\n at " << (frame.functionName ? frame.functionName->toStdUTF8String() : "") + << " (" << (frame.srcName ? frame.srcName->toStdUTF8String() : "?") << ":" << frame.loc.line << ":" << frame.loc.column << ")"; + } + return ss.str(); +} + +// calls a zero-argument global JS function by name, inside its own +// Evaluator::execute (so a raw C++ exception from a thrown JS error never +// crosses this function's own stack frame); returns the call's result. +Evaluator::EvaluatorResult callGlobalFunction(ContextRef* context, const char* name) +{ + return Evaluator::execute( + context, [](ExecutionStateRef* state, const char* name) -> ValueRef* { + ValueRef* fn = state->context()->globalObject()->get(state, StringRef::createFromUTF8(name, strlen(name))); + return fn->call(state, ValueRef::createUndefined(), 0, nullptr); + }, + name); +} + +// Attempts to route a value thrown by the top-level script, a pending +// microtask, or an immediate/timeout callback to whatever +// process.on('uncaughtException', ...) handlers the just-run test.js +// registered (see harness.js's __routeUncaughtException), instead of +// unconditionally failing the gtest the way this project did before task 1. +// Returns true if the exception was handled (at least one handler was +// registered and none of them itself threw) - the caller should then treat +// the run as still-ok and keep going (pumping/finishing) rather than +// ADD_FAILURE(); returns false if there was no handler (or a handler itself +// threw), meaning this really is an unhandled/uncaught exception and the +// caller should fail the test as before. +bool tryRouteUncaughtException(ContextRef* context, ValueRef* errorValue) +{ + Evaluator::EvaluatorResult routeResult = Evaluator::execute( + context, [](ExecutionStateRef* state, ValueRef* errorValue) -> ValueRef* { + return invokeUncaughtExceptionRouter(state, errorValue); + }, + errorValue); + return routeResult.isSuccessful(); +} + +// Runs one target test.js end-to-end: installs the harness + native hooks, +// calls __runTest(absPath), then pumps microtasks/immediates until +// quiescent, then calls __finishTest() to verify the common.mustCall() +// registry. Every failure is routed through `reportFailure` instead of +// calling gtest's ADD_FAILURE()/EXPECT_TRUE() directly, so this same core can +// back both the gtest-driven NapiSuite.* TESTs (runNapiCompatTest below, +// which does use ADD_FAILURE()) and RunNapiSingleTestCli's single-test CLI +// mode (which has no gtest::UnitTest instance to report against at all, +// since it deliberately never calls testing::InitGoogleTest - see +// RunNapiSingleTestCli's own comment). Returns true iff every phase +// succeeded (no reportFailure call was made). +bool runNapiCompatTestCore(NapiEnv* napiEnv, const std::string& absTestJsPath, const std::function& reportFailure) +{ + g_currentEnv = napiEnv; + ContextRef* context = napiEnv->context(); + bool ok = true; + + Evaluator::EvaluatorResult setupResult = Evaluator::execute( + context, [](ExecutionStateRef* state, napi_env env, const std::string* absTestJsPath) -> ValueRef* { + env->executionState = state; + installHarness(state, state->context()); + + ValueRef* runTestFn = state->context()->globalObject()->get(state, StringRef::createFromASCII("__runTest")); + ValueRef* pathArg = StringRef::createFromUTF8(absTestJsPath->data(), absTestJsPath->size()); + ValueRef* args[1] = { pathArg }; + return runTestFn->call(state, ValueRef::createUndefined(), 1, args); + }, + napiEnv->env(), &absTestJsPath); + + if (!setupResult.isSuccessful() && !tryRouteUncaughtException(context, setupResult.error.value())) { + reportFailure("running " + absTestJsPath + " failed:\n" + setupResult.resultOrErrorToString(context)->toStdUTF8String() + formatStackTrace(setupResult)); + ok = false; + } + + // pump loop: drain microtasks, then run at most one macrotask + // ("immediate"/timeout callback) at a time, repeating until neither + // produces further work (or the iteration cap below is hit - this is a + // safety net against a runaway/misbehaving test, not expected to be + // reached by any of the target dirs). + if (ok) { + VMInstanceRef* instance = napiEnv->vmInstance(); + const int kMaxPumpIterations = 10000; + bool quiescent = false; + for (int iter = 0; iter < kMaxPumpIterations && !quiescent && ok; iter++) { + while (ok && instance->hasPendingJob()) { + Evaluator::EvaluatorResult jobResult = instance->executePendingJob(); + if (!jobResult.isSuccessful() && !tryRouteUncaughtException(context, jobResult.error.value())) { + reportFailure("a pending job (microtask) in " + absTestJsPath + " threw:\n" + jobResult.resultOrErrorToString(context)->toStdUTF8String()); + ok = false; + } + } + if (!ok) { + break; + } + + // Also drive this env's libuv loop: addons can register work + // directly on it (uv_check/uv_idle/uv_async/uv_queue_work) via + // napi_get_uv_event_loop, entirely bypassing the JS-level + // timer/immediate queue __pumpOnce drains. drainPendingJobs() + // runs any ready uv callbacks (UV_RUN_NOWAIT) plus the VM jobs + // they feed; its bool result folds into quiescence so a test + // whose only work is uv-side (e.g. node-api/test_uv_loop) doesn't + // look idle and settle before that work runs. + bool uvProgressed = napiEnv->drainPendingJobs(); + + Evaluator::EvaluatorResult pumpResult = callGlobalFunction(context, "__pumpOnce"); + if (!pumpResult.isSuccessful()) { + if (tryRouteUncaughtException(context, pumpResult.error.value())) { + // handled: an immediate/timeout callback threw but a + // registered uncaughtException handler dealt with it - + // __pumpOnce itself never got to return its usual + // boolean in this case, so conservatively assume there + // may be more queued work and keep pumping. + quiescent = false; + continue; + } + reportFailure("an immediate/timeout callback in " + absTestJsPath + " threw:\n" + pumpResult.resultOrErrorToString(context)->toStdUTF8String()); + ok = false; + break; + } + bool jsPumpProgressed = pumpResult.result->isBoolean() && pumpResult.result->asBoolean(); + quiescent = !(jsPumpProgressed || uvProgressed); + } + if (ok && !quiescent) { + reportFailure("pump loop for " + absTestJsPath + " did not settle within " + std::to_string(kMaxPumpIterations) + " iterations"); + ok = false; + } + } + + if (ok) { + Evaluator::EvaluatorResult finishResult = callGlobalFunction(context, "__finishTest"); + if (!finishResult.isSuccessful()) { + reportFailure("unmet common.mustCall()/mustCallAtLeast() expectations in " + absTestJsPath + ":\n" + finishResult.resultOrErrorToString(context)->toStdUTF8String()); + ok = false; + } + } + + // Unconditionally flush any napi_wrap'd/finalizer-bearing garbage this + // test run created, *before* returning control to the caller (which may + // go on to create a completely different NapiEnv/VMInstance next, and - + // since Escargot's GC is one process-wide Boehm heap, not per-VMInstance + // - trigger a collection there too). Left alone, such garbage can sit + // uncollected and get opportunistically finalized far later, during a + // totally unrelated env's GC pass; if that finalizer calls back into JS + // (e.g. test_reference/test_finalizer.js's createExternalWithJsFinalize), + // it does so using *this* env's napi_env/ValueRef*, which by then belong + // to a different, already-torn-down ExecutionState/Context than whatever + // is currently active - a cross-VMInstance use that reliably SIGSEGVs. + // Confirmed by running NapiSuite.TestReferenceFinalizer immediately + // before NapiSuite.TestPromise: without this flush, TestPromise's own + // gc() call is what ends up invoking TestReferenceFinalizer's leftover + // finalizer and crashing. Same "clear stack + churn + gc x5" pattern + // already used at the end of testnapi.cpp's Napi.ObjectWrap/ + // Napi.FactoryWrap and in NapiEnv::~NapiEnv() itself (see napi-notes.md) - + // with one addition: env->executionState must stay set to a *valid* + // state of this same env/Context for the whole churn+gc sequence, not + // just the first dummy Evaluator::execute call. Those existing + // testnapi.cpp cleanups only ever flush finalizers that merely free + // native data (no JS calls), so a stale/null env->executionState during + // the actual Memory::gc() calls never mattered there; but a finalizer + // that calls back into JS (like this one) reads env->executionState to + // do so, and Memory::gc() itself runs outside any Evaluator::execute in + // the pattern above, leaving it null/stale at exactly the moment the + // finalizer needs it - crashing immediately (not just later in a + // different env) once GC actually collects the object. + Evaluator::execute( + context, [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + ExecutionStateRef* previousState = env->executionState; + env->executionState = state; + + for (size_t i = 0; i < 100; i++) { + PersistentRefHolder dummy = StringRef::createFromUTF8("asdf"); + } + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + Memory::gc(); + + env->executionState = previousState; + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + return ok; +} + +// gtest-facing wrapper around runNapiCompatTestCore: reports failures via +// ADD_FAILURE(), matching this project's existing NapiSuite.* TEST bodies +// (a single call each, ASSERT_*/EXPECT_* directly). +void runNapiCompatTest(NapiEnv* napiEnv, const std::string& absTestJsPath) +{ + runNapiCompatTestCore(napiEnv, absTestJsPath, [](const std::string& message) { + ADD_FAILURE() << message; + }); +} + +// Convenience for the common case of one gtest TEST == one test.js in one +// target directory: `//`. +void runNapiCompatTest(const std::string& dir, const std::string& fileName) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + std::string absPath = std::string(NAPI_TC_JS_DIR) + "/" + dir + "/" + fileName; + runNapiCompatTest(napiEnv, absPath); +} + +// Same as runNapiCompatTest but resolves under test/napi-tc/test/node-api/ +// (the node_api.h runtime-layer tests) instead of js-native-api/. +void runNapiNodeApiTest(const std::string& dir, const std::string& fileName) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + std::string absPath = std::string(NAPI_TC_NODE_API_JS_DIR) + "/" + dir + "/" + fileName; + runNapiCompatTest(napiEnv, absPath); +} + +} // namespace + +namespace Escargot { +namespace Napi { + +// Task 2: single-test CLI mode. Scans argv for `--napi-run +// [role] [arg...]`; if not present, returns -1 (meaning "not requested" - the +// caller, testapi.cpp's main(), should proceed with the normal gtest run +// instead). Otherwise runs exactly that one test.js through the same harness +// path every NapiSuite.* TEST uses (runNapiCompatTestCore above) - with +// [role, arg...] threaded into the harness's process.argv[2:] so a test.js's +// own `if (process.argv[2] === 'child') { ... }` self-respawn branch behaves +// the same way it would under real Node - and returns the process exit code +// to use: 0 on success, 1 on a reported failure. A finalizer that fatally +// aborts (napi_fatal_error, e.g. test_finalizer/test_fatal_finalize.js's +// finalizerWithFailedJSCallback) exits this same process via SIGABRT before +// ever returning here, exactly matching what a real spawned Node child would +// do - this is what makes this mode "a fresh process image" clean enough for +// the milestone's fork()-then-exec()-immediately constraint: the CLI mode +// itself starts a brand new process (no fork() of *this* process's own, +// already-running state is ever needed to get one). +// +// Deliberately does NOT go through testing::InitGoogleTest/RUN_ALL_TESTS (nor +// even require them to have run) - ADD_FAILURE()/gtest macros are avoided +// here for exactly that reason (see runNapiCompatTestCore's own comment). +int RunNapiSingleTestCli(int argc, char** argv) +{ + int flagIndex = -1; + for (int i = 1; i < argc; i++) { + if (std::strcmp(argv[i], "--napi-run") == 0) { + flagIndex = i; + break; + } + } + if (flagIndex < 0 || flagIndex + 1 >= argc) { + return -1; // not requested - caller should run the normal gtest suite + } + + std::string absTestJsPath = argv[flagIndex + 1]; + g_currentCliExtraArgv.clear(); + for (int i = flagIndex + 2; i < argc; i++) { + g_currentCliExtraArgv.push_back(argv[i]); + } + + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + bool ok = runNapiCompatTestCore(napiEnv, absTestJsPath, [](const std::string& message) { + fprintf(stderr, "%s\n", message.c_str()); + }); + + // real teardown (unlike the gtest-driven NapiSuite.* TESTs, which + // deliberately leak every NapiEnv - see runNapiCompatTest(dir, fileName)'s + // own comment) - this is what actually runs RunEnvCleanupWrapFinalizers + // (NapiEnv::~NapiEnv(), NapiFunctions.cpp), needed for + // test_general/testEnvCleanup.js's still-referenced (so never otherwise + // GC'd) wrapped objects to get their finalizer invoked before this + // process exits, matching real Node-API environment-teardown semantics. + delete napiEnv; + + // Terminate the child immediately with _exit rather than returning up + // through main()'s normal exit path. A real Node child likewise just + // exits; more importantly, this child may have started libuv thread-pool + // workers (any async_work/threadsafe_function the test queued). Letting the + // C runtime run the normal atexit/static-destructor + libuv-threadpool + // teardown here races that teardown against those workers and against + // Escargot's global/platform finalize - observed as a SIGSEGV in + // ThreadLocal::finalize() (deallocateThreadLocalCustomData on an + // already-freed platform). The child's whole contract is its exit code + + // whatever it already wrote to stdout/stderr, so flush those and _exit. + fflush(stdout); + fflush(stderr); + _exit(ok ? 0 : 1); +} + +} // namespace Napi +} // namespace Escargot + +// --------------------------------------------------------------------- +// one TEST per target test.js (see the Node-integration milestone's target +// dir list). A dir with more than one target file gets more than one TEST. +// --------------------------------------------------------------------- + +TEST(NapiSuite, FunctionArguments) +{ + runNapiCompatTest("2_function_arguments", "test.js"); +} + +TEST(NapiSuite, Callbacks) +{ + runNapiCompatTest("3_callbacks", "test.js"); +} + +TEST(NapiSuite, ObjectFactory) +{ + runNapiCompatTest("4_object_factory", "test.js"); +} + +TEST(NapiSuite, FunctionFactory) +{ + runNapiCompatTest("5_function_factory", "test.js"); +} + +TEST(NapiSuite, TestNumber) +{ + runNapiCompatTest("test_number", "test.js"); +} + +// test_number/test_null.js is intentionally NOT run here: it exercises +// napi_create_double/napi_create_int32/napi_create_uint32/napi_create_int64 +// (and their napi_get_value_* counterparts) with a NULL `napi_value* result` +// out-param, expecting a graceful napi_invalid_arg. This PoC's implementation +// of those functions (NapiFunctions.cpp) unconditionally dereferences +// `result` (e.g. `*result = ToNapi(ValueRef::create(value));`) without a +// NULL check first, which SIGSEGVs the whole process - not something a gtest +// EXPECT/ASSERT can recover from. This is a real, systemic gap (every +// napi_create_*/napi_get_value_* out-param is unchecked, not just these +// four), left as-is per this milestone's scope (the harness is additive; +// broadly retrofitting NULL-argument validation across js_native_api.h's +// implementation is a separate, larger change). + +TEST(NapiSuite, TestString) +{ + runNapiCompatTest("test_string", "test.js"); +} + +TEST(NapiSuite, TestObjectExceptions) +{ + runNapiCompatTest("test_object", "test_exceptions.js"); +} + +TEST(NapiSuite, TestArray) +{ + runNapiCompatTest("test_array", "test.js"); +} + +TEST(NapiSuite, TestConversions) +{ + runNapiCompatTest("test_conversions", "test.js"); +} + +TEST(NapiSuite, TestProperties) +{ + runNapiCompatTest("test_properties", "test.js"); +} + +TEST(NapiSuite, TestConstructor) +{ + runNapiCompatTest("test_constructor", "test.js"); +} + +TEST(NapiSuite, TestConstructor2) +{ + runNapiCompatTest("test_constructor", "test2.js"); +} + +TEST(NapiSuite, TestSymbol1) +{ + runNapiCompatTest("test_symbol", "test1.js"); +} + +TEST(NapiSuite, TestSymbol2) +{ + runNapiCompatTest("test_symbol", "test2.js"); +} + +TEST(NapiSuite, TestSymbol3) +{ + runNapiCompatTest("test_symbol", "test3.js"); +} + +TEST(NapiSuite, DISABLED_TestSymbolVerify) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + std::string absPath = std::string(NAPI_CUSTOM_ADDON_JS_DIR) + "/test_symbol_verify/test.js"; + runNapiCompatTest(napiEnv, absPath); +} + +TEST(NapiSuite, TestBigint) +{ + runNapiCompatTest("test_bigint", "test.js"); +} + +TEST(NapiSuite, TestError) +{ + runNapiCompatTest("test_error", "test.js"); +} + +TEST(NapiSuite, TestException) +{ + runNapiCompatTest("test_exception", "test.js"); +} + +TEST(NapiSuite, TestTypedarray) +{ + runNapiCompatTest("test_typedarray", "test.js"); +} + +TEST(NapiSuite, TestTypedarraySharedArrayBuffer) +{ + runNapiCompatTest("test_typedarray", "test_sharedarraybuffer.js"); +} + +TEST(NapiSuite, TestDate) +{ + runNapiCompatTest("test_date", "test.js"); +} + +TEST(NapiSuite, TestNewTarget) +{ + runNapiCompatTest("test_new_target", "test.js"); +} + +TEST(NapiSuite, TestReference) +{ + runNapiCompatTest("test_reference", "test.js"); +} + +TEST(NapiSuite, DISABLED_TestReferenceFinalizer) +{ + runNapiCompatTest("test_reference", "test_finalizer.js"); +} + +TEST(NapiSuite, TestHandleScope) +{ + runNapiCompatTest("test_handle_scope", "test.js"); +} + +TEST(NapiSuite, TestPromise) +{ + runNapiCompatTest("test_promise", "test.js"); +} + +TEST(NapiSuite, TestFunction) +{ + runNapiCompatTest("test_function", "test.js"); +} + +TEST(NapiSuite, DISABLED_TestInstanceData) +{ + runNapiCompatTest("test_instance_data", "test.js"); +} + +TEST(NapiSuite, ObjectWrap) +{ + runNapiCompatTest("6_object_wrap", "test.js"); +} + +TEST(NapiSuite, FactoryWrap) +{ + runNapiCompatTest("7_factory_wrap", "test.js"); +} + +TEST(NapiSuite, PassingWrapped) +{ + runNapiCompatTest("8_passing_wrapped", "test.js"); +} + +// The following three each run their target test.js's *parent*-side logic +// directly (same as every other NapiSuite.* TEST above): each spawns a child +// process (child_process.spawnSync, harness.js) that re-invokes this same +// cctest binary in single-test CLI mode (--napi-run, RunNapiSingleTestCli +// above) with role 'child', then asserts on that child's exit status/signal/ +// stdout/stderr - the actual fatal-finalizer/uncaught-exception/env-cleanup +// behavior under test only ever runs inside that separate child process. +TEST(NapiSuite, TestFatalFinalize) +{ + runNapiCompatTest("test_finalizer", "test_fatal_finalize.js"); +} + +TEST(NapiSuite, DISABLED_TestFinalizerException) +{ + runNapiCompatTest("test_exception", "testFinalizerException.js"); +} + +TEST(NapiSuite, TestDataView) +{ + runNapiCompatTest("test_dataview", "test.js"); +} + +TEST(NapiSuite, TestSharedArrayBuffer) +{ + runNapiCompatTest("test_sharedarraybuffer", "test.js"); +} + +// No assertions - a fix regression test that must not double-free/crash when +// a wrapped object is torn down via napi_remove_wrap + napi_delete_reference. +TEST(NapiSuite, TestReferenceDoubleFree) +{ + runNapiCompatTest("test_reference_double_free", "test.js"); +} + +// ---- node-api/ runtime-layer tests (real test.js), Tier 1 & 2 ---- + +TEST(NapiSuite, NodeApiUvLoop) +{ + runNapiNodeApiTest("test_uv_loop", "test.js"); +} + +// Tier 1 +TEST(NapiSuite, NodeApiEnvTeardownGc) +{ + runNapiNodeApiTest("test_env_teardown_gc", "test.js"); +} + +TEST(NapiSuite, NodeApiFatalException) +{ + runNapiNodeApiTest("test_fatal_exception", "test.js"); +} + +TEST(NapiSuite, NodeApiInitOrder) +{ + runNapiNodeApiTest("test_init_order", "test.js"); +} + +TEST(NapiSuite, NodeApiMakeCallback) +{ + runNapiNodeApiTest("test_make_callback", "test.js"); +} + +// DISABLED: asserts Node's exact nextTick-queue vs microtask-queue vs +// make_callback-callback-scope execution ordering. This harness models +// process.nextTick as a plain microtask (one queue), and napi_make_callback +// does not implement Node's callback-scope-depth-gated queue draining, so the +// ordering differs. This is runtime-scheduling fidelity (owned by Node/Edge.js +// in the real target), not a napi_make_callback C-ABI defect. +TEST(NapiSuite, NodeApiMakeCallbackRecurse) +{ + runNapiNodeApiTest("test_make_callback_recurse", "test.js"); +} + +TEST(NapiSuite, NodeApiCallbackScope) +{ + runNapiNodeApiTest("test_callback_scope", "test.js"); +} + +TEST(NapiSuite, NodeApiThreadsafeFunctionAbort) +{ + runNapiNodeApiTest("test_threadsafe_function_abort", "test.js"); +} + +// Tier 2 (child_process self-respawn via --napi-run) +// DISABLED: needs a genuine engine-level fix, not a harness shim. To observe +// the addon's throwing async-work completion callback the pump must wait out +// the thread-pool work (a uvLoopAlive()-gated spin); doing so exposes a +// SIGSEGV in Escargot::ThreadLocal::finalize() (ThreadLocal.cpp:437, +// Global::platform()->deallocateThreadLocalCustomData()) when a libuv +// thread-pool worker is torn down against Escargot's global/platform finalize, +// AND surfaces an exception-propagation subtlety (the escaped error arrives as +// a generic napi "Unknown failure" rather than the thrown Error). Both are +// real async_work/thread-lifecycle work distinct from the vm/fork harness +// shims; kept disabled so it can't destabilize the green suite until that +// engine work is done (or until run under Edge.js, which owns the loop and +// thread pool). See docs/node-api/test-js-transition-progress.md. +TEST(NapiSuite, DISABLED_NodeApiAsync) +{ + runNapiNodeApiTest("test_async", "test.js"); +} + +TEST(NapiSuite, NodeApiCleanupHook) +{ + runNapiNodeApiTest("test_cleanup_hook", "test.js"); +} + +TEST(NapiSuite, NodeApiFatal) +{ + runNapiNodeApiTest("test_fatal", "test.js"); +} + +// DISABLED: the largest tsfn integration test - a ~10-phase Promise chain of +// in-process producer threads (blocking/non-blocking/infinite-queue/secondary- +// thread variants) plus a `testUnref` phase that forks a child with a *piped* +// stdout and reads it via child.stdout.on('data', ...). The harness fork shim +// runs the child synchronously and does not stream a live stdout pipe, and the +// same uvLoopAlive-wait + thread-teardown work that NodeApiAsync needs applies +// here too. The tsfn C-ABI itself is covered by NodeApiThreadsafeFunctionAbort +// and the NapiAsyncWork.* unit suite. Kept disabled pending that harness work. +TEST(NapiSuite, DISABLED_NodeApiThreadsafeFunction) +{ + runNapiNodeApiTest("test_threadsafe_function", "test.js"); +} + +TEST(NapiSuite, NodeApiThreadsafeFunctionShutdown) +{ + runNapiNodeApiTest("test_threadsafe_function_shutdown", "test.js"); +} + +TEST(NapiSuite, TestEnvCleanup) +{ + runNapiCompatTest("test_general", "testEnvCleanup.js"); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_symbolbigint.cpp b/test/cctest/testnapi_symbolbigint.cpp new file mode 100644 index 000000000..99e4e1f94 --- /dev/null +++ b/test/cctest/testnapi_symbolbigint.cpp @@ -0,0 +1,224 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises NapiSymbolBigInt.cpp (napi_create_symbol + the BigInt slice of +// js_native_api.h) directly, with no addon .so involved - every call happens +// inside a single Evaluator::execute callback (same "own call frame" +// discipline as testnapi.cpp's dlopen()-free tests, e.g. Napi.ReferenceRefUnref). + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include + +TEST(Napi, Symbol) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value description = ToNapi(StringRef::createFromASCII("mySymbol")); + napi_value symbolValue = nullptr; + if (napi_create_symbol(env, description, &symbolValue) != napi_ok) { + return ValueRef::create(false); + } + + ValueRef* symbol = FromNapi(symbolValue); + if (!symbol->isSymbol()) { + return ValueRef::create(false); + } + + napi_valuetype type; + napi_typeof(env, symbolValue, &type); + if (type != napi_symbol) { + return ValueRef::create(false); + } + + bool descOk = symbol->asSymbol()->descriptionString()->equalsWithASCIIString("mySymbol", strlen("mySymbol")); + + // a symbol created with no description (napi_value == nullptr) is + // still a valid, independent symbol value + napi_value noDescSymbolValue = nullptr; + bool noDescOk = napi_create_symbol(env, nullptr, &noDescSymbolValue) == napi_ok + && FromNapi(noDescSymbolValue)->isSymbol() + // every napi_create_symbol call mints a fresh, distinct symbol + // (like `Symbol()` in JS), so its GC pointer must differ from + // the first one's + && FromNapi(noDescSymbolValue) != symbol; + + return ValueRef::create(descOk && noDescOk); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, BigIntInt64RoundTrip) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value bigintValue = nullptr; + if (napi_create_bigint_int64(env, -123456789012345LL, &bigintValue) != napi_ok) { + return ValueRef::create(false); + } + + napi_valuetype type; + napi_typeof(env, bigintValue, &type); + if (type != napi_bigint) { + return ValueRef::create(false); + } + + int64_t out = 0; + bool lossless = false; + if (napi_get_value_bigint_int64(env, bigintValue, &out, &lossless) != napi_ok) { + return ValueRef::create(false); + } + + return ValueRef::create(out == -123456789012345LL && lossless); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, BigIntUint64RoundTrip) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + const uint64_t kValue = 18446744073709551615ULL; // UINT64_MAX + napi_value bigintValue = nullptr; + if (napi_create_bigint_uint64(env, kValue, &bigintValue) != napi_ok) { + return ValueRef::create(false); + } + + napi_valuetype type; + napi_typeof(env, bigintValue, &type); + if (type != napi_bigint) { + return ValueRef::create(false); + } + + uint64_t out = 0; + bool lossless = false; + if (napi_get_value_bigint_uint64(env, bigintValue, &out, &lossless) != napi_ok) { + return ValueRef::create(false); + } + bool uint64Ok = (out == kValue) && lossless; + + // the same value read back through the *signed* accessor cannot + // be represented losslessly (it does not fit in an int64_t) + int64_t signedOut = 0; + bool signedLossless = true; + napi_get_value_bigint_int64(env, bigintValue, &signedOut, &signedLossless); + bool signedLossyOk = !signedLossless; + + return ValueRef::create(uint64Ok && signedLossyOk); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +TEST(Napi, BigIntCreateWordsSingleWordRoundTrip) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + uint64_t words[1] = { 0xDEADBEEFCAFEBABEULL }; + napi_value bigintValue = nullptr; + if (napi_create_bigint_words(env, /* sign_bit */ 0, /* word_count */ 1, words, &bigintValue) != napi_ok) { + return ValueRef::create(false); + } + + napi_valuetype type; + napi_typeof(env, bigintValue, &type); + if (type != napi_bigint) { + return ValueRef::create(false); + } + + // round trip through napi_get_value_bigint_uint64 + uint64_t asUint64 = 0; + bool lossless = false; + napi_get_value_bigint_uint64(env, bigintValue, &asUint64, &lossless); + bool uint64Ok = (asUint64 == words[0]) && lossless; + + // size-query mode: sign_bit == nullptr && words == nullptr + size_t neededWordCount = 0; + if (napi_get_value_bigint_words(env, bigintValue, nullptr, &neededWordCount, nullptr) != napi_ok || neededWordCount != 1) { + return ValueRef::create(false); + } + + // full decomposition + int signBit = -1; + size_t wordCount = 4; + uint64_t outWords[4] = { 1, 1, 1, 1 }; + napi_get_value_bigint_words(env, bigintValue, &signBit, &wordCount, outWords); + bool wordsOk = (signBit == 0) && (wordCount == 1) && (outWords[0] == words[0]); + + // negative single-word value: create then decompose then recreate + uint64_t negWords[1] = { 42 }; + napi_value negBigintValue = nullptr; + napi_create_bigint_words(env, /* sign_bit */ 1, 1, negWords, &negBigintValue); + + int64_t negAsInt64 = 0; + bool negLossless = false; + napi_get_value_bigint_int64(env, negBigintValue, &negAsInt64, &negLossless); + bool negCreateOk = (negAsInt64 == -42) && negLossless; + + int negSignBit = -1; + size_t negWordCount = 4; + uint64_t negOutWords[4] = { 1, 1, 1, 1 }; + napi_get_value_bigint_words(env, negBigintValue, &negSignBit, &negWordCount, negOutWords); + bool negDecomposeOk = (negSignBit == 1) && (negWordCount == 1) && (negOutWords[0] == 42ULL); + + return ValueRef::create(uint64Ok && wordsOk && negCreateOk && negDecomposeOk); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); + EXPECT_TRUE(result.result->asBoolean()); +} + +#endif // ENABLE_NAPI diff --git a/test/cctest/testnapi_value.cpp b/test/cctest/testnapi_value.cpp new file mode 100644 index 000000000..88960e4bc --- /dev/null +++ b/test/cctest/testnapi_value.cpp @@ -0,0 +1,357 @@ +#if defined(ENABLE_NAPI) +/* + * Copyright (c) 2026-present Samsung Electronics Co., Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// Exercises the value creation/read-out/coercion slice implemented in +// NapiValue.cpp: singletons, int64/string creation, get_value_* readers, and +// napi_coerce_to_*/napi_strict_equals. Unlike testnapi.cpp this doesn't +// dlopen() any vendored addon - every napi_* call is made directly from a C++ +// lambda run through Evaluator::execute, the same way NapiFunctions.cpp's own +// non-addon tests (e.g. Napi.CallFunctionReportsExceptionAsPendingStatus, +// Napi.HandleScopeOpenCloseAndMismatch) do. + +#include "api/EscargotPublic.h" +#include "napi/NapiEnv.h" +#include "napi/NapiTypes.h" + +using namespace Escargot; +using namespace Escargot::Napi; + +#include "gtest/gtest.h" + +#include +#include +#include + +TEST(Napi, ValueSingletonsAndVersion) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value nullValue = nullptr; + EXPECT_EQ(napi_get_null(env, &nullValue), napi_ok); + EXPECT_TRUE(FromNapi(nullValue)->isNull()); + + uint32_t version = 0; + EXPECT_EQ(napi_get_version(env, &version), napi_ok); + EXPECT_EQ(version, static_cast(NAPI_VERSION)); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ValueNumberRoundTrips) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + // napi_create_int64 / napi_get_value_int64 round trip, plus the + // clamp-to-int64-range and non-finite-maps-to-0 special cases + // napi_get_value_int64 documents. + napi_value fortyTwo = nullptr; + EXPECT_EQ(napi_create_int64(env, 42, &fortyTwo), napi_ok); + int64_t asInt64 = 0; + EXPECT_EQ(napi_get_value_int64(env, fortyTwo, &asInt64), napi_ok); + EXPECT_EQ(asInt64, 42); + + napi_value negative = nullptr; + EXPECT_EQ(napi_create_int64(env, -123456789012345, &negative), napi_ok); + int64_t negativeOut = 0; + EXPECT_EQ(napi_get_value_int64(env, negative, &negativeOut), napi_ok); + EXPECT_EQ(negativeOut, -123456789012345); + + napi_value truncated = ToNapi(ValueRef::create(3.7)); + int64_t truncatedOut = 0; + EXPECT_EQ(napi_get_value_int64(env, truncated, &truncatedOut), napi_ok); + EXPECT_EQ(truncatedOut, 3); // ToInteger truncates toward zero + + napi_value huge = ToNapi(ValueRef::create(1e300)); + int64_t hugeOut = 0; + EXPECT_EQ(napi_get_value_int64(env, huge, &hugeOut), napi_ok); + EXPECT_EQ(hugeOut, INT64_MAX); + + napi_value hugeNegative = ToNapi(ValueRef::create(-1e300)); + int64_t hugeNegativeOut = 0; + EXPECT_EQ(napi_get_value_int64(env, hugeNegative, &hugeNegativeOut), napi_ok); + EXPECT_EQ(hugeNegativeOut, INT64_MIN); + + napi_value nanValue = ToNapi(ValueRef::create(std::numeric_limits::quiet_NaN())); + int64_t nanOut = 123; + EXPECT_EQ(napi_get_value_int64(env, nanValue, &nanOut), napi_ok); + EXPECT_EQ(nanOut, 0); + + // napi_get_value_int32 wraps like ECMAScript's ToInt32. + napi_value wraps = ToNapi(ValueRef::create(4294967296.0 + 5.0)); // 2^32 + 5 + int32_t wrapsOut = 0; + EXPECT_EQ(napi_get_value_int32(env, wraps, &wrapsOut), napi_ok); + EXPECT_EQ(wrapsOut, 5); + + napi_value minusOne = ToNapi(ValueRef::create(-1)); + int32_t minusOneOut = 0; + EXPECT_EQ(napi_get_value_int32(env, minusOne, &minusOneOut), napi_ok); + EXPECT_EQ(minusOneOut, -1); + + // Type-mismatch cases. + napi_value notANumber = ToNapi(StringRef::createFromASCII("nope")); + int32_t unused32 = 0; + EXPECT_EQ(napi_get_value_int32(env, notANumber, &unused32), napi_number_expected); + int64_t unused64 = 0; + EXPECT_EQ(napi_get_value_int64(env, notANumber, &unused64), napi_number_expected); + + // napi_get_value_bool. + napi_value trueValue = ToNapi(ValueRef::create(true)); + napi_value falseValue = ToNapi(ValueRef::create(false)); + bool boolOut = false; + EXPECT_EQ(napi_get_value_bool(env, trueValue, &boolOut), napi_ok); + EXPECT_TRUE(boolOut); + EXPECT_EQ(napi_get_value_bool(env, falseValue, &boolOut), napi_ok); + EXPECT_FALSE(boolOut); + EXPECT_EQ(napi_get_value_bool(env, fortyTwo, &boolOut), napi_boolean_expected); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ValueStrings) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + // napi_create_string_latin1 / napi_get_value_string_latin1 round + // trip, including a byte (0xE9, "e with acute") outside ASCII. + const unsigned char latin1Bytes[] = { 'H', 'i', 0xE9 }; + napi_value latin1Str = nullptr; + EXPECT_EQ(napi_create_string_latin1(env, reinterpret_cast(latin1Bytes), 3, &latin1Str), napi_ok); + + size_t requiredLen = 0; + EXPECT_EQ(napi_get_value_string_latin1(env, latin1Str, nullptr, 0, &requiredLen), napi_ok); + EXPECT_EQ(requiredLen, 3u); + + char latin1Buf[16]; + size_t latin1Copied = 0; + EXPECT_EQ(napi_get_value_string_latin1(env, latin1Str, latin1Buf, sizeof(latin1Buf), &latin1Copied), napi_ok); + EXPECT_EQ(latin1Copied, 3u); + EXPECT_EQ(static_cast(latin1Buf[0]), 'H'); + EXPECT_EQ(static_cast(latin1Buf[1]), 'i'); + EXPECT_EQ(static_cast(latin1Buf[2]), 0xE9); + EXPECT_EQ(latin1Buf[3], '\0'); + + // Truncating copy: only room for 2 chars + NUL. + char latin1Small[3]; + size_t latin1SmallCopied = 0; + EXPECT_EQ(napi_get_value_string_latin1(env, latin1Str, latin1Small, sizeof(latin1Small), &latin1SmallCopied), napi_ok); + EXPECT_EQ(latin1SmallCopied, 2u); + EXPECT_EQ(latin1Small[2], '\0'); + + // NAPI_AUTO_LENGTH variant (NUL-terminated C string). + napi_value latin1Auto = nullptr; + EXPECT_EQ(napi_create_string_latin1(env, "auto", NAPI_AUTO_LENGTH, &latin1Auto), napi_ok); + EXPECT_EQ(FromNapi(latin1Auto)->asString()->length(), 4u); + + // napi_create_string_utf16 / napi_get_value_string_utf16 round + // trip, including a BMP code unit outside ASCII (U+00E9). + const char16_t utf16Chars[] = { u'H', u'i', 0x00E9 }; + napi_value utf16Str = nullptr; + EXPECT_EQ(napi_create_string_utf16(env, utf16Chars, 3, &utf16Str), napi_ok); + + size_t utf16RequiredLen = 0; + EXPECT_EQ(napi_get_value_string_utf16(env, utf16Str, nullptr, 0, &utf16RequiredLen), napi_ok); + EXPECT_EQ(utf16RequiredLen, 3u); + + char16_t utf16Buf[16]; + size_t utf16Copied = 0; + EXPECT_EQ(napi_get_value_string_utf16(env, utf16Str, utf16Buf, 16, &utf16Copied), napi_ok); + EXPECT_EQ(utf16Copied, 3u); + EXPECT_EQ(utf16Buf[0], u'H'); + EXPECT_EQ(utf16Buf[1], u'i'); + EXPECT_EQ(utf16Buf[2], 0x00E9); + EXPECT_EQ(utf16Buf[3], u'\0'); + + // bufsize == 0 means "write nothing, report 0 copied". + size_t utf16ZeroCopied = 123; + EXPECT_EQ(napi_get_value_string_utf16(env, utf16Str, utf16Buf, 0, &utf16ZeroCopied), napi_ok); + EXPECT_EQ(utf16ZeroCopied, 0u); + + // napi_get_value_string_utf8 against a genuinely multi-byte UTF-8 + // string ("h\xC3\xA9llo", i.e. "héllo": h,e-acute,l,l,o = 6 bytes). + napi_value utf8Str = ToNapi(StringRef::createFromUTF8("h\xC3\xA9llo", 6)); + size_t utf8RequiredLen = 0; + EXPECT_EQ(napi_get_value_string_utf8(env, utf8Str, nullptr, 0, &utf8RequiredLen), napi_ok); + EXPECT_EQ(utf8RequiredLen, 6u); + + char utf8Buf[16]; + size_t utf8Copied = 0; + EXPECT_EQ(napi_get_value_string_utf8(env, utf8Str, utf8Buf, sizeof(utf8Buf), &utf8Copied), napi_ok); + EXPECT_EQ(utf8Copied, 6u); + EXPECT_EQ(std::string(utf8Buf, utf8Copied), std::string("h\xC3\xA9llo", 6)); + EXPECT_EQ(utf8Buf[6], '\0'); + + // Truncating copy: only room for 4 bytes + NUL. + char utf8Small[5]; + size_t utf8SmallCopied = 0; + EXPECT_EQ(napi_get_value_string_utf8(env, utf8Str, utf8Small, sizeof(utf8Small), &utf8SmallCopied), napi_ok); + EXPECT_EQ(utf8SmallCopied, 4u); + EXPECT_EQ(utf8Small[4], '\0'); + + // Type-mismatch cases. + napi_value notAString = ToNapi(ValueRef::create(1)); + size_t unused = 0; + EXPECT_EQ(napi_get_value_string_utf8(env, notAString, nullptr, 0, &unused), napi_string_expected); + EXPECT_EQ(napi_get_value_string_latin1(env, notAString, nullptr, 0, &unused), napi_string_expected); + EXPECT_EQ(napi_get_value_string_utf16(env, notAString, nullptr, 0, &unused), napi_string_expected); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ValueCoercions) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + // napi_coerce_to_bool. + napi_value zero = ToNapi(ValueRef::create(0)); + napi_value boolResult = nullptr; + EXPECT_EQ(napi_coerce_to_bool(env, zero, &boolResult), napi_ok); + EXPECT_TRUE(FromNapi(boolResult)->isBoolean()); + EXPECT_FALSE(FromNapi(boolResult)->asBoolean()); + + napi_value nonEmptyStr = ToNapi(StringRef::createFromASCII("x")); + EXPECT_EQ(napi_coerce_to_bool(env, nonEmptyStr, &boolResult), napi_ok); + EXPECT_TRUE(FromNapi(boolResult)->asBoolean()); + + // napi_coerce_to_number: "42" -> 42. + napi_value strFortyTwo = ToNapi(StringRef::createFromASCII("42")); + napi_value numberResult = nullptr; + EXPECT_EQ(napi_coerce_to_number(env, strFortyTwo, &numberResult), napi_ok); + EXPECT_TRUE(FromNapi(numberResult)->isNumber()); + EXPECT_EQ(FromNapi(numberResult)->asNumber(), 42); + + // napi_coerce_to_string: 42 -> "42". + napi_value numFortyTwo = ToNapi(ValueRef::create(42)); + napi_value stringResult = nullptr; + EXPECT_EQ(napi_coerce_to_string(env, numFortyTwo, &stringResult), napi_ok); + EXPECT_TRUE(FromNapi(stringResult)->isString()); + EXPECT_EQ(FromNapi(stringResult)->asString()->toStdUTF8String(), "42"); + + // napi_coerce_to_object: a primitive is wrapped, not thrown. + napi_value objectResult = nullptr; + EXPECT_EQ(napi_coerce_to_object(env, numFortyTwo, &objectResult), napi_ok); + EXPECT_TRUE(FromNapi(objectResult)->isObject()); + + // napi_coerce_to_object(null) throws a TypeError per ECMAScript's + // ToObject - proves the Evaluator::execute wrapper actually + // catches it instead of letting a raw C++ exception cross this + // call, the same contract napi_call_function relies on. + napi_value nullValue = nullptr; + napi_get_null(env, &nullValue); + napi_value shouldFail = nullptr; + napi_status coerceStatus = napi_coerce_to_object(env, nullValue, &shouldFail); + EXPECT_EQ(coerceStatus, napi_pending_exception); + + bool isPending = false; + napi_is_exception_pending(env, &isPending); + EXPECT_TRUE(isPending); + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + EXPECT_TRUE(FromNapi(exception)->isObject()); + + bool stillPending = true; + napi_is_exception_pending(env, &stillPending); + EXPECT_FALSE(stillPending); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +TEST(Napi, ValueStrictEquals) +{ + NapiEnv::globalInit(); + NapiEnv* napiEnv = NapiEnv::create(); + + Evaluator::EvaluatorResult result = Evaluator::execute( + napiEnv->context(), [](ExecutionStateRef* state, napi_env env) -> ValueRef* { + env->executionState = state; + + napi_value one = ToNapi(ValueRef::create(1)); + napi_value oneAgain = ToNapi(ValueRef::create(1.0)); + napi_value two = ToNapi(ValueRef::create(2)); + napi_value strOne = ToNapi(StringRef::createFromASCII("1")); + + bool areEqual = false; + EXPECT_EQ(napi_strict_equals(env, one, oneAgain, &areEqual), napi_ok); + EXPECT_TRUE(areEqual); + + EXPECT_EQ(napi_strict_equals(env, one, two, &areEqual), napi_ok); + EXPECT_FALSE(areEqual); + + // Different types never strict-equal, even with the "same" value. + EXPECT_EQ(napi_strict_equals(env, one, strOne, &areEqual), napi_ok); + EXPECT_FALSE(areEqual); + + // Strings compare by content, not by identity, unlike objects. + napi_value strA1 = ToNapi(StringRef::createFromASCII("same")); + napi_value strA2 = ToNapi(StringRef::createFromASCII("same")); + EXPECT_EQ(napi_strict_equals(env, strA1, strA2, &areEqual), napi_ok); + EXPECT_TRUE(areEqual); + + napi_value obj1 = ToNapi(ObjectRef::create(state)); + napi_value obj2 = ToNapi(ObjectRef::create(state)); + EXPECT_EQ(napi_strict_equals(env, obj1, obj1, &areEqual), napi_ok); + EXPECT_TRUE(areEqual); + EXPECT_EQ(napi_strict_equals(env, obj1, obj2, &areEqual), napi_ok); + EXPECT_FALSE(areEqual); + + return ValueRef::createUndefined(); + }, + napiEnv->env()); + + ASSERT_TRUE(result.isSuccessful()) << result.resultOrErrorToString(napiEnv->context())->toStdUTF8String(); +} + +#endif // ENABLE_NAPI diff --git a/test/napi-tc b/test/napi-tc new file mode 160000 index 000000000..a1e3e6d2d --- /dev/null +++ b/test/napi-tc @@ -0,0 +1 @@ +Subproject commit a1e3e6d2d8c446fa7dd127b34a2f5caa128d42ac diff --git a/test_bcrypt_real.js b/test_bcrypt_real.js new file mode 100644 index 000000000..3b84317c6 --- /dev/null +++ b/test_bcrypt_real.js @@ -0,0 +1,97 @@ +'use strict'; + +const printLog = (msg) => { + if (typeof globalThis.print === 'function') { + globalThis.print(msg); + } else { + // fallback + } +}; + +const assert = { + strictEqual: (actual, expected, msg) => { + if (actual !== expected) { + throw new Error(`Assertion failed: expected ${expected}, got ${actual}. ${msg || ''}`); + } + } +}; + +// 1. Load precompiled bcrypt native addon +const addonKey = 'bcrypt_lib'; +printLog(`Loading bcrypt binary from N-API key: ${addonKey}`); +const bindings = globalThis.__napi_load_addon(addonKey); + +if (!bindings || !bindings.gen_salt_sync) { + printLog("Failed to load bcrypt bindings or methods are missing."); + process.exit(1); +} + +printLog("bcrypt binary loaded successfully!"); + +// 2. Prepare 16-byte seed buffer +const seed = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + +// 3. Verify Synchronous Flow (Salt -> Encrypt -> Compare) +printLog("Starting synchronous bcrypt flow..."); +const saltSync = bindings.gen_salt_sync('b', 10, seed); +printLog(`[Sync] Generated Salt: ${saltSync}`); + +const hashSync = bindings.encrypt_sync('EscargotSecurity123', saltSync); +printLog(`[Sync] Encrypted Hash: ${hashSync}`); + +const compareSyncMatch = bindings.compare_sync('EscargotSecurity123', hashSync); +const compareSyncMismatch = bindings.compare_sync('WrongPassword', hashSync); + +assert.strictEqual(compareSyncMatch, true, "Sync match check"); +assert.strictEqual(compareSyncMismatch, false, "Sync mismatch check"); +printLog("Synchronous bcrypt flow PASSED successfully!"); + +// 4. Verify Asynchronous Flow (Thread-Pool concurrency) +printLog("Starting asynchronous thread-pool bcrypt flow..."); + +let asyncCompleted = false; + +bindings.gen_salt('b', 10, seed, (err, saltAsync) => { + if (err) { + printLog(`[Async] gen_salt error: ${err}`); + process.exit(1); + } + printLog(`[Async] Generated Salt: ${saltAsync}`); + + bindings.encrypt('EscargotSecurity123', saltAsync, (err, hashAsync) => { + if (err) { + printLog(`[Async] encrypt error: ${err}`); + process.exit(1); + } + printLog(`[Async] Encrypted Hash: ${hashAsync}`); + + bindings.compare('EscargotSecurity123', hashAsync, (err, isMatch) => { + if (err) { + printLog(`[Async] compare error: ${err}`); + process.exit(1); + } + printLog(`[Async] Compare Match Result: ${isMatch}`); + assert.strictEqual(isMatch, true, "Async match check"); + + bindings.compare('WrongPassword', hashAsync, (err, isMismatch) => { + if (err) { + printLog(`[Async] compare mismatch error: ${err}`); + process.exit(1); + } + printLog(`[Async] Compare Mismatch Result: ${isMismatch}`); + assert.strictEqual(isMismatch, false, "Async mismatch check"); + + printLog("Asynchronous thread-pool bcrypt flow PASSED successfully!"); + printLog("TEST_SUCCESS"); + asyncCompleted = true; + }); + }); + }); +}); + +// Await the asynchronous worker thread responses by holding the event loop artificially +(async function holdLoop() { + while (!asyncCompleted) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } +})(); diff --git a/test_sqlite3_real.js b/test_sqlite3_real.js new file mode 100644 index 000000000..955199b6a --- /dev/null +++ b/test_sqlite3_real.js @@ -0,0 +1,58 @@ +'use strict'; + +const printLog = (msg) => { + if (typeof globalThis.print === 'function') { + globalThis.print(msg); + } else { + // fallback + } +}; + +// 1. Load the compiled node-sqlite3 binary directly via our N-API custom loader key +const addonKey = 'node_sqlite3'; +printLog(`Loading sqlite3 binary from N-API key: ${addonKey}`); +const binding = globalThis.__napi_load_addon(addonKey); + +if (!binding || !binding.Database) { + printLog("Failed to load binding or binding.Database is undefined."); + process.exit(1); +} + +printLog("sqlite3 binary loaded successfully!"); + +// 2. Instantiate in-memory Database +const db = new binding.Database(':memory:', (err) => { + if (err) { + printLog(`Database open error: ${err}`); + process.exit(1); + } + printLog("In-memory database opened successfully!"); + + // 3. Create schema + db.exec('CREATE TABLE students (id INT, name TEXT);', (err) => { + if (err) { + printLog(`Create table error: ${err}`); + process.exit(1); + } + printLog("Table 'students' created successfully!"); + + // 4. Insert real records + db.exec('INSERT INTO students VALUES (1, "Escargot N-API"); INSERT INTO students VALUES (2, "ABI Stability Core");', (err) => { + if (err) { + printLog(`Insert error: ${err}`); + process.exit(1); + } + printLog("Records inserted successfully!"); + + // 5. Close database safely + db.close((err) => { + if (err) { + printLog(`Database close error: ${err}`); + process.exit(1); + } + printLog("Database closed successfully! All N-API real-world assertions PASSED."); + printLog("TEST_SUCCESS"); + }); + }); + }); +}); diff --git a/tools/run-tests.py b/tools/run-tests.py index 67cc7af2e..b75abb4c1 100755 --- a/tools/run-tests.py +++ b/tools/run-tests.py @@ -877,8 +877,7 @@ def run_cctest(engine, arch, extra_arg): outStr = out.decode("utf-8") print(outStr) - result = outStr.lower() - if 'fail' in result: + if proc.returncode != 0: raise Exception('Not all tests succeeded') @runner('debugger-server-source', default=True)