diff --git a/quickjs/CMakeLists.txt b/quickjs/CMakeLists.txt index ebacc18..1476d40 100644 --- a/quickjs/CMakeLists.txt +++ b/quickjs/CMakeLists.txt @@ -5,7 +5,60 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -option(NAPI_QUICKJS_BUILD_TESTS "Build napi-quickjs tier1 tests" ON) +macro(xoption OPTION_NAME OPTION_TEXT OPTION_DEFAULT) + option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT}) + if(DEFINED ENV{${OPTION_NAME}}) + # Allow setting the option through an environment variable. + set(${OPTION_NAME} $ENV{${OPTION_NAME}}) + endif() + if(${OPTION_NAME}) + add_definitions(-D${OPTION_NAME}) + endif() + message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}") +endmacro() + +xoption(NAPI_QUICKJS_BUILD_TESTS "Build napi-quickjs tier1 tests" ON) +xoption(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER + "Enable QuickJS N-API lifetime tracking diagnostics" + OFF +) +xoption(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS + "Print QuickJS N-API value/ref allocator stats every 10 seconds" + OFF +) +xoption(NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS + "Print QuickJS N-API active value/ref JSValue tag stats with periodic stats" + OFF +) +xoption(NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + "Print active QuickJS N-API string and symbol values with periodic stats" + OFF +) + +if(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS AND NOT NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) + message(STATUS + "NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS requires " + "NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER; enabling lifetime tracker" + ) + set(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER ON) +endif() +if(NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS AND NOT NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS) + message(STATUS + "NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS requires " + "NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS; enabling periodic stats" + ) + set(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS ON) +endif() +if(NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP AND NOT NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS) + message(STATUS + "NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP requires " + "NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS; enabling tag stats" + ) + set(NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS ON) +endif() +if(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS AND NOT NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) + set(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER ON) +endif() set(NAPI_QUICKJS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}") @@ -67,11 +120,9 @@ add_library(napi_quickjs STATIC src/internal/napi_deferred.cc src/internal/napi_env.cc src/internal/napi_env_cleanup_hook.cc - src/internal/napi_escapable_handle_scope.cc src/internal/napi_external.cc src/internal/napi_external_backing_store_hint.cc src/internal/napi_function.cc - src/internal/napi_handle_scope.cc src/internal/napi_module_wrap.cc src/internal/napi_promises.cc src/internal/napi_ref.cc @@ -82,6 +133,9 @@ add_library(napi_quickjs STATIC src/internal/napi_value.cc src/unofficial_napi.cc ) +if(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) + target_sources(napi_quickjs PRIVATE src/internal/napi_lifetime_tracker.cc) +endif() target_include_directories(napi_quickjs PUBLIC @@ -97,6 +151,10 @@ target_compile_definitions(napi_quickjs NAPI_BUNDLED_QUICKJS=1 PRIVATE NAPI_QUICKJS_ENABLE_TRACE_DIAGNOSTICS=$,1,0> + $<$:NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER=1> + $<$:NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS=1> + $<$:NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS=1> + $<$:NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP=1> ) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") target_compile_options(napi_quickjs PRIVATE -fno-rtti) diff --git a/quickjs/src/internal/napi_allocator.h b/quickjs/src/internal/napi_allocator.h new file mode 100644 index 0000000..8bfc7c2 --- /dev/null +++ b/quickjs/src/internal/napi_allocator.h @@ -0,0 +1,420 @@ +#ifndef NAPI_QUICKJS_ALLOCATOR_H_ +#define NAPI_QUICKJS_ALLOCATOR_H_ + +#include "napi_lifetime_tracker.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +template +concept napi_allocator_payload__ = + std::default_initializable && + requires(T value) { + { value.release() } -> std::same_as; + }; + +template +concept napi_allocator_owner__ = std::is_class_v; + +template +concept napi_allocator_initializable_payload__ = + napi_allocator_payload__ && + requires(T value, Args &&...args) { + { value.initialize(static_cast(args)...) } -> std::same_as; + }; + +template +class napi_allocator__ +{ + static_assert(N > 0, "N must be greater than zero"); + +private: + static constexpr size_t next_power_of_two__(size_t value) + { + size_t result = 1; + while (result < value) + result <<= 1; + return result; + } + + struct slot__ + { + // Stored payload and allocator free-list linkage. + T data; + slot__ *next_free = nullptr; + bool active = false; + + slot__() = default; + slot__(const slot__ &) = delete; + slot__ &operator=(const slot__ &) = delete; + }; + + struct block_layout__ + { + // Stable storage for payload slots. + std::array slots; + + // Block occupancy and free-list state. + slot__ *first_free = nullptr; + size_t active_count = 0; + + // Membership flags for allocator side lists. + bool listed_available = false; + bool listed_full = false; + }; + + static constexpr size_t block_alignment__ = + next_power_of_two__(sizeof(block_layout__)); + static_assert((block_alignment__ & (block_alignment__ - 1)) == 0, + "block alignment must be a power of two"); + + struct alignas(block_alignment__) block__ : block_layout__ + { + block__() + { + reset_free_list(); + } + + block__(const block__ &) = delete; + block__ &operator=(const block__ &) = delete; + + slot__ *allocate() + { + if (this->first_free == nullptr) + return nullptr; + + slot__ *slot = this->first_free; + this->first_free = slot->next_free; + slot->next_free = nullptr; + slot->active = true; + ++this->active_count; + return slot; + } + + void release(slot__ *slot) + { + if (slot == nullptr || !slot->active) + return; + + slot->active = false; + slot->next_free = this->first_free; + this->first_free = slot; + --this->active_count; + } + + void close(Owner_ *owner) + { + for (size_t i = N; i > 0; --i) + { + slot__ &slot = this->slots[i - 1]; + if (slot.active) + { + if constexpr (quickjs::detail::napi_lifetime_tracked__) + quickjs::detail::napi_lifetime__::record_release(owner, &slot.data); + slot.data.release(); + slot.active = false; + } + } + + this->active_count = 0; + this->listed_available = false; + this->listed_full = false; + reset_free_list(); + } + + bool is_full() const + { + return this->active_count == N; + } + + bool is_available() const + { + return this->active_count < N; + } + + private: + void reset_free_list() + { + this->first_free = nullptr; + for (size_t i = N; i > 0; --i) + { + slot__ &slot = this->slots[i - 1]; + slot.next_free = this->first_free; + this->first_free = &slot; + } + } + }; + static_assert(sizeof(block__) == block_alignment__, + "napi_allocator__ block must fit exactly in its alignment region"); + +public: + explicit napi_allocator__(Owner_ *owner = nullptr) : owner_(owner) {} + napi_allocator__(const napi_allocator__ &) = delete; + napi_allocator__ &operator=(const napi_allocator__ &) = delete; + + napi_allocator__(napi_allocator__ &&other) noexcept : owner_(other.owner_) + { + *this = static_cast(other); + } + + napi_allocator__ &operator=(napi_allocator__ &&other) noexcept + { + if (this == &other) + return *this; + + close(); + owner_ = other.owner_; + blocks_ = static_cast &&>(other.blocks_); + available_blocks_ = static_cast &&>(other.available_blocks_); + full_blocks_ = static_cast &&>(other.full_blocks_); + return *this; + } + + ~napi_allocator__() + { + close(); + } + + void set_owner(Owner_ *owner) + { + owner_ = owner; + } + + template + requires napi_allocator_initializable_payload__ + T *allocate(Args &&...args) + { + block__ *block = first_available_block(); + if (block == nullptr) + return nullptr; + + slot__ *slot = block->allocate(); + if (slot == nullptr) + return nullptr; + + slot->data.initialize(static_cast(args)...); + if constexpr (quickjs::detail::napi_lifetime_tracked__) + quickjs::detail::napi_lifetime__::record_create(owner_, &slot->data); + + if (block->is_full()) + { + block->listed_available = false; + available_blocks_.pop_back(); + block->listed_full = true; + full_blocks_.push_back(block); + } + + return &slot->data; + } + + T *get(T *handle) + { + slot__ *slot = slot_from_handle(handle); + return slot != nullptr && slot->active ? &slot->data : nullptr; + } + + const T *get(T *handle) const + { + const slot__ *slot = slot_from_handle(handle); + return slot != nullptr && slot->active ? &slot->data : nullptr; + } + + void release(T *handle) + { + slot__ *slot = slot_from_handle(handle); + if (slot == nullptr || !slot->active) + return; + if constexpr (quickjs::detail::napi_lifetime_tracked__) + quickjs::detail::napi_lifetime__::record_release(owner_, &slot->data); + + block__ *block = block_from_slot(slot); + bool was_full = block != nullptr && block->is_full(); + slot->data.release(); + if (block != nullptr) + block->release(slot); + + if (was_full) + move_full_block_to_available(block); + } + + void close() + { + for (auto it = blocks_.rbegin(); it != blocks_.rend(); ++it) + it->close(owner_); + blocks_.clear(); + available_blocks_.clear(); + full_blocks_.clear(); + } + + void reserve_prefix(size_t count) + { + (void)count; + } + + size_t slot_count() const + { + return storage_slot_count(); + } + + size_t storage_slot_count() const + { + return blocks_.size() * N; + } + + size_t active_count() const + { + size_t count = 0; + for (const auto &block : blocks_) + count += block.active_count; + return count; + } + + template + void for_each_active(Fn fn) const + { + for (const auto &block : blocks_) + { + for (const auto &slot : block.slots) + { + if (slot.active) + fn(slot.data); + } + } + } + +private: + block__ *first_available_block() + { + while (!available_blocks_.empty()) + { + block__ *block = available_blocks_.back(); + if (block != nullptr && block->is_available()) + return block; + + available_blocks_.pop_back(); + if (block != nullptr) + block->listed_available = false; + } + + blocks_.emplace_back(); + block__ *block = &blocks_.back(); + block->listed_available = true; + available_blocks_.push_back(block); + return block; + } + + slot__ *slot_from_handle(T *handle) + { + if (handle == nullptr) + return nullptr; + + auto *slot = reinterpret_cast( + reinterpret_cast(handle) - offsetof(slot__, data)); + return owns_slot(slot, handle) ? slot : nullptr; + } + + const slot__ *slot_from_handle(T *handle) const + { + if (handle == nullptr) + return nullptr; + + auto *slot = reinterpret_cast( + reinterpret_cast(handle) - offsetof(slot__, data)); + return owns_slot(slot, handle) ? slot : nullptr; + } + + block__ *block_from_slot(slot__ *slot) + { + if (slot == nullptr) + return nullptr; + + auto *block = reinterpret_cast( + reinterpret_cast(slot) & ~(static_cast(block_alignment__) - 1)); + return owns_block(block) && slot_index(block, slot) < N ? block : nullptr; + } + + const block__ *block_from_slot(const slot__ *slot) const + { + if (slot == nullptr) + return nullptr; + + auto *block = reinterpret_cast( + reinterpret_cast(slot) & ~(static_cast(block_alignment__) - 1)); + return owns_block(block) && slot_index(block, slot) < N ? block : nullptr; + } + + bool owns_slot(const slot__ *slot, const T *handle) const + { + if (slot == nullptr || handle == nullptr || &slot->data != handle) + return false; + + const block__ *block = block_from_slot(slot); + return block != nullptr && slot->active; + } + + static size_t slot_index(const block__ *block, const slot__ *slot) + { + if (block == nullptr || slot == nullptr) + return N; + + const slot__ *begin = block->slots.data(); + const slot__ *end = begin + N; + if (slot < begin || slot >= end) + return N; + + return static_cast(slot - begin); + } + + bool owns_block(const block__ *block) const + { + if (block == nullptr) + return false; + + for (const auto &owned : blocks_) + { + if (&owned == block) + return true; + } + return false; + } + + void move_full_block_to_available(block__ *block) + { + if (block == nullptr || block->listed_available) + return; + + if (block->listed_full) + { + for (auto it = full_blocks_.begin(); it != full_blocks_.end(); ++it) + { + if (*it == block) + { + full_blocks_.erase(it); + break; + } + } + block->listed_full = false; + } + + block->listed_available = true; + available_blocks_.push_back(block); + } + + // Stable slot blocks owned by this allocator. + std::list blocks_; + + // Cached block lists used to find reusable slots quickly. + std::vector available_blocks_; + std::vector full_blocks_; + + // Owner used by lifetime hooks to attribute slot churn. + Owner_ *owner_ = nullptr; +}; + +#endif // NAPI_QUICKJS_ALLOCATOR_H_ diff --git a/quickjs/src/internal/napi_callback_info.cc b/quickjs/src/internal/napi_callback_info.cc index 3b666ef..e2d1f9e 100644 --- a/quickjs/src/internal/napi_callback_info.cc +++ b/quickjs/src/internal/napi_callback_info.cc @@ -15,6 +15,10 @@ napi_callback_info__::napi_callback_info__(napi_env env, { } +napi_callback_info__::~napi_callback_info__() +{ +} + napi_env napi_callback_info__::env() const { return env_; diff --git a/quickjs/src/internal/napi_callback_info.h b/quickjs/src/internal/napi_callback_info.h index 7d08f7a..3b59f61 100644 --- a/quickjs/src/internal/napi_callback_info.h +++ b/quickjs/src/internal/napi_callback_info.h @@ -14,6 +14,7 @@ struct napi_callback_info__ int argc, JSValueConst *argv, void *data); + ~napi_callback_info__(); napi_env env() const; JSValueConst this_value() const; @@ -23,9 +24,12 @@ struct napi_callback_info__ void *data() const; private: + // Callback invocation context. napi_env env_; JSValueConst this_val_; JSValue new_target_; + + // Callback arguments and addon data. int argc_; JSValueConst *argv_; void *data_; diff --git a/quickjs/src/internal/napi_contextify.cc b/quickjs/src/internal/napi_contextify.cc index b452b9f..84ac064 100644 --- a/quickjs/src/internal/napi_contextify.cc +++ b/quickjs/src/internal/napi_contextify.cc @@ -21,8 +21,16 @@ namespace quickjs::detail napi_contextify__::~napi_contextify__() { + teardown(); + } + + void napi_contextify__::teardown() + { + if (torn_down_) + return; JS_FreeValue(ctx_, source_map_error_source_callback_); source_map_error_source_callback_ = JS_UNDEFINED; + torn_down_ = true; } bool napi_contextify__::compile_trace_enabled() const @@ -194,15 +202,15 @@ namespace quickjs::detail { if (!napi_util__::check_env(env_)) return napi_invalid_arg; - if (callback != nullptr && !JS_IsUndefined(callback->get_inner()) && - !JS_IsNull(callback->get_inner()) && !JS_IsFunction(ctx_, callback->get_inner())) + if (callback != nullptr && !JS_IsUndefined(napi_quickjs_value_inner(env_, callback)) && + !JS_IsNull(napi_quickjs_value_inner(env_, callback)) && !JS_IsFunction(ctx_, napi_quickjs_value_inner(env_, callback))) { return napi_invalid_arg; } JS_FreeValue(ctx_, source_map_error_source_callback_); source_map_error_source_callback_ = - callback == nullptr ? JS_UNDEFINED : JS_DupValue(ctx_, callback->get_inner()); + callback == nullptr ? JS_UNDEFINED : JS_DupValue(ctx_, napi_quickjs_value_inner(env_, callback)); return napi_ok; } @@ -211,7 +219,7 @@ namespace quickjs::detail { if (!napi_util__::check_env(env_) || error == nullptr || result_out == nullptr) return napi_invalid_arg; - JSValue value = JS_GetPropertyStr(ctx_, error->get_inner(), "node:arrowMessage"); + JSValue value = JS_GetPropertyStr(ctx_, napi_quickjs_value_inner(env_, error), "node:arrowMessage"); if (JS_IsException(value)) return napi_pending_exception; if (JS_IsUndefined(value)) @@ -260,7 +268,7 @@ namespace quickjs::detail (void)host_defined_option_id; if (!napi_util__::check_env(env_) || sandbox_or_symbol == nullptr || result_out == nullptr) return napi_invalid_arg; - JSValue sandbox = sandbox_or_symbol->get_inner(); + JSValue sandbox = napi_quickjs_value_inner(env_, sandbox_or_symbol); if (!JS_IsObject(sandbox)) return napi_invalid_arg; JS_SetPropertyStr(ctx_, sandbox, "__quickjs_contextified", JS_NewBool(ctx_, true)); @@ -289,14 +297,14 @@ namespace quickjs::detail if (!napi_util__::check_env(env_) || source == nullptr || result_out == nullptr) return napi_invalid_arg; env_->module_wrap().register_dynamic_import_referrer(filename, host_defined_option_id); - if (sandbox_or_null != nullptr && !JS_IsNull(sandbox_or_null->get_inner()) && + if (sandbox_or_null != nullptr && !JS_IsNull(napi_quickjs_value_inner(env_, sandbox_or_null)) && !napi_util__::is_truthy_property(env_, sandbox_or_null, "__quickjs_contextified")) return napi_invalid_arg; std::string src = napi_util__::to_utf8(env_, source); std::string label = filename == nullptr ? "" : napi_util__::to_utf8(env_, filename); JSValue result = JS_UNDEFINED; - if (sandbox_or_null != nullptr && !JS_IsNull(sandbox_or_null->get_inner())) + if (sandbox_or_null != nullptr && !JS_IsNull(napi_quickjs_value_inner(env_, sandbox_or_null))) { const char *wrapper_source = "(function(__sandbox, __source) { with (__sandbox) { return eval(__source); } })"; JSValue wrapper = JS_Eval(ctx_, @@ -306,7 +314,7 @@ namespace quickjs::detail JS_EVAL_TYPE_GLOBAL); if (JS_IsException(wrapper)) return napi_pending_exception; - JSValue argv[] = {sandbox_or_null->get_inner(), source->get_inner()}; + JSValue argv[] = {napi_quickjs_value_inner(env_, sandbox_or_null), napi_quickjs_value_inner(env_, source)}; result = JS_Call(ctx_, wrapper, JS_UNDEFINED, 2, argv); JS_FreeValue(ctx_, wrapper); } @@ -323,7 +331,7 @@ namespace quickjs::detail { if (!napi_util__::check_env(env_) || sandbox_or_context_global == nullptr) return napi_invalid_arg; - JSValue sandbox = sandbox_or_context_global->get_inner(); + JSValue sandbox = napi_quickjs_value_inner(env_, sandbox_or_context_global); if (!JS_IsObject(sandbox)) return napi_invalid_arg; JS_SetPropertyStr(ctx_, sandbox, "__quickjs_contextified", JS_NewBool(ctx_, false)); @@ -351,23 +359,23 @@ namespace quickjs::detail return napi_invalid_arg; std::vector argv; - if (params_or_undefined != nullptr && JS_IsArray(params_or_undefined->get_inner())) + if (params_or_undefined != nullptr && JS_IsArray(napi_quickjs_value_inner(env_, params_or_undefined))) { uint32_t length = 0; - JSValue len_val = JS_GetPropertyStr(ctx_, params_or_undefined->get_inner(), "length"); + JSValue len_val = JS_GetPropertyStr(ctx_, napi_quickjs_value_inner(env_, params_or_undefined), "length"); JS_ToUint32(ctx_, &length, len_val); JS_FreeValue(ctx_, len_val); for (uint32_t i = 0; i < length; ++i) { - JSValue param = JS_GetPropertyUint32(ctx_, params_or_undefined->get_inner(), i); + JSValue param = JS_GetPropertyUint32(ctx_, napi_quickjs_value_inner(env_, params_or_undefined), i); argv.push_back(param); } } std::string source = napi_util__::to_utf8(env_, code); std::string source_url; - JSValue code_arg = JS_DupValue(ctx_, code->get_inner()); - if (filename != nullptr && !JS_IsUndefined(filename->get_inner()) && !JS_IsNull(filename->get_inner())) + JSValue code_arg = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, code)); + if (filename != nullptr && !JS_IsUndefined(napi_quickjs_value_inner(env_, filename)) && !JS_IsNull(napi_quickjs_value_inner(env_, filename))) { source_url = napi_util__::to_utf8(env_, filename); if (!source.empty() && !source_url.empty()) diff --git a/quickjs/src/internal/napi_contextify.h b/quickjs/src/internal/napi_contextify.h index efea2d9..3f534ad 100644 --- a/quickjs/src/internal/napi_contextify.h +++ b/quickjs/src/internal/napi_contextify.h @@ -16,6 +16,8 @@ namespace quickjs::detail napi_contextify__(napi_env env, JSContext *context); ~napi_contextify__(); + void teardown(); + napi_contextify__(const napi_contextify__ &) = delete; napi_contextify__ &operator=(const napi_contextify__ &) = delete; @@ -88,10 +90,16 @@ namespace quickjs::detail int32_t line_offset, int32_t column_offset); + // Owning environment and QuickJS context. napi_env env_; JSContext *ctx_; + + // Source-map and error-formatting state. bool source_maps_enabled_ = false; JSValue source_map_error_source_callback_; + + // Subsystem lifecycle. + bool torn_down_ = false; }; } diff --git a/quickjs/src/internal/napi_deferred.cc b/quickjs/src/internal/napi_deferred.cc index 500a751..7878cdb 100644 --- a/quickjs/src/internal/napi_deferred.cc +++ b/quickjs/src/internal/napi_deferred.cc @@ -3,34 +3,48 @@ #include "internal/napi_env.h" #include "internal/napi_value.h" -#include +napi_deferred__::~napi_deferred__() +{ + release(); +} -napi_deferred__::napi_deferred__(napi_env env, JSValue resolve, JSValue reject) - : env_(env), - resolve_(resolve), - reject_(reject) +void napi_deferred__::initialize(napi_env env, JSValue resolve, JSValue reject) { + release(); + env_ = env; + resolve_ = resolve; + reject_ = reject; } -napi_deferred__::~napi_deferred__() +void napi_deferred__::release() { - if (env_ != nullptr && env_->context() != nullptr) + if (env_ == nullptr) + return; + + napi_env env = env_; + JSValue resolve = resolve_; + JSValue reject = reject_; + env_ = nullptr; + resolve_ = JS_UNDEFINED; + reject_ = JS_UNDEFINED; + + if (env != nullptr && env->context() != nullptr) { - JS_FreeValue(env_->context(), resolve_); - JS_FreeValue(env_->context(), reject_); + JS_FreeValue(env->context(), resolve); + JS_FreeValue(env->context(), reject); } } +napi_env napi_deferred__::env() const +{ + return env_; +} + napi_deferred__ *napi_deferred__::create(napi_env env, JSValue resolve, JSValue reject) { if (env == nullptr || env->context() == nullptr) return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_deferred__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_deferred__(env, resolve, reject); + return env->create_deferred(resolve, reject); } void napi_deferred__::destroy(napi_deferred__ *deferred) @@ -39,19 +53,18 @@ void napi_deferred__::destroy(napi_deferred__ *deferred) return; napi_env env = deferred->env_; - deferred->~napi_deferred__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), deferred); + if (env != nullptr) + env->destroy_deferred(deferred); } JSValue napi_deferred__::call_resolve(napi_value resolution) { - JSValue arg = resolution->get_inner(); + JSValue arg = napi_quickjs_value_inner(env_, resolution); return JS_Call(env_->context(), resolve_, JS_UNDEFINED, 1, &arg); } JSValue napi_deferred__::call_reject(napi_value rejection) { - JSValue arg = rejection->get_inner(); + JSValue arg = napi_quickjs_value_inner(env_, rejection); return JS_Call(env_->context(), reject_, JS_UNDEFINED, 1, &arg); } diff --git a/quickjs/src/internal/napi_deferred.h b/quickjs/src/internal/napi_deferred.h index 9743338..f9ef96a 100644 --- a/quickjs/src/internal/napi_deferred.h +++ b/quickjs/src/internal/napi_deferred.h @@ -7,20 +7,23 @@ struct napi_deferred__ { + napi_deferred__() = default; static napi_deferred__ *create(napi_env env, JSValue resolve, JSValue reject); static void destroy(napi_deferred__ *deferred); ~napi_deferred__(); + void initialize(napi_env env, JSValue resolve, JSValue reject); + void release(); + napi_env env() const; JSValue call_resolve(napi_value resolution); JSValue call_reject(napi_value rejection); private: - napi_deferred__(napi_env env, JSValue resolve, JSValue reject); - - napi_env env_; - JSValue resolve_; - JSValue reject_; + // Owning environment and stored promise callbacks. + napi_env env_ = nullptr; + JSValue resolve_ = JS_UNDEFINED; + JSValue reject_ = JS_UNDEFINED; }; #endif // NAPI_QUICKJS_DEFERRED_H_ diff --git a/quickjs/src/internal/napi_env.cc b/quickjs/src/internal/napi_env.cc index d931f72..8dce656 100644 --- a/quickjs/src/internal/napi_env.cc +++ b/quickjs/src/internal/napi_env.cc @@ -1,41 +1,87 @@ #include "internal/napi_env.h" +#include "internal/napi_lifetime_macros.h" + #include +#include #include napi_env__::napi_env__(JSContext *context, int32_t module_api_version) : context_(context), - last_exception_(JS_UNDEFINED), module_api_version_(module_api_version), + last_exception_(JS_UNDEFINED), + scopes_(this), + refs_(this), + cleanup_hooks_(this), + deferreds_(this), + external_backing_store_hints_(this), promises_(this, context), contextify_(this, context), module_wrap_(this, context) { - root_scope_ = napi_scope__::create(this, nullptr); + root_scope_ = create_scope(nullptr); current_scope_ = root_scope_; clear_last_error(); } napi_env__::~napi_env__() { - for (auto it = env_cleanup_hooks_.rbegin(); it != env_cleanup_hooks_.rend(); ++it) + prepare_teardown(); + finalize_instance_data(); +} + +void napi_env__::prepare_teardown() +{ + if (torn_down_) + return; + + NAPI_QUICKJS_LIFETIME_DUMP(this, "napi_env__ teardown begin"); + while (!env_cleanup_hooks_.empty()) { - auto *entry = *it; + auto *entry = env_cleanup_hooks_.back(); + env_cleanup_hooks_.pop_back(); if (entry != nullptr) { entry->run(); napi_env_cleanup_hook__::destroy(entry); } } - env_cleanup_hooks_.clear(); - - if (instance_data_ != nullptr && instance_data_finalize_cb_ != nullptr) - instance_data_finalize_cb_(this, instance_data_, instance_data_finalize_hint_); + cleanup_hooks_.close(); + deferreds_.close(); clear_last_exception(); + refs_.close(); - root_scope_ = nullptr; + napi_scope__ *root_scope = scope_from_handle(root_scope_); + if (root_scope != nullptr) + { + root_scope->close(); + if (context_ != nullptr) + JS_RunGC(JS_GetRuntime(context_)); + } + scopes_.close(); current_scope_ = nullptr; + root_scope_ = nullptr; + weak_refs_.clear(); + module_wrap_.teardown(); + contextify_.teardown(); + promises_.teardown(); + NAPI_QUICKJS_LIFETIME_DUMP(this, "napi_env__ teardown end"); + torn_down_ = true; +} + +void napi_env__::finalize_instance_data() +{ + void *data = instance_data_; + napi_finalize finalize_cb = instance_data_finalize_cb_; + void *finalize_hint = instance_data_finalize_hint_; + + instance_data_ = nullptr; + instance_data_finalize_cb_ = nullptr; + instance_data_finalize_hint_ = nullptr; + + if (data != nullptr && finalize_cb != nullptr) + finalize_cb(this, data, finalize_hint); } JSContext *napi_env__::context() const @@ -48,26 +94,137 @@ int32_t napi_env__::module_api_version() const return module_api_version_; } -napi_scope__ *napi_env__::root_scope() const +napi_handle_scope napi_env__::root_scope() const { return root_scope_; } -napi_scope__ *napi_env__::current_scope() const +napi_handle_scope napi_env__::current_scope() const { return current_scope_; } -bool napi_env__::is_current_scope(napi_scope__ *scope) const +napi_handle_scope napi_env__::create_scope(napi_handle_scope parent) +{ + if (context_ == nullptr) + return nullptr; + auto *handle = scopes_.allocate(this, parent); + return reinterpret_cast(handle); +} + +void napi_env__::destroy_scope(napi_handle_scope scope) +{ + scopes_.release(reinterpret_cast(scope)); +} + +napi_scope__ *napi_env__::scope_from_handle(napi_handle_scope scope) const +{ + return const_cast &>(scopes_).get( + reinterpret_cast(scope)); +} + +napi_value napi_env__::wrap_value_in_current_scope(JSValue value, bool owned) +{ + napi_scope__ *scope = scope_from_handle(current_scope_); + return scope == nullptr ? nullptr : scope->wrap_value(value, owned); +} + +void napi_env__::delete_value_from_current_scope(napi_value value) +{ + napi_scope__ *scope = scope_from_handle(current_scope_); + if (scope != nullptr) + scope->delete_value(value); +} + +napi_value__ *napi_env__::value_from_current_scope(napi_value value) +{ + napi_scope__ *scope = scope_from_handle(current_scope_); + return scope == nullptr ? nullptr : scope->value_from_handle(value); +} + +napi_ref napi_env__::wrap_ref_in_root_scope(JSValueConst value, uint32_t initial_ref_count) +{ + if (context_ == nullptr) + return nullptr; + + napi_ref wrapped = refs_.allocate(this, value, initial_ref_count); + return wrapped; +} + +void napi_env__::delete_ref_from_root_scope(napi_ref ref) +{ + refs_.release(ref); +} + +napi_ref__ *napi_env__::ref_from_root_scope(napi_ref ref) +{ + return refs_.get(ref); +} + +size_t napi_env__::ref_storage_slot_count() const +{ + return refs_.storage_slot_count(); +} + +size_t napi_env__::active_ref_count() const +{ + return refs_.active_count(); +} + +bool napi_env__::is_current_scope(napi_handle_scope scope) const { return current_scope_ == scope; } -void napi_env__::set_current_scope(napi_scope__ *scope) +void napi_env__::set_current_scope(napi_handle_scope scope) { current_scope_ = scope; } +size_t napi_env__::scope_storage_slot_count() const +{ + return scopes_.storage_slot_count(); +} + +size_t napi_env__::active_scope_count() const +{ + return scopes_.active_count(); +} + +#if defined(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) && defined(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS) +bool napi_env__::should_dump_lifetime_stats(int64_t now_ms) +{ + constexpr int64_t interval_ms = 2000; + if (lifetime_last_stats_ms_ == 0) + { + lifetime_last_stats_ms_ = now_ms; + return false; + } + + if (now_ms - lifetime_last_stats_ms_ < interval_ms) + return false; + + lifetime_last_stats_ms_ = now_ms; + return true; +} + +bool napi_env__::should_dump_lifetime_string_symbol_values(int64_t now_ms) +{ + constexpr int64_t interval_ms = 10000; + if (lifetime_last_string_symbol_values_ms_ == 0) + { + lifetime_last_string_symbol_values_ms_ = now_ms; + return false; + } + + if (now_ms - lifetime_last_string_symbol_values_ms_ < interval_ms) + return false; + + lifetime_last_string_symbol_values_ms_ = now_ms; + return true; +} +#endif + const napi_extended_error_info *napi_env__::last_error_info() const { return &last_error_; @@ -129,8 +286,16 @@ void *napi_env__::instance_data() const void napi_env__::set_instance_data(void *data, napi_finalize finalize_cb, void *finalize_hint) { - if (instance_data_ != nullptr && instance_data_finalize_cb_ != nullptr) - instance_data_finalize_cb_(this, instance_data_, instance_data_finalize_hint_); + void *old_data = instance_data_; + napi_finalize old_finalize_cb = instance_data_finalize_cb_; + void *old_finalize_hint = instance_data_finalize_hint_; + + instance_data_ = nullptr; + instance_data_finalize_cb_ = nullptr; + instance_data_finalize_hint_ = nullptr; + + if (old_data != nullptr && old_finalize_cb != nullptr) + old_finalize_cb(this, old_data, old_finalize_hint); instance_data_ = data; instance_data_finalize_cb_ = finalize_cb; @@ -162,20 +327,93 @@ napi_status napi_env__::remove_cleanup_hook(napi_cleanup_hook hook, void *arg) return napi_invalid_arg; } +napi_env_cleanup_hook__ *napi_env__::create_cleanup_hook(napi_cleanup_hook hook, void *arg) +{ + if (context_ == nullptr || hook == nullptr) + return nullptr; + return cleanup_hooks_.allocate(this, hook, arg); +} + +void napi_env__::destroy_cleanup_hook(napi_env_cleanup_hook__ *entry) +{ + cleanup_hooks_.release(entry); +} + +napi_deferred__ *napi_env__::create_deferred(JSValue resolve, JSValue reject) +{ + if (context_ == nullptr) + return nullptr; + return deferreds_.allocate(this, resolve, reject); +} + +void napi_env__::destroy_deferred(napi_deferred__ *deferred) +{ + deferreds_.release(deferred); +} + +napi_external_backing_store_hint__ *napi_env__::create_external_backing_store_hint( + void *external_data, + node_api_basic_finalize finalize_cb, + void *finalize_hint) +{ + if (context_ == nullptr) + return nullptr; + return external_backing_store_hints_.allocate(this, external_data, finalize_cb, finalize_hint); +} + +void napi_env__::destroy_external_backing_store_hint(napi_external_backing_store_hint__ *hint) +{ + external_backing_store_hints_.release(hint); +} + void napi_env__::track_weak_ref(napi_ref ref) { - if (ref == nullptr || !ref->is_weak()) + napi_ref__ *slot = napi_quickjs_ref_slot(this, ref); + if (slot == nullptr || !slot->is_weak()) + return; + + JSValueConst value = slot->get_inner(); + if (!JS_VALUE_HAS_REF_COUNT(value)) return; - if (std::find(weak_refs_.begin(), weak_refs_.end(), ref) == weak_refs_.end()) - weak_refs_.push_back(ref); + void *identity = JS_VALUE_GET_PTR(value); + auto range = weak_refs_.equal_range(identity); + for (auto it = range.first; it != range.second; ++it) + { + if (it->second == ref) + return; + } + weak_refs_.emplace(identity, ref); } void napi_env__::remove_weak_ref(napi_ref ref) { - auto it = std::find(weak_refs_.begin(), weak_refs_.end(), ref); - if (it != weak_refs_.end()) - weak_refs_.erase(it); + napi_ref__ *slot = napi_quickjs_ref_slot(this, ref); + if (slot != nullptr) + { + JSValueConst value = slot->get_inner(); + if (JS_VALUE_HAS_REF_COUNT(value)) + { + void *identity = JS_VALUE_GET_PTR(value); + auto range = weak_refs_.equal_range(identity); + for (auto it = range.first; it != range.second;) + { + if (it->second == ref) + it = weak_refs_.erase(it); + else + ++it; + } + return; + } + } + + for (auto it = weak_refs_.begin(); it != weak_refs_.end();) + { + if (it->second == ref) + it = weak_refs_.erase(it); + else + ++it; + } } void napi_env__::clear_weak_refs_for_value(JSValueConst value) @@ -183,10 +421,43 @@ void napi_env__::clear_weak_refs_for_value(JSValueConst value) if (!JS_VALUE_HAS_REF_COUNT(value)) return; - for (auto *ref : weak_refs_) + void *identity = JS_VALUE_GET_PTR(value); + auto range = weak_refs_.equal_range(identity); + for (auto it = range.first; it != range.second; ++it) + { + napi_ref ref = it->second; + napi_ref__ *slot = napi_quickjs_ref_slot(this, ref); + if (slot != nullptr) + slot->clear_if_matches(value); + } +} + +void napi_env__::track_external_array_buffer_hint( + JSValueConst arraybuffer, + napi_external_backing_store_hint__ *hint) +{ + if (hint == nullptr) + return; + external_array_buffer_hints_.emplace(JS_VALUE_GET_PTR(arraybuffer), hint); +} + +napi_external_backing_store_hint__ *napi_env__::external_array_buffer_hint(JSValueConst arraybuffer) const +{ + void *identity = JS_VALUE_GET_PTR(arraybuffer); + auto range = external_array_buffer_hints_.equal_range(identity); + if (range.first != range.second) + return range.first->second; + return nullptr; +} + +void napi_env__::untrack_external_array_buffer_hint(napi_external_backing_store_hint__ *hint) +{ + for (auto it = external_array_buffer_hints_.begin(); it != external_array_buffer_hints_.end();) { - if (ref != nullptr) - ref->clear_if_matches(value); + if (it->second == hint) + it = external_array_buffer_hints_.erase(it); + else + ++it; } } diff --git a/quickjs/src/internal/napi_env.h b/quickjs/src/internal/napi_env.h index e519180..3c3c0d1 100644 --- a/quickjs/src/internal/napi_env.h +++ b/quickjs/src/internal/napi_env.h @@ -7,8 +7,7 @@ #include "napi_contextify.h" #include "napi_deferred.h" #include "napi_env_cleanup_hook.h" -#include "napi_escapable_handle_scope.h" -#include "napi_handle_scope.h" +#include "napi_external_backing_store_hint.h" #include "napi_module_wrap.h" #include "napi_promises.h" #include "napi_ref.h" @@ -17,21 +16,58 @@ #include #include +#include +#include #include #include +struct napi_external_backing_store_hint__; + struct napi_env__ { explicit napi_env__(JSContext *context, int32_t module_api_version); ~napi_env__(); + void prepare_teardown(); + void finalize_instance_data(); + JSContext *context() const; int32_t module_api_version() const; - napi_scope__ *root_scope() const; - napi_scope__ *current_scope() const; - bool is_current_scope(napi_scope__ *scope) const; - void set_current_scope(napi_scope__ *scope); + napi_handle_scope root_scope() const; + napi_handle_scope current_scope() const; + napi_handle_scope create_scope(napi_handle_scope parent); + void destroy_scope(napi_handle_scope scope); + napi_scope__ *scope_from_handle(napi_handle_scope scope) const; + napi_value wrap_value_in_current_scope(JSValue value, bool owned); + void delete_value_from_current_scope(napi_value value); + napi_value__ *value_from_current_scope(napi_value value); + napi_ref wrap_ref_in_root_scope(JSValueConst value, uint32_t initial_ref_count); + void delete_ref_from_root_scope(napi_ref ref); + napi_ref__ *ref_from_root_scope(napi_ref ref); + size_t ref_storage_slot_count() const; + size_t active_ref_count() const; + bool is_current_scope(napi_handle_scope scope) const; + void set_current_scope(napi_handle_scope scope); + size_t scope_storage_slot_count() const; + size_t active_scope_count() const; + + template + void for_each_active_scope(Fn fn) const + { + scopes_.for_each_active(fn); + } + + template + void for_each_active_ref(Fn fn) const + { + refs_.for_each_active(fn); + } + +#if defined(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) && defined(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS) + bool should_dump_lifetime_stats(int64_t now_ms); + bool should_dump_lifetime_string_symbol_values(int64_t now_ms); +#endif const napi_extended_error_info *last_error_info() const; napi_status set_last_error(napi_status status, const char *message); @@ -47,10 +83,22 @@ struct napi_env__ napi_status add_cleanup_hook(napi_cleanup_hook hook, void *arg); napi_status remove_cleanup_hook(napi_cleanup_hook hook, void *arg); + napi_env_cleanup_hook__ *create_cleanup_hook(napi_cleanup_hook hook, void *arg); + void destroy_cleanup_hook(napi_env_cleanup_hook__ *entry); + napi_deferred__ *create_deferred(JSValue resolve, JSValue reject); + void destroy_deferred(napi_deferred__ *deferred); + napi_external_backing_store_hint__ *create_external_backing_store_hint( + void *external_data, + node_api_basic_finalize finalize_cb, + void *finalize_hint); + void destroy_external_backing_store_hint(napi_external_backing_store_hint__ *hint); void track_weak_ref(napi_ref ref); void remove_weak_ref(napi_ref ref); void clear_weak_refs_for_value(JSValueConst value); + void track_external_array_buffer_hint(JSValueConst arraybuffer, napi_external_backing_store_hint__ *hint); + napi_external_backing_store_hint__ *external_array_buffer_hint(JSValueConst arraybuffer) const; + void untrack_external_array_buffer_hint(napi_external_backing_store_hint__ *hint); int64_t adjust_external_memory(int64_t change_in_bytes); @@ -62,23 +110,57 @@ struct napi_env__ const quickjs::detail::napi_module_wrap__ &module_wrap() const; private: + // QuickJS context and API version. JSContext *context_; + int32_t module_api_version_ = 8; + + // Last native/JS error state. napi_extended_error_info last_error_{}; std::string last_error_message_; JSValue last_exception_; bool has_last_exception_ = false; - int32_t module_api_version_ = 8; + + // Instance data finalizer. void *instance_data_ = nullptr; napi_finalize instance_data_finalize_cb_ = nullptr; void *instance_data_finalize_hint_ = nullptr; + + // Cleanup hooks in registration order. std::vector env_cleanup_hooks_; - std::vector weak_refs_; - napi_scope__ *root_scope_ = nullptr; - napi_scope__ *current_scope_ = nullptr; + + // Weak refs grouped by referenced QuickJS object identity. + std::unordered_multimap weak_refs_; + + // External ArrayBuffer hints grouped by QuickJS object identity. + std::unordered_multimap external_array_buffer_hints_; + + // Scope stack. + napi_handle_scope root_scope_ = nullptr; + napi_handle_scope current_scope_ = nullptr; + + // Env-owned slot allocators. + napi_allocator__ scopes_; + napi_allocator__ refs_; + napi_allocator__ cleanup_hooks_; + napi_allocator__ deferreds_; + napi_allocator__ external_backing_store_hints_; + +#if defined(NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER) && defined(NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS) + // Periodic lifetime dump scheduling. + int64_t lifetime_last_stats_ms_ = 0; + int64_t lifetime_last_string_symbol_values_ms_ = 0; +#endif + + // External memory accounting. int64_t external_memory_ = 0; + + // Env-owned subsystems. napi_promises__ promises_; quickjs::detail::napi_contextify__ contextify_; quickjs::detail::napi_module_wrap__ module_wrap_; + + // Env teardown state. + bool torn_down_ = false; }; napi_status napi_quickjs_set_last_error(napi_env env, diff --git a/quickjs/src/internal/napi_env_cleanup_hook.cc b/quickjs/src/internal/napi_env_cleanup_hook.cc index 4c1ad4e..3bdd525 100644 --- a/quickjs/src/internal/napi_env_cleanup_hook.cc +++ b/quickjs/src/internal/napi_env_cleanup_hook.cc @@ -2,25 +2,35 @@ #include "internal/napi_env.h" -#include +napi_env_cleanup_hook__::~napi_env_cleanup_hook__() +{ + release(); +} + +void napi_env_cleanup_hook__::initialize(napi_env env, napi_cleanup_hook hook, void *arg) +{ + env_ = env; + hook_ = hook; + arg_ = arg; +} -napi_env_cleanup_hook__::napi_env_cleanup_hook__(napi_env env, napi_cleanup_hook hook, void *arg) - : env_(env), - hook_(hook), - arg_(arg) +void napi_env_cleanup_hook__::release() { + env_ = nullptr; + hook_ = nullptr; + arg_ = nullptr; +} + +napi_env napi_env_cleanup_hook__::env() const +{ + return env_; } napi_env_cleanup_hook__ *napi_env_cleanup_hook__::create(napi_env env, napi_cleanup_hook hook, void *arg) { if (env == nullptr || env->context() == nullptr || hook == nullptr) return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_env_cleanup_hook__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_env_cleanup_hook__(env, hook, arg); + return env->create_cleanup_hook(hook, arg); } void napi_env_cleanup_hook__::destroy(napi_env_cleanup_hook__ *entry) @@ -29,9 +39,8 @@ void napi_env_cleanup_hook__::destroy(napi_env_cleanup_hook__ *entry) return; napi_env env = entry->env_; - entry->~napi_env_cleanup_hook__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), entry); + if (env != nullptr) + env->destroy_cleanup_hook(entry); } void napi_env_cleanup_hook__::run() const diff --git a/quickjs/src/internal/napi_env_cleanup_hook.h b/quickjs/src/internal/napi_env_cleanup_hook.h index a12de67..0aa7a00 100644 --- a/quickjs/src/internal/napi_env_cleanup_hook.h +++ b/quickjs/src/internal/napi_env_cleanup_hook.h @@ -6,18 +6,22 @@ struct napi_env_cleanup_hook__ { + napi_env_cleanup_hook__() = default; static napi_env_cleanup_hook__ *create(napi_env env, napi_cleanup_hook hook, void *arg); static void destroy(napi_env_cleanup_hook__ *entry); + ~napi_env_cleanup_hook__(); + void initialize(napi_env env, napi_cleanup_hook hook, void *arg); + void release(); + napi_env env() const; void run() const; bool matches(napi_cleanup_hook hook, void *arg) const; private: - napi_env_cleanup_hook__(napi_env env, napi_cleanup_hook hook, void *arg); - - napi_env env_; - napi_cleanup_hook hook_; - void *arg_; + // Owning environment and registered cleanup callback. + napi_env env_ = nullptr; + napi_cleanup_hook hook_ = nullptr; + void *arg_ = nullptr; }; #endif // NAPI_QUICKJS_ENV_CLEANUP_HOOK_H_ diff --git a/quickjs/src/internal/napi_escapable_handle_scope.cc b/quickjs/src/internal/napi_escapable_handle_scope.cc deleted file mode 100644 index ecbf087..0000000 --- a/quickjs/src/internal/napi_escapable_handle_scope.cc +++ /dev/null @@ -1,43 +0,0 @@ -#include "internal/napi_escapable_handle_scope.h" - -#include "internal/napi_env.h" - -#include - -napi_escapable_handle_scope__::napi_escapable_handle_scope__(napi_env env, napi_scope__ *parent) - : napi_scope__(env, parent) -{ -} - -napi_escapable_handle_scope__ *napi_escapable_handle_scope__::create(napi_env env, napi_scope__ *parent) -{ - if (env == nullptr || env->context() == nullptr) - return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_escapable_handle_scope__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_escapable_handle_scope__(env, parent); -} - -void napi_escapable_handle_scope__::destroy(napi_escapable_handle_scope__ *scope) -{ - if (scope == nullptr) - return; - - napi_env env = scope->env(); - scope->~napi_escapable_handle_scope__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), scope); -} - -bool napi_escapable_handle_scope__::has_escaped() const -{ - return escaped_; -} - -void napi_escapable_handle_scope__::mark_escaped() -{ - escaped_ = true; -} diff --git a/quickjs/src/internal/napi_escapable_handle_scope.h b/quickjs/src/internal/napi_escapable_handle_scope.h deleted file mode 100644 index 1c507b7..0000000 --- a/quickjs/src/internal/napi_escapable_handle_scope.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef NAPI_QUICKJS_ESCAPABLE_HANDLE_SCOPE_H_ -#define NAPI_QUICKJS_ESCAPABLE_HANDLE_SCOPE_H_ - -#include "napi_scope.h" - -struct napi_escapable_handle_scope__ : napi_scope__ -{ - static napi_escapable_handle_scope__ *create(napi_env env, napi_scope__ *parent); - static void destroy(napi_escapable_handle_scope__ *scope); - - bool has_escaped() const; - void mark_escaped(); - -private: - napi_escapable_handle_scope__(napi_env env, napi_scope__ *parent); - - bool escaped_ = false; -}; - -#endif // NAPI_QUICKJS_ESCAPABLE_HANDLE_SCOPE_H_ diff --git a/quickjs/src/internal/napi_external.cc b/quickjs/src/internal/napi_external.cc index d135cda..05082bf 100644 --- a/quickjs/src/internal/napi_external.cc +++ b/quickjs/src/internal/napi_external.cc @@ -128,24 +128,27 @@ napi_status napi_external__::get_buffer_info(napi_env env, JSValueConst value, v void napi_external__::free_external_array_buffer_data(JSRuntime *rt, void *opaque, void *ptr) { - (void)rt; (void)ptr; auto *hint = reinterpret_cast(opaque); if (hint == nullptr) return; hint->invoke_finalizer(); - napi_external_backing_store_hint__::destroy(hint); + if (hint->is_detaching()) + return; + if (hint->env() != nullptr) + hint->env()->untrack_external_array_buffer_hint(hint); + napi_external_backing_store_hint__::destroy_with_runtime(rt, hint); } void napi_external__::finalizer(JSRuntime *rt, JSValue value) { - (void)rt; auto *hint = static_cast(JS_GetOpaque(value, external_class_id)); if (hint == nullptr) return; JSValue target = hint->finalizer_target(value); - hint->env()->clear_weak_refs_for_value(target); + if (hint->env() != nullptr) + hint->env()->clear_weak_refs_for_value(target); hint->invoke_finalizer(); - napi_external_backing_store_hint__::destroy(hint); + napi_external_backing_store_hint__::destroy_with_runtime(rt, hint); } diff --git a/quickjs/src/internal/napi_external_backing_store_hint.cc b/quickjs/src/internal/napi_external_backing_store_hint.cc index 64b943d..c1690ed 100644 --- a/quickjs/src/internal/napi_external_backing_store_hint.cc +++ b/quickjs/src/internal/napi_external_backing_store_hint.cc @@ -2,18 +2,38 @@ #include "internal/napi_env.h" -#include +napi_external_backing_store_hint__::~napi_external_backing_store_hint__() +{ + release(); +} -napi_external_backing_store_hint__::napi_external_backing_store_hint__( +void napi_external_backing_store_hint__::initialize( napi_env env, void *external_data, node_api_basic_finalize finalize_cb, void *finalize_hint) - : env_(env), - external_data_(external_data), - finalize_cb_(finalize_cb), - finalize_hint_(finalize_hint) { + release(); + env_ = env; + rt_ = JS_GetRuntime(env->context()); + external_data_ = external_data; + finalize_cb_ = finalize_cb; + finalize_hint_ = finalize_hint; + finalize_invoked_ = false; + detaching_ = false; + weak_target_ = JS_UNDEFINED; +} + +void napi_external_backing_store_hint__::release() +{ + env_ = nullptr; + rt_ = nullptr; + external_data_ = nullptr; + finalize_cb_ = nullptr; + finalize_hint_ = nullptr; + finalize_invoked_ = false; + detaching_ = false; + weak_target_ = JS_UNDEFINED; } napi_external_backing_store_hint__ *napi_external_backing_store_hint__::create( @@ -24,36 +44,64 @@ napi_external_backing_store_hint__ *napi_external_backing_store_hint__::create( { if (env == nullptr || env->context() == nullptr) return nullptr; + return env->create_external_backing_store_hint(external_data, finalize_cb, finalize_hint); +} - void *memory = js_mallocz(env->context(), sizeof(napi_external_backing_store_hint__)); - if (memory == nullptr) - return nullptr; +void napi_external_backing_store_hint__::destroy(napi_external_backing_store_hint__ *hint) +{ + if (hint == nullptr) + return; - return new (memory) napi_external_backing_store_hint__(env, external_data, finalize_cb, finalize_hint); + destroy_with_runtime(hint->rt_, hint); } -void napi_external_backing_store_hint__::destroy(napi_external_backing_store_hint__ *hint) +void napi_external_backing_store_hint__::destroy_with_runtime( + JSRuntime *rt, + napi_external_backing_store_hint__ *hint) { if (hint == nullptr) return; + (void)rt; napi_env env = hint->env_; - hint->~napi_external_backing_store_hint__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), hint); + if (env != nullptr) + env->destroy_external_backing_store_hint(hint); } -void napi_external_backing_store_hint__::invoke_finalizer() const +void napi_external_backing_store_hint__::invoke_finalizer() { + if (finalize_invoked_) + return; + finalize_invoked_ = true; if (finalize_cb_ != nullptr) finalize_cb_(env_, external_data_, finalize_hint_); } +void napi_external_backing_store_hint__::begin_detach() +{ + detaching_ = true; +} + +void napi_external_backing_store_hint__::end_detach() +{ + detaching_ = false; +} + +bool napi_external_backing_store_hint__::is_detaching() const +{ + return detaching_; +} + napi_env napi_external_backing_store_hint__::env() const { return env_; } +JSRuntime *napi_external_backing_store_hint__::runtime() const +{ + return rt_; +} + void *napi_external_backing_store_hint__::external_data() const { return external_data_; diff --git a/quickjs/src/internal/napi_external_backing_store_hint.h b/quickjs/src/internal/napi_external_backing_store_hint.h index 93ba675..cf21ed4 100644 --- a/quickjs/src/internal/napi_external_backing_store_hint.h +++ b/quickjs/src/internal/napi_external_backing_store_hint.h @@ -8,30 +8,45 @@ struct napi_external_backing_store_hint__ { + napi_external_backing_store_hint__() = default; static napi_external_backing_store_hint__ *create( napi_env env, void *external_data, node_api_basic_finalize finalize_cb, void *finalize_hint); static void destroy(napi_external_backing_store_hint__ *hint); - - void invoke_finalizer() const; + static void destroy_with_runtime(JSRuntime *rt, napi_external_backing_store_hint__ *hint); + ~napi_external_backing_store_hint__(); + + void initialize(napi_env env, + void *external_data, + node_api_basic_finalize finalize_cb, + void *finalize_hint); + void release(); + void invoke_finalizer(); + void begin_detach(); + void end_detach(); + bool is_detaching() const; napi_env env() const; + JSRuntime *runtime() const; void *external_data() const; JSValue weak_target() const; JSValue finalizer_target(JSValue fallback) const; void set_weak_target(JSValue weak_target); private: - napi_external_backing_store_hint__(napi_env env, - void *external_data, - node_api_basic_finalize finalize_cb, - void *finalize_hint); - - napi_env env_; - void *external_data_; - node_api_basic_finalize finalize_cb_; - void *finalize_hint_; + // Owning runtime state. + napi_env env_ = nullptr; + JSRuntime *rt_ = nullptr; + + // Embedder backing store and finalizer. + void *external_data_ = nullptr; + node_api_basic_finalize finalize_cb_ = nullptr; + void *finalize_hint_ = nullptr; + bool finalize_invoked_ = false; + + // Detach/finalizer coordination. + bool detaching_ = false; JSValue weak_target_ = JS_UNDEFINED; }; diff --git a/quickjs/src/internal/napi_function.cc b/quickjs/src/internal/napi_function.cc index d660455..f9d2a62 100644 --- a/quickjs/src/internal/napi_function.cc +++ b/quickjs/src/internal/napi_function.cc @@ -8,6 +8,42 @@ #include +namespace +{ +class quickjs_callback_handle_scope__ +{ +public: + explicit quickjs_callback_handle_scope__(napi_env env) + : env_(env), + parent_(env != nullptr ? env->current_scope() : nullptr), + scope_(env != nullptr ? env->create_scope(parent_) : nullptr) + { + if (scope_ != nullptr) + env_->set_current_scope(scope_); + } + + ~quickjs_callback_handle_scope__() + { + if (scope_ == nullptr || env_ == nullptr) + return; + + if (env_->is_current_scope(scope_)) + env_->set_current_scope(parent_); + env_->destroy_scope(scope_); + } + + bool is_open() const + { + return scope_ != nullptr; + } + +private: + napi_env env_ = nullptr; + napi_handle_scope parent_ = nullptr; + napi_handle_scope scope_ = nullptr; +}; +} // namespace + JSValue napi_function__::create_internal(napi_env env, const char *utf8name, napi_callback cb, @@ -18,7 +54,7 @@ JSValue napi_function__::create_internal(napi_env env, if (status != napi_ok) return JS_EXCEPTION; - return JS_DupValue(env->context(), fn_val->get_inner()); + return JS_DupValue(env->context(), napi_quickjs_value_inner(env, fn_val)); } JSValue napi_function__::trampoline(JSContext *ctx, @@ -38,6 +74,9 @@ JSValue napi_function__::trampoline(JSContext *ctx, auto cb = reinterpret_cast(cb_ptr); auto user_data = napi_external__::get_value(func_data[1]); + quickjs_callback_handle_scope__ callback_scope(env); + if (!callback_scope.is_open()) + return JS_ThrowOutOfMemory(ctx); JSValue effective_this = this_val; JSValue new_target = JS_UNDEFINED; @@ -73,8 +112,8 @@ JSValue napi_function__::trampoline(JSContext *ctx, if (result != nullptr) { JS_FreeValue(ctx, effective_this); - returned = JS_DupValue(ctx, result->get_inner()); - env->current_scope()->delete_value(result); + returned = JS_DupValue(ctx, napi_quickjs_value_inner(env, result)); + env->delete_value_from_current_scope(result); } else { @@ -83,8 +122,8 @@ JSValue napi_function__::trampoline(JSContext *ctx, } else if (result != nullptr) { - returned = JS_DupValue(ctx, result->get_inner()); - env->current_scope()->delete_value(result); + returned = JS_DupValue(ctx, napi_quickjs_value_inner(env, result)); + env->delete_value_from_current_scope(result); } return returned; @@ -118,8 +157,8 @@ napi_status napi_function__::create(napi_env env, napi_create_external(env, data, nullptr, nullptr, &data_external); JSValue data_values[2]; - data_values[0] = JS_DupValue(env->context(), cb_external->get_inner()); - data_values[1] = JS_DupValue(env->context(), data_external->get_inner()); + data_values[0] = JS_DupValue(env->context(), napi_quickjs_value_inner(env, cb_external)); + data_values[1] = JS_DupValue(env->context(), napi_quickjs_value_inner(env, data_external)); JSValue fn = JS_NewCFunctionData(env->context(), trampoline, 0, magic, 2, data_values); JS_FreeValue(env->context(), data_values[0]); @@ -136,6 +175,6 @@ napi_status napi_function__::create(napi_env env, JS_PROP_CONFIGURABLE); } - *result = env->current_scope()->wrap_value(fn, true); + *result = env->wrap_value_in_current_scope(fn, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } diff --git a/quickjs/src/internal/napi_handle_scope.cc b/quickjs/src/internal/napi_handle_scope.cc deleted file mode 100644 index bbb1281..0000000 --- a/quickjs/src/internal/napi_handle_scope.cc +++ /dev/null @@ -1,33 +0,0 @@ -#include "internal/napi_handle_scope.h" - -#include "internal/napi_env.h" - -#include - -napi_handle_scope__::napi_handle_scope__(napi_env env, napi_scope__ *parent) - : napi_scope__(env, parent) -{ -} - -napi_handle_scope__ *napi_handle_scope__::create(napi_env env, napi_scope__ *parent) -{ - if (env == nullptr || env->context() == nullptr) - return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_handle_scope__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_handle_scope__(env, parent); -} - -void napi_handle_scope__::destroy(napi_handle_scope__ *scope) -{ - if (scope == nullptr) - return; - - napi_env env = scope->env(); - scope->~napi_handle_scope__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), scope); -} diff --git a/quickjs/src/internal/napi_handle_scope.h b/quickjs/src/internal/napi_handle_scope.h deleted file mode 100644 index 1f6ac6d..0000000 --- a/quickjs/src/internal/napi_handle_scope.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef NAPI_QUICKJS_HANDLE_SCOPE_H_ -#define NAPI_QUICKJS_HANDLE_SCOPE_H_ - -#include "napi_scope.h" - -struct napi_handle_scope__ : napi_scope__ -{ - static napi_handle_scope__ *create(napi_env env, napi_scope__ *parent); - static void destroy(napi_handle_scope__ *scope); - -private: - napi_handle_scope__(napi_env env, napi_scope__ *parent); -}; - -#endif // NAPI_QUICKJS_HANDLE_SCOPE_H_ diff --git a/quickjs/src/internal/napi_lifetime_macros.h b/quickjs/src/internal/napi_lifetime_macros.h new file mode 100644 index 0000000..df96aa7 --- /dev/null +++ b/quickjs/src/internal/napi_lifetime_macros.h @@ -0,0 +1,13 @@ +#ifndef NAPI_QUICKJS_LIFETIME_MACROS_H_ +#define NAPI_QUICKJS_LIFETIME_MACROS_H_ + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER +#include "internal/napi_lifetime_tracker.h" + +#define NAPI_QUICKJS_LIFETIME_DUMP(env, reason) \ + quickjs::detail::napi_lifetime_tracker__::dump(env, reason) +#else +#define NAPI_QUICKJS_LIFETIME_DUMP(env, reason) ((void)0) +#endif + +#endif // NAPI_QUICKJS_LIFETIME_MACROS_H_ diff --git a/quickjs/src/internal/napi_lifetime_tracker.cc b/quickjs/src/internal/napi_lifetime_tracker.cc new file mode 100644 index 0000000..0860762 --- /dev/null +++ b/quickjs/src/internal/napi_lifetime_tracker.cc @@ -0,0 +1,1214 @@ +#include "internal/napi_lifetime_tracker.h" + +#include "internal/napi_deferred.h" +#include "internal/napi_env.h" +#include "internal/napi_env_cleanup_hook.h" +#include "internal/napi_external_backing_store_hint.h" +#include "internal/napi_ref.h" +#include "internal/napi_scope.h" +#include "internal/napi_value.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace quickjs::detail +{ +namespace +{ +bool enabled() +{ + const char *value = std::getenv("EDGE_TRACE_NAPI_LIFETIME"); + return value != nullptr && value[0] != '\0' && value[0] != '0'; +} + +bool periodic_stats_enabled() +{ + const char *value = std::getenv("EDGE_TRACE_NAPI_LIFETIME_STATS"); + if (value != nullptr && value[0] != '\0') + return value[0] != '0'; + return enabled(); +} + +int64_t monotonic_milliseconds() +{ + using clock = std::chrono::steady_clock; + return std::chrono::duration_cast( + clock::now().time_since_epoch()) + .count(); +} + +#if defined(NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS) || \ + defined(NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP) +constexpr int k_min_known_tag = -9; +constexpr int k_max_known_tag = 8; +constexpr size_t k_known_tag_count = + static_cast(k_max_known_tag - k_min_known_tag + 1); +constexpr size_t k_unknown_tag_index = k_known_tag_count; +constexpr size_t k_tag_bucket_count = k_known_tag_count + 1; + +size_t tag_bucket_index(int tag) +{ + if (tag < k_min_known_tag || tag > k_max_known_tag) + return k_unknown_tag_index; + return static_cast(tag - k_min_known_tag); +} + +const char *tag_bucket_name(size_t index) +{ + if (index == k_unknown_tag_index) + return "unknown"; + + switch (static_cast(index) + k_min_known_tag) + { + case -9: + return "big_int"; + case -8: + return "symbol"; + case -7: + return "string"; + case -6: + return "string_rope"; + case -5: + return "tag_-5"; + case -4: + return "tag_-4"; + case -3: + return "module"; + case -2: + return "function_bytecode"; + case -1: + return "object"; + case 0: + return "int"; + case 1: + return "bool"; + case 2: + return "null"; + case 3: + return "undefined"; + case 4: + return "uninitialized"; + case 5: + return "catch_offset"; + case 6: + return "exception"; + case 7: + return "short_big_int"; + case 8: + return "float64"; + default: + return "unknown"; + } +} + +struct tag_counters +{ + size_t slots[k_tag_bucket_count] = {}; +}; + +void add_tag(tag_counters &counters, int tag) +{ + ++counters.slots[tag_bucket_index(tag)]; +} + +void remove_tag(tag_counters &counters, int tag) +{ + size_t &slot = counters.slots[tag_bucket_index(tag)]; + if (slot > 0) + --slot; +} + +bool has_tags(const tag_counters &counters) +{ + for (size_t i = 0; i < k_tag_bucket_count; ++i) + { + if (counters.slots[i] != 0) + return true; + } + return false; +} +#endif + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP +constexpr size_t k_value_dump_max_bytes = 240; + +struct string_symbol_entry +{ + int tag = 0; + std::string value; + size_t count = 0; +}; + +struct object_type_entry +{ + std::string prototype_name; + size_t count = 0; +}; + +void append_hex_escape(std::string &out, unsigned char c) +{ + constexpr char hex[] = "0123456789abcdef"; + out.push_back('\\'); + out.push_back('x'); + out.push_back(hex[(c >> 4) & 0x0f]); + out.push_back(hex[c & 0x0f]); +} + +std::string escaped_value_fragment(const char *value, size_t value_length) +{ + size_t limit = value_length < k_value_dump_max_bytes ? value_length : k_value_dump_max_bytes; + std::string out; + out.reserve(limit + 3); + for (size_t i = 0; i < limit; ++i) + { + unsigned char c = static_cast(value[i]); + switch (c) + { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (c >= 0x20 && c <= 0x7e) + out.push_back(static_cast(c)); + else + append_hex_escape(out, c); + break; + } + } + if (value_length > limit) + out += "..."; + return out; +} + +void add_string_symbol_entry(std::vector &entries, + int tag, + const std::string &value) +{ + for (auto &entry : entries) + { + if (entry.tag == tag && entry.value == value) + { + ++entry.count; + return; + } + } + + string_symbol_entry entry; + entry.tag = tag; + entry.value = value; + entry.count = 1; + entries.push_back(std::move(entry)); +} + +void remove_string_symbol_entry(std::vector &entries, + int tag, + const std::string &value) +{ + for (auto it = entries.begin(); it != entries.end(); ++it) + { + if (it->tag == tag && it->value == value) + { + if (it->count > 1) + --it->count; + else + entries.erase(it); + return; + } + } +} + +void add_object_type_entry(std::vector &entries, + const std::string &prototype_name) +{ + for (auto &entry : entries) + { + if (entry.prototype_name == prototype_name) + { + ++entry.count; + return; + } + } + + object_type_entry entry; + entry.prototype_name = prototype_name; + entry.count = 1; + entries.push_back(std::move(entry)); +} + +void remove_object_type_entry(std::vector &entries, + const std::string &prototype_name) +{ + for (auto it = entries.begin(); it != entries.end(); ++it) + { + if (it->prototype_name == prototype_name) + { + if (it->count > 1) + --it->count; + else + entries.erase(it); + return; + } + } +} + +void clear_exception(JSContext *ctx) +{ + if (ctx == nullptr || !JS_HasException(ctx)) + return; + JSValue exception = JS_GetException(ctx); + JS_FreeValue(ctx, exception); +} + +bool js_value_to_escaped_string(JSContext *ctx, JSValueConst value, std::string &out) +{ + size_t text_length = 0; + const char *text = JS_ToCStringLen(ctx, &text_length, value); + if (text == nullptr) + { + clear_exception(ctx); + return false; + } + + out = escaped_value_fragment(text, text_length); + JS_FreeCString(ctx, text); + return true; +} + +std::string class_name_fallback(JSContext *ctx, JSValueConst value) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSClassID class_id = JS_GetClassID(value); + JSAtom class_name = JS_GetClassName(rt, class_id); + if (class_name == JS_ATOM_NULL) + return ""; + + const char *text = JS_AtomToCString(ctx, class_name); + std::string result = text == nullptr ? "" : escaped_value_fragment(text, std::strlen(text)); + if (text != nullptr) + JS_FreeCString(ctx, text); + JS_FreeAtomRT(rt, class_name); + clear_exception(ctx); + return result.empty() ? "" : result; +} + +std::string object_prototype_name(napi_env env, JSValueConst value) +{ + JSContext *ctx = env->context(); + JSValue proto = JS_GetPrototype(ctx, value); + if (JS_IsException(proto)) + { + clear_exception(ctx); + return class_name_fallback(ctx, value); + } + + if (JS_IsNull(proto) || JS_IsUndefined(proto)) + { + JS_FreeValue(ctx, proto); + return ""; + } + + JSValue ctor = JS_GetPropertyStr(ctx, proto, "constructor"); + JS_FreeValue(ctx, proto); + if (JS_IsException(ctor)) + { + clear_exception(ctx); + return class_name_fallback(ctx, value); + } + + JSValue name = JS_UNDEFINED; + if (JS_IsObject(ctor)) + name = JS_GetPropertyStr(ctx, ctor, "name"); + JS_FreeValue(ctx, ctor); + if (JS_IsException(name)) + { + clear_exception(ctx); + return class_name_fallback(ctx, value); + } + + std::string result; + bool has_name = !JS_IsUndefined(name) && + !JS_IsNull(name) && + js_value_to_escaped_string(ctx, name, result) && + !result.empty(); + JS_FreeValue(ctx, name); + + return has_name ? result : class_name_fallback(ctx, value); +} +#endif + +struct value_snapshot +{ + napi_env env = nullptr; + int tag = JS_TAG_UNDEFINED; +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + bool has_string_symbol = false; + std::string string_symbol_value; + bool has_object_type = false; + std::string object_type; +#endif +}; + +struct basic_snapshot +{ + napi_env env = nullptr; +}; + +template +struct tracked_type_stats +{ + size_t created = 0; + size_t released = 0; + size_t active = 0; + size_t peak = 0; + std::unordered_map live; +}; + +struct value_type_stats : tracked_type_stats +{ +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS + tag_counters tags; +#endif +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + std::vector string_symbols; + std::vector object_types; +#endif +}; + +struct lifetime_stats +{ + std::mutex mutex; + value_type_stats values; + value_type_stats refs; + tracked_type_stats cleanup_hooks; + tracked_type_stats deferreds; + tracked_type_stats external_backing_store_hints; + size_t scope_escape_calls = 0; + size_t scope_escape_succeeded = 0; + size_t scope_escape_failed = 0; + std::unordered_map> counter_history; + std::unordered_map counter_history_size; + std::unordered_map counter_history_next; +}; + +lifetime_stats g_lifetime; + +struct counter_trend +{ + bool available = false; + size_t current = 0; + long long speed = 0; + long long acceleration = 0; +}; + +counter_trend observe_counter(const std::string &key, size_t value) +{ + counter_trend trend; + size_t &history_size = g_lifetime.counter_history_size[key]; + size_t &next = g_lifetime.counter_history_next[key]; + auto &history = g_lifetime.counter_history[key]; + + history[next] = value; + next = (next + 1) % history.size(); + if (history_size < history.size()) + ++history_size; + + if (history_size == history.size()) + { + size_t i_minus_2_index = next; + size_t i_minus_1_index = (next + 1) % history.size(); + size_t i_index = (next + 2) % history.size(); + long long x_i_minus_2 = static_cast(history[i_minus_2_index]); + long long x_i_minus_1 = static_cast(history[i_minus_1_index]); + long long x_i = static_cast(history[i_index]); + trend.available = true; + trend.current = history[i_minus_1_index]; + trend.speed = x_i - x_i_minus_2; + trend.acceleration = x_i - 2 * x_i_minus_1 + x_i_minus_2; + } + return trend; +} + +void format_center_value(char *buffer, size_t buffer_size, const counter_trend &trend) +{ + if (!trend.available) + { + std::snprintf(buffer, buffer_size, "%s", "-"); + return; + } + + std::snprintf(buffer, buffer_size, "%zu", trend.current); +} + +void format_delta_value(char *buffer, size_t buffer_size, const counter_trend &trend, bool acceleration) +{ + if (!trend.available) + { + std::snprintf(buffer, buffer_size, "%s", "-"); + return; + } + + std::snprintf(buffer, + buffer_size, + "%lld", + acceleration ? trend.acceleration : trend.speed); +} + +void dump_metric_row(const char *metric, size_t value, const std::string &key) +{ + counter_trend trend = observe_counter(key, value); + char current[32]; + char speed[32]; + char acceleration[32]; + format_center_value(current, sizeof(current), trend); + format_delta_value(speed, sizeof(speed), trend, false); + format_delta_value(acceleration, sizeof(acceleration), trend, true); + std::fprintf(stderr, + " %-36s %10s %10s %10s\n", + metric, + current, + speed, + acceleration); +} + +void dump_counter_header(const char *title) +{ + std::fprintf(stderr, "%s\n", title); + std::fprintf(stderr, " %-36s %10s %10s %10s\n", "metric", "x[i-1]", "speed", "accel"); +} + +value_snapshot capture_value_snapshot(napi_env env, JSValueConst value) +{ + value_snapshot snapshot; + snapshot.env = env; + snapshot.tag = JS_VALUE_GET_NORM_TAG(value); + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + if (env != nullptr && env->context() != nullptr) + { + JSContext *ctx = env->context(); + if ((snapshot.tag == JS_TAG_STRING || + snapshot.tag == JS_TAG_STRING_ROPE) && + js_value_to_escaped_string(ctx, value, snapshot.string_symbol_value)) + { + snapshot.has_string_symbol = true; + } + + if (snapshot.tag == JS_TAG_OBJECT) + { + snapshot.object_type = object_prototype_name(env, value); + snapshot.has_object_type = true; + } + } +#endif + + return snapshot; +} + +void add_value_snapshot(value_type_stats &stats, const value_snapshot &snapshot) +{ +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS + add_tag(stats.tags, snapshot.tag); +#endif +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + if (snapshot.has_string_symbol) + add_string_symbol_entry(stats.string_symbols, snapshot.tag, snapshot.string_symbol_value); + if (snapshot.has_object_type) + add_object_type_entry(stats.object_types, snapshot.object_type); +#endif +} + +void remove_value_snapshot(value_type_stats &stats, const value_snapshot &snapshot) +{ +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS + remove_tag(stats.tags, snapshot.tag); +#endif +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + if (snapshot.has_string_symbol) + remove_string_symbol_entry(stats.string_symbols, snapshot.tag, snapshot.string_symbol_value); + if (snapshot.has_object_type) + remove_object_type_entry(stats.object_types, snapshot.object_type); +#endif +} + +template +void record_create_locked(Stats &stats, + const void *handle, + Snapshot snapshot, + AddSnapshot add_snapshot) +{ + auto existing = stats.live.find(handle); + if (existing != stats.live.end()) + { + if (stats.active > 0) + --stats.active; + stats.live.erase(existing); + } + + ++stats.created; + ++stats.active; + stats.peak = std::max(stats.peak, stats.active); + add_snapshot(stats, snapshot); + stats.live.emplace(handle, std::move(snapshot)); +} + +template +napi_env record_release_locked(Stats &stats, + const void *handle, + RemoveSnapshot remove_snapshot) +{ + auto existing = stats.live.find(handle); + if (existing == stats.live.end()) + return nullptr; + + napi_env env = existing->second.env; + remove_snapshot(stats, existing->second); + stats.live.erase(existing); + ++stats.released; + if (stats.active > 0) + --stats.active; + return env; +} + +void add_basic_snapshot(tracked_type_stats &, const basic_snapshot &) +{ +} + +void remove_basic_snapshot(tracked_type_stats &, const basic_snapshot &) +{ +} + +struct env_slot_scan +{ + size_t value_slots_total = 0; + size_t active_values = 0; + size_t ref_slots_total = 0; + size_t active_refs = 0; + size_t scope_slots_total = 0; + size_t active_scopes = 0; + std::vector> active_values_by_scope_level; +}; + +env_slot_scan scan_env_slots(napi_env env) +{ + env_slot_scan scan; + if (env == nullptr) + return scan; + + scan.scope_slots_total = env->scope_storage_slot_count(); + scan.active_scopes = env->active_scope_count(); + scan.ref_slots_total = env->ref_storage_slot_count(); + scan.active_refs = env->active_ref_count(); + std::unordered_map active_values_by_scope_level; + env->for_each_active_scope([&](const napi_scope__ &scope) { + size_t active_values = scope.active_value_count(); + scan.value_slots_total += scope.value_storage_slot_count(); + scan.active_values += active_values; + active_values_by_scope_level[scope.level()] += active_values; + }); + scan.active_values_by_scope_level.reserve(active_values_by_scope_level.size()); + for (const auto &[level, count] : active_values_by_scope_level) + scan.active_values_by_scope_level.push_back({level, count}); + std::sort(scan.active_values_by_scope_level.begin(), + scan.active_values_by_scope_level.end(), + [](const auto &left, const auto &right) { + return left.first < right.first; + }); + return scan; +} + +template +void dump_type_row(const char *label, const Stats &stats) +{ + counter_trend trend = observe_counter(std::string("type.") + label + ".active", stats.active); + char current[32]; + char speed[32]; + char acceleration[32]; + format_center_value(current, sizeof(current), trend); + format_delta_value(speed, sizeof(speed), trend, false); + format_delta_value(acceleration, sizeof(acceleration), trend, true); + std::fprintf(stderr, + " %-33s %10zu %10zu %10s %10zu %10s %10s\n", + label, + stats.created, + stats.released, + current, + stats.peak, + speed, + acceleration); +} + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS +void dump_tag_table(const char *label, const tag_counters &counters) +{ + if (!has_tags(counters)) + return; + + std::fprintf(stderr, "[napi-lifetime-tags] owner=%s\n", label); + std::fprintf(stderr, " %-16s %10s %10s %10s\n", "tag", "x[i-1]", "speed", "accel"); + for (size_t i = 0; i < k_tag_bucket_count; ++i) + { + size_t count = counters.slots[i]; + if (count == 0) + continue; + const char *name = tag_bucket_name(i); + counter_trend trend = observe_counter(std::string("tag.") + label + "." + name, count); + char current[32]; + char speed[32]; + char acceleration[32]; + format_center_value(current, sizeof(current), trend); + format_delta_value(speed, sizeof(speed), trend, false); + format_delta_value(acceleration, sizeof(acceleration), trend, true); + std::fprintf(stderr, " %-16s %10s %10s %10s\n", name, current, speed, acceleration); + } +} +#endif + +void dump_scope_level_table(const env_slot_scan &scan) +{ + if (scan.active_values_by_scope_level.empty()) + return; + + std::fprintf(stderr, "[napi-lifetime-scopes]\n"); + std::fprintf(stderr, " %-16s %10s %10s %10s\n", "level", "x[i-1]", "speed", "accel"); + for (const auto &[level, count] : scan.active_values_by_scope_level) + { + counter_trend trend = observe_counter(std::string("scope.level.") + std::to_string(level), count); + char current[32]; + char speed[32]; + char acceleration[32]; + format_center_value(current, sizeof(current), trend); + format_delta_value(speed, sizeof(speed), trend, false); + format_delta_value(acceleration, sizeof(acceleration), trend, true); + std::fprintf(stderr, " %-16zu %10s %10s %10s\n", level, current, speed, acceleration); + } +} + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP +struct named_count +{ + std::string name; + size_t count = 0; +}; + +struct named_dual_count +{ + std::string name; + size_t values = 0; + size_t refs = 0; +}; + +void format_trend_columns(const counter_trend &trend, + char *current, + size_t current_size, + char *speed, + size_t speed_size, + char *acceleration, + size_t acceleration_size) +{ + format_center_value(current, current_size, trend); + format_delta_value(speed, speed_size, trend, false); + format_delta_value(acceleration, acceleration_size, trend, true); +} + +void dump_string_entries(const std::vector &entries) +{ + size_t singular_count = 0; + std::unordered_map counts; + for (const auto &entry : entries) + { + if (entry.tag != JS_TAG_STRING && entry.tag != JS_TAG_STRING_ROPE) + continue; + counts[entry.value] += entry.count; + } + + std::vector sorted; + sorted.reserve(counts.size()); + for (const auto &[value, count] : counts) + sorted.push_back({value, count}); + + std::sort(sorted.begin(), sorted.end(), [](const auto &left, const auto &right) { + if (left.count != right.count) + return left.count > right.count; + return left.name < right.name; + }); + + if (sorted.empty()) + return; + + std::fprintf(stderr, "[napi-lifetime-strings]\n"); + std::fprintf(stderr, " %-36s %10s %10s %10s\n", "string", "x[i-1]", "speed", "accel"); + for (const auto &entry : sorted) + { + if (entry.count < 2) + { + ++singular_count; + continue; + } + + counter_trend trend = + observe_counter(std::string("string.napi_value.") + entry.name, entry.count); + char current[32]; + char speed[32]; + char acceleration[32]; + format_trend_columns(trend, + current, + sizeof(current), + speed, + sizeof(speed), + acceleration, + sizeof(acceleration)); + std::fprintf(stderr, + " %-36s %10s %10s %10s\n", + entry.name.c_str(), + current, + speed, + acceleration); + } + if (singular_count != 0) + dump_metric_row("count == 1", singular_count, "string.napi_value.count_eq_1"); +} + +std::unordered_map object_type_counts( + const std::vector &entries) +{ + std::unordered_map counts; + for (const auto &entry : entries) + counts[entry.prototype_name] += entry.count; + return counts; +} + +void dump_object_type_entries(const std::vector &value_entries, + const std::vector &ref_entries) +{ + size_t singular_value_count = 0; + size_t singular_ref_count = 0; + std::unordered_map values = object_type_counts(value_entries); + std::unordered_map refs = object_type_counts(ref_entries); + std::unordered_map combined; + + for (const auto &[name, count] : values) + { + combined[name].name = name; + combined[name].values = count; + } + for (const auto &[name, count] : refs) + { + combined[name].name = name; + combined[name].refs = count; + } + + std::vector sorted; + sorted.reserve(combined.size()); + for (const auto &[_, count] : combined) + sorted.push_back(count); + + std::sort(sorted.begin(), sorted.end(), [](const auto &left, const auto &right) { + size_t left_total = left.values + left.refs; + size_t right_total = right.values + right.refs; + if (left_total != right_total) + return left_total > right_total; + if (left.values != right.values) + return left.values > right.values; + if (left.refs != right.refs) + return left.refs > right.refs; + return left.name < right.name; + }); + + if (sorted.empty()) + return; + + std::fprintf(stderr, "[napi-lifetime-objects]\n"); + std::fprintf(stderr, + " %-28s %10s %10s %10s %10s %10s %10s\n", + "type", + "values:x", + "speed", + "accel", + "refs:x", + "speed", + "accel"); + for (const auto &entry : sorted) + { + bool has_printable_values = entry.values >= 2; + bool has_printable_refs = entry.refs >= 2; + if (!has_printable_values && entry.values != 0) + ++singular_value_count; + if (!has_printable_refs && entry.refs != 0) + ++singular_ref_count; + if (!has_printable_values && !has_printable_refs) + continue; + + counter_trend value_trend = + observe_counter(std::string("object.napi_value.") + entry.name, entry.values); + counter_trend ref_trend = + observe_counter(std::string("object.napi_ref.") + entry.name, entry.refs); + char value_current[32]; + char value_speed[32]; + char value_acceleration[32]; + char ref_current[32]; + char ref_speed[32]; + char ref_acceleration[32]; + if (has_printable_values) + { + format_trend_columns(value_trend, + value_current, + sizeof(value_current), + value_speed, + sizeof(value_speed), + value_acceleration, + sizeof(value_acceleration)); + } + else + { + std::snprintf(value_current, sizeof(value_current), "%s", "-"); + std::snprintf(value_speed, sizeof(value_speed), "%s", "-"); + std::snprintf(value_acceleration, sizeof(value_acceleration), "%s", "-"); + } + + if (has_printable_refs) + { + format_trend_columns(ref_trend, + ref_current, + sizeof(ref_current), + ref_speed, + sizeof(ref_speed), + ref_acceleration, + sizeof(ref_acceleration)); + } + else + { + std::snprintf(ref_current, sizeof(ref_current), "%s", "-"); + std::snprintf(ref_speed, sizeof(ref_speed), "%s", "-"); + std::snprintf(ref_acceleration, sizeof(ref_acceleration), "%s", "-"); + } + + std::fprintf(stderr, + " %-28s %10s %10s %10s %10s %10s %10s\n", + entry.name.c_str(), + value_current, + value_speed, + value_acceleration, + ref_current, + ref_speed, + ref_acceleration); + } + + if (singular_value_count != 0 || singular_ref_count != 0) + { + counter_trend value_trend = + observe_counter("object.napi_value.count_eq_1", singular_value_count); + counter_trend ref_trend = + observe_counter("object.napi_ref.count_eq_1", singular_ref_count); + char value_current[32]; + char value_speed[32]; + char value_acceleration[32]; + char ref_current[32]; + char ref_speed[32]; + char ref_acceleration[32]; + format_trend_columns(value_trend, + value_current, + sizeof(value_current), + value_speed, + sizeof(value_speed), + value_acceleration, + sizeof(value_acceleration)); + format_trend_columns(ref_trend, + ref_current, + sizeof(ref_current), + ref_speed, + sizeof(ref_speed), + ref_acceleration, + sizeof(ref_acceleration)); + std::fprintf(stderr, + " %-28s %10s %10s %10s %10s %10s %10s\n", + "count == 1", + value_current, + value_speed, + value_acceleration, + ref_current, + ref_speed, + ref_acceleration); + } +} +#endif + +void dump_stats_locked(napi_env env, bool include_string_symbol_values) +{ + std::fprintf(stderr, "NAPI LIFETIME TRACKER\n=====================\n"); + env_slot_scan scan = scan_env_slots(env); + dump_counter_header("[napi-lifetime-slots]"); + dump_metric_row("napi_value.slots_total", scan.value_slots_total, "slots.value.total"); + dump_metric_row("napi_value.active", scan.active_values, "slots.value.active"); + dump_metric_row("napi_value.tracked_active", g_lifetime.values.active, "slots.value.tracked_active"); + dump_metric_row("napi_ref.slots_total", scan.ref_slots_total, "slots.ref.total"); + dump_metric_row("napi_ref.active", scan.active_refs, "slots.ref.active"); + dump_metric_row("napi_ref.tracked_active", g_lifetime.refs.active, "slots.ref.tracked_active"); + dump_metric_row("napi_scope.slots_total", scan.scope_slots_total, "slots.scope.total"); + dump_metric_row("napi_scope.active", scan.active_scopes, "slots.scope.active"); + dump_metric_row("napi_scope.escape_value.calls", g_lifetime.scope_escape_calls, "scope.escape.calls"); + dump_metric_row("napi_scope.escape_value.succeeded", g_lifetime.scope_escape_succeeded, "scope.escape.succeeded"); + dump_metric_row("napi_scope.escape_value.failed", g_lifetime.scope_escape_failed, "scope.escape.failed"); + dump_scope_level_table(scan); + + std::fprintf(stderr, "[napi-lifetime-types]\n"); + std::fprintf(stderr, + " %-33s %10s %10s %10s %10s %10s %10s\n", + "type", + "created", + "released", + "x[i-1]", + "peak", + "speed", + "accel"); + dump_type_row("napi_value", g_lifetime.values); + dump_type_row("napi_ref", g_lifetime.refs); + dump_type_row("napi_env_cleanup_hook", g_lifetime.cleanup_hooks); + dump_type_row("napi_deferred", g_lifetime.deferreds); + dump_type_row("napi_external_backing_store_hint", g_lifetime.external_backing_store_hints); + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TAG_STATS + dump_tag_table("napi_value", g_lifetime.values.tags); + dump_tag_table("napi_ref", g_lifetime.refs.tags); +#endif +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + if (include_string_symbol_values) + { + dump_string_entries(g_lifetime.values.string_symbols); + dump_object_type_entries(g_lifetime.values.object_types, g_lifetime.refs.object_types); + } +#else + (void)include_string_symbol_values; +#endif + std::fprintf(stderr, "\n"); +} + +void dump_summary_locked(napi_env env) +{ + env_slot_scan scan = scan_env_slots(env); + std::fprintf(stderr, + "[napi-lifetime-stats] napi_value slots_total=%zu active=%zu " + "napi_ref slots_total=%zu active=%zu " + "napi_scope slots_total=%zu active=%zu\n", + scan.value_slots_total, + scan.active_values, + scan.ref_slots_total, + scan.active_refs, + scan.scope_slots_total, + scan.active_scopes); +} + +void dump_lifetime(napi_env env, const char *reason, bool include_string_symbol_values) +{ + if (reason != nullptr) + std::fprintf(stderr, "[napi-lifetime] dump env=%p reason=%s\n", env, reason); + + std::lock_guard lock(g_lifetime.mutex); + dump_stats_locked(env, include_string_symbol_values); +} + +void dump_lifetime_summary(napi_env env) +{ + std::lock_guard lock(g_lifetime.mutex); + dump_summary_locked(env); +} + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_PERIODIC_STATS +void maybe_dump_periodic_stats(napi_env env) +{ + if (env == nullptr || !periodic_stats_enabled()) + return; + + int64_t now = monotonic_milliseconds(); + bool should_dump_summary = env->should_dump_lifetime_stats(now); + bool should_dump_full = false; +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_STRING_SYMBOL_DUMP + should_dump_full = env->should_dump_lifetime_string_symbol_values(now); +#endif + + if (should_dump_full) + { + dump_lifetime(env, nullptr, true); + return; + } + + if (should_dump_summary) + dump_lifetime_summary(env); +} +#else +void maybe_dump_periodic_stats(napi_env env) +{ + (void)env; +} +#endif + +void record_value_create(value_type_stats &stats, + const void *handle, + value_snapshot snapshot) +{ + std::lock_guard lock(g_lifetime.mutex); + record_create_locked(stats, handle, std::move(snapshot), add_value_snapshot); +} + +napi_env record_value_release(value_type_stats &stats, const void *handle) +{ + std::lock_guard lock(g_lifetime.mutex); + return record_release_locked(stats, handle, remove_value_snapshot); +} + +void record_basic_create(tracked_type_stats &stats, + const void *handle, + basic_snapshot snapshot) +{ + std::lock_guard lock(g_lifetime.mutex); + record_create_locked(stats, handle, std::move(snapshot), add_basic_snapshot); +} + +napi_env record_basic_release(tracked_type_stats &stats, + const void *handle) +{ + std::lock_guard lock(g_lifetime.mutex); + return record_release_locked(stats, handle, remove_basic_snapshot); +} +} // namespace + +void napi_lifetime__::record_create(napi_scope__ *owner, napi_value__ *val) +{ + if (owner == nullptr || val == nullptr) + return; + + napi_env env = owner->env(); + value_snapshot snapshot = capture_value_snapshot(env, val->get_inner()); + record_value_create(g_lifetime.values, val, std::move(snapshot)); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_release(napi_scope__ *owner, napi_value__ *val) +{ + (void)owner; + napi_env env = record_value_release(g_lifetime.values, val); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_create(napi_env__ *owner, napi_ref__ *val) +{ + if (owner == nullptr || val == nullptr) + return; + + value_snapshot snapshot = capture_value_snapshot(owner, val->get_inner()); + napi_env env = snapshot.env; + record_value_create(g_lifetime.refs, val, std::move(snapshot)); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_release(napi_env__ *owner, napi_ref__ *val) +{ + (void)owner; + napi_env env = record_value_release(g_lifetime.refs, val); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_create(napi_env__ *owner, napi_env_cleanup_hook__ *val) +{ + if (owner == nullptr || val == nullptr) + return; + + basic_snapshot snapshot{owner}; + napi_env env = snapshot.env; + record_basic_create(g_lifetime.cleanup_hooks, val, snapshot); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_release(napi_env__ *owner, napi_env_cleanup_hook__ *val) +{ + (void)owner; + napi_env env = record_basic_release(g_lifetime.cleanup_hooks, val); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_create(napi_env__ *owner, napi_deferred__ *val) +{ + if (owner == nullptr || val == nullptr) + return; + + basic_snapshot snapshot{owner}; + napi_env env = snapshot.env; + record_basic_create(g_lifetime.deferreds, val, snapshot); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_release(napi_env__ *owner, napi_deferred__ *val) +{ + (void)owner; + napi_env env = record_basic_release(g_lifetime.deferreds, val); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_create( + napi_env__ *owner, + napi_external_backing_store_hint__ *val) +{ + if (owner == nullptr || val == nullptr) + return; + + basic_snapshot snapshot{owner}; + napi_env env = snapshot.env; + record_basic_create(g_lifetime.external_backing_store_hints, val, snapshot); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime__::record_release( + napi_env__ *owner, + napi_external_backing_store_hint__ *val) +{ + (void)owner; + napi_env env = record_basic_release(g_lifetime.external_backing_store_hints, val); + maybe_dump_periodic_stats(env); +} + +void napi_lifetime_tracker__::record_scope_escape(napi_env env, bool succeeded) +{ + { + std::lock_guard lock(g_lifetime.mutex); + ++g_lifetime.scope_escape_calls; + if (succeeded) + ++g_lifetime.scope_escape_succeeded; + else + ++g_lifetime.scope_escape_failed; + } + maybe_dump_periodic_stats(env); +} + +void napi_lifetime_tracker__::dump(napi_env env, const char *reason) +{ + if (env == nullptr || (!enabled() && !periodic_stats_enabled())) + return; + + dump_lifetime(env, reason, true); +} +} // namespace quickjs::detail + +extern "C" void napi_quickjs_lifetime_dump(napi_env env, const char *reason) +{ + quickjs::detail::napi_lifetime_tracker__::dump(env, reason); +} diff --git a/quickjs/src/internal/napi_lifetime_tracker.h b/quickjs/src/internal/napi_lifetime_tracker.h new file mode 100644 index 0000000..d9c8674 --- /dev/null +++ b/quickjs/src/internal/napi_lifetime_tracker.h @@ -0,0 +1,104 @@ +#ifndef NAPI_QUICKJS_LIFETIME_TRACKER_H_ +#define NAPI_QUICKJS_LIFETIME_TRACKER_H_ + +#include +#include + +struct napi_deferred__; +struct napi_env__; +struct napi_env_cleanup_hook__; +struct napi_external_backing_store_hint__; +struct napi_ref__; +struct napi_scope__; +struct napi_value__; + +namespace quickjs::detail +{ + +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER +template +struct napi_lifetime__; + +template +concept napi_lifetime_tracked__ = requires(Owner_ *owner, T *val) { + { napi_lifetime__::record_create(owner, val) } -> std::same_as; + { napi_lifetime__::record_release(owner, val) } -> std::same_as; +}; + +template <> +struct napi_lifetime__ +{ + static void record_create(napi_scope__ *owner, napi_value__ *val); + static void record_release(napi_scope__ *owner, napi_value__ *val); +}; + +template <> +struct napi_lifetime__ +{ + static void record_create(napi_env__ *owner, napi_ref__ *val); + static void record_release(napi_env__ *owner, napi_ref__ *val); +}; + +template <> +struct napi_lifetime__ +{ + static void record_create(napi_env__ *owner, napi_env_cleanup_hook__ *val); + static void record_release(napi_env__ *owner, napi_env_cleanup_hook__ *val); +}; + +template <> +struct napi_lifetime__ +{ + static void record_create(napi_env__ *owner, napi_deferred__ *val); + static void record_release(napi_env__ *owner, napi_deferred__ *val); +}; + +template <> +struct napi_lifetime__ +{ + static void record_create(napi_env__ *owner, napi_external_backing_store_hint__ *val); + static void record_release(napi_env__ *owner, napi_external_backing_store_hint__ *val); +}; +#else +template +concept napi_lifetime_tracked__ = true; + +template +struct napi_lifetime__ +{ + template + static void record_create(Owner_ *owner, T *val) + { + (void)owner; + (void)val; + } + + template + static void record_release(Owner_ *owner, T *val) + { + (void)owner; + (void)val; + } +}; +#endif + +class napi_lifetime_tracker__ +{ +public: +#ifdef NAPI_QUICKJS_ENABLE_LIFETIME_TRACKER + static void record_scope_escape(napi_env__ *env, bool succeeded); +#else + static void record_scope_escape(napi_env__ *env, bool succeeded) + { + (void)env; + (void)succeeded; + } +#endif + static void dump(napi_env__ *env, const char *reason); +}; + +} // namespace quickjs::detail + +extern "C" void napi_quickjs_lifetime_dump(napi_env__ *env, const char *reason); + +#endif // NAPI_QUICKJS_LIFETIME_TRACKER_H_ diff --git a/quickjs/src/internal/napi_module_wrap.cc b/quickjs/src/internal/napi_module_wrap.cc index bb20dcd..22d4b99 100644 --- a/quickjs/src/internal/napi_module_wrap.cc +++ b/quickjs/src/internal/napi_module_wrap.cc @@ -26,19 +26,19 @@ constexpr int32_t kErrored = 5; constexpr int32_t kSourcePhase = 1; constexpr int32_t kEvaluationPhase = 2; -bool is_nullish(napi_value value) +bool is_nullish(napi_env env, napi_value value) { if (value == nullptr) return true; - JSValueConst inner = value->get_inner(); + JSValueConst inner = napi_quickjs_value_inner(env, value); return JS_IsUndefined(inner) || JS_IsNull(inner); } -JSValue js_dup_or_undefined(JSContext *ctx, napi_value value) +JSValue js_dup_or_undefined(napi_env env, JSContext *ctx, napi_value value) { - if (is_nullish(value)) + if (is_nullish(env, value)) return JS_UNDEFINED; - return JS_DupValue(ctx, value->get_inner()); + return JS_DupValue(ctx, napi_quickjs_value_inner(env, value)); } std::string value_to_string(JSContext *ctx, JSValueConst value) @@ -136,6 +136,14 @@ napi_module_wrap__::napi_module_wrap__(napi_env env, JSContext *context) napi_module_wrap__::~napi_module_wrap__() { + teardown(); +} + +void napi_module_wrap__::teardown() +{ + if (torn_down_) + return; + JSRuntime *rt = JS_GetRuntime(ctx_); JS_SetModuleImportMetaInitFunc(rt, nullptr, nullptr); JS_SetModuleDynamicImportFunc(rt, nullptr, nullptr); @@ -147,7 +155,10 @@ napi_module_wrap__::~napi_module_wrap__() script_referrers_.clear(); JS_FreeValue(ctx_, import_module_dynamically_callback_); + import_module_dynamically_callback_ = JS_UNDEFINED; JS_FreeValue(ctx_, initialize_import_meta_callback_); + initialize_import_meta_callback_ = JS_UNDEFINED; + torn_down_ = true; } napi_module_wrap__::record *napi_module_wrap__::find(void *handle) const @@ -282,8 +293,8 @@ JSValue napi_module_wrap__::get_or_create_host_defined_option(napi_value wrapper const char *description) const { JSValue value = JS_UNDEFINED; - if (!is_nullish(candidate) && JS_IsSymbol(candidate->get_inner())) - value = JS_DupValue(ctx_, candidate->get_inner()); + if (!is_nullish(env_, candidate) && JS_IsSymbol(napi_quickjs_value_inner(env_, candidate))) + value = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, candidate)); else value = JS_NewSymbol(ctx_, description, false); set_host_defined_option(wrapper, value); @@ -292,7 +303,7 @@ JSValue napi_module_wrap__::get_or_create_host_defined_option(napi_value wrapper void napi_module_wrap__::set_host_defined_option(napi_value wrapper, JSValueConst value) const { - if (wrapper == nullptr || !JS_IsObject(wrapper->get_inner())) + if (wrapper == nullptr || !JS_IsObject(napi_quickjs_value_inner(env_, wrapper))) return; JSValue symbol = get_host_defined_option_symbol(); @@ -301,12 +312,12 @@ void napi_module_wrap__::set_host_defined_option(napi_value wrapper, JSValueCons JSAtom atom = JS_ValueToAtom(ctx_, symbol); if (atom != JS_ATOM_NULL) { - JS_SetProperty(ctx_, wrapper->get_inner(), atom, JS_DupValue(ctx_, value)); + JS_SetProperty(ctx_, napi_quickjs_value_inner(env_, wrapper), atom, JS_DupValue(ctx_, value)); JS_FreeAtom(ctx_, atom); } } JS_FreeValue(ctx_, symbol); - JS_SetPropertyStr(ctx_, wrapper->get_inner(), "contextifyHostDefinedOptionId", + JS_SetPropertyStr(ctx_, napi_quickjs_value_inner(env_, wrapper), "contextifyHostDefinedOptionId", JS_DupValue(ctx_, value)); } @@ -483,7 +494,7 @@ napi_status napi_module_wrap__::create_source_text(napi_value wrapper, entry->owner = this; entry->module = module; entry->module_value = module_value; - entry->wrapper = JS_DupValue(ctx_, wrapper->get_inner()); + entry->wrapper = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, wrapper)); entry->host_defined_option_id = host_id; records_.push_back(entry); @@ -494,9 +505,9 @@ napi_status napi_module_wrap__::create_source_text(napi_value wrapper, return status; } - JS_SetPropertyStr(ctx_, wrapper->get_inner(), "sourceURL", + JS_SetPropertyStr(ctx_, napi_quickjs_value_inner(env_, wrapper), "sourceURL", JS_NewString(ctx_, url_string.c_str())); - JS_SetPropertyStr(ctx_, wrapper->get_inner(), "sourceMapURL", JS_UNDEFINED); + JS_SetPropertyStr(ctx_, napi_quickjs_value_inner(env_, wrapper), "sourceMapURL", JS_UNDEFINED); *handle_out = entry; return napi_ok; } @@ -525,11 +536,11 @@ napi_status napi_module_wrap__::create_synthetic(napi_value wrapper, return return_pending_exception("Failed to create synthetic module"); int64_t length = 0; - if (JS_GetLength(ctx_, export_names->get_inner(), &length) < 0) + if (JS_GetLength(ctx_, napi_quickjs_value_inner(env_, export_names), &length) < 0) return return_pending_exception("Failed to read synthetic export names"); for (int64_t i = 0; i < length; ++i) { - JSValue name_value = JS_GetPropertyUint32(ctx_, export_names->get_inner(), + JSValue name_value = JS_GetPropertyUint32(ctx_, napi_quickjs_value_inner(env_, export_names), static_cast(i)); if (JS_IsException(name_value)) return return_pending_exception("Failed to read synthetic export name"); @@ -545,9 +556,9 @@ napi_status napi_module_wrap__::create_synthetic(napi_value wrapper, entry->owner = this; entry->module = module; entry->module_value = JS_DupModuleValue(ctx_, module); - entry->wrapper = JS_DupValue(ctx_, wrapper->get_inner()); + entry->wrapper = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, wrapper)); entry->host_defined_option_id = JS_UNDEFINED; - entry->synthetic_eval_steps = JS_DupValue(ctx_, synthetic_eval_steps->get_inner()); + entry->synthetic_eval_steps = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, synthetic_eval_steps)); entry->synthetic = true; records_.push_back(entry); @@ -808,14 +819,14 @@ napi_status napi_module_wrap__::set_export(void *handle, !napi_util__::check_value(env_, export_name) || !napi_util__::check_value(env_, export_value)) return napi_util__::invalid_arg(env_); - if (!JS_IsString(export_name->get_inner())) + if (!JS_IsString(napi_quickjs_value_inner(env_, export_name))) { env_->set_last_exception(JS_NewTypeError(ctx_, "Synthetic module export name must be a string")); return napi_pending_exception; } std::string name = napi_util__::to_utf8(env_, export_name); if (JS_SetModuleExport(ctx_, entry->module, name.c_str(), - JS_DupValue(ctx_, export_value->get_inner())) < 0) + JS_DupValue(ctx_, napi_quickjs_value_inner(env_, export_value))) < 0) { env_->set_last_exception(JS_NewReferenceError(ctx_, "Synthetic module export is not defined")); return napi_pending_exception; @@ -830,7 +841,7 @@ napi_status napi_module_wrap__::set_module_source_object(void *handle, if (entry == nullptr) return napi_util__::invalid_arg(env_); JS_FreeValue(ctx_, entry->source_object); - entry->source_object = js_dup_or_undefined(ctx_, source_object); + entry->source_object = js_dup_or_undefined(env_, ctx_, source_object); return napi_ok; } @@ -863,8 +874,8 @@ napi_status napi_module_wrap__::set_import_module_dynamically_callback(napi_valu { JS_FreeValue(ctx_, import_module_dynamically_callback_); import_module_dynamically_callback_ = - (!is_nullish(callback) && JS_IsFunction(ctx_, callback->get_inner())) - ? JS_DupValue(ctx_, callback->get_inner()) + (!is_nullish(env_, callback) && JS_IsFunction(ctx_, napi_quickjs_value_inner(env_, callback))) + ? JS_DupValue(ctx_, napi_quickjs_value_inner(env_, callback)) : JS_UNDEFINED; return napi_ok; } @@ -873,8 +884,8 @@ napi_status napi_module_wrap__::set_initialize_import_meta_object_callback(napi_ { JS_FreeValue(ctx_, initialize_import_meta_callback_); initialize_import_meta_callback_ = - (!is_nullish(callback) && JS_IsFunction(ctx_, callback->get_inner())) - ? JS_DupValue(ctx_, callback->get_inner()) + (!is_nullish(env_, callback) && JS_IsFunction(ctx_, napi_quickjs_value_inner(env_, callback))) + ? JS_DupValue(ctx_, napi_quickjs_value_inner(env_, callback)) : JS_UNDEFINED; return napi_ok; } @@ -892,15 +903,15 @@ napi_status napi_module_wrap__::import_module_dynamically(size_t argc, if (argc >= 5) { for (size_t i = 0; i < 5; ++i) - args[i] = js_dup_or_undefined(ctx_, argv[i]); + args[i] = js_dup_or_undefined(env_, ctx_, argv[i]); } else { args[0] = get_symbols_binding_property("vm_dynamic_import_default_internal"); - args[1] = argc >= 1 ? js_dup_or_undefined(ctx_, argv[0]) : JS_UNDEFINED; + args[1] = argc >= 1 ? js_dup_or_undefined(env_, ctx_, argv[0]) : JS_UNDEFINED; args[2] = JS_NewInt32(ctx_, kEvaluationPhase); args[3] = create_frozen_null_proto_object(); - args[4] = argc >= 2 ? js_dup_or_undefined(ctx_, argv[1]) : JS_UNDEFINED; + args[4] = argc >= 2 ? js_dup_or_undefined(env_, ctx_, argv[1]) : JS_UNDEFINED; } JSValue result = call_callback(import_module_dynamically_callback_, 5, args); @@ -982,8 +993,8 @@ napi_status napi_module_wrap__::create_required_module_facade(void *handle, void napi_module_wrap__::register_dynamic_import_referrer(napi_value referrer_name, napi_value host_defined_option_id) { - if (is_nullish(referrer_name) || is_nullish(host_defined_option_id) || - !JS_IsSymbol(host_defined_option_id->get_inner())) + if (is_nullish(env_, referrer_name) || is_nullish(env_, host_defined_option_id) || + !JS_IsSymbol(napi_quickjs_value_inner(env_, host_defined_option_id))) return; std::string name = napi_util__::to_utf8(env_, referrer_name); @@ -995,14 +1006,14 @@ void napi_module_wrap__::register_dynamic_import_referrer(napi_value referrer_na if (referrer.name == name) { JS_FreeValue(ctx_, referrer.host_defined_option_id); - referrer.host_defined_option_id = JS_DupValue(ctx_, host_defined_option_id->get_inner()); + referrer.host_defined_option_id = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, host_defined_option_id)); return; } } script_referrer referrer; referrer.name = std::move(name); - referrer.host_defined_option_id = JS_DupValue(ctx_, host_defined_option_id->get_inner()); + referrer.host_defined_option_id = JS_DupValue(ctx_, napi_quickjs_value_inner(env_, host_defined_option_id)); script_referrers_.push_back(std::move(referrer)); } diff --git a/quickjs/src/internal/napi_module_wrap.h b/quickjs/src/internal/napi_module_wrap.h index 5cc5cd1..48f03bb 100644 --- a/quickjs/src/internal/napi_module_wrap.h +++ b/quickjs/src/internal/napi_module_wrap.h @@ -18,6 +18,8 @@ class napi_module_wrap__ napi_module_wrap__(napi_env env, JSContext *context); ~napi_module_wrap__(); + void teardown(); + napi_status create_source_text(napi_value wrapper, napi_value url, napi_value context_or_undefined, @@ -115,13 +117,21 @@ class napi_module_wrap__ JSModuleImportPhaseEnum phase, void *opaque); + // Owning environment and QuickJS context. napi_env env_; JSContext *ctx_; + + // Host module callbacks. JSValue import_module_dynamically_callback_; JSValue initialize_import_meta_callback_; + + // Module records and referrer metadata. std::vector records_; std::vector script_referrers_; uint64_t facade_counter_ = 0; + + // Subsystem lifecycle. + bool torn_down_ = false; }; } // namespace quickjs::detail diff --git a/quickjs/src/internal/napi_promises.cc b/quickjs/src/internal/napi_promises.cc index 9e59d34..077d8c5 100644 --- a/quickjs/src/internal/napi_promises.cc +++ b/quickjs/src/internal/napi_promises.cc @@ -1,6 +1,7 @@ #include "internal/napi_promises.h" #include "internal/napi_env.h" +#include "internal/napi_value.h" namespace { @@ -21,7 +22,15 @@ napi_promises__::napi_promises__(napi_env env, JSContext *context) napi_promises__::~napi_promises__() { + teardown(); +} + +void napi_promises__::teardown() +{ + if (torn_down_) + return; clear_stored_values(); + torn_down_ = true; } napi_status napi_promises__::set_optional_function(napi_value value, JSValue *target) @@ -30,19 +39,19 @@ napi_status napi_promises__::set_optional_function(napi_value value, JSValue *ta return napi_invalid_arg; if (value != nullptr && - !JS_IsUndefined(value->get_inner()) && - !JS_IsNull(value->get_inner()) && - !JS_IsFunction(context_, value->get_inner())) + !JS_IsUndefined(napi_quickjs_value_inner(env_, value)) && + !JS_IsNull(napi_quickjs_value_inner(env_, value)) && + !JS_IsFunction(context_, napi_quickjs_value_inner(env_, value))) { return napi_function_expected; } JSValue replacement = JS_UNDEFINED; if (value != nullptr && - !JS_IsUndefined(value->get_inner()) && - !JS_IsNull(value->get_inner())) + !JS_IsUndefined(napi_quickjs_value_inner(env_, value)) && + !JS_IsNull(napi_quickjs_value_inner(env_, value))) { - replacement = JS_DupValue(context_, value->get_inner()); + replacement = JS_DupValue(context_, napi_quickjs_value_inner(env_, value)); } JS_FreeValue(context_, *target); @@ -132,18 +141,18 @@ napi_status napi_promises__::set_hooks(napi_value init, { napi_value callback = callbacks[i]; if (callback == nullptr || - JS_IsUndefined(callback->get_inner()) || - JS_IsNull(callback->get_inner())) + JS_IsUndefined(napi_quickjs_value_inner(env_, callback)) || + JS_IsNull(napi_quickjs_value_inner(env_, callback))) { continue; } - if (!JS_IsFunction(context_, callback->get_inner())) + if (!JS_IsFunction(context_, napi_quickjs_value_inner(env_, callback))) { for (JSValue replacement : replacements) JS_FreeValue(context_, replacement); return napi_function_expected; } - replacements[i] = JS_DupValue(context_, callback->get_inner()); + replacements[i] = JS_DupValue(context_, napi_quickjs_value_inner(env_, callback)); } for (size_t i = 0; i < 4; ++i) diff --git a/quickjs/src/internal/napi_promises.h b/quickjs/src/internal/napi_promises.h index 064bc35..0713b9e 100644 --- a/quickjs/src/internal/napi_promises.h +++ b/quickjs/src/internal/napi_promises.h @@ -16,6 +16,8 @@ class napi_promises__ napi_promises__(napi_env env, JSContext *context); ~napi_promises__(); + void teardown(); + napi_status set_reject_callback(napi_value callback); bool has_reject_callback() const; JSValue dup_reject_callback() const; @@ -51,13 +53,21 @@ class napi_promises__ void call_hook(JSValueConst hook, int argc, JSValueConst *argv); void clear_pending_exception_if_any(); + // Owning environment and QuickJS context. napi_env env_ = nullptr; JSContext *context_ = nullptr; + + // Promise reject callback and lifecycle hooks. JSValue promise_reject_callback_; std::array promise_hooks_; + + // Continuation and async context tracking. JSValue continuation_preserved_embedder_data_; std::unordered_map promise_context_frames_; std::vector promise_context_frame_stack_; + + // Subsystem lifecycle. + bool torn_down_ = false; }; #endif // NAPI_QUICKJS_INTERNAL_NAPI_PROMISES_H_ diff --git a/quickjs/src/internal/napi_ref.cc b/quickjs/src/internal/napi_ref.cc index a82b044..5cfd8c6 100644 --- a/quickjs/src/internal/napi_ref.cc +++ b/quickjs/src/internal/napi_ref.cc @@ -2,8 +2,6 @@ #include "internal/napi_env.h" -#include - namespace { bool IsEmptyValue(JSValueConst value) @@ -20,43 +18,73 @@ bool SameRefCountedValue(JSValueConst left, JSValueConst right) } } // namespace -napi_ref__::napi_ref__(napi_env env, JSValueConst value, uint32_t initial_ref_count) - : env_(env), - value_(value), - can_be_weak_(JS_VALUE_HAS_REF_COUNT(value)), - ref_count_(initial_ref_count) +napi_ref__::napi_ref__(napi_ref__ &&other) noexcept + : env_(other.env_), + value_(other.value_), + can_be_weak_(other.can_be_weak_), + ref_count_(other.ref_count_) { - if (ref_count_ > 0) - JS_DupValue(env_->context(), value_); + other.env_ = nullptr; + other.value_ = JS_UNDEFINED; + other.can_be_weak_ = false; + other.ref_count_ = 0; +} + +napi_ref__ &napi_ref__::operator=(napi_ref__ &&other) noexcept +{ + if (this == &other) + return *this; + + release(); + env_ = other.env_; + value_ = other.value_; + can_be_weak_ = other.can_be_weak_; + ref_count_ = other.ref_count_; + other.env_ = nullptr; + other.value_ = JS_UNDEFINED; + other.can_be_weak_ = false; + other.ref_count_ = 0; + return *this; } napi_ref__::~napi_ref__() { - if (env_ != nullptr && env_->context() != nullptr && ref_count_ > 0) - JS_FreeValue(env_->context(), value_); + release(); } -napi_ref__ *napi_ref__::create(napi_env env, JSValueConst value, uint32_t initial_ref_count) +void napi_ref__::initialize(napi_env env, + JSValueConst value, + uint32_t initial_ref_count) { - if (env == nullptr || env->context() == nullptr) - return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_ref__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_ref__(env, value, initial_ref_count); + release(); + env_ = env; + value_ = value; + can_be_weak_ = JS_VALUE_HAS_REF_COUNT(value); + ref_count_ = initial_ref_count; + if (ref_count_ > 0) + JS_DupValue(env_->context(), value_); } -void napi_ref__::destroy(napi_ref__ *ref) +void napi_ref__::release() { - if (ref == nullptr) + if (env_ == nullptr) return; - napi_env env = ref->env_; - ref->~napi_ref__(); - if (env != nullptr && env->context() != nullptr) - js_free(env->context(), ref); + napi_env env = env_; + JSValue value = value_; + uint32_t ref_count = ref_count_; + env_ = nullptr; + value_ = JS_UNDEFINED; + can_be_weak_ = false; + ref_count_ = 0; + + if (env != nullptr && env->context() != nullptr && ref_count > 0) + JS_FreeValue(env->context(), value); +} + +bool napi_ref__::is_active() const +{ + return env_ != nullptr; } uint32_t napi_ref__::add_ref() @@ -76,13 +104,20 @@ uint32_t napi_ref__::rem_ref() return ref_count_; --ref_count_; - if (ref_count_ == 0) + uint32_t ref_count = ref_count_; + if (ref_count == 0) { - JS_FreeValue(env_->context(), value_); - if (!can_be_weak_) + napi_env env = env_; + JSValue value = value_; + bool can_be_weak = can_be_weak_; + if (!can_be_weak) + { value_ = JS_UNDEFINED; + } + if (env != nullptr && env->context() != nullptr) + JS_FreeValue(env->context(), value); } - return ref_count_; + return ref_count; } uint32_t napi_ref__::ref_count() const @@ -105,6 +140,11 @@ bool napi_ref__::is_weak() const return can_be_weak_ && ref_count_ == 0 && !is_empty(); } +napi_env napi_ref__::env() const +{ + return env_; +} + JSValueConst napi_ref__::get_inner() const { return value_; @@ -118,5 +158,14 @@ JSValue napi_ref__::dup_inner() const void napi_ref__::clear_if_matches(JSValueConst value) { if (is_weak() && SameRefCountedValue(value_, value)) + { value_ = JS_UNDEFINED; + } +} + +napi_ref__ *napi_quickjs_ref_slot(napi_env env, napi_ref ref) +{ + if (env == nullptr || ref == nullptr) + return nullptr; + return env->ref_from_root_scope(ref); } diff --git a/quickjs/src/internal/napi_ref.h b/quickjs/src/internal/napi_ref.h index c78ba94..2bfef5c 100644 --- a/quickjs/src/internal/napi_ref.h +++ b/quickjs/src/internal/napi_ref.h @@ -3,33 +3,45 @@ #include "../../../include/js_native_api.h" +#include #include #include struct napi_ref__ { - static napi_ref__ *create(napi_env env, JSValueConst value, uint32_t initial_ref_count); - static void destroy(napi_ref__ *ref); - + napi_ref__() = default; + napi_ref__(const napi_ref__ &) = delete; + napi_ref__ &operator=(const napi_ref__ &) = delete; + napi_ref__(napi_ref__ &&other) noexcept; + napi_ref__ &operator=(napi_ref__ &&other) noexcept; ~napi_ref__(); + void initialize(napi_env env, + JSValueConst value, + uint32_t initial_ref_count); + void release(); + bool is_active() const; uint32_t add_ref(); uint32_t rem_ref(); uint32_t ref_count() const; bool can_be_weak() const; bool is_empty() const; bool is_weak() const; + napi_env env() const; JSValueConst get_inner() const; JSValue dup_inner() const; void clear_if_matches(JSValueConst value); private: - napi_ref__(napi_env env, JSValueConst value, uint32_t initial_ref_count); + // Owning environment and referenced QuickJS value. + napi_env env_ = nullptr; + JSValue value_ = JS_UNDEFINED; - napi_env env_; - JSValue value_; - bool can_be_weak_; - uint32_t ref_count_; + // Reference strength. + bool can_be_weak_ = false; + uint32_t ref_count_ = 0; }; +napi_ref__ *napi_quickjs_ref_slot(napi_env env, napi_ref ref); + #endif // NAPI_QUICKJS_REF_H_ diff --git a/quickjs/src/internal/napi_scope.cc b/quickjs/src/internal/napi_scope.cc index 88f9677..cb5107a 100644 --- a/quickjs/src/internal/napi_scope.cc +++ b/quickjs/src/internal/napi_scope.cc @@ -1,44 +1,78 @@ #include "internal/napi_scope.h" #include "internal/napi_env.h" -#include "internal/napi_value.h" +#include "internal/napi_lifetime_tracker.h" -#include - -napi_scope__::napi_scope__(napi_env env, napi_scope__ *parent) - : env_(env), - parent_(parent) +napi_scope__::napi_scope__() : values_(this) { } napi_scope__::~napi_scope__() { - close(); + release(); } -napi_scope__ *napi_scope__::create(napi_env env, napi_scope__ *parent) +napi_scope__::napi_scope__(napi_scope__ &&other) noexcept { - if (env == nullptr || env->context() == nullptr) - return nullptr; + *this = static_cast(other); +} - void *memory = js_mallocz(env->context(), sizeof(napi_scope__)); - if (memory == nullptr) - return nullptr; +napi_scope__ &napi_scope__::operator=(napi_scope__ &&other) noexcept +{ + if (this == &other) + return *this; + + release(); + env_ = other.env_; + level_ = other.level_; + parent_ = other.parent_; + values_ = static_cast &&>(other.values_); + values_.set_owner(this); + closed_ = other.closed_; + escaped_ = other.escaped_; + other.env_ = nullptr; + other.level_ = 0; + other.parent_ = nullptr; + other.closed_ = true; + other.escaped_ = false; + return *this; +} - return new (memory) napi_scope__(env, parent); +void napi_scope__::initialize(napi_env env, napi_handle_scope parent) +{ + release(); + env_ = env; + values_.set_owner(this); + parent_ = parent; + closed_ = false; + escaped_ = false; + + napi_scope__ *parent_scope = this->parent(); + level_ = parent_scope == nullptr ? 0 : parent_scope->level() + 1; + if (parent_scope != nullptr) + values_.reserve_prefix(parent_scope->value_slot_count()); } -void napi_scope__::destroy(napi_scope__ *scope) +void napi_scope__::release() { - if (scope == nullptr) + if (env_ == nullptr) return; - napi_env env = scope->env_; - scope->~napi_scope__(); - if (env != nullptr && env->context() != nullptr) - { - js_free(env->context(), scope); - } + close(); + env_ = nullptr; + level_ = 0; + parent_ = nullptr; + escaped_ = false; +} + +bool napi_scope__::is_active() const +{ + return env_ != nullptr; +} + +size_t napi_scope__::level() const +{ + return level_; } napi_value napi_scope__::wrap_value(JSValue value, bool owned) @@ -50,7 +84,7 @@ napi_value napi_scope__::wrap_value(JSValue value, bool owned) return nullptr; } - napi_value wrapped = napi_value__::create(env_, value, owned); + napi_value wrapped = values_.allocate(env_, value, owned); if (wrapped == nullptr) { if (owned && env_ != nullptr && env_->context() != nullptr) @@ -58,16 +92,24 @@ napi_value napi_scope__::wrap_value(JSValue value, bool owned) return nullptr; } - values_.push_back(wrapped); return wrapped; } napi_value napi_scope__::escape_value(napi_value value) { + auto record_escape = [this](napi_value escaped) { + quickjs::detail::napi_lifetime_tracker__::record_scope_escape(env_, escaped != nullptr); + return escaped; + }; + if (parent_ == nullptr || value == nullptr) - return nullptr; + return record_escape(nullptr); - return parent_->wrap_value(value->get_inner(), false); + napi_value__ *slot = value_from_handle(value); + if (slot == nullptr) + return record_escape(nullptr); + napi_scope__ *parent_scope = parent(); + return record_escape(parent_scope == nullptr ? nullptr : parent_scope->wrap_value(slot->get_inner(), false)); } void napi_scope__::delete_value(napi_value value) @@ -75,36 +117,66 @@ void napi_scope__::delete_value(napi_value value) if (value == nullptr) return; - for (auto it = values_.begin(); it != values_.end(); ++it) + if (values_.get(value) != nullptr) { - if (*it == value) - { - values_.erase(it); - napi_value__::destroy(value); - break; - } + values_.release(value); } } +napi_value__ *napi_scope__::value_from_handle(napi_value value) +{ + napi_value__ *slot = values_.get(value); + if (slot != nullptr) + return slot; + napi_scope__ *parent_scope = parent(); + return parent_scope == nullptr ? nullptr : parent_scope->value_from_handle(value); +} + void napi_scope__::close() { if (closed_) return; - for (auto it = values_.rbegin(); it != values_.rend(); ++it) - { - napi_value__::destroy(*it); - } - values_.clear(); + values_.close(); closed_ = true; } -napi_scope__ *napi_scope__::parent() const +size_t napi_scope__::value_slot_count() const +{ + return values_.slot_count(); +} + +size_t napi_scope__::value_storage_slot_count() const +{ + return values_.storage_slot_count(); +} + +size_t napi_scope__::active_value_count() const +{ + return values_.active_count(); +} + +napi_handle_scope napi_scope__::parent_handle() const { return parent_; } +napi_scope__ *napi_scope__::parent() const +{ + return env_ == nullptr ? nullptr : env_->scope_from_handle(parent_); +} + napi_env napi_scope__::env() const { return env_; } + +bool napi_scope__::has_escaped() const +{ + return escaped_; +} + +void napi_scope__::mark_escaped() +{ + escaped_ = true; +} diff --git a/quickjs/src/internal/napi_scope.h b/quickjs/src/internal/napi_scope.h index 994e33f..919a2f6 100644 --- a/quickjs/src/internal/napi_scope.h +++ b/quickjs/src/internal/napi_scope.h @@ -2,33 +2,63 @@ #define NAPI_QUICKJS_SCOPE_H_ #include "../../../include/js_native_api.h" +#include "napi_allocator.h" +#include "napi_value.h" +#include #include #include struct napi_scope__ { - static napi_scope__ *create(napi_env env, napi_scope__ *parent); - static void destroy(napi_scope__ *scope); - ~napi_scope__(); + void initialize(napi_env env, napi_handle_scope parent); + void release(); + bool is_active() const; + size_t level() const; + napi_value wrap_value(JSValue value, bool owned); napi_value escape_value(napi_value value); void delete_value(napi_value value); + napi_value__ *value_from_handle(napi_value value); + void close(); + size_t value_slot_count() const; + size_t value_storage_slot_count() const; + size_t active_value_count() const; + napi_handle_scope parent_handle() const; napi_scope__ *parent() const; napi_env env() const; + bool has_escaped() const; + void mark_escaped(); + + template + void for_each_active_value(Fn fn) const + { + values_.for_each_active(fn); + } -protected: - napi_scope__(napi_env env, napi_scope__ *parent); + napi_scope__(); + napi_scope__(napi_scope__ &&other) noexcept; + napi_scope__ &operator=(napi_scope__ &&other) noexcept; + + napi_scope__(const napi_scope__ &) = delete; + napi_scope__ &operator=(const napi_scope__ &) = delete; private: - napi_env env_; - napi_scope__ *parent_; - std::vector values_; + // Scope hierarchy. + napi_env env_ = nullptr; + size_t level_ = 0; + napi_handle_scope parent_ = nullptr; + + // Local value storage. + napi_allocator__ values_; + + // Scope lifecycle flags. bool closed_ = false; + bool escaped_ = false; }; #endif // NAPI_QUICKJS_SCOPE_H_ diff --git a/quickjs/src/internal/napi_serdes.cc b/quickjs/src/internal/napi_serdes.cc index 1741df6..d398111 100644 --- a/quickjs/src/internal/napi_serdes.cc +++ b/quickjs/src/internal/napi_serdes.cc @@ -41,7 +41,7 @@ namespace quickjs::detail return false; JSContext *ctx = napi_util__::context(env); - JSValueConst input = value->get_inner(); + JSValueConst input = napi_quickjs_value_inner(env, value); uint8_t *data = nullptr; size_t length = 0; @@ -260,7 +260,7 @@ namespace quickjs::detail size_t size = 0; uint8_t *bytes = JS_WriteObject(napi_util__::context(env), &size, - value->get_inner(), + napi_quickjs_value_inner(env, value), JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE); if (bytes == nullptr) { @@ -669,7 +669,7 @@ namespace quickjs::detail size_t size = 0; uint8_t *bytes = JS_WriteObject(napi_util__::context(env), &size, - value->get_inner(), + napi_quickjs_value_inner(env, value), JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE); if (bytes == nullptr) return napi_generic_failure; diff --git a/quickjs/src/internal/napi_util.cc b/quickjs/src/internal/napi_util.cc index b080d38..3675ab2 100644 --- a/quickjs/src/internal/napi_util.cc +++ b/quickjs/src/internal/napi_util.cc @@ -3,6 +3,7 @@ #include "internal/napi_env.h" #include "internal/napi_value.h" +#include #include #include #include @@ -59,7 +60,7 @@ bool napi_util__::check_env(napi_env env) bool napi_util__::check_value(napi_env env, napi_value value) { - return check_env(env) && value != nullptr; + return check_env(env) && value != nullptr && napi_quickjs_value_slot(env, value) != nullptr; } JSContext *napi_util__::context(napi_env env) @@ -269,7 +270,7 @@ std::string napi_util__::to_utf8(napi_env env, napi_value value) { if (!check_env(env) || value == nullptr) return {}; - return to_utf8(context(env), value->get_inner()); + return to_utf8(context(env), napi_quickjs_value_inner(env, value)); } std::string napi_util__::to_utf8(JSContext *ctx, JSValueConst value) @@ -295,7 +296,7 @@ void napi_util__::set_string_property(JSContext *ctx, bool napi_util__::is_truthy_property(napi_env env, napi_value object, const char *name) { JSContext *ctx = context(env); - JSValue prop = JS_GetPropertyStr(ctx, object->get_inner(), name); + JSValue prop = JS_GetPropertyStr(ctx, napi_quickjs_value_inner(env, object), name); if (JS_IsException(prop)) return false; bool out = JS_ToBool(ctx, prop); @@ -310,7 +311,7 @@ napi_status napi_util__::wrap_owned(napi_env env, JSValue value, napi_value *res JS_FreeValue(context(env), value); return napi_invalid_arg; } - *result = env->current_scope()->wrap_value(value, true); + *result = env->wrap_value_in_current_scope(value, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -338,7 +339,19 @@ napi_value napi_util__::undefined_value(napi_env env) bool napi_util__::is_callable(napi_env env, napi_value value) { - return value != nullptr && JS_IsFunction(context(env), value->get_inner()); + return value != nullptr && JS_IsFunction(context(env), napi_quickjs_value_inner(env, value)); +} + +std::vector napi_util__::prepare_call_args(napi_env env, size_t argc, const napi_value *argv) +{ + std::vector js_argv(argc); + if (argc > 0) + { + std::transform(argv, argv + argc, js_argv.begin(), [env](napi_value value) { + return napi_quickjs_value_inner(env, value); + }); + } + return js_argv; } napi_status napi_util__::run_pending_jobs(napi_env env) @@ -604,7 +617,7 @@ napi_status napi_util__::get_property_names(napi_env env, return invalid_arg(env); JSContext *ctx = env->context(); - JSValue obj = object->get_inner(); + JSValue obj = napi_quickjs_value_inner(env, object); if (!JS_IsObject(obj)) return napi_object_expected; @@ -710,7 +723,7 @@ napi_status napi_util__::get_property_names(napi_env env, JS_FreeValue(ctx, proto); } - *result = env->current_scope()->wrap_value(arr, true); + *result = env->wrap_value_in_current_scope(arr, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -734,7 +747,7 @@ napi_status napi_util__::create_plain_error_common(napi_env env, if (!check_env(env) || msg == nullptr || result == nullptr) return napi_invalid_arg; - JSValue msg_val = msg->get_inner(); + JSValue msg_val = napi_quickjs_value_inner(env, msg); if (!JS_IsString(msg_val)) return napi_string_expected; @@ -742,14 +755,14 @@ napi_status napi_util__::create_plain_error_common(napi_env env, JSValue error = create_plain_error(env->context(), msg_str); JS_FreeCString(env->context(), msg_str); - if (code != nullptr && !JS_IsUndefined(code->get_inner()) && !JS_IsNull(code->get_inner())) + if (code != nullptr && !JS_IsUndefined(napi_quickjs_value_inner(env, code)) && !JS_IsNull(napi_quickjs_value_inner(env, code))) { - const char *code_str = JS_ToCString(env->context(), code->get_inner()); + const char *code_str = JS_ToCString(env->context(), napi_quickjs_value_inner(env, code)); JS_SetPropertyStr(env->context(), error, "code", JS_NewString(env->context(), code_str)); JS_FreeCString(env->context(), code_str); } - *result = env->current_scope()->wrap_value(error, true); + *result = env->wrap_value_in_current_scope(error, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -774,14 +787,14 @@ napi_status napi_util__::create_error_common(napi_env env, if (!check_env(env) || msg == nullptr || result == nullptr) return napi_invalid_arg; - JSValue msg_val = msg->get_inner(); + JSValue msg_val = napi_quickjs_value_inner(env, msg); if (!JS_IsString(msg_val)) return napi_string_expected; const char *msg_str = JS_ToCString(env->context(), msg_val); const char *code_str = nullptr; - if (code != nullptr && !JS_IsUndefined(code->get_inner()) && !JS_IsNull(code->get_inner())) - code_str = JS_ToCString(env->context(), code->get_inner()); + if (code != nullptr && !JS_IsUndefined(napi_quickjs_value_inner(env, code)) && !JS_IsNull(napi_quickjs_value_inner(env, code))) + code_str = JS_ToCString(env->context(), napi_quickjs_value_inner(env, code)); JSValue error = create_error_object(env->context(), factory, code_str, msg_str); @@ -789,6 +802,6 @@ napi_status napi_util__::create_error_common(napi_env env, if (code_str != nullptr) JS_FreeCString(env->context(), code_str); - *result = env->current_scope()->wrap_value(error, true); + *result = env->wrap_value_in_current_scope(error, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } diff --git a/quickjs/src/internal/napi_util.h b/quickjs/src/internal/napi_util.h index 4dc7715..0f0433a 100644 --- a/quickjs/src/internal/napi_util.h +++ b/quickjs/src/internal/napi_util.h @@ -45,6 +45,7 @@ class napi_util__ static napi_status create_undefined(napi_env env, napi_value *result); static napi_value undefined_value(napi_env env); static bool is_callable(napi_env env, napi_value value); + static std::vector prepare_call_args(napi_env env, size_t argc, const napi_value *argv); static napi_status run_pending_jobs(napi_env env); static JSValue get_constructor_name_value(napi_env env, JSValueConst value); static napi_status unsupported_if_valid_env(napi_env env); diff --git a/quickjs/src/internal/napi_value.cc b/quickjs/src/internal/napi_value.cc index f08104a..9fa2440 100644 --- a/quickjs/src/internal/napi_value.cc +++ b/quickjs/src/internal/napi_value.cc @@ -1,49 +1,79 @@ #include "internal/napi_value.h" #include "internal/napi_env.h" +#include "internal/napi_scope.h" -#include +napi_value__::napi_value__(napi_value__ &&other) noexcept + : env_(other.env_), + value_(other.value_) +{ + other.env_ = nullptr; + other.value_ = JS_UNDEFINED; +} -napi_value__::napi_value__(napi_env env, JSValue value, bool owned) - : env_(env), - value_(owned ? value : JS_DupValue(env->context(), value)) +napi_value__ &napi_value__::operator=(napi_value__ &&other) noexcept { + if (this == &other) + return *this; + + release(); + env_ = other.env_; + value_ = other.value_; + other.env_ = nullptr; + other.value_ = JS_UNDEFINED; + return *this; } napi_value__::~napi_value__() { - if (env_ != nullptr && env_->context() != nullptr) - { - JS_FreeValue(env_->context(), value_); - } + release(); } -napi_value__ *napi_value__::create(napi_env env, JSValue value, bool owned) +void napi_value__::initialize(napi_env env, JSValue value, bool owned) { - if (env == nullptr || env->context() == nullptr) - return nullptr; - - void *memory = js_mallocz(env->context(), sizeof(napi_value__)); - if (memory == nullptr) - return nullptr; - - return new (memory) napi_value__(env, value, owned); + release(); + env_ = env; + value_ = owned ? value : JS_DupValue(env->context(), value); } -void napi_value__::destroy(napi_value__ *value) +void napi_value__::release() { - if (value == nullptr) + if (env_ == nullptr) return; - napi_env env = value->env_; - value->~napi_value__(); + napi_env env = env_; + JSValue value = value_; + env_ = nullptr; + value_ = JS_UNDEFINED; + if (env != nullptr && env->context() != nullptr) - { - js_free(env->context(), value); - } + JS_FreeValue(env->context(), value); +} + +bool napi_value__::is_active() const +{ + return env_ != nullptr; +} + +napi_env napi_value__::env() const +{ + return env_; } JSValueConst napi_value__::get_inner() const { return value_; } + +napi_value__ *napi_quickjs_value_slot(napi_env env, napi_value value) +{ + if (env == nullptr || value == nullptr || env->current_scope() == nullptr) + return nullptr; + return env->value_from_current_scope(value); +} + +JSValueConst napi_quickjs_value_inner(napi_env env, napi_value value) +{ + napi_value__ *slot = napi_quickjs_value_slot(env, value); + return slot == nullptr ? JS_UNDEFINED : slot->get_inner(); +} diff --git a/quickjs/src/internal/napi_value.h b/quickjs/src/internal/napi_value.h index 812db1d..597cabf 100644 --- a/quickjs/src/internal/napi_value.h +++ b/quickjs/src/internal/napi_value.h @@ -3,22 +3,31 @@ #include "../../../include/js_native_api.h" +#include #include struct napi_value__ { - static napi_value__ *create(napi_env env, JSValue value, bool owned); - static void destroy(napi_value__ *value); - + napi_value__() = default; + napi_value__(const napi_value__ &) = delete; + napi_value__ &operator=(const napi_value__ &) = delete; + napi_value__(napi_value__ &&other) noexcept; + napi_value__ &operator=(napi_value__ &&other) noexcept; ~napi_value__(); + void initialize(napi_env env, JSValue value, bool owned); + void release(); + bool is_active() const; + napi_env env() const; JSValueConst get_inner() const; private: - napi_value__(napi_env env, JSValue value, bool owned); - - napi_env env_; - JSValue value_; + // Owning environment and wrapped QuickJS value. + napi_env env_ = nullptr; + JSValue value_ = JS_UNDEFINED; }; +JSValueConst napi_quickjs_value_inner(napi_env env, napi_value value); +napi_value__ *napi_quickjs_value_slot(napi_env env, napi_value value); + #endif // NAPI_QUICKJS_VALUE_H_ diff --git a/quickjs/src/js_native_api_quickjs.cc b/quickjs/src/js_native_api_quickjs.cc index 9919407..e74c4ca 100644 --- a/quickjs/src/js_native_api_quickjs.cc +++ b/quickjs/src/js_native_api_quickjs.cc @@ -31,7 +31,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_UNDEFINED, true); + *result = env->wrap_value_in_current_scope(JS_UNDEFINED, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -39,7 +39,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NULL, true); + *result = env->wrap_value_in_current_scope(JS_NULL, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -48,7 +48,7 @@ extern "C" if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; auto context = env->context(); - *result = env->current_scope()->wrap_value(JS_GetGlobalObject(context), true); + *result = env->wrap_value_in_current_scope(JS_GetGlobalObject(context), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -58,7 +58,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewBool(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewBool(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -68,7 +68,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewFloat64(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewFloat64(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -78,7 +78,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewInt32(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewInt32(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -88,7 +88,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewInt64(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewInt64(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -98,7 +98,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewUint32(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewUint32(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -108,7 +108,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewBigInt64(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewBigInt64(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -118,7 +118,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewBigUint64(env->context(), value), true); + *result = env->wrap_value_in_current_scope(JS_NewBigUint64(env->context(), value), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -151,7 +151,7 @@ extern "C" if (used_words == 0) { - *result = env->current_scope()->wrap_value(JS_NewBigInt64(env->context(), 0), true); + *result = env->wrap_value_in_current_scope(JS_NewBigInt64(env->context(), 0), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -179,7 +179,7 @@ extern "C" if (JS_IsException(bigint)) return napi_util__::return_pending_if_caught(env, "Failed to create BigInt from words"); - *result = env->current_scope()->wrap_value(bigint, true); + *result = env->wrap_value_in_current_scope(bigint, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -192,7 +192,7 @@ extern "C" { return napi_util__::return_pending_if_caught(env, "Failed to create date"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -200,7 +200,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewObject(env->context()), true); + *result = env->wrap_value_in_current_scope(JS_NewObject(env->context()), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -208,7 +208,7 @@ extern "C" { if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - *result = env->current_scope()->wrap_value(JS_NewArray(env->context()), true); + *result = env->wrap_value_in_current_scope(JS_NewArray(env->context()), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -241,7 +241,7 @@ extern "C" JS_SetOpaque(obj, hint); // 4. wrap it in a napi_value and return - *result = env->current_scope()->wrap_value(obj, true); + *result = env->wrap_value_in_current_scope(obj, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -252,7 +252,7 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - *result = napi_external__::get_value(value->get_inner()); + *result = napi_external__::get_value(napi_quickjs_value_inner(env, value)); return napi_ok; } @@ -282,7 +282,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Failed to create array buffer"); } - *result = env->current_scope()->wrap_value(ab, true); + *result = env->wrap_value_in_current_scope(ab, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -330,9 +330,10 @@ extern "C" napi_external_backing_store_hint__::destroy(hint); return napi_util__::return_pending_if_caught(env, "Failed to create external array"); } + env->track_external_array_buffer_hint(out, hint); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -340,7 +341,7 @@ extern "C" { if (!napi_util__::check_env(env) || value == nullptr || result == nullptr) return napi_invalid_arg; - *result = JS_GetTypedArrayType(value->get_inner()) >= 0; + *result = JS_GetTypedArrayType(napi_quickjs_value_inner(env, value)) >= 0; return napi_quickjs_clear_last_error(env); } @@ -355,7 +356,7 @@ extern "C" return napi_util__::invalid_arg(env); JSValue argv[] = { - arraybuffer->get_inner(), + napi_quickjs_value_inner(env, arraybuffer), JS_NewInt64(env->context(), static_cast(byte_offset)), JS_NewInt64(env->context(), static_cast(length))}; @@ -370,7 +371,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Failed to create TypedArray"); } - *result = env->current_scope()->wrap_value(view, true); + *result = env->wrap_value_in_current_scope(view, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -385,7 +386,7 @@ extern "C" if (!napi_util__::check_value(env, typedarray)) return napi_util__::invalid_arg(env); - JSValue local = typedarray->get_inner(); + JSValue local = napi_quickjs_value_inner(env, typedarray); int type_idx = JS_GetTypedArrayType(local); if (type_idx < 0) return napi_invalid_arg; @@ -455,7 +456,7 @@ extern "C" if (arraybuffer != nullptr) { - *arraybuffer = env->current_scope()->wrap_value(abuf, true); + *arraybuffer = env->wrap_value_in_current_scope(abuf, true); if (*arraybuffer == nullptr) { JS_FreeValue(env->context(), abuf); @@ -474,12 +475,17 @@ extern "C" { if (!napi_util__::check_value(env, arraybuffer)) return napi_util__::invalid_arg(env); - JSValue local = arraybuffer->get_inner(); + JSValue local = napi_quickjs_value_inner(env, arraybuffer); if (!JS_IsArrayBuffer(local)) return napi_arraybuffer_expected; + napi_external_backing_store_hint__ *hint = env->external_array_buffer_hint(local); + if (hint != nullptr) + hint->begin_detach(); JS_DetachArrayBuffer(env->context(), local); + if (hint != nullptr) + hint->end_detach(); return napi_ok; } @@ -489,7 +495,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (JS_IsUndefined(local) || JS_IsNull(local)) { @@ -563,7 +569,7 @@ extern "C" } } - *result = env->current_scope()->wrap_value(arr, true); + *result = env->wrap_value_in_current_scope(arr, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -598,7 +604,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Cannot create string"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_quickjs_clear_last_error(env); } @@ -684,7 +690,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Cannot create string"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_quickjs_clear_last_error(env); } @@ -724,7 +730,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Cannot create string"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_quickjs_clear_last_error(env); } @@ -793,7 +799,7 @@ extern "C" if (length == NAPI_AUTO_LENGTH) length = strlen(str); - *result = env->current_scope()->wrap_value(JS_NewStringLen(env->context(), str, length), true); + *result = env->wrap_value_in_current_scope(JS_NewStringLen(env->context(), str, length), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -816,7 +822,7 @@ extern "C" length = static_cast(p - str); } - *result = env->current_scope()->wrap_value( + *result = env->wrap_value_in_current_scope( JS_NewStringUTF16(env->context(), reinterpret_cast(str), length), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -831,7 +837,7 @@ extern "C" const char *desc_str = nullptr; if (description != nullptr) { - JSValue desc_val = description->get_inner(); + JSValue desc_val = napi_quickjs_value_inner(env, description); if (!JS_IsString(desc_val)) return napi_string_expected; desc_str = JS_ToCString(env->context(), desc_val); @@ -842,7 +848,7 @@ extern "C" if (desc_str != nullptr) JS_FreeCString(env->context(), desc_str); - *result = env->current_scope()->wrap_value(sym, true); + *result = env->wrap_value_in_current_scope(sym, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -861,7 +867,7 @@ extern "C" // JS_NewSymbol with is_global=true mirrors Symbol.for() — same key returns same symbol. JSValue sym = JS_NewSymbol(env->context(), desc, true); - *result = env->current_scope()->wrap_value(sym, true); + *result = env->wrap_value_in_current_scope(sym, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -872,7 +878,7 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (JS_IsNumber(local)) { @@ -935,7 +941,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsNumber(local)) { return napi_quickjs_set_last_error(env, napi_number_expected, "A number was expected"); @@ -955,7 +961,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsNumber(local)) { return napi_quickjs_set_last_error(env, napi_number_expected, "A number was expected"); @@ -975,7 +981,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsNumber(local)) { return napi_quickjs_set_last_error(env, napi_number_expected, "A number was expected"); @@ -995,7 +1001,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsNumber(local)) { return napi_quickjs_set_last_error(env, napi_number_expected, "A number was expected"); @@ -1035,7 +1041,7 @@ extern "C" if (!napi_util__::check_env(env) || value == nullptr || result == nullptr || lossless == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsBigInt(local)) return napi_bigint_expected; @@ -1062,7 +1068,7 @@ extern "C" if (!napi_util__::check_env(env) || value == nullptr || result == nullptr || lossless == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsBigInt(local)) return napi_bigint_expected; @@ -1089,7 +1095,7 @@ extern "C" if (!napi_util__::check_env(env) || value == nullptr || word_count == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsBigInt(local)) return napi_bigint_expected; @@ -1122,7 +1128,7 @@ extern "C" if (!napi_util__::check_value(env, value) || is_date == nullptr) return napi_invalid_arg; - *is_date = JS_IsDate(value->get_inner()); + *is_date = JS_IsDate(napi_quickjs_value_inner(env, value)); return napi_ok; } @@ -1131,7 +1137,7 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsDate(local)) return napi_date_expected; @@ -1159,7 +1165,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (JS_IsUndefined(local) || JS_IsNull(local)) { *result = false; @@ -1178,7 +1184,7 @@ extern "C" return napi_util__::invalid_arg(env); size_t len = 0; - uint8_t *ptr = JS_GetArrayBuffer(env->context(), &len, arraybuffer->get_inner()); + uint8_t *ptr = JS_GetArrayBuffer(env->context(), &len, napi_quickjs_value_inner(env, arraybuffer)); if (ptr == nullptr && len == 0 && JS_HasException(env->context())) { JSValue exc = JS_GetException(env->context()); @@ -1200,14 +1206,14 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - if (JS_IsArrayBuffer(value->get_inner())) + if (JS_IsArrayBuffer(napi_quickjs_value_inner(env, value))) { *result = false; return napi_quickjs_clear_last_error(env); } size_t len = 0; - uint8_t *ptr = JS_GetArrayBuffer(env->context(), &len, value->get_inner()); + uint8_t *ptr = JS_GetArrayBuffer(env->context(), &len, napi_quickjs_value_inner(env, value)); if (ptr == nullptr && len == 0 && JS_HasException(env->context())) { JSValue exc = JS_GetException(env->context()); @@ -1241,7 +1247,7 @@ extern "C" if (data != nullptr) *data = buf; - *result = env->current_scope()->wrap_value(sab, true); + *result = env->wrap_value_in_current_scope(sab, true); return (*result == nullptr) ? napi_generic_failure : napi_quickjs_clear_last_error(env); } @@ -1255,7 +1261,7 @@ extern "C" return napi_util__::invalid_arg(env); size_t ab_len = 0; - uint8_t *ab_data = JS_GetArrayBuffer(env->context(), &ab_len, arraybuffer->get_inner()); + uint8_t *ab_data = JS_GetArrayBuffer(env->context(), &ab_len, napi_quickjs_value_inner(env, arraybuffer)); if (ab_data == nullptr && ab_len == 0 && JS_HasException(env->context())) return napi_util__::return_pending_if_caught(env, "ArrayBuffer expected"); @@ -1266,7 +1272,7 @@ extern "C" return napi_util__::return_pending_if_caught(env, "Failed to get DataView constructor"); JSValue args[] = { - arraybuffer->get_inner(), + napi_quickjs_value_inner(env, arraybuffer), JS_NewInt64(env->context(), static_cast(byte_offset)), JS_NewInt64(env->context(), static_cast(length))}; JSValue view = JS_CallConstructor(env->context(), ctor, 3, args); @@ -1276,7 +1282,7 @@ extern "C" if (JS_IsException(view)) return napi_util__::return_pending_if_caught(env, "DataView construction threw"); - *result = env->current_scope()->wrap_value(view, true); + *result = env->wrap_value_in_current_scope(view, true); return (*result == nullptr) ? napi_generic_failure : napi_quickjs_clear_last_error(env); } @@ -1284,7 +1290,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - *result = JS_IsDataView(value->get_inner()); + *result = JS_IsDataView(napi_quickjs_value_inner(env, value)); return napi_quickjs_clear_last_error(env); } @@ -1297,7 +1303,7 @@ extern "C" { if (!napi_util__::check_value(env, dataview)) return napi_util__::invalid_arg(env); - JSValue view = dataview->get_inner(); + JSValue view = napi_quickjs_value_inner(env, dataview); if (!JS_IsDataView(view)) return napi_util__::invalid_arg(env); @@ -1336,7 +1342,7 @@ extern "C" } if (arraybuffer != nullptr) { - *arraybuffer = env->current_scope()->wrap_value(buffer_val, true); + *arraybuffer = env->wrap_value_in_current_scope(buffer_val, true); if (*arraybuffer == nullptr) { JS_FreeValue(env->context(), buffer_val); @@ -1354,7 +1360,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - *result = JS_IsArray(value->get_inner()); + *result = JS_IsArray(napi_quickjs_value_inner(env, value)); return napi_ok; } @@ -1364,7 +1370,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsArray(local)) return napi_array_expected; @@ -1391,7 +1397,7 @@ extern "C" { if (!napi_util__::check_value(env, object) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; @@ -1400,7 +1406,7 @@ extern "C" { return napi_util__::return_pending_if_caught(env, "Exception while getting element"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -1411,12 +1417,12 @@ extern "C" { if (!napi_util__::check_value(env, object) || value == nullptr) return napi_util__::invalid_arg(env); - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; // JS_SetPropertyUint32 consumes the value, so we must duplicate it - if (JS_SetPropertyUint32(env->context(), local, index, JS_DupValue(env->context(), value->get_inner())) < 0) + if (JS_SetPropertyUint32(env->context(), local, index, JS_DupValue(env->context(), napi_quickjs_value_inner(env, value))) < 0) { return napi_util__::return_pending_if_caught(env, "Exception while setting element"); } @@ -1433,8 +1439,8 @@ extern "C" return napi_util__::invalid_arg(env); } - JSValue val = object->get_inner(); - JSValue ctor = constructor->get_inner(); + JSValue val = napi_quickjs_value_inner(env, object); + JSValue ctor = napi_quickjs_value_inner(env, constructor); // Node-API generally expects the constructor to be a function. // The commented-out V8 code in your file also performs this check. @@ -1462,7 +1468,7 @@ extern "C" { if (!napi_util__::check_value(env, object) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; @@ -1489,7 +1495,7 @@ extern "C" { if (!napi_util__::check_value(env, object)) return napi_util__::invalid_arg(env); - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; @@ -1538,14 +1544,14 @@ extern "C" for (i = 0; i < count; i++) { - argv[i] = env->current_scope()->wrap_value(info->arg(i), false); + argv[i] = env->wrap_value_in_current_scope(info->arg(i), false); } // Node-API Rule: If the user provided a larger buffer than actual arguments, // fill the remaining slots with 'undefined'. for (; i < *argc; i++) { - argv[i] = env->current_scope()->wrap_value(JS_UNDEFINED, true); + argv[i] = env->wrap_value_in_current_scope(JS_UNDEFINED, true); } } @@ -1556,7 +1562,7 @@ extern "C" // 2. Handle the 'this' argument if (this_arg != nullptr) { - *this_arg = env->current_scope()->wrap_value(info->this_value(), false); + *this_arg = env->wrap_value_in_current_scope(info->this_value(), false); } // 3. Handle the user data pointer @@ -1585,7 +1591,7 @@ extern "C" } else { - *result = env->current_scope()->wrap_value(cbinfo->new_target(), false); + *result = env->wrap_value_in_current_scope(cbinfo->new_target(), false); } return napi_ok; @@ -1596,7 +1602,7 @@ extern "C" if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - auto *scope = napi_handle_scope__::create(env, env->current_scope()); + napi_handle_scope scope = env->create_scope(env->current_scope()); if (scope == nullptr) return napi_generic_failure; @@ -1612,8 +1618,11 @@ extern "C" if (!env->is_current_scope(scope)) return napi_util__::invalid_arg(env); - env->set_current_scope(scope->parent()); - napi_handle_scope__::destroy(scope); + napi_scope__ *inner = env->scope_from_handle(scope); + if (inner == nullptr) + return napi_util__::invalid_arg(env); + env->set_current_scope(inner->parent_handle()); + env->destroy_scope(scope); return napi_ok; } @@ -1623,12 +1632,12 @@ extern "C" if (!napi_util__::check_env(env) || result == nullptr) return napi_invalid_arg; - auto *scope = napi_escapable_handle_scope__::create(env, env->current_scope()); + napi_handle_scope scope = env->create_scope(env->current_scope()); if (scope == nullptr) return napi_generic_failure; env->set_current_scope(scope); - *result = scope; + *result = reinterpret_cast(scope); return napi_ok; } @@ -1637,11 +1646,15 @@ extern "C" { if (!napi_util__::check_env(env) || scope == nullptr) return napi_invalid_arg; - if (!env->is_current_scope(scope)) + napi_handle_scope handle = reinterpret_cast(scope); + if (!env->is_current_scope(handle)) return napi_util__::invalid_arg(env); - env->set_current_scope(scope->parent()); - napi_escapable_handle_scope__::destroy(scope); + napi_scope__ *inner = env->scope_from_handle(handle); + if (inner == nullptr) + return napi_util__::invalid_arg(env); + env->set_current_scope(inner->parent_handle()); + env->destroy_scope(handle); return napi_ok; } @@ -1653,11 +1666,16 @@ extern "C" if (!napi_util__::check_env(env) || scope == nullptr || escapee == nullptr || result == nullptr) return napi_invalid_arg; - if (scope->has_escaped()) + napi_handle_scope handle = reinterpret_cast(scope); + napi_scope__ *inner = env->scope_from_handle(handle); + if (inner == nullptr) + return napi_util__::invalid_arg(env); + + if (inner->has_escaped()) return napi_escape_called_twice; - scope->mark_escaped(); - *result = scope->escape_value(escapee); + inner->mark_escaped(); + *result = inner->escape_value(escapee); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -1702,7 +1720,7 @@ extern "C" return status; // 2. Setup the prototype chain - JSValue ctor = ctor_napi_value->get_inner(); + JSValue ctor = napi_quickjs_value_inner(env, ctor_napi_value); JSValue proto = JS_NewObject(ctx); JS_SetConstructor(ctx, ctor, proto); @@ -1724,7 +1742,7 @@ extern "C" } else if (desc.name != nullptr) { - JSValue name_val = desc.name->get_inner(); + JSValue name_val = napi_quickjs_value_inner(env, desc.name); if (!JS_IsString(name_val) && !JS_IsSymbol(name_val)) { JS_FreeValue(ctx, proto); @@ -1759,7 +1777,7 @@ extern "C" if (status == napi_ok) { JS_DefinePropertyValue(ctx, target, key, - JS_DupValue(ctx, method_val->get_inner()), + JS_DupValue(ctx, napi_quickjs_value_inner(env, method_val)), method_flags); } } @@ -1774,7 +1792,7 @@ extern "C" napi_value g_val = nullptr; if (napi_function__::create(env, desc.utf8name, NAPI_AUTO_LENGTH, desc.getter, desc.data, JS_CFUNC_getter_magic, &g_val) == napi_ok) { - getter = JS_DupValue(ctx, g_val->get_inner()); + getter = JS_DupValue(ctx, napi_quickjs_value_inner(env, g_val)); } } if (desc.setter != nullptr) @@ -1782,7 +1800,7 @@ extern "C" napi_value s_val = nullptr; if (napi_function__::create(env, desc.utf8name, NAPI_AUTO_LENGTH, desc.setter, desc.data, JS_CFUNC_setter_magic, &s_val) == napi_ok) { - setter = JS_DupValue(ctx, s_val->get_inner()); + setter = JS_DupValue(ctx, napi_quickjs_value_inner(env, s_val)); } } @@ -1796,7 +1814,7 @@ extern "C" if (desc.attributes & napi_writable) value_flags |= JS_PROP_WRITABLE; - JSValue val = JS_DupValue(ctx, desc.value->get_inner()); + JSValue val = JS_DupValue(ctx, napi_quickjs_value_inner(env, desc.value)); JS_DefinePropertyValue(ctx, target, key, val, value_flags); } @@ -1831,25 +1849,21 @@ extern "C" // 2. Unwrap the constructor JSValue // Assuming 'unwrap_value' is your helper to get the JSValue from napi_value - JSValue ctor_val = constructor->get_inner(); + JSValue ctor_val = napi_quickjs_value_inner(env, constructor); // 3. Prepare the arguments // QuickJS's JS_CallConstructor expects an array of JSValue. // Since napi_value is usually a typedef for JSValue (or a pointer to it), // we can often cast the array if the memory layout matches, // but a safe stack allocation is better for compatibility. - JSValue *js_argv = nullptr; - if (argc > 0) - { - js_argv = (JSValue *)alloca(sizeof(JSValue) * argc); - for (size_t i = 0; i < argc; ++i) - { - js_argv[i] = argv[i]->get_inner(); - } - } + std::vector js_argv = napi_util__::prepare_call_args(env, argc, argv); // 4. Call the constructor - JSValue instance = JS_CallConstructor(ctx, ctor_val, (int)argc, js_argv); + const size_t js_argc = js_argv.size(); + const bool has_args = js_argc != 0; + JSValue instance = has_args + ? JS_CallConstructor(ctx, ctor_val, (int)js_argc, js_argv.data()) + : JS_CallConstructor(ctx, ctor_val, 0, nullptr); // 5. Check for exceptions if (JS_IsException(instance)) @@ -1859,7 +1873,7 @@ extern "C" // 6. Wrap and return // 'wrap_value' converts the JSValue back into the napi_value handle - *result = env->current_scope()->wrap_value(instance, true); + *result = env->wrap_value_in_current_scope(instance, true); return napi_ok; } @@ -1881,7 +1895,7 @@ extern "C" return napi_util__::invalid_arg(env); } - JSValue js_func = func->get_inner(); + JSValue js_func = napi_quickjs_value_inner(env, func); // 2. Ensure the target is actually a function if (!JS_IsFunction(env->context(), js_func)) @@ -1891,23 +1905,17 @@ extern "C" // 3. Resolve the receiver ('this' object) // If recv is null, Node-API defaults to 'undefined' - JSValue js_recv = (recv != nullptr) ? recv->get_inner() : JS_UNDEFINED; + JSValue js_recv = (recv != nullptr) ? napi_quickjs_value_inner(env, recv) : JS_UNDEFINED; // 4. Prepare the arguments array - // We use a small stack buffer for performance, falling back to a heap vector for many args. - JSValue *js_argv = nullptr; - if (argc > 0) - { - // TODO: Use JSRuntime allocator, and ensure memory is freed afterwards! - js_argv = static_cast(alloca(sizeof(JSValue) * argc)); - for (size_t i = 0; i < argc; i++) - { - js_argv[i] = argv[i]->get_inner(); - } - } + std::vector js_argv = napi_util__::prepare_call_args(env, argc, argv); // 5. Perform the call - JSValue js_result = JS_Call(env->context(), js_func, js_recv, (int)argc, js_argv); + const size_t js_argc = js_argv.size(); + const bool has_args = js_argc != 0; + JSValue js_result = has_args + ? JS_Call(env->context(), js_func, js_recv, (int)js_argc, js_argv.data()) + : JS_Call(env->context(), js_func, js_recv, 0, nullptr); // 6. Handle Exceptions if (JS_IsException(js_result)) @@ -1919,7 +1927,7 @@ extern "C" if (result != nullptr) { // wrap the result; the current handle scope now owns the returned JSValue - *result = env->current_scope()->wrap_value(js_result, true); + *result = env->wrap_value_in_current_scope(js_result, true); } else { @@ -1941,7 +1949,7 @@ extern "C" if (property_count > 0 && properties == nullptr) return napi_util__::invalid_arg(env); - JSValue obj = object->get_inner(); + JSValue obj = napi_quickjs_value_inner(env, object); if (!JS_IsObject(obj)) return napi_object_expected; @@ -1957,7 +1965,7 @@ extern "C" } else if (p.name != nullptr) { - JSValue name_val = p.name->get_inner(); + JSValue name_val = napi_quickjs_value_inner(env, p.name); prop_name = JS_ValueToAtom(env->context(), name_val); } else @@ -2048,7 +2056,7 @@ extern "C" if (p.attributes & napi_writable) data_flags |= JS_PROP_WRITABLE | JS_PROP_HAS_WRITABLE; - JSValue value = JS_DupValue(env->context(), p.value->get_inner()); + JSValue value = JS_DupValue(env->context(), napi_quickjs_value_inner(env, p.value)); if (JS_DefinePropertyValue(env->context(), obj, prop_name, value, data_flags) < 0) { status = napi_util__::return_pending_if_caught(env, "Failed to define data property"); @@ -2085,7 +2093,7 @@ extern "C" } *deferred = d; - *promise = env->current_scope()->wrap_value(p, true); + *promise = env->wrap_value_in_current_scope(p, true); return (*promise == nullptr) ? napi_generic_failure : napi_ok; } @@ -2128,7 +2136,7 @@ extern "C" if (!napi_util__::check_value(env, value) || is_promise == nullptr) return napi_invalid_arg; - *is_promise = JS_IsPromise(value->get_inner()); + *is_promise = JS_IsPromise(napi_quickjs_value_inner(env, value)); return napi_ok; } @@ -2141,7 +2149,7 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; @@ -2169,11 +2177,11 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; - JSValue key_value = key->get_inner(); + JSValue key_value = napi_quickjs_value_inner(env, key); if (!JS_IsString(key_value) && !JS_IsSymbol(key_value)) return napi_quickjs_set_last_error(env, napi_name_expected, "A string or symbol was expected"); @@ -2181,7 +2189,7 @@ extern "C" if (prop == JS_ATOM_NULL) return napi_util__::return_pending_if_caught(env, "Invalid key"); - int res = set_property_with_node_compat(env->context(), local, prop, value->get_inner()); + int res = set_property_with_node_compat(env->context(), local, prop, napi_quickjs_value_inner(env, value)); JS_FreeAtom(env->context(), prop); if (res < 0) @@ -2200,11 +2208,11 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; - JSValue key_value = key->get_inner(); + JSValue key_value = napi_quickjs_value_inner(env, key); if (!JS_IsString(key_value) && !JS_IsSymbol(key_value)) return napi_quickjs_set_last_error(env, napi_name_expected, "A string or symbol was expected"); @@ -2219,7 +2227,7 @@ extern "C" { return napi_util__::return_pending_if_caught(env, "Exception while getting property"); } - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2232,11 +2240,11 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; - JSValue key_value = key->get_inner(); + JSValue key_value = napi_quickjs_value_inner(env, key); if (!JS_IsString(key_value) && !JS_IsSymbol(key_value)) return napi_quickjs_set_last_error(env, napi_name_expected, "A string or symbol was expected"); @@ -2264,11 +2272,11 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; - JSValue key_value = key->get_inner(); + JSValue key_value = napi_quickjs_value_inner(env, key); if (!JS_IsString(key_value) && !JS_IsSymbol(key_value)) return napi_quickjs_set_last_error(env, napi_name_expected, "A string or symbol was expected"); @@ -2299,11 +2307,11 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; - JSValue key_value = key->get_inner(); + JSValue key_value = napi_quickjs_value_inner(env, key); if (!JS_IsString(key_value) && !JS_IsSymbol(key_value)) return napi_quickjs_set_last_error(env, napi_name_expected, "A string or symbol was expected"); @@ -2356,7 +2364,7 @@ extern "C" return napi_util__::invalid_arg(env); } - JSValue obj = object->get_inner(); + JSValue obj = napi_quickjs_value_inner(env, object); // 2. Ensure the target is an object if (!JS_IsObject(obj)) @@ -2365,7 +2373,7 @@ extern "C" } // 3. unwrap the value to be set - JSValue val = value->get_inner(); + JSValue val = napi_quickjs_value_inner(env, value); // 4. Set the property. JSAtom key = JS_NewAtom(env->context(), utf8name); @@ -2390,7 +2398,7 @@ extern "C" { return napi_util__::invalid_arg(env); } - JSValue local = object->get_inner(); + JSValue local = napi_quickjs_value_inner(env, object); if (!JS_IsObject(local)) return napi_object_expected; @@ -2399,7 +2407,7 @@ extern "C" { return napi_util__::return_pending_if_caught(env, "Exception while getting named property"); } - *result = env->current_scope()->wrap_value(prop, true); + *result = env->wrap_value_in_current_scope(prop, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2410,7 +2418,7 @@ extern "C" if (!napi_util__::check_value(env, object) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue target = object->get_inner(); + JSValue target = napi_quickjs_value_inner(env, object); if (!JS_IsObject(target)) return napi_object_expected; @@ -2418,7 +2426,7 @@ extern "C" if (JS_IsException(proto)) return napi_util__::return_pending_if_caught(env, "Exception while getting prototype"); - *result = env->current_scope()->wrap_value(proto, true); + *result = env->wrap_value_in_current_scope(proto, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2429,11 +2437,11 @@ extern "C" if (!napi_util__::check_value(env, object) || !napi_util__::check_value(env, value)) return napi_invalid_arg; - JSValue target = object->get_inner(); + JSValue target = napi_quickjs_value_inner(env, object); if (!JS_IsObject(target)) return napi_object_expected; - if (JS_SetPrototype(env->context(), target, value->get_inner()) < 0) + if (JS_SetPrototype(env->context(), target, napi_quickjs_value_inner(env, value)) < 0) return napi_util__::return_pending_if_caught(env, "Exception while setting prototype"); return napi_ok; @@ -2447,7 +2455,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsBool(local)) { return napi_quickjs_set_last_error(env, napi_boolean_expected, "A boolean was expected"); @@ -2469,7 +2477,7 @@ extern "C" { return napi_quickjs_set_last_error(env, napi_invalid_arg, "Invalid argument"); } - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsString(local)) { return napi_quickjs_set_last_error(env, napi_string_expected, "A string was expected"); @@ -2517,7 +2525,7 @@ extern "C" if (!napi_util__::check_value(env, value)) return napi_util__::invalid_arg(env); - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsString(local)) return napi_quickjs_set_last_error(env, napi_string_expected, "A string was expected"); @@ -2565,7 +2573,7 @@ extern "C" if (!napi_util__::check_value(env, value)) return napi_util__::invalid_arg(env); - JSValue local = value->get_inner(); + JSValue local = napi_quickjs_value_inner(env, value); if (!JS_IsString(local)) return napi_quickjs_set_last_error(env, napi_string_expected, "A string was expected"); @@ -2605,11 +2613,11 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue coerced = JS_ToBoolean(env->context(), value->get_inner()); + JSValue coerced = JS_ToBoolean(env->context(), napi_quickjs_value_inner(env, value)); if (JS_IsException(coerced)) return napi_util__::return_pending_if_caught(env, "Failed to coerce to bool"); - *result = env->current_scope()->wrap_value(coerced, true); + *result = env->wrap_value_in_current_scope(coerced, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2618,7 +2626,7 @@ extern "C" if (!napi_util__::check_value(env, lhs) || !napi_util__::check_value(env, rhs) || result == nullptr) return napi_invalid_arg; - *result = JS_IsStrictEqual(env->context(), lhs->get_inner(), rhs->get_inner()); + *result = JS_IsStrictEqual(env->context(), napi_quickjs_value_inner(env, lhs), napi_quickjs_value_inner(env, rhs)); return napi_ok; } @@ -2628,10 +2636,10 @@ extern "C" return napi_util__::invalid_arg(env); double d; - if (JS_ToFloat64(env->context(), &d, value->get_inner()) != 0) + if (JS_ToFloat64(env->context(), &d, napi_quickjs_value_inner(env, value)) != 0) return napi_util__::return_pending_if_caught(env, "Failed to coerce to number"); - *result = env->current_scope()->wrap_value(JS_NewFloat64(env->context(), d), true); + *result = env->wrap_value_in_current_scope(JS_NewFloat64(env->context(), d), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2640,11 +2648,11 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue obj = JS_ToObject(env->context(), value->get_inner()); + JSValue obj = JS_ToObject(env->context(), napi_quickjs_value_inner(env, value)); if (JS_IsException(obj)) return napi_util__::return_pending_if_caught(env, "Failed to coerce to object"); - *result = env->current_scope()->wrap_value(obj, true); + *result = env->wrap_value_in_current_scope(obj, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2653,11 +2661,11 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - JSValue str = JS_ToString(env->context(), value->get_inner()); + JSValue str = JS_ToString(env->context(), napi_quickjs_value_inner(env, value)); if (JS_IsException(str)) return napi_util__::return_pending_if_caught(env, "Failed to coerce to string"); - *result = env->current_scope()->wrap_value(str, true); + *result = env->wrap_value_in_current_scope(str, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2666,7 +2674,7 @@ extern "C" if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - *result = napi_ref__::create(env, value->get_inner(), initial_ref_count); + *result = env->wrap_ref_in_root_scope(napi_quickjs_value_inner(env, value), initial_ref_count); if (*result == nullptr) return napi_generic_failure; @@ -2680,7 +2688,7 @@ extern "C" return napi_invalid_arg; env->remove_weak_ref(ref); - napi_ref__::destroy(ref); + env->delete_ref_from_root_scope(ref); return napi_ok; } @@ -2691,10 +2699,14 @@ extern "C" if (!napi_util__::check_env(env) || ref == nullptr) return napi_invalid_arg; - if (ref->is_weak()) + napi_ref__ *slot = napi_quickjs_ref_slot(env, ref); + if (slot == nullptr) + return napi_invalid_arg; + + if (slot->is_weak()) env->remove_weak_ref(ref); - uint32_t count = ref->add_ref(); + uint32_t count = slot->add_ref(); if (result != nullptr) *result = count; @@ -2709,8 +2721,12 @@ extern "C" if (!napi_util__::check_env(env) || ref == nullptr) return napi_invalid_arg; - uint32_t count = ref->rem_ref(); - if (ref->is_weak()) + napi_ref__ *slot = napi_quickjs_ref_slot(env, ref); + if (slot == nullptr) + return napi_invalid_arg; + + uint32_t count = slot->rem_ref(); + if (slot->is_weak()) env->track_weak_ref(ref); if (result != nullptr) @@ -2725,12 +2741,15 @@ extern "C" { if (!napi_util__::check_env(env) || ref == nullptr || result == nullptr) return napi_invalid_arg; - if (ref->is_empty()) + napi_ref__ *slot = napi_quickjs_ref_slot(env, ref); + if (slot == nullptr) + return napi_invalid_arg; + if (slot->is_empty()) { *result = nullptr; return napi_ok; } - *result = env->current_scope()->wrap_value(ref->dup_inner(), true); + *result = env->wrap_value_in_current_scope(slot->dup_inner(), true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2741,7 +2760,7 @@ extern "C" if (!napi_util__::check_value(env, js_object)) return napi_invalid_arg; - auto obj = js_object->get_inner(); + auto obj = napi_quickjs_value_inner(env, js_object); if (!JS_IsObject(obj)) return napi_object_expected; @@ -2786,7 +2805,7 @@ extern "C" if (!napi_util__::check_value(env, js_object) || result == nullptr) return napi_invalid_arg; - auto obj = js_object->get_inner(); + auto obj = napi_quickjs_value_inner(env, js_object); auto *wrap = napi_external__::get_wrap_record(env->context(), obj); if (wrap == nullptr) return napi_invalid_arg; @@ -2800,7 +2819,7 @@ extern "C" if (!napi_util__::check_value(env, js_object) || result == nullptr) return napi_invalid_arg; - auto obj = js_object->get_inner(); + auto obj = napi_quickjs_value_inner(env, js_object); if (!JS_IsObject(obj)) return napi_object_expected; @@ -2868,7 +2887,7 @@ extern "C" { if (!napi_util__::check_value(env, error)) return napi_invalid_arg; - napi_util__::set_last_exception(env, JS_DupValue(env->context(), error->get_inner())); + napi_util__::set_last_exception(env, JS_DupValue(env->context(), napi_quickjs_value_inner(env, error))); return napi_pending_exception; } @@ -2876,7 +2895,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_invalid_arg; - JSValue val = value->get_inner(); + JSValue val = napi_quickjs_value_inner(env, value); JSAtom message_atom = JS_NewAtom(env->context(), "message"); JSAtom stack_atom = JS_NewAtom(env->context(), "stack"); *result = JS_IsObject(val) && @@ -2916,7 +2935,7 @@ extern "C" if (!napi_util__::check_env(env) || err == nullptr) return napi_invalid_arg; - napi_util__::set_last_exception(env, JS_DupValue(env->context(), err->get_inner())); + napi_util__::set_last_exception(env, JS_DupValue(env->context(), napi_quickjs_value_inner(env, err))); return napi_ok; } @@ -2964,7 +2983,7 @@ extern "C" // Take ownership of the exception before clearing JSValue ex = env->take_last_exception(); - *result = env->current_scope()->wrap_value(ex, true); + *result = env->wrap_value_in_current_scope(ex, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -2999,7 +3018,7 @@ extern "C" if (!napi_util__::check_value(env, script) || result == nullptr) return napi_invalid_arg; - JSValue source = script->get_inner(); + JSValue source = napi_quickjs_value_inner(env, script); if (!JS_IsString(source)) return napi_string_expected; @@ -3030,7 +3049,7 @@ extern "C" JS_FreeCString(env->context(), str); - *result = env->current_scope()->wrap_value(out, true); + *result = env->wrap_value_in_current_scope(out, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -3124,7 +3143,7 @@ extern "C" { if (!napi_util__::check_value(env, value) || result == nullptr) return napi_util__::invalid_arg(env); - *result = napi_external__::is_buffer(env, value->get_inner()); + *result = napi_external__::is_buffer(env, napi_quickjs_value_inner(env, value)); return napi_quickjs_clear_last_error(env); } @@ -3135,7 +3154,7 @@ extern "C" { if (!napi_util__::check_value(env, value)) return napi_util__::invalid_arg(env); - return napi_external__::get_buffer_info(env, value->get_inner(), data, length); + return napi_external__::get_buffer_info(env, napi_quickjs_value_inner(env, value), data, length); } napi_status NAPI_CDECL node_api_create_buffer_from_arraybuffer( @@ -3150,7 +3169,7 @@ extern "C" size_t arraybuffer_length = 0; uint8_t *arraybuffer_data = JS_GetArrayBuffer( - env->context(), &arraybuffer_length, arraybuffer->get_inner()); + env->context(), &arraybuffer_length, napi_quickjs_value_inner(env, arraybuffer)); if (arraybuffer_data == nullptr && JS_HasException(env->context())) { JSValue exc = JS_GetException(env->context()); @@ -3161,7 +3180,7 @@ extern "C" return napi_util__::invalid_arg(env); JSValue argv[] = { - arraybuffer->get_inner(), + napi_quickjs_value_inner(env, arraybuffer), JS_NewInt64(env->context(), static_cast(byte_offset)), JS_NewInt64(env->context(), static_cast(byte_length))}; JSValue buffer = JS_NewTypedArray(env->context(), 3, argv, JS_TYPED_ARRAY_UINT8); @@ -3176,7 +3195,7 @@ extern "C" JS_FreeValue(env->context(), buffer); return status; } - *result = env->current_scope()->wrap_value(buffer, true); + *result = env->wrap_value_in_current_scope(buffer, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } @@ -3201,7 +3220,7 @@ extern "C" if (!napi_util__::check_value(env, js_object) || finalize_cb == nullptr) return napi_util__::invalid_arg(env); - JSValue obj = js_object->get_inner(); + JSValue obj = napi_quickjs_value_inner(env, js_object); if (!JS_IsObject(obj)) return napi_object_expected; @@ -3249,7 +3268,7 @@ extern "C" if (!napi_util__::check_value(env, object)) return napi_invalid_arg; - JSValue target = object->get_inner(); + JSValue target = napi_quickjs_value_inner(env, object); if (!JS_IsObject(target)) return napi_object_expected; @@ -3264,7 +3283,7 @@ extern "C" if (!napi_util__::check_value(env, object)) return napi_invalid_arg; - JSValue target = object->get_inner(); + JSValue target = napi_quickjs_value_inner(env, object); if (!JS_IsObject(target)) return napi_object_expected; @@ -3281,7 +3300,7 @@ extern "C" if (!napi_util__::check_value(env, value) || type_tag == nullptr) return napi_invalid_arg; - JSValue target = value->get_inner(); + JSValue target = napi_quickjs_value_inner(env, value); if (!JS_IsObject(target)) return napi_invalid_arg; @@ -3308,7 +3327,7 @@ extern "C" *result = false; - JSValue target = value->get_inner(); + JSValue target = napi_quickjs_value_inner(env, value); if (!JS_IsObject(target)) return napi_ok; @@ -3351,7 +3370,7 @@ extern "C" if (prototype_or_null != nullptr) { - JSValue proto = prototype_or_null->get_inner(); + JSValue proto = napi_quickjs_value_inner(env, prototype_or_null); if (!JS_IsNull(proto) && !JS_IsObject(proto)) { JS_FreeValue(ctx, obj); @@ -3372,8 +3391,8 @@ extern "C" return napi_invalid_arg; } - JSAtom key = JS_ValueToAtom(ctx, property_names[i]->get_inner()); - int rc = set_property_with_node_compat(ctx, obj, key, property_values[i]->get_inner()); + JSAtom key = JS_ValueToAtom(ctx, napi_quickjs_value_inner(env, property_names[i])); + int rc = set_property_with_node_compat(ctx, obj, key, napi_quickjs_value_inner(env, property_values[i])); JS_FreeAtom(ctx, key); if (rc < 0) @@ -3383,7 +3402,7 @@ extern "C" } } - *result = env->current_scope()->wrap_value(obj, true); + *result = env->wrap_value_in_current_scope(obj, true); return (*result == nullptr) ? napi_generic_failure : napi_ok; } diff --git a/quickjs/src/unofficial_napi.cc b/quickjs/src/unofficial_napi.cc index 3278ae6..b8aae42 100644 --- a/quickjs/src/unofficial_napi.cc +++ b/quickjs/src/unofficial_napi.cc @@ -38,7 +38,7 @@ namespace JS_SetPromiseHook(rt, nullptr, nullptr); JS_SetHostPromiseRejectionTracker(rt, nullptr, nullptr); JS_SetContextOpaque(ctx, nullptr); - delete env; + env->prepare_teardown(); return napi_ok; } @@ -49,8 +49,10 @@ namespace auto *scope = static_cast(scope_ptr); napi_status status = napi_ok; + napi_env env_to_delete = nullptr; if (scope->env != nullptr) { + env_to_delete = scope->env; status = DestroyEnvInstance(scope->env); scope->env = nullptr; } @@ -61,9 +63,14 @@ namespace } if (scope->rt != nullptr) { - // JS_FreeRuntime(scope->rt); + JS_FreeRuntime(scope->rt); scope->rt = nullptr; } + if (env_to_delete != nullptr) + { + env_to_delete->finalize_instance_data(); + } + delete env_to_delete; delete scope; return status; } @@ -179,7 +186,7 @@ extern "C" auto ctx = JS_NewContext(rt); if (ctx == nullptr) { - // JS_FreeRuntime(rt); + JS_FreeRuntime(rt); return napi_generic_failure; } @@ -187,7 +194,7 @@ extern "C" if (scope == nullptr) { JS_FreeContext(ctx); - // JS_FreeRuntime(rt); + JS_FreeRuntime(rt); return napi_generic_failure; } @@ -196,7 +203,7 @@ extern "C" { delete scope; JS_FreeContext(ctx); - // JS_FreeRuntime(rt); + JS_FreeRuntime(rt); return (status == napi_ok) ? napi_generic_failure : status; } @@ -349,7 +356,7 @@ extern "C" if (!napi_util__::check_env(env) || !napi_util__::is_callable(env, callback)) return napi_invalid_arg; JSContext *ctx = napi_util__::context(env); - JSValueConst argv[] = {callback->get_inner()}; + JSValueConst argv[] = {napi_quickjs_value_inner(env, callback)}; if (JS_EnqueueJob(ctx, napi_promises__::microtask_job, 1, argv) < 0) return JS_HasException(ctx) ? napi_pending_exception : napi_generic_failure; return napi_ok; @@ -436,14 +443,14 @@ extern "C" if (!napi_util__::check_env(env) || promise == nullptr || state_out == nullptr || has_result_out == nullptr) return napi_invalid_arg; JSContext *ctx = napi_util__::context(env); - JSPromiseStateEnum state = JS_PromiseState(ctx, promise->get_inner()); + JSPromiseStateEnum state = JS_PromiseState(ctx, napi_quickjs_value_inner(env, promise)); if (state == JS_PROMISE_NOT_A_PROMISE) return napi_invalid_arg; *state_out = static_cast(state); *has_result_out = state != JS_PROMISE_PENDING; JSValue result = JS_UNDEFINED; if (*has_result_out) - result = JS_PromiseResult(ctx, promise->get_inner()); + result = JS_PromiseResult(ctx, napi_quickjs_value_inner(env, promise)); if (result_out != nullptr) return napi_util__::wrap_owned(env, result, result_out); JS_FreeValue(ctx, result); @@ -561,7 +568,7 @@ extern "C" { if (!napi_util__::check_env(env) || value == nullptr || result_out == nullptr) return napi_invalid_arg; - JSValue buffer = JS_GetTypedArrayBuffer(napi_util__::context(env), value->get_inner(), nullptr, nullptr, nullptr); + JSValue buffer = JS_GetTypedArrayBuffer(napi_util__::context(env), napi_quickjs_value_inner(env, value), nullptr, nullptr, nullptr); if (JS_IsException(buffer)) { JSValue exc = JS_GetException(napi_util__::context(env)); @@ -580,7 +587,7 @@ extern "C" { if (!napi_util__::check_env(env) || value == nullptr || name_out == nullptr) return napi_invalid_arg; - JSValue name = napi_util__::get_constructor_name_value(env, value->get_inner()); + JSValue name = napi_util__::get_constructor_name_value(env, napi_quickjs_value_inner(env, value)); if (JS_IsException(name)) return napi_pending_exception; return napi_util__::wrap_owned(env, name, name_out); @@ -595,7 +602,7 @@ extern "C" if (!napi_util__::check_env(env) || value == nullptr || result_out == nullptr) return napi_invalid_arg; JSContext *ctx = napi_util__::context(env); - JSValue obj = value->get_inner(); + JSValue obj = napi_quickjs_value_inner(env, value); if (!JS_IsObject(obj)) return napi_object_expected; @@ -715,7 +722,7 @@ extern "C" size_t size = 0; uint8_t *bytes = JS_WriteObject(napi_util__::context(env), &size, - value->get_inner(), + napi_quickjs_value_inner(env, value), JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE); if (bytes == nullptr) return napi_generic_failure; @@ -729,7 +736,7 @@ extern "C" if (transfer_list_or_null != nullptr) { - JSValueConst transfer_list = transfer_list_or_null->get_inner(); + JSValueConst transfer_list = napi_quickjs_value_inner(env, transfer_list_or_null); if (!JS_IsUndefined(transfer_list) && !JS_IsNull(transfer_list) && JS_IsArray(transfer_list)) { @@ -968,7 +975,7 @@ extern "C" { if (!napi_util__::check_env(env) || value == nullptr) return napi_invalid_arg; - env->promises().set_continuation_preserved_embedder_data(value->get_inner()); + env->promises().set_continuation_preserved_embedder_data(napi_quickjs_value_inner(env, value)); return napi_ok; } diff --git a/tests/js-native-api/test_function/test.js b/tests/js-native-api/test_function/test.js index a976540..00197a7 100644 --- a/tests/js-native-api/test_function/test.js +++ b/tests/js-native-api/test_function/test.js @@ -31,6 +31,10 @@ assert.strictEqual(test_function.TestCall(func4, 1), 2); assert.strictEqual(test_function.TestName.name, 'Name'); assert.strictEqual(test_function.TestNameShort.name, 'Name_'); +const parentScopeValue = test_function.ReturnParentScopeValue(); +assert.strictEqual(parentScopeValue, test_function.ParentScopeValue); +assert.strictEqual(test_function.ReturnParentScopeValue(), parentScopeValue); + let tracked_function = test_function.MakeTrackedFunction(common.mustCall()); assert(!!tracked_function); tracked_function = null; diff --git a/tests/js-native-api/test_function/test_function.c b/tests/js-native-api/test_function/test_function.c index 2b53df6..22b64d7 100644 --- a/tests/js-native-api/test_function/test_function.c +++ b/tests/js-native-api/test_function/test_function.c @@ -82,6 +82,12 @@ static napi_value TestFunctionName(napi_env env, napi_callback_info info) { return NULL; } +static napi_value parent_scope_value; + +static napi_value ReturnParentScopeValue(napi_env env, napi_callback_info info) { + return parent_scope_value; +} + static void finalize_function(napi_env env, void* data, void* hint) { napi_ref ref = data; @@ -184,6 +190,14 @@ napi_value Init(napi_env env, napi_value exports) { env, "TestBadReturnExceptionPending", NAPI_AUTO_LENGTH, TestBadReturnExceptionPending, NULL, &fn6)); + napi_value fn7; + NODE_API_CALL(env, + napi_create_function( + env, "ReturnParentScopeValue", NAPI_AUTO_LENGTH, + ReturnParentScopeValue, NULL, &fn7)); + + NODE_API_CALL(env, napi_create_object(env, &parent_scope_value)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "TestCall", fn1)); NODE_API_CALL(env, napi_set_named_property(env, exports, "TestName", fn2)); NODE_API_CALL(env, @@ -198,6 +212,12 @@ napi_value Init(napi_env env, napi_value exports) { NODE_API_CALL(env, napi_set_named_property( env, exports, "TestBadReturnExceptionPending", fn6)); + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "ReturnParentScopeValue", fn7)); + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "ParentScopeValue", parent_scope_value)); return exports; } diff --git a/tests/js-native-api/test_instance_data/test.js b/tests/js-native-api/test_instance_data/test.js index 23efb32..66f73e6 100644 --- a/tests/js-native-api/test_instance_data/test.js +++ b/tests/js-native-api/test_instance_data/test.js @@ -19,6 +19,10 @@ if (module !== require.main) { // Test that the instance data can be accessed from a finalizer. test_instance_data.objectWithFinalizer(common.mustCall()); global.gc(); + + // Test that cleanup hooks can remove still-pending cleanup hooks while the + // environment is tearing down. + test_instance_data.registerCleanupHookRemoval(); } else { // When launched as a script, run tests in either a child process or in a // worker thread. diff --git a/tests/js-native-api/test_instance_data/test_instance_data.c b/tests/js-native-api/test_instance_data/test_instance_data.c index 5e33ddd..d1ac2e7 100644 --- a/tests/js-native-api/test_instance_data/test_instance_data.c +++ b/tests/js-native-api/test_instance_data/test_instance_data.c @@ -1,15 +1,40 @@ #include +#include #include #include #include "../common.h" #include "../entry_point.h" +NAPI_EXTERN napi_status NAPI_CDECL napi_add_env_cleanup_hook( + node_api_basic_env env, napi_cleanup_hook fun, void* arg); +NAPI_EXTERN napi_status NAPI_CDECL napi_remove_env_cleanup_hook( + node_api_basic_env env, napi_cleanup_hook fun, void* arg); + typedef struct { size_t value; bool print; napi_ref js_cb_ref; } AddonData; +typedef struct { + napi_env env; +} CleanupHookState; + +static CleanupHookState cleanup_hook_state; + +static void RemovedCleanupHook(void* arg) { + (void)arg; + abort(); +} + +static void RemovingCleanupHook(void* arg) { + CleanupHookState* state = arg; + NODE_API_BASIC_CALL_RETURN_VOID( + state->env, + napi_remove_env_cleanup_hook( + state->env, RemovedCleanupHook, state)); +} + static napi_value Increment(napi_env env, napi_callback_info info) { AddonData* data; napi_value result; @@ -72,6 +97,23 @@ static napi_value ObjectWithFinalizer(napi_env env, napi_callback_info info) { return result; } +static napi_value RegisterCleanupHookRemoval(napi_env env, + napi_callback_info info) { + (void)info; + + cleanup_hook_state.env = env; + NODE_API_CALL( + env, + napi_add_env_cleanup_hook( + env, RemovedCleanupHook, &cleanup_hook_state)); + NODE_API_CALL( + env, + napi_add_env_cleanup_hook( + env, RemovingCleanupHook, &cleanup_hook_state)); + + return NULL; +} + EXTERN_C_START napi_value Init(napi_env env, napi_value exports) { AddonData* data = malloc(sizeof(*data)); @@ -85,6 +127,8 @@ napi_value Init(napi_env env, napi_value exports) { DECLARE_NODE_API_PROPERTY("increment", Increment), DECLARE_NODE_API_PROPERTY("setPrintOnDelete", SetPrintOnDelete), DECLARE_NODE_API_PROPERTY("objectWithFinalizer", ObjectWithFinalizer), + DECLARE_NODE_API_PROPERTY( + "registerCleanupHookRemoval", RegisterCleanupHookRemoval), }; NODE_API_CALL(env,