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