|
| 1 | +#include <napi/env.h> |
| 2 | +#include "js_native_api_quickjs.h" |
| 3 | +#include <stdexcept> |
| 4 | +#if defined(__clang__) |
| 5 | +#pragma clang diagnostic push |
| 6 | +#pragma clang diagnostic ignored "-Wshorten-64-to-32" |
| 7 | +#endif |
| 8 | +#include <quickjs.h> |
| 9 | +#if defined(__clang__) |
| 10 | +#pragma clang diagnostic pop |
| 11 | +#endif |
| 12 | + |
| 13 | +namespace Napi |
| 14 | +{ |
| 15 | + Env Attach(JSContext* context) |
| 16 | + { |
| 17 | + napi_env env_ptr{new napi_env__}; |
| 18 | + env_ptr->context = context; |
| 19 | + env_ptr->current_context = env_ptr->context; |
| 20 | + |
| 21 | + // Cache Object.prototype.hasOwnProperty for napi_has_own_property's |
| 22 | + // fast path. These lookups are fundamental to any valid context, so a |
| 23 | + // failure here signals a broken context: throw rather than silently |
| 24 | + // leaving has_own_property_function undefined (matching how the other |
| 25 | + // engines fail Attach loudly). |
| 26 | + JSValue global = JS_GetGlobalObject(context); |
| 27 | + JSValue object = JS_GetPropertyStr(context, global, "Object"); |
| 28 | + JS_FreeValue(context, global); |
| 29 | + if (JS_IsException(object) || !JS_IsObject(object)) |
| 30 | + { |
| 31 | + JS_FreeValue(context, object); |
| 32 | + delete env_ptr; |
| 33 | + throw std::runtime_error{"Napi::Attach: failed to resolve the global 'Object' constructor"}; |
| 34 | + } |
| 35 | + |
| 36 | + // Use the constructor's "prototype" property to get Object.prototype. |
| 37 | + // JS_GetPrototype(object) would return the Object *constructor's* |
| 38 | + // [[Prototype]] (Function.prototype), from which hasOwnProperty is only |
| 39 | + // reachable by inheritance - correct by luck, but not by intent. |
| 40 | + JSValue prototype = JS_GetPropertyStr(context, object, "prototype"); |
| 41 | + JS_FreeValue(context, object); |
| 42 | + if (JS_IsException(prototype) || !JS_IsObject(prototype)) |
| 43 | + { |
| 44 | + JS_FreeValue(context, prototype); |
| 45 | + delete env_ptr; |
| 46 | + throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype"}; |
| 47 | + } |
| 48 | + |
| 49 | + JSValue hasOwnProperty = JS_GetPropertyStr(context, prototype, "hasOwnProperty"); |
| 50 | + JS_FreeValue(context, prototype); |
| 51 | + if (JS_IsException(hasOwnProperty) || !JS_IsFunction(context, hasOwnProperty)) |
| 52 | + { |
| 53 | + JS_FreeValue(context, hasOwnProperty); |
| 54 | + delete env_ptr; |
| 55 | + throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype.hasOwnProperty"}; |
| 56 | + } |
| 57 | + |
| 58 | + env_ptr->has_own_property_function = hasOwnProperty; |
| 59 | + |
| 60 | + return {env_ptr}; |
| 61 | + } |
| 62 | + |
| 63 | + void Detach(Env env) |
| 64 | + { |
| 65 | + napi_env env_ptr{env}; |
| 66 | + if (env_ptr) |
| 67 | + { |
| 68 | + // Release every strong napi_ref still outstanding. This mirrors |
| 69 | + // the V8 impl (napi_env__::DeleteMe) and is essential on QuickJS: |
| 70 | + // any surviving strong ref pins a JS value from outside the GC |
| 71 | + // graph, which prevents the teardown cascade in JS_FreeContext |
| 72 | + // from running and triggers list_empty(gc_obj_list) assert in |
| 73 | + // JS_FreeRuntime. |
| 74 | + // |
| 75 | + // Freeing a value can synchronously run a napi_wrap finalizer |
| 76 | + // whose C++ destructor releases *other* embedded napi_refs (e.g. |
| 77 | + // an AbortController destroying its AbortSignal ObjectReference). |
| 78 | + // Those nested napi_delete_reference calls must not perform a real |
| 79 | + // JS_FreeValue - otherwise a value can be freed twice - and must |
| 80 | + // not mutate refs_list while we iterate it. So we first neutralize |
| 81 | + // every ref (count/value zeroed, list cleared) and only then free |
| 82 | + // the snapshotted values. Any finalizer-driven |
| 83 | + // napi_delete_reference then sees count == 0 and is a safe no-op. |
| 84 | + std::vector<JSValue> strongValues; |
| 85 | + strongValues.reserve(env_ptr->refs_list.size()); |
| 86 | + for (void* p : env_ptr->refs_list) |
| 87 | + { |
| 88 | + auto* info = reinterpret_cast<RefInfo*>(p); |
| 89 | + if (info->count > 0) |
| 90 | + { |
| 91 | + strongValues.push_back(info->value); |
| 92 | + } |
| 93 | + info->count = 0; |
| 94 | + info->value = JS_UNDEFINED; |
| 95 | + } |
| 96 | + env_ptr->refs_list.clear(); |
| 97 | + env_ptr->detached = true; |
| 98 | + |
| 99 | + for (JSValue value : strongValues) |
| 100 | + { |
| 101 | + JS_FreeValue(env_ptr->context, value); |
| 102 | + } |
| 103 | + |
| 104 | + if (!JS_IsUndefined(env_ptr->has_own_property_function)) |
| 105 | + { |
| 106 | + JS_FreeValue(env_ptr->context, env_ptr->has_own_property_function); |
| 107 | + env_ptr->has_own_property_function = JS_UNDEFINED; |
| 108 | + } |
| 109 | + |
| 110 | + // Free all remaining JSValues in the handle scope stack |
| 111 | + for (auto& ptr : env_ptr->handle_scope_stack) |
| 112 | + { |
| 113 | + JS_FreeValue(env_ptr->context, *ptr); |
| 114 | + } |
| 115 | + env_ptr->handle_scope_stack.clear(); |
| 116 | + |
| 117 | + // Run the cycle collector so napi_wrap finalizers (which |
| 118 | + // destroy C++ wrapper objects and release any embedded |
| 119 | + // napi_refs) get a chance to execute while the env is still |
| 120 | + // valid. A second pass picks up anything unpinned by the |
| 121 | + // first pass's finalizers. |
| 122 | + JSRuntime* rt = JS_GetRuntime(env_ptr->context); |
| 123 | + JS_RunGC(rt); |
| 124 | + JS_RunGC(rt); |
| 125 | + |
| 126 | + // Drop the initial owner reference taken in Attach. If every |
| 127 | + // ExternalData finalizer already ran during the GC passes above, |
| 128 | + // this deletes the env now. If some are deferred to the engine's |
| 129 | + // JS_FreeContext/JS_FreeRuntime teardown cascade (the common case), |
| 130 | + // each still holds a count, so the env survives until the last such |
| 131 | + // finalizer completes and drops the final count - deleting the env |
| 132 | + // exactly once, after its last use, with no leak. |
| 133 | + // |
| 134 | + // NOTE: env_ptr must not be touched after this point; it may have |
| 135 | + // already been deleted. |
| 136 | + env_ptr->Unref(); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + JSContext* GetContext(Env env) |
| 141 | + { |
| 142 | + napi_env env_ptr{env}; |
| 143 | + return env_ptr->context; |
| 144 | + } |
| 145 | +} |
0 commit comments