From 084734db64a11615b7e0dc6d5414f1c5e3675804 Mon Sep 17 00:00:00 2001 From: 190n Date: Fri, 6 Sep 2024 23:55:19 -0800 Subject: [PATCH] Implement napi_handle_scope and napi_escapable_handle_scope (#13756) --- src/bun.js/bindings/BunProcess.cpp | 4 + src/bun.js/bindings/ZigGlobalObject.cpp | 8 + src/bun.js/bindings/ZigGlobalObject.h | 9 + src/bun.js/bindings/napi.cpp | 126 +++---- src/bun.js/bindings/napi.h | 21 +- src/bun.js/bindings/napi_handle_scope.cpp | 133 ++++++++ src/bun.js/bindings/napi_handle_scope.h | 97 ++++++ .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + src/napi/napi.zig | 312 +++++++++++------- test/napi/napi-app/main.cpp | 298 +++++++++++++++++ test/napi/napi-app/main.js | 8 +- test/napi/napi-app/module.js | 9 + test/napi/napi.test.ts | 53 ++- 14 files changed, 887 insertions(+), 193 deletions(-) create mode 100644 src/bun.js/bindings/napi_handle_scope.cpp create mode 100644 src/bun.js/bindings/napi_handle_scope.h create mode 100644 test/napi/napi-app/module.js diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 8ba8181c68..282200d76d 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -33,6 +33,8 @@ #include "AsyncContextFrame.h" +#include "napi_handle_scope.h" + #ifndef WIN32 #include #include @@ -406,6 +408,8 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, return JSC::JSValue::encode(JSC::JSValue {}); } + NapiHandleScope handleScope(globalObject); + EncodedJSValue exportsValue = JSC::JSValue::encode(exports); JSC::JSValue resultValue = JSValue::decode(napi_register_module_v1(globalObject, exportsValue)); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 02f5ec4f6c..8c02316b80 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -129,6 +129,7 @@ #include "libusockets.h" #include "ModuleLoader.h" #include "napi_external.h" +#include "napi_handle_scope.h" #include "napi.h" #include "NodeHTTP.h" #include "NodeVM.h" @@ -2901,6 +2902,10 @@ void GlobalObject::finishCreation(VM& vm) Bun::NapiPrototype::createStructure(init.vm, init.owner, init.owner->objectPrototype())); }); + m_NapiHandleScopeImplStructure.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(Bun::NapiHandleScopeImpl::createStructure(init.vm, init.owner)); + }); + m_cachedNodeVMGlobalObjectStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { init.set(WebCore::createNodeVMGlobalObjectStructure(init.vm)); @@ -3582,6 +3587,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_pendingNapiModuleAndExports[0]); visitor.append(thisObject->m_pendingNapiModuleAndExports[1]); + visitor.append(thisObject->m_currentNapiHandleScopeImpl); + thisObject->m_asyncBoundFunctionStructure.visit(visitor); thisObject->m_bunObject.visit(visitor); thisObject->m_cachedNodeVMGlobalObjectStructure.visit(visitor); @@ -3620,6 +3627,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_NapiExternalStructure.visit(visitor); thisObject->m_NAPIFunctionStructure.visit(visitor); thisObject->m_NapiPrototypeStructure.visit(visitor); + thisObject->m_NapiHandleScopeImplStructure.visit(visitor); thisObject->m_nativeMicrotaskTrampoline.visit(visitor); thisObject->m_navigatorObject.visit(visitor); thisObject->m_NodeVMScriptClassStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 7b67d27f5c..d4ef700fb7 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -29,6 +29,7 @@ class Performance; namespace Bun { class InternalModuleRegistry; +class NapiHandleScopeImpl; } // namespace Bun namespace v8 { @@ -283,6 +284,7 @@ public: Structure* NapiExternalStructure() const { return m_NapiExternalStructure.getInitializedOnMainThread(this); } Structure* NapiPrototypeStructure() const { return m_NapiPrototypeStructure.getInitializedOnMainThread(this); } Structure* NAPIFunctionStructure() const { return m_NAPIFunctionStructure.getInitializedOnMainThread(this); } + Structure* NapiHandleScopeImplStructure() const { return m_NapiHandleScopeImplStructure.getInitializedOnMainThread(this); } Structure* JSSQLStatementStructure() const { return m_JSSQLStatementStructure.getInitializedOnMainThread(this); } @@ -405,6 +407,11 @@ public: // When a napi module initializes on dlopen, we need to know what the value is mutable JSC::WriteBarrier m_pendingNapiModuleAndExports[2]; + // The handle scope where all new NAPI values will be created. You must not pass any napi_values + // back to a NAPI function without putting them in the handle scope, as the NAPI function may + // move them off the stack which will cause them to get collected if not in the handle scope. + JSC::WriteBarrier m_currentNapiHandleScopeImpl; + // The original, unmodified Error.prepareStackTrace. // // We set a default value for this to mimick Node.js behavior It is a @@ -564,6 +571,8 @@ public: LazyProperty m_NapiExternalStructure; LazyProperty m_NapiPrototypeStructure; LazyProperty m_NAPIFunctionStructure; + LazyProperty m_NapiHandleScopeImplStructure; + LazyProperty m_JSSQLStatementStructure; LazyProperty m_V8GlobalInternals; diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index b2d68be3ee..58c1649092 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -7,7 +7,7 @@ #include "JavaScriptCore/JSGlobalObject.h" #include "JavaScriptCore/SourceCode.h" #include "js_native_api_types.h" -#include "v8/V8HandleScope.h" +#include "napi_handle_scope.h" #include "helpers.h" #include @@ -148,7 +148,7 @@ void NapiFinalizer::call(JSC::JSGlobalObject* globalObject, void* data) { if (this->finalize_cb) { NAPI_PREMABLE - this->finalize_cb(reinterpret_cast(globalObject), data, this->finalize_hint); + this->finalize_cb(toNapi(globalObject), data, this->finalize_hint); } } @@ -375,10 +375,10 @@ public: // and receives the actual count of args. napi_value* argv, // [out] Array of values napi_value* this_arg, // [out] Receives the JS 'this' arg for the call - void** data) + void** data, Zig::GlobalObject* globalObject) { if (this_arg != nullptr) { - *this_arg = toNapi(callframe.thisValue()); + *this_arg = toNapi(callframe.thisValue(), globalObject); } if (data != nullptr) { @@ -404,7 +404,7 @@ public: if (overflow > 0) { while (overflow--) { - *argv = toNapi(jsUndefined()); + *argv = toNapi(jsUndefined(), globalObject); argv++; } } @@ -442,7 +442,7 @@ public: NAPICallFrame frame(JSC::ArgList(args), function->m_dataPtr); auto scope = DECLARE_THROW_SCOPE(vm); - v8::HandleScope handleScope(v8::Isolate::fromGlobalObject(static_cast(globalObject))); + Bun::NapiHandleScope handleScope(jsCast(globalObject)); auto result = callback(env, NAPICallFrame::toNapiCallbackInfo(frame)); @@ -701,7 +701,7 @@ extern "C" napi_status napi_get_property(napi_env env, napi_value object, auto keyProp = toJS(key); JSC::EnsureStillAliveScope ensureAlive2(keyProp); auto scope = DECLARE_CATCH_SCOPE(vm); - *result = toNapi(target->getIfPropertyExists(globalObject, keyProp.toPropertyKey(globalObject))); + *result = toNapi(target->getIfPropertyExists(globalObject, keyProp.toPropertyKey(globalObject)), globalObject); RETURN_IF_EXCEPTION(scope, napi_generic_failure); scope.clearException(); @@ -727,7 +727,7 @@ extern "C" napi_status napi_delete_property(napi_env env, napi_value object, RETURN_IF_EXCEPTION(scope, napi_generic_failure); if (LIKELY(result)) { - *result = toNapi(deleteResult); + *result = deleteResult; } scope.clearException(); @@ -752,7 +752,7 @@ extern "C" napi_status napi_has_own_property(napi_env env, napi_value object, auto keyProp = toJS(key); auto scope = DECLARE_CATCH_SCOPE(vm); - *result = toNapi(target->hasOwnProperty(globalObject, JSC::PropertyName(keyProp.toPropertyKey(globalObject)))); + *result = target->hasOwnProperty(globalObject, JSC::PropertyName(keyProp.toPropertyKey(globalObject))); RETURN_IF_EXCEPTION(scope, napi_generic_failure); scope.clearException(); @@ -799,7 +799,7 @@ extern "C" napi_status napi_create_arraybuffer(napi_env env, { NAPI_PREMABLE - JSC::JSGlobalObject* globalObject = toJS(env); + Zig::GlobalObject* globalObject = toJS(env); if (UNLIKELY(!globalObject || !result)) { return napi_invalid_arg; } @@ -822,7 +822,7 @@ extern "C" napi_status napi_create_arraybuffer(napi_env env, if (LIKELY(data && jsArrayBuffer->impl())) { *data = jsArrayBuffer->impl()->data(); } - *result = toNapi(jsArrayBuffer); + *result = toNapi(jsArrayBuffer, globalObject); return napi_ok; } @@ -886,7 +886,7 @@ extern "C" napi_status napi_get_named_property(napi_env env, napi_value object, PROPERTY_NAME_FROM_UTF8(name); auto scope = DECLARE_CATCH_SCOPE(vm); - *result = toNapi(target->getIfPropertyExists(globalObject, name)); + *result = toNapi(target->getIfPropertyExists(globalObject, name), globalObject); RETURN_IF_EXCEPTION(scope, napi_generic_failure); scope.clearException(); @@ -916,14 +916,14 @@ node_api_create_external_string_latin1(napi_env env, #if NAPI_VERBOSE printf("[napi] string finalize_callback\n"); #endif - finalize_callback(reinterpret_cast(defaultGlobalObject()), nullptr, hint); + finalize_callback(toNapi(defaultGlobalObject()), nullptr, hint); } }); - JSGlobalObject* globalObject = defaultGlobalObject(env); + Zig::GlobalObject* globalObject = toJS(env); JSString* out = JSC::jsString(globalObject->vm(), WTF::String(impl)); ensureStillAliveHere(out); - *result = toNapi(out); + *result = toNapi(out, globalObject); ensureStillAliveHere(out); return napi_ok; @@ -954,14 +954,14 @@ node_api_create_external_string_utf16(napi_env env, #endif if (finalize_callback) { - finalize_callback(reinterpret_cast(defaultGlobalObject()), nullptr, hint); + finalize_callback(toNapi(defaultGlobalObject()), nullptr, hint); } }); - JSGlobalObject* globalObject = defaultGlobalObject(env); + Zig::GlobalObject* globalObject = toJS(env); JSString* out = JSC::jsString(globalObject->vm(), WTF::String(impl)); ensureStillAliveHere(out); - *result = toNapi(out); + *result = toNapi(out, globalObject); ensureStillAliveHere(out); return napi_ok; @@ -997,7 +997,8 @@ extern "C" void napi_module_register(napi_module* mod) JSC::Strong strongObject = { vm, object }; - JSValue resultValue = toJS(mod->nm_register_func(toNapi(globalObject), toNapi(object))); + Bun::NapiHandleScope handleScope(globalObject); + JSValue resultValue = toJS(mod->nm_register_func(toNapi(globalObject), toNapi(object, globalObject))); RETURN_IF_EXCEPTION(scope, void()); @@ -1170,7 +1171,7 @@ extern "C" napi_status napi_create_function(napi_env env, const char* utf8name, auto* function = NAPIFunction::create(vm, globalObject, length, name, method, data); ASSERT(function->isCallable()); - *result = toNapi(JSC::JSValue(function)); + *result = toNapi(JSC::JSValue(function), globalObject); return napi_ok; } @@ -1187,9 +1188,10 @@ extern "C" napi_status napi_get_cb_info( NAPI_PREMABLE JSC::CallFrame* callFrame = reinterpret_cast(cbinfo); + Zig::GlobalObject* globalObject = toJS(env); if (NAPICallFrame* frame = NAPICallFrame::get(callFrame).value_or(nullptr)) { - NAPICallFrame::extract(*frame, argc, argv, this_arg, data); + NAPICallFrame::extract(*frame, argc, argv, this_arg, data, globalObject); return napi_ok; } @@ -1204,14 +1206,14 @@ extern "C" napi_status napi_get_cb_info( memcpy(argv, callFrame->addressOfArgumentsStart(), argsToCopy * sizeof(JSC::JSValue)); for (size_t i = outputArgsCount; i < inputArgsCount; i++) { - argv[i] = toNapi(JSC::jsUndefined()); + argv[i] = toNapi(JSC::jsUndefined(), globalObject); } } JSC::JSValue thisValue = callFrame->thisValue(); if (this_arg != nullptr) { - *this_arg = toNapi(thisValue); + *this_arg = toNapi(thisValue, globalObject); } if (data != nullptr) { @@ -1456,7 +1458,7 @@ extern "C" napi_status napi_get_reference_value(napi_env env, napi_ref ref, return napi_invalid_arg; } NapiRef* napiRef = toJS(ref); - *result = toNapi(napiRef->value()); + *result = toNapi(napiRef->value(), toJS(env)); return napi_ok; } @@ -1582,7 +1584,7 @@ extern "C" napi_status napi_get_and_clear_last_exception(napi_env env, } auto globalObject = toJS(env); - *result = toNapi(JSC::JSValue(globalObject->vm().lastException())); + *result = toNapi(JSC::JSValue(globalObject->vm().lastException()), globalObject); globalObject->vm().clearLastException(); return napi_ok; } @@ -1632,7 +1634,7 @@ extern "C" napi_status node_api_symbol_for(napi_env env, } auto description = WTF::String::fromUTF8({ utf8description, length == NAPI_AUTO_LENGTH ? strlen(utf8description) : length }); - *result = toNapi(JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey(description))); + *result = toNapi(JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey(description)), globalObject); return napi_ok; } @@ -1655,7 +1657,7 @@ extern "C" napi_status node_api_create_syntax_error(napi_env env, return napi_generic_failure; } - *result = toNapi(err); + *result = toNapi(err, toJS(env)); return napi_ok; } @@ -1707,7 +1709,7 @@ extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, return napi_generic_failure; } - *result = toNapi(err); + *result = toNapi(err, toJS(env)); return napi_ok; } @@ -1733,7 +1735,7 @@ extern "C" napi_status napi_create_error(napi_env env, napi_value code, return napi_generic_failure; } - *result = toNapi(err); + *result = toNapi(err, toJS(env)); return napi_ok; } extern "C" napi_status napi_throw_range_error(napi_env env, const char* code, @@ -1760,8 +1762,7 @@ extern "C" napi_status napi_object_freeze(napi_env env, napi_value object_value) JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); - JSC::EncodedJSValue encodedValue = reinterpret_cast(object_value); - JSC::JSValue value = JSC::JSValue::decode(encodedValue); + JSC::JSValue value = toJS(object_value); if (!value.isObject()) { return NAPI_OBJECT_EXPECTED; } @@ -1780,8 +1781,7 @@ extern "C" napi_status napi_object_seal(napi_env env, napi_value object_value) JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); - JSC::EncodedJSValue encodedValue = reinterpret_cast(object_value); - JSC::JSValue value = JSC::JSValue::decode(encodedValue); + JSC::JSValue value = toJS(object_value); if (UNLIKELY(!value.isObject())) { return NAPI_OBJECT_EXPECTED; @@ -1804,7 +1804,7 @@ extern "C" napi_status napi_get_global(napi_env env, napi_value* result) } Zig::GlobalObject* globalObject = toJS(env); - *result = reinterpret_cast(globalObject->globalThis()); + *result = toNapi(globalObject->globalThis(), globalObject); return napi_ok; } @@ -1829,7 +1829,7 @@ extern "C" napi_status napi_create_range_error(napi_env env, napi_value code, if (UNLIKELY(!err)) { return napi_generic_failure; } - *result = toNapi(err); + *result = toNapi(err, toJS(env)); return napi_ok; } @@ -1848,12 +1848,12 @@ extern "C" napi_status napi_get_new_target(napi_env env, CallFrame* callFrame = reinterpret_cast(cbinfo); if (NAPICallFrame* frame = NAPICallFrame::get(callFrame).value_or(nullptr)) { - *result = toNapi(frame->newTarget); + *result = toNapi(frame->newTarget, toJS(env)); return napi_ok; } JSC::JSValue newTarget = callFrame->newTarget(); - *result = reinterpret_cast(JSC::JSValue::encode(newTarget)); + *result = toNapi(newTarget, toJS(env)); return napi_ok; } @@ -1872,14 +1872,14 @@ extern "C" napi_status napi_create_dataview(napi_env env, size_t length, JSC::VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); - JSC::EncodedJSValue encodedArraybuffer = reinterpret_cast(arraybuffer); - auto arraybufferValue = JSC::jsDynamicCast(JSC::JSValue::decode(encodedArraybuffer)); - if (!arraybufferValue) { + JSC::JSValue arraybufferValue = toJS(arraybuffer); + auto arraybufferPtr = JSC::jsDynamicCast(arraybufferValue); + if (!arraybufferPtr) { return napi_arraybuffer_expected; } - auto dataView = JSC::DataView::create(arraybufferValue->impl(), byte_offset, length); + auto dataView = JSC::DataView::create(arraybufferPtr->impl(), byte_offset, length); - *result = reinterpret_cast(dataView->wrap(globalObject, globalObject)); + *result = toNapi(dataView->wrap(globalObject, globalObject), globalObject); return napi_ok; } @@ -1932,6 +1932,7 @@ JSC_DEFINE_HOST_FUNCTION(NapiClass_ConstructorFunction, }); NAPICallFrame frame(JSC::ArgList(args), nullptr); frame.newTarget = newTarget; + Bun::NapiHandleScope handleScope(jsCast(globalObject)); napi->constructor()(globalObject, reinterpret_cast(NAPICallFrame::toNapiCallbackInfo(frame))); RETURN_IF_EXCEPTION(scope, {}); @@ -2019,7 +2020,7 @@ extern "C" napi_status napi_get_all_property_names( JSC::JSArray* exportKeys = ownPropertyKeys(globalObject, object, jsc_property_mode, jsc_key_mode); // TODO: filter - *result = toNapi(JSC::JSValue::encode(exportKeys)); + *result = toNapi(JSC::JSValue(exportKeys), globalObject); return napi_ok; } @@ -2091,7 +2092,7 @@ extern "C" napi_status napi_define_class(napi_env env, napiClass->dataPtr = data; } - *result = toNapi(value); + *result = toNapi(value, globalObject); return napi_ok; } @@ -2113,10 +2114,10 @@ extern "C" napi_status napi_coerce_to_string(napi_env env, napi_value value, // .toString() can throw JSC::JSValue resultValue = JSC::JSValue(jsValue.toString(globalObject)); JSC::EnsureStillAliveScope ensureStillAlive1(resultValue); - *result = toNapi(resultValue); + *result = toNapi(resultValue, globalObject); if (UNLIKELY(scope.exception())) { - *result = reinterpret_cast(JSC::JSValue::encode(JSC::jsUndefined())); + *result = toNapi(JSC::jsUndefined(), globalObject); return napi_generic_failure; } scope.clearException(); @@ -2144,13 +2145,13 @@ extern "C" napi_status napi_get_property_names(napi_env env, napi_value object, JSC::EnsureStillAliveScope ensureStillAlive(jsValue); JSC::JSValue value = JSC::ownPropertyKeys(globalObject, jsValue.getObject(), PropertyNameMode::Strings, DontEnumPropertiesMode::Include); if (UNLIKELY(scope.exception())) { - *result = reinterpret_cast(JSC::JSValue::encode(JSC::jsUndefined())); + *result = toNapi(JSC::jsUndefined(), globalObject); return napi_generic_failure; } scope.clearException(); JSC::EnsureStillAliveScope ensureStillAlive1(value); - *result = toNapi(value); + *result = toNapi(value, globalObject); return napi_ok; } @@ -2180,7 +2181,7 @@ extern "C" napi_status napi_create_external_buffer(napi_env env, size_t length, auto* buffer = JSC::JSUint8Array::create(globalObject, subclassStructure, WTFMove(arrayBuffer), 0, length); - *result = toNapi(buffer); + *result = toNapi(buffer, globalObject); return napi_ok; } @@ -2207,7 +2208,7 @@ extern "C" napi_status napi_create_external_arraybuffer(napi_env env, void* exte auto* buffer = JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Shared), WTFMove(arrayBuffer)); - *result = toNapi(buffer); + *result = toNapi(buffer, globalObject); return napi_ok; } @@ -2220,7 +2221,7 @@ extern "C" napi_status napi_create_double(napi_env env, double value, return napi_invalid_arg; } - *result = toNapi(jsDoubleNumber(value)); + *result = toNapi(jsDoubleNumber(value), toJS(env)); return napi_ok; } @@ -2336,7 +2337,7 @@ extern "C" napi_status napi_get_element(napi_env env, napi_value objectValue, JSValue element = object->getIndex(toJS(env), index); RETURN_IF_EXCEPTION(scope, napi_generic_failure); - *result = toNapi(element); + *result = toNapi(element, toJS(env)); return napi_ok; } @@ -2375,7 +2376,7 @@ extern "C" napi_status napi_create_object(napi_env env, napi_value* result) JSValue value = JSValue(NapiPrototype::create(vm, globalObject->NapiPrototypeStructure())); - *result = toNapi(value); + *result = toNapi(value, globalObject); JSC::EnsureStillAliveScope ensureStillAlive(value); return napi_ok; @@ -2396,7 +2397,7 @@ extern "C" napi_status napi_create_external(napi_env env, void* data, auto* structure = globalObject->NapiExternalStructure(); JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, reinterpret_cast(finalize_cb)); JSC::EnsureStillAliveScope ensureStillAlive(value); - *result = toNapi(value); + *result = toNapi(value, globalObject); return napi_ok; } @@ -2571,7 +2572,7 @@ extern "C" napi_status napi_run_script(napi_env env, napi_value script, { NAPI_PREMABLE - JSC::JSGlobalObject* globalObject = toJS(env); + Zig::GlobalObject* globalObject = toJS(env); if (UNLIKELY(result == nullptr)) { return napi_invalid_arg; } @@ -2599,7 +2600,7 @@ extern "C" napi_status napi_run_script(napi_env env, napi_value script, } if (result != nullptr) { - *result = toNapi(value); + *result = toNapi(value, globalObject); } RELEASE_AND_RETURN(throwScope, napi_ok); @@ -2652,7 +2653,7 @@ extern "C" napi_status napi_create_bigint_words(napi_env env, } } - *result = toNapi(bigint); + *result = toNapi(bigint, globalObject); return napi_ok; } @@ -2680,12 +2681,13 @@ extern "C" napi_status napi_create_symbol(napi_env env, napi_value description, } if (descriptionString->length() > 0) { - *result = toNapi(JSC::Symbol::createWithDescription(vm, descriptionString->value(globalObject))); + *result = toNapi(JSC::Symbol::createWithDescription(vm, descriptionString->value(globalObject)), + globalObject); return napi_ok; } } - *result = toNapi(JSC::Symbol::create(vm)); + *result = toNapi(JSC::Symbol::create(vm), globalObject); return napi_ok; } @@ -2721,7 +2723,7 @@ extern "C" napi_status napi_new_instance(napi_env env, napi_value constructor, auto value = construct(globalObject, constructorObject, constructData, args); RETURN_IF_EXCEPTION(throwScope, napi_pending_exception); - *result = toNapi(value); + *result = toNapi(value, globalObject); RELEASE_AND_RETURN(throwScope, napi_ok); } @@ -2761,9 +2763,9 @@ extern "C" napi_status napi_call_function(napi_env env, napi_value recv_napi, if (result_ptr) { if (result.isEmpty()) { - *result_ptr = toNapi(JSC::jsUndefined()); + *result_ptr = toNapi(JSC::jsUndefined(), globalObject); } else { - *result_ptr = toNapi(result); + *result_ptr = toNapi(result, globalObject); } } diff --git a/src/bun.js/bindings/napi.h b/src/bun.js/bindings/napi.h index 5e4e173f6c..b644bf4d56 100644 --- a/src/bun.js/bindings/napi.h +++ b/src/bun.js/bindings/napi.h @@ -1,9 +1,5 @@ #pragma once -namespace Zig { -class GlobalObject; -} - #include "root.h" #include #include @@ -14,6 +10,8 @@ class GlobalObject; #include "js_native_api_types.h" #include #include "JSFFIFunction.h" +#include "ZigGlobalObject.h" +#include "napi_handle_scope.h" namespace JSC { class JSGlobalObject; @@ -38,14 +36,12 @@ static inline Zig::GlobalObject* toJS(napi_env val) return reinterpret_cast(val); } -static inline napi_value toNapi(JSC::EncodedJSValue val) +static inline napi_value toNapi(JSC::JSValue val, Zig::GlobalObject* globalObject) { - return reinterpret_cast(val); -} - -static inline napi_value toNapi(JSC::JSValue val) -{ - return toNapi(JSC::JSValue::encode(val)); + if (val.isCell()) { + globalObject->m_currentNapiHandleScopeImpl.get()->append(val); + } + return reinterpret_cast(JSC::JSValue::encode(val)); } static inline napi_env toNapi(JSC::JSGlobalObject* val) @@ -61,7 +57,6 @@ public: void call(JSC::JSGlobalObject* globalObject, void* data); }; - // This is essentially JSC::JSWeakValue, except with a JSCell* instead of a // JSObject*. Sometimes, a napi embedder might want to store a JSC::Exception, a // JSC::HeapBigInt, JSC::Symbol, etc inside of a NapiRef. So we can't limit it @@ -313,4 +308,4 @@ static inline NapiRef* toJS(napi_ref val) Structure* createNAPIFunctionStructure(VM& vm, JSC::JSGlobalObject* globalObject); -} \ No newline at end of file +} diff --git a/src/bun.js/bindings/napi_handle_scope.cpp b/src/bun.js/bindings/napi_handle_scope.cpp new file mode 100644 index 0000000000..5aa599ced6 --- /dev/null +++ b/src/bun.js/bindings/napi_handle_scope.cpp @@ -0,0 +1,133 @@ +#include "napi_handle_scope.h" + +#include "ZigGlobalObject.h" + +namespace Bun { + +// for CREATE_METHOD_TABLE +namespace JSCastingHelpers = JSC::JSCastingHelpers; + +const JSC::ClassInfo NapiHandleScopeImpl::s_info = { + "NapiHandleScopeImpl"_s, + nullptr, + nullptr, + nullptr, + CREATE_METHOD_TABLE(NapiHandleScopeImpl) +}; + +NapiHandleScopeImpl::NapiHandleScopeImpl(JSC::VM& vm, JSC::Structure* structure, NapiHandleScopeImpl* parent, bool escapable) + : Base(vm, structure) + , m_parent(parent) + , m_escapeSlot(nullptr) +{ + if (escapable) { + m_escapeSlot = parent->reserveSlot(); + } +} + +NapiHandleScopeImpl* NapiHandleScopeImpl::create(JSC::VM& vm, + JSC::Structure* structure, + NapiHandleScopeImpl* parent, + bool escapable) +{ + NapiHandleScopeImpl* buffer = new (NotNull, JSC::allocateCell(vm)) + NapiHandleScopeImpl(vm, structure, parent, escapable); + buffer->finishCreation(vm); + return buffer; +} + +template +void NapiHandleScopeImpl::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + NapiHandleScopeImpl* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + WTF::Locker locker { thisObject->cellLock() }; + + for (auto& handle : thisObject->m_storage) { + visitor.append(handle); + } + + if (thisObject->m_parent) { + visitor.appendUnbarriered(thisObject->m_parent); + } +} + +DEFINE_VISIT_CHILDREN(NapiHandleScopeImpl); + +void NapiHandleScopeImpl::append(JSC::JSValue val) +{ + m_storage.append(Slot(vm(), this, val)); +} + +bool NapiHandleScopeImpl::escape(JSC::JSValue val) +{ + if (!m_escapeSlot) { + return false; + } + + m_escapeSlot->set(vm(), m_parent, val); + m_escapeSlot = nullptr; + return true; +} + +NapiHandleScopeImpl::Slot* NapiHandleScopeImpl::reserveSlot() +{ + m_storage.append(Slot()); + return &m_storage.last(); +} + +NapiHandleScopeImpl* NapiHandleScope::push(Zig::GlobalObject* globalObject, bool escapable) +{ + auto* impl = NapiHandleScopeImpl::create(globalObject->vm(), + globalObject->NapiHandleScopeImplStructure(), + globalObject->m_currentNapiHandleScopeImpl.get(), + escapable); + globalObject->m_currentNapiHandleScopeImpl.set(globalObject->vm(), globalObject, impl); + return impl; +} + +void NapiHandleScope::pop(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current) +{ + RELEASE_ASSERT_WITH_MESSAGE(current == globalObject->m_currentNapiHandleScopeImpl.get(), + "Unbalanced napi_handle_scope opens and closes"); + if (auto* parent = current->parent()) { + globalObject->m_currentNapiHandleScopeImpl.set(globalObject->vm(), globalObject, parent); + } else { + globalObject->m_currentNapiHandleScopeImpl.clear(); + } +} + +NapiHandleScope::NapiHandleScope(Zig::GlobalObject* globalObject) + : m_globalObject(globalObject) + , m_impl(NapiHandleScope::push(globalObject, false)) +{ +} + +NapiHandleScope::~NapiHandleScope() +{ + NapiHandleScope::pop(m_globalObject, m_impl); +} + +extern "C" NapiHandleScopeImpl* NapiHandleScope__push(Zig::GlobalObject* globalObject, bool escapable) +{ + return NapiHandleScope::push(globalObject, escapable); +} + +extern "C" void NapiHandleScope__pop(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current) +{ + return NapiHandleScope::pop(globalObject, current); +} + +extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value) +{ + globalObject->m_currentNapiHandleScopeImpl.get()->append(JSC::JSValue::decode(value)); +} + +extern "C" bool NapiHandleScope__escape(NapiHandleScopeImpl* handleScope, JSC::EncodedJSValue value) +{ + return handleScope->escape(JSC::JSValue::decode(value)); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/napi_handle_scope.h b/src/bun.js/bindings/napi_handle_scope.h new file mode 100644 index 0000000000..891dbbb9de --- /dev/null +++ b/src/bun.js/bindings/napi_handle_scope.h @@ -0,0 +1,97 @@ +#pragma once + +#include "BunClientData.h" +#include "root.h" + +namespace Bun { + +// An array of write barriers (so that newly-added objects are not lost by GC) to JSValues. Unlike +// the V8 version, pointer stability is not required (because napi_values don't point into this +// structure) so we can use a regular WTF::Vector +// +// Don't use this directly, use NapiHandleScope. Most NAPI functions won't even need to use that as +// a handle scope is created before calling a native function. +class NapiHandleScopeImpl : public JSC::JSCell { +public: + using Base = JSC::JSCell; + + static NapiHandleScopeImpl* create( + JSC::VM& vm, + JSC::Structure* structure, + NapiHandleScopeImpl* parent, + bool escapable = false); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) + { + return JSC::Structure::create(vm, globalObject, JSC::jsNull(), JSC::TypeInfo(JSC::CellType, StructureFlags), info(), 0, 0); + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForNapiHandleScopeImpl.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNapiHandleScopeImpl = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForNapiHandleScopeImpl.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForNapiHandleScopeImpl = std::forward(space); }); + } + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + // Store val in the handle scope + void append(JSC::JSValue val); + NapiHandleScopeImpl* parent() const { return m_parent; } + // Returns false if this handle scope is not escapable or if it is but escape() has already + // been called + bool escape(JSC::JSValue val); + +private: + using Slot = JSC::WriteBarrier; + + NapiHandleScopeImpl* m_parent; + WTF::Vector m_storage; + Slot* m_escapeSlot; + + Slot* reserveSlot(); + + NapiHandleScopeImpl(JSC::VM& vm, JSC::Structure* structure, NapiHandleScopeImpl* parent, bool escapable); +}; + +// Wrapper class used to push a new handle scope and pop it when this instance goes out of scope +class NapiHandleScope { +public: + NapiHandleScope(Zig::GlobalObject* globalObject); + ~NapiHandleScope(); + + // Create a new handle scope in the given environment + static NapiHandleScopeImpl* push(Zig::GlobalObject* globalObject, bool escapable); + + // Pop the most recently created handle scope in the given environment and restore the old one. + // Asserts that `current` is the active handle scope. + static void pop(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current); + +private: + NapiHandleScopeImpl* m_impl; + Zig::GlobalObject* m_globalObject; +}; + +// Create a new handle scope in the given environment +extern "C" NapiHandleScopeImpl* NapiHandleScope__push(Zig::GlobalObject* globalObject, bool escapable); + +// Pop the most recently created handle scope in the given environment and restore the old one. +// Asserts that `current` is the active handle scope. +extern "C" void NapiHandleScope__pop(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current); + +// Store a value in the active handle scope in the given environment +extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value); + +// Put a value from the current handle scope into its escape slot reserved in the outer handle +// scope. Returns false if the current handle scope is not escapable or if escape has already been +// called on it. +extern "C" bool NapiHandleScope__escape(NapiHandleScopeImpl* handle_scope, JSC::EncodedJSValue value); + +} // namespace Bun diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 0a3bf4b400..0059dd1401 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -49,6 +49,7 @@ public: std::unique_ptr m_clientSubspaceForJSNextTickQueue; std::unique_ptr m_clientSubspaceForNAPIFunction; std::unique_ptr m_clientSubspaceForTTYWrapObject; + std::unique_ptr m_clientSubspaceForNapiHandleScopeImpl; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 196c93e073..03cd9e07f9 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -49,6 +49,7 @@ public: std::unique_ptr m_subspaceForJSNextTickQueue; std::unique_ptr m_subspaceForNAPIFunction; std::unique_ptr m_subspaceForTTYWrapObject; + std::unique_ptr m_subspaceForNapiHandleScopeImpl; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 3422727ef3..088960a4b4 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -64,12 +64,60 @@ pub const Ref = opaque { extern fn napi_delete_reference_internal(ref: *Ref) void; extern fn napi_set_ref(ref: *Ref, value: JSC.JSValue) void; }; -pub const napi_handle_scope = napi_env; -pub const napi_escapable_handle_scope = napi_env; +pub const NapiHandleScope = opaque { + extern fn NapiHandleScope__push(globalObject: *JSC.JSGlobalObject, escapable: bool) *NapiHandleScope; + extern fn NapiHandleScope__pop(globalObject: *JSC.JSGlobalObject, current: *NapiHandleScope) void; + extern fn NapiHandleScope__append(globalObject: *JSC.JSGlobalObject, value: JSC.JSValueReprInt) void; + extern fn NapiHandleScope__escape(handleScope: *NapiHandleScope, value: JSC.JSValueReprInt) bool; + + pub fn push(env: napi_env, escapable: bool) *NapiHandleScope { + return NapiHandleScope__push(env, escapable); + } + + pub fn pop(self: *NapiHandleScope, env: napi_env) void { + NapiHandleScope__pop(env, self); + } + + pub fn append(env: napi_env, value: JSC.JSValue) void { + NapiHandleScope__append(env, @intFromEnum(value)); + } + + pub fn escape(self: *NapiHandleScope, value: JSC.JSValue) error{EscapeCalledTwice}!void { + if (!NapiHandleScope__escape(self, @intFromEnum(value))) { + return error.EscapeCalledTwice; + } + } +}; + +pub const napi_handle_scope = *NapiHandleScope; +pub const napi_escapable_handle_scope = *NapiHandleScope; pub const napi_callback_info = *JSC.CallFrame; pub const napi_deferred = *JSC.JSPromise.Strong; -pub const napi_value = JSC.JSValue; +/// To ensure napi_values are not collected prematurely after being returned into a native module, +/// you must use these functions rather than convert between napi_value and JSC.JSValue directly +pub const napi_value = enum(JSC.JSValueReprInt) { + _, + + pub fn set( + self: *napi_value, + env: napi_env, + val: JSC.JSValue, + ) void { + NapiHandleScope.append(env, val); + self.* = @enumFromInt(@intFromEnum(val)); + } + + pub fn get(self: *const napi_value) JSC.JSValue { + return @enumFromInt(@intFromEnum(self.*)); + } + + pub fn create(env: napi_env, val: JSC.JSValue) napi_value { + NapiHandleScope.append(env, val); + return @enumFromInt(@intFromEnum(val)); + } +}; + pub const struct_napi_escapable_handle_scope__ = opaque {}; const char16_t = u16; @@ -205,29 +253,29 @@ pub const napi_type_tag = extern struct { upper: u64, }; pub extern fn napi_get_last_error_info(env: napi_env, result: [*c][*c]const napi_extended_error_info) napi_status; -pub export fn napi_get_undefined(_: napi_env, result_: ?*napi_value) napi_status { +pub export fn napi_get_undefined(env: napi_env, result_: ?*napi_value) napi_status { log("napi_get_undefined", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsUndefined(); + result.set(env, JSValue.jsUndefined()); return .ok; } -pub export fn napi_get_null(_: napi_env, result_: ?*napi_value) napi_status { +pub export fn napi_get_null(env: napi_env, result_: ?*napi_value) napi_status { log("napi_get_null", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsNull(); + result.set(env, JSValue.jsNull()); return .ok; } pub extern fn napi_get_global(env: napi_env, result: *napi_value) napi_status; -pub export fn napi_get_boolean(_: napi_env, value: bool, result_: ?*napi_value) napi_status { +pub export fn napi_get_boolean(env: napi_env, value: bool, result_: ?*napi_value) napi_status { log("napi_get_boolean", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsBoolean(value); + result.set(env, JSValue.jsBoolean(value)); return .ok; } pub export fn napi_create_array(env: napi_env, result_: ?*napi_value) napi_status { @@ -235,7 +283,7 @@ pub export fn napi_create_array(env: napi_env, result_: ?*napi_value) napi_statu const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.createEmptyArray(env, 0); + result.set(env, JSValue.createEmptyArray(env, 0)); return .ok; } const prefilled_undefined_args_array: [128]JSC.JSValue = brk: { @@ -262,38 +310,34 @@ pub export fn napi_create_array_with_length(env: napi_env, length: usize, result } array.ensureStillAlive(); - result.* = array; + result.set(env, array); return .ok; } pub extern fn napi_create_double(_: napi_env, value: f64, result: *napi_value) napi_status; -pub export fn napi_create_int32(_: napi_env, value: i32, result_: ?*napi_value) napi_status { +pub export fn napi_create_int32(env: napi_env, value: i32, result_: ?*napi_value) napi_status { log("napi_create_int32", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsNumber(value); + result.set(env, JSValue.jsNumber(value)); return .ok; } -pub export fn napi_create_uint32(_: napi_env, value: u32, result_: ?*napi_value) napi_status { +pub export fn napi_create_uint32(env: napi_env, value: u32, result_: ?*napi_value) napi_status { log("napi_create_uint32", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsNumber(value); + result.set(env, JSValue.jsNumber(value)); return .ok; } -pub export fn napi_create_int64(_: napi_env, value: i64, result_: ?*napi_value) napi_status { +pub export fn napi_create_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status { log("napi_create_int64", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsNumber(value); + result.set(env, JSValue.jsNumber(value)); return .ok; } -inline fn setNapiValue(result: *napi_value, value: JSValue) void { - value.ensureStillAlive(); - result.* = value; -} pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { const result: *napi_value = result_ orelse { return invalidArg(); @@ -315,7 +359,7 @@ pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length log("napi_create_string_latin1: {s}", .{slice}); if (slice.len == 0) { - setNapiValue(result, bun.String.empty.toJS(env)); + result.set(env, bun.String.empty.toJS(env)); return .ok; } @@ -324,7 +368,7 @@ pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length @memcpy(bytes, slice); - setNapiValue(result, string.toJS(env)); + result.set(env, string.toJS(env)); return .ok; } pub export fn napi_create_string_utf8(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { @@ -352,7 +396,7 @@ pub export fn napi_create_string_utf8(env: napi_env, str: ?[*]const u8, length: } defer string.deref(); - setNapiValue(result, string.toJS(env)); + result.set(env, string.toJS(env)); return .ok; } pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, length: usize, result_: ?*napi_value) napi_status { @@ -377,7 +421,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, l log("napi_create_string_utf16: {d} {any}", .{ slice.len, bun.fmt.FormatUTF16{ .buf = slice[0..@min(slice.len, 512)] } }); if (slice.len == 0) { - setNapiValue(result, bun.String.empty.toJS(env)); + result.set(env, bun.String.empty.toJS(env)); } var string, const chars = bun.String.createUninitialized(.utf16, slice.len); @@ -385,7 +429,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, l @memcpy(chars, slice); - setNapiValue(result, string.toJS(env)); + result.set(env, string.toJS(env)); return .ok; } pub extern fn napi_create_symbol(env: napi_env, description: napi_value, result: *napi_value) napi_status; @@ -394,44 +438,48 @@ pub extern fn napi_create_type_error(env: napi_env, code: napi_value, msg: napi_ pub extern fn napi_create_range_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_typeof(env: napi_env, value: napi_value, result: *napi_valuetype) napi_status; pub extern fn napi_get_value_double(env: napi_env, value: napi_value, result: *f64) napi_status; -pub export fn napi_get_value_int32(_: napi_env, value: napi_value, result_: ?*i32) napi_status { +pub export fn napi_get_value_int32(_: napi_env, value_: napi_value, result_: ?*i32) napi_status { log("napi_get_value_int32", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); if (!value.isNumber()) { return .number_expected; } result.* = value.to(i32); return .ok; } -pub export fn napi_get_value_uint32(_: napi_env, value: napi_value, result_: ?*u32) napi_status { +pub export fn napi_get_value_uint32(_: napi_env, value_: napi_value, result_: ?*u32) napi_status { log("napi_get_value_uint32", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); if (!value.isNumber()) { return .number_expected; } result.* = value.to(u32); return .ok; } -pub export fn napi_get_value_int64(_: napi_env, value: napi_value, result_: ?*i64) napi_status { +pub export fn napi_get_value_int64(_: napi_env, value_: napi_value, result_: ?*i64) napi_status { log("napi_get_value_int64", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); if (!value.isNumber()) { return .number_expected; } result.* = value.to(i64); return .ok; } -pub export fn napi_get_value_bool(_: napi_env, value: napi_value, result_: ?*bool) napi_status { +pub export fn napi_get_value_bool(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_get_value_bool", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = value.to(bool); return .ok; @@ -441,8 +489,9 @@ inline fn maybeAppendNull(ptr: anytype, doit: bool) void { ptr.* = 0; } } -pub export fn napi_get_value_string_latin1(env: napi_env, value: napi_value, buf_ptr_: ?[*:0]c_char, bufsize: usize, result_ptr: ?*usize) napi_status { +pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, buf_ptr_: ?[*:0]c_char, bufsize: usize, result_ptr: ?*usize) napi_status { log("napi_get_value_string_latin1", .{}); + const value = value_.get(); defer value.ensureStillAlive(); const buf_ptr = @as(?[*:0]u8, @ptrCast(buf_ptr_)); @@ -498,8 +547,9 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value: napi_value, buf /// via the result parameter. /// The result argument is optional unless buf is NULL. pub extern fn napi_get_value_string_utf8(env: napi_env, value: napi_value, buf_ptr: [*c]u8, bufsize: usize, result_ptr: ?*usize) napi_status; -pub export fn napi_get_value_string_utf16(env: napi_env, value: napi_value, buf_ptr: ?[*]char16_t, bufsize: usize, result_ptr: ?*usize) napi_status { +pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf_ptr: ?[*]char16_t, bufsize: usize, result_ptr: ?*usize) napi_status { log("napi_get_value_string_utf16", .{}); + const value = value_.get(); defer value.ensureStillAlive(); const str = value.toBunString(env); defer str.deref(); @@ -546,40 +596,44 @@ pub export fn napi_get_value_string_utf16(env: napi_env, value: napi_value, buf_ return .ok; } -pub export fn napi_coerce_to_bool(env: napi_env, value: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_coerce_to_bool(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_bool", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.jsBoolean(value.coerce(bool, env)); + const value = value_.get(); + result.set(env, JSValue.jsBoolean(value.coerce(bool, env))); return .ok; } -pub export fn napi_coerce_to_number(env: napi_env, value: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_coerce_to_number(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_number", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSC.JSValue.jsNumber(JSC.C.JSValueToNumber(env.ref(), value.asObjectRef(), TODO_EXCEPTION)); + const value = value_.get(); + result.set(env, JSC.JSValue.jsNumber(JSC.C.JSValueToNumber(env.ref(), value.asObjectRef(), TODO_EXCEPTION))); return .ok; } -pub export fn napi_coerce_to_object(env: napi_env, value: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_coerce_to_object(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { log("napi_coerce_to_object", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.c(JSC.C.JSValueToObject(env.ref(), value.asObjectRef(), TODO_EXCEPTION)); + const value = value_.get(); + result.set(env, JSValue.c(JSC.C.JSValueToObject(env.ref(), value.asObjectRef(), TODO_EXCEPTION))); return .ok; } -pub export fn napi_get_prototype(env: napi_env, object: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_get_prototype(env: napi_env, object_: napi_value, result_: ?*napi_value) napi_status { log("napi_get_prototype", .{}); const result = result_ orelse { return invalidArg(); }; + const object = object_.get(); if (!object.isObject()) { return .object_expected; } - result.* = JSValue.c(JSC.C.JSObjectGetPrototype(env.ref(), object.asObjectRef())); + result.set(env, JSValue.c(JSC.C.JSObjectGetPrototype(env.ref(), object.asObjectRef()))); return .ok; } // TODO: bind JSC::ownKeys @@ -591,8 +645,10 @@ pub export fn napi_get_prototype(env: napi_env, object: napi_value, result_: ?*n // result.* = // } -pub export fn napi_set_element(env: napi_env, object: napi_value, index: c_uint, value: napi_value) napi_status { +pub export fn napi_set_element(env: napi_env, object_: napi_value, index: c_uint, value_: napi_value) napi_status { log("napi_set_element", .{}); + const object = object_.get(); + const value = value_.get(); if (!object.jsType().isIndexable()) { return .array_expected; } @@ -601,11 +657,12 @@ pub export fn napi_set_element(env: napi_env, object: napi_value, index: c_uint, JSC.C.JSObjectSetPropertyAtIndex(env.ref(), object.asObjectRef(), index, value.asObjectRef(), TODO_EXCEPTION); return .ok; } -pub export fn napi_has_element(env: napi_env, object: napi_value, index: c_uint, result_: ?*bool) napi_status { +pub export fn napi_has_element(env: napi_env, object_: napi_value, index: c_uint, result_: ?*bool) napi_status { log("napi_has_element", .{}); const result = result_ orelse { return invalidArg(); }; + const object = object_.get(); if (!object.jsType().isIndexable()) { return .array_expected; @@ -617,19 +674,21 @@ pub export fn napi_has_element(env: napi_env, object: napi_value, index: c_uint, pub extern fn napi_get_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_delete_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_define_properties(env: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) napi_status; -pub export fn napi_is_array(_: napi_env, value: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_array(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_array", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = value.jsType().isArray(); return .ok; } -pub export fn napi_get_array_length(env: napi_env, value: napi_value, result_: [*c]u32) napi_status { +pub export fn napi_get_array_length(env: napi_env, value_: napi_value, result_: [*c]u32) napi_status { log("napi_get_array_length", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); if (!value.jsType().isArray()) { return .array_expected; @@ -638,22 +697,24 @@ pub export fn napi_get_array_length(env: napi_env, value: napi_value, result_: [ result.* = @as(u32, @truncate(value.getLength(env))); return .ok; } -pub export fn napi_strict_equals(env: napi_env, lhs: napi_value, rhs: napi_value, result_: ?*bool) napi_status { +pub export fn napi_strict_equals(env: napi_env, lhs_: napi_value, rhs_: napi_value, result_: ?*bool) napi_status { log("napi_strict_equals", .{}); const result = result_ orelse { return invalidArg(); }; + const lhs, const rhs = .{ lhs_.get(), rhs_.get() }; // there is some nuance with NaN here i'm not sure about result.* = lhs.isSameValue(rhs, env); return .ok; } pub extern fn napi_call_function(env: napi_env, recv: napi_value, func: napi_value, argc: usize, argv: [*c]const napi_value, result: *napi_value) napi_status; pub extern fn napi_new_instance(env: napi_env, constructor: napi_value, argc: usize, argv: [*c]const napi_value, result_: ?*napi_value) napi_status; -pub export fn napi_instanceof(env: napi_env, object: napi_value, constructor: napi_value, result_: ?*bool) napi_status { +pub export fn napi_instanceof(env: napi_env, object_: napi_value, constructor_: napi_value, result_: ?*bool) napi_status { log("napi_instanceof", .{}); const result = result_ orelse { return invalidArg(); }; + const object, const constructor = .{ object_.get(), constructor_.get() }; // TODO: does this throw object_expected in node? result.* = object.isObject() and object.isInstanceOf(env, constructor); return .ok; @@ -683,20 +744,18 @@ pub extern fn napi_reference_unref(env: napi_env, ref: *Ref, result: [*c]u32) na pub extern fn napi_get_reference_value(env: napi_env, ref: *Ref, result: *napi_value) napi_status; pub extern fn napi_get_reference_value_internal(ref: *Ref) JSC.JSValue; -// JSC scans the stack -// we don't need this pub export fn napi_open_handle_scope(env: napi_env, result_: ?*napi_handle_scope) napi_status { log("napi_open_handle_scope", .{}); const result = result_ orelse { return invalidArg(); }; - result.* = env; + result.* = NapiHandleScope.push(env, false); return .ok; } -// JSC scans the stack -// we don't need this -pub export fn napi_close_handle_scope(_: napi_env, _: napi_handle_scope) napi_status { + +pub export fn napi_close_handle_scope(env: napi_env, handle_scope: napi_handle_scope) napi_status { log("napi_close_handle_scope", .{}); + handle_scope.pop(env); return .ok; } @@ -714,8 +773,9 @@ pub export fn napi_async_destroy(_: napi_env, _: *anyopaque) napi_status { } // this is just a regular function call -pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv: napi_value, func: napi_value, arg_count: usize, args: ?[*]const napi_value, result: ?*napi_value) napi_status { +pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value, func_: napi_value, arg_count: usize, args: ?[*]const napi_value, maybe_result: ?*napi_value) napi_status { log("napi_make_callback", .{}); + const recv, const func = .{ recv_.get(), func_.get() }; if (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.vm())) { return .function_expected; } @@ -732,8 +792,8 @@ pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv: napi_value, &.{}, ); - if (result) |result_| { - result_.* = res; + if (maybe_result) |result| { + result.set(env, res); } // TODO: this is likely incorrect @@ -761,26 +821,26 @@ fn notImplementedYet(comptime name: []const u8) void { ); } -// JSC stack scanning will handle this -pub export fn napi_open_escapable_handle_scope(env: napi_env, handle_: ?*napi_escapable_handle_scope) napi_status { +pub export fn napi_open_escapable_handle_scope(env: napi_env, result_: ?*napi_escapable_handle_scope) napi_status { log("napi_open_escapable_handle_scope", .{}); - const handle = handle_ orelse { + const result = result_ orelse { return invalidArg(); }; - handle.* = env; + result.* = NapiHandleScope.push(env, true); return .ok; } -pub export fn napi_close_escapable_handle_scope(_: napi_env, _: napi_escapable_handle_scope) napi_status { +pub export fn napi_close_escapable_handle_scope(env: napi_env, scope: napi_escapable_handle_scope) napi_status { log("napi_close_escapable_handle_scope", .{}); + scope.pop(env); return .ok; } -pub export fn napi_escape_handle(_: napi_env, _: napi_escapable_handle_scope, value: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_escape_handle(_: napi_env, scope: napi_escapable_handle_scope, escapee: napi_value, result_: ?*napi_value) napi_status { log("napi_escape_handle", .{}); const result = result_ orelse { return invalidArg(); }; - value.ensureStillAlive(); - result.* = value; + scope.escape(escapee.get()) catch return .escape_called_twice; + result.* = escapee; return .ok; } pub export fn napi_type_tag_object(_: napi_env, _: napi_value, _: [*c]const napi_type_tag) napi_status { @@ -807,18 +867,20 @@ pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status; pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_type_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_range_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; -pub export fn napi_is_error(_: napi_env, value: napi_value, result: *bool) napi_status { +pub export fn napi_is_error(_: napi_env, value_: napi_value, result: *bool) napi_status { log("napi_is_error", .{}); + const value = value_.get(); result.* = value.isAnyError(); return .ok; } pub extern fn napi_is_exception_pending(env: napi_env, result: *bool) napi_status; pub extern fn napi_get_and_clear_last_exception(env: napi_env, result: *napi_value) napi_status; -pub export fn napi_is_arraybuffer(_: napi_env, value: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_arraybuffer(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_arraybuffer", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = !value.isNumber() and value.jsTypeLoose() == .ArrayBuffer; return .ok; } @@ -826,8 +888,9 @@ pub extern fn napi_create_arraybuffer(env: napi_env, byte_length: usize, data: [ pub extern fn napi_create_external_arraybuffer(env: napi_env, external_data: ?*anyopaque, byte_length: usize, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; -pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer: napi_value, data: ?*[*]u8, byte_length: ?*usize) napi_status { +pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer_: napi_value, data: ?*[*]u8, byte_length: ?*usize) napi_status { log("napi_get_arraybuffer_info", .{}); + const arraybuffer = arraybuffer_.get(); const array_buffer = arraybuffer.asArrayBuffer(env) orelse return .arraybuffer_expected; const slice = array_buffer.slice(); if (data) |dat| @@ -836,18 +899,20 @@ pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer: napi_value, len.* = slice.len; return .ok; } -pub export fn napi_is_typedarray(_: napi_env, value: napi_value, result: ?*bool) napi_status { +pub export fn napi_is_typedarray(_: napi_env, value_: napi_value, result: ?*bool) napi_status { log("napi_is_typedarray", .{}); + const value = value_.get(); if (result != null) result.?.* = value.jsTypeLoose().isTypedArray(); return if (result != null) .ok else invalidArg(); } -pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_type, length: usize, arraybuffer: napi_value, byte_offset: usize, result_: ?*napi_value) napi_status { +pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_type, length: usize, arraybuffer_: napi_value, byte_offset: usize, result_: ?*napi_value) napi_status { log("napi_create_typedarray", .{}); + const arraybuffer = arraybuffer_.get(); const result = result_ orelse { return invalidArg(); }; - result.* = JSValue.c( + result.set(env, JSValue.c( JSC.C.JSObjectMakeTypedArrayWithArrayBufferAndOffset( env.ref(), @"type".toC(), @@ -856,64 +921,74 @@ pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_typ length, TODO_EXCEPTION, ), - ); + )); return .ok; } pub export fn napi_get_typedarray_info( env: napi_env, - typedarray: napi_value, - @"type": ?*napi_typedarray_type, - length: ?*usize, - data: ?*[*]u8, - arraybuffer: ?*napi_value, - byte_offset: ?*usize, + typedarray_: napi_value, + maybe_type: ?*napi_typedarray_type, + maybe_length: ?*usize, + maybe_data: ?*[*]u8, + maybe_arraybuffer: ?*napi_value, + maybe_byte_offset: ?*usize, ) napi_status { log("napi_get_typedarray_info", .{}); + const typedarray = typedarray_.get(); if (typedarray.isEmptyOrUndefinedOrNull()) return invalidArg(); defer typedarray.ensureStillAlive(); const array_buffer = typedarray.asArrayBuffer(env) orelse return invalidArg(); - if (@"type" != null) - @"type".?.* = napi_typedarray_type.fromJSType(array_buffer.typed_array_type) orelse return invalidArg(); + if (maybe_type) |@"type"| + @"type".* = napi_typedarray_type.fromJSType(array_buffer.typed_array_type) orelse return invalidArg(); // TODO: handle detached - if (data != null) - data.?.* = array_buffer.ptr; + if (maybe_data) |data| + data.* = array_buffer.ptr; - if (length != null) - length.?.* = array_buffer.len; + if (maybe_length) |length| + length.* = array_buffer.len; - if (arraybuffer != null) - arraybuffer.?.* = JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), typedarray.asObjectRef(), null)); + if (maybe_arraybuffer) |arraybuffer| + arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), typedarray.asObjectRef(), null))); - if (byte_offset != null) - byte_offset.?.* = array_buffer.offset; + if (maybe_byte_offset) |byte_offset| + byte_offset.* = array_buffer.offset; return .ok; } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; -pub export fn napi_is_dataview(_: napi_env, value: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_dataview(_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_dataview", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = !value.isEmptyOrUndefinedOrNull() and value.jsTypeLoose() == .DataView; return .ok; } -pub export fn napi_get_dataview_info(env: napi_env, dataview: napi_value, bytelength: ?*usize, data: ?*[*]u8, arraybuffer: ?*napi_value, byte_offset: ?*usize) napi_status { +pub export fn napi_get_dataview_info( + env: napi_env, + dataview_: napi_value, + maybe_bytelength: ?*usize, + maybe_data: ?*[*]u8, + maybe_arraybuffer: ?*napi_value, + maybe_byte_offset: ?*usize, +) napi_status { log("napi_get_dataview_info", .{}); + const dataview = dataview_.get(); const array_buffer = dataview.asArrayBuffer(env) orelse return .object_expected; - if (bytelength != null) - bytelength.?.* = array_buffer.byte_len; + if (maybe_bytelength) |bytelength| + bytelength.* = array_buffer.byte_len; - if (data != null) - data.?.* = array_buffer.ptr; + if (maybe_data) |data| + data.* = array_buffer.ptr; - if (arraybuffer != null) - arraybuffer.?.* = JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), dataview.asObjectRef(), null)); + if (maybe_arraybuffer) |arraybuffer| + arraybuffer.set(env, JSValue.c(JSC.C.JSObjectGetTypedArrayBuffer(env.ref(), dataview.asObjectRef(), null))); - if (byte_offset != null) - byte_offset.?.* = array_buffer.offset; + if (maybe_byte_offset) |byte_offset| + byte_offset.* = array_buffer.offset; return .ok; } @@ -935,27 +1010,30 @@ pub export fn napi_create_promise(env: napi_env, deferred_: ?*napi_deferred, pro }; deferred.* = bun.default_allocator.create(JSC.JSPromise.Strong) catch @panic("failed to allocate napi_deferred"); deferred.*.* = JSC.JSPromise.Strong.init(env); - promise.* = deferred.*.get().asValue(env); + promise.set(env, deferred.*.get().asValue(env)); return .ok; } -pub export fn napi_resolve_deferred(env: napi_env, deferred: napi_deferred, resolution: napi_value) napi_status { +pub export fn napi_resolve_deferred(env: napi_env, deferred: napi_deferred, resolution_: napi_value) napi_status { log("napi_resolve_deferred", .{}); + const resolution = resolution_.get(); var prom = deferred.get(); prom.resolve(env, resolution); deferred.deinit(); bun.default_allocator.destroy(deferred); return .ok; } -pub export fn napi_reject_deferred(env: napi_env, deferred: napi_deferred, rejection: napi_value) napi_status { +pub export fn napi_reject_deferred(env: napi_env, deferred: napi_deferred, rejection_: napi_value) napi_status { log("napi_reject_deferred", .{}); + const rejection = rejection_.get(); var prom = deferred.get(); prom.reject(env, rejection); deferred.deinit(); bun.default_allocator.destroy(deferred); return .ok; } -pub export fn napi_is_promise(_: napi_env, value: napi_value, is_promise_: ?*bool) napi_status { +pub export fn napi_is_promise(_: napi_env, value_: napi_value, is_promise_: ?*bool) napi_status { log("napi_is_promise", .{}); + const value = value_.get(); const is_promise = is_promise_ orelse { return invalidArg(); }; @@ -975,14 +1053,15 @@ pub export fn napi_create_date(env: napi_env, time: f64, result_: ?*napi_value) return invalidArg(); }; var args = [_]JSC.C.JSValueRef{JSC.JSValue.jsNumber(time).asObjectRef()}; - result.* = JSValue.c(JSC.C.JSObjectMakeDate(env.ref(), 1, &args, TODO_EXCEPTION)); + result.set(env, JSValue.c(JSC.C.JSObjectMakeDate(env.ref(), 1, &args, TODO_EXCEPTION))); return .ok; } -pub export fn napi_is_date(_: napi_env, value: napi_value, is_date_: ?*bool) napi_status { +pub export fn napi_is_date(_: napi_env, value_: napi_value, is_date_: ?*bool) napi_status { log("napi_is_date", .{}); const is_date = is_date_ orelse { return invalidArg(); }; + const value = value_.get(); is_date.* = value.jsTypeLoose() == .JSDate; return .ok; } @@ -993,7 +1072,7 @@ pub export fn napi_create_bigint_int64(env: napi_env, value: i64, result_: ?*nap const result = result_ orelse { return invalidArg(); }; - result.* = JSC.JSValue.fromInt64NoTruncate(env, value); + result.set(env, JSC.JSValue.fromInt64NoTruncate(env, value)); return .ok; } pub export fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*napi_value) napi_status { @@ -1001,25 +1080,27 @@ pub export fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*na const result = result_ orelse { return invalidArg(); }; - result.* = JSC.JSValue.fromUInt64NoTruncate(env, value); + result.set(env, JSC.JSValue.fromUInt64NoTruncate(env, value)); return .ok; } pub extern fn napi_create_bigint_words(env: napi_env, sign_bit: c_int, word_count: usize, words: [*c]const u64, result: *napi_value) napi_status; // TODO: lossless -pub export fn napi_get_value_bigint_int64(_: napi_env, value: napi_value, result_: ?*i64, _: *bool) napi_status { +pub export fn napi_get_value_bigint_int64(_: napi_env, value_: napi_value, result_: ?*i64, _: *bool) napi_status { log("napi_get_value_bigint_int64", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = value.toInt64(); return .ok; } // TODO: lossless -pub export fn napi_get_value_bigint_uint64(_: napi_env, value: napi_value, result_: ?*u64, _: *bool) napi_status { +pub export fn napi_get_value_bigint_uint64(_: napi_env, value_: napi_value, result_: ?*u64, _: *bool) napi_status { log("napi_get_value_bigint_uint64", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = value.toUInt64NoTruncate(); return .ok; } @@ -1113,6 +1194,8 @@ pub const napi_async_work = struct { } pub fn runFromJS(this: *napi_async_work) void { + const handle_scope = NapiHandleScope.push(this.global, false); + defer handle_scope.pop(this.global); this.complete.?( this.global, if (this.status.load(.seq_cst) == @intFromEnum(Status.cancelled)) @@ -1196,7 +1279,7 @@ pub export fn napi_create_buffer(env: napi_env, length: usize, data: ?**anyopaqu ptr.* = buffer.asArrayBuffer(env).?.ptr; } } - result.* = buffer; + result.set(env, buffer); return .ok; } pub extern fn napi_create_external_buffer(env: napi_env, length: usize, data: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; @@ -1215,20 +1298,22 @@ pub export fn napi_create_buffer_copy(env: napi_env, length: usize, data: [*]u8, } } - result.* = buffer; + result.set(env, buffer); return .ok; } -pub export fn napi_is_buffer(env: napi_env, value: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_buffer(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_buffer", .{}); const result = result_ orelse { return invalidArg(); }; + const value = value_.get(); result.* = value.isBuffer(env); return .ok; } -pub export fn napi_get_buffer_info(env: napi_env, value: napi_value, data: ?*[*]u8, length: ?*usize) napi_status { +pub export fn napi_get_buffer_info(env: napi_env, value_: napi_value, data: ?*[*]u8, length: ?*usize) napi_status { log("napi_get_buffer_info", .{}); + const value = value_.get(); const array_buf = value.asArrayBuffer(env) orelse { // TODO: is invalid_arg what to return here? return .arraybuffer_expected; @@ -1491,7 +1576,9 @@ pub const ThreadSafeFunction = struct { log("call() {}", .{str}); } - cb.napi_threadsafe_function_call_js(globalObject, cb.js, this.ctx, task); + const handle_scope = NapiHandleScope.push(globalObject, false); + defer handle_scope.pop(globalObject); + cb.napi_threadsafe_function_call_js(globalObject, napi_value.create(globalObject, cb.js), this.ctx, task); }, } } @@ -1572,7 +1659,7 @@ pub const ThreadSafeFunction = struct { pub export fn napi_create_threadsafe_function( env: napi_env, - func: napi_value, + func_: napi_value, _: napi_value, _: napi_value, max_queue_size: usize, @@ -1587,6 +1674,7 @@ pub export fn napi_create_threadsafe_function( const result = result_ orelse { return invalidArg(); }; + const func = func_.get(); if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.vm()))) { return napi_status.function_expected; diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index 0975f48868..6f510c3dda 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -46,6 +46,7 @@ napi_value test_issue_7685(const Napi::CallbackInfo &info) { napi_assert(info[5].IsNumber()); napi_assert(info[6].IsNumber()); napi_assert(info[7].IsNumber()); +#undef napi_assert return ok(env); } @@ -146,6 +147,290 @@ test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) { return ok(env); } +napi_value test_napi_handle_scope_string(const Napi::CallbackInfo &info) { + // this is mostly a copy of test_handle_scope_gc from + // test/v8/v8-module/main.cpp -- see comments there for explanation + Napi::Env env = info.Env(); + + constexpr size_t num_small_strings = 10000; + constexpr size_t num_large_strings = 100; + constexpr size_t large_string_size = 20'000'000; + + auto *small_strings = new napi_value[num_small_strings]; + auto *large_strings = new napi_value[num_large_strings]; + auto *string_data = new char[large_string_size]; + string_data[large_string_size - 1] = 0; + + for (size_t i = 0; i < num_small_strings; i++) { + std::string cpp_str = std::to_string(i); + assert(napi_create_string_utf8(env, cpp_str.c_str(), cpp_str.size(), + &small_strings[i]) == napi_ok); + } + + for (size_t i = 0; i < num_large_strings; i++) { + memset(string_data, i + 1, large_string_size); + assert(napi_create_string_utf8(env, string_data, large_string_size, + &large_strings[i]) == napi_ok); + + for (size_t j = 0; j < num_small_strings; j++) { + char buf[16]; + size_t result; + assert(napi_get_value_string_utf8(env, small_strings[j], buf, sizeof buf, + &result) == napi_ok); + printf("%s\n", buf); + assert(atoi(buf) == (int)j); + } + } + + delete[] small_strings; + delete[] large_strings; + delete[] string_data; + return ok(env); +} + +napi_value test_napi_handle_scope_bigint(const Napi::CallbackInfo &info) { + // this is mostly a copy of test_handle_scope_gc from + // test/v8/v8-module/main.cpp -- see comments there for explanation + Napi::Env env = info.Env(); + + constexpr size_t num_small_ints = 100; + constexpr size_t num_large_ints = 10000; + constexpr size_t small_int_size = 16; + // JSC bigint size limit = 1<<20 bits + constexpr size_t large_int_size = (1 << 20) / 64; + + auto *small_ints = new napi_value[num_small_ints]; + auto *large_ints = new napi_value[num_large_ints]; + std::vector int_words(large_int_size); + + for (size_t i = 0; i < num_small_ints; i++) { + std::array words; + words.fill(i + 1); + assert(napi_create_bigint_words(env, 0, small_int_size, words.data(), + &small_ints[i]) == napi_ok); + } + + for (size_t i = 0; i < num_large_ints; i++) { + std::fill(int_words.begin(), int_words.end(), i + 1); + assert(napi_create_bigint_words(env, 0, large_int_size, int_words.data(), + &large_ints[i]) == napi_ok); + + for (size_t j = 0; j < num_small_ints; j++) { + std::array words; + int sign; + size_t word_count = words.size(); + assert(napi_get_value_bigint_words(env, small_ints[j], &sign, &word_count, + words.data()) == napi_ok); + printf("%d, %zu\n", sign, word_count); + assert(sign == 0 && word_count == words.size()); + assert(std::all_of(words.begin(), words.end(), + [j](const uint64_t &w) { return w == j + 1; })); + } + } + + delete[] small_ints; + delete[] large_ints; + return ok(env); +} + +napi_value test_napi_delete_property(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + napi_value object = info[0]; + napi_valuetype type; + assert(napi_typeof(env, object, &type) == napi_ok); + assert(type == napi_object); + + napi_value key; + assert(napi_create_string_utf8(env, "foo", 3, &key) == napi_ok); + + napi_value non_configurable_key; + assert(napi_create_string_utf8(env, "bar", 3, &non_configurable_key) == + napi_ok); + + napi_value val; + assert(napi_create_int32(env, 42, &val) == napi_ok); + + bool delete_result; + assert(napi_delete_property(env, object, non_configurable_key, + &delete_result) == napi_ok); + assert(delete_result == false); + + assert(napi_delete_property(env, object, key, &delete_result) == napi_ok); + assert(delete_result == true); + + bool has_property; + assert(napi_has_property(env, object, key, &has_property) == napi_ok); + assert(has_property == false); + + return ok(env); +} + +void store_escaped_handle(napi_env env, napi_value *out, const char *str) { + napi_escapable_handle_scope ehs; + assert(napi_open_escapable_handle_scope(env, &ehs) == napi_ok); + napi_value s; + assert(napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &s) == napi_ok); + napi_value escaped; + assert(napi_escape_handle(env, ehs, s, &escaped) == napi_ok); + // can't call a second time + assert(napi_escape_handle(env, ehs, s, &escaped) == napi_escape_called_twice); + assert(napi_close_escapable_handle_scope(env, ehs) == napi_ok); + *out = escaped; + + // try to defeat stack scanning + *(volatile napi_value *)(&s) = nullptr; + *(volatile napi_value *)(&escaped) = nullptr; +} + +napi_value test_napi_escapable_handle_scope(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + // allocate space for a napi_value on the heap + // use store_escaped_handle to put the value into it + // allocate some big objects to trigger GC + // the napi_value should still be valid even though it can't be found on the + // stack, because it escaped into the current handle scope + + constexpr const char *str = "this is a long string meow meow meow"; + + napi_value *hidden = new napi_value; + store_escaped_handle(env, hidden, str); + + constexpr size_t big_string_length = 20'000'000; + auto *string_data = new char[big_string_length]; + for (int i = 0; i < 100; i++) { + napi_value s; + memset(string_data, i + 1, big_string_length); + assert(napi_create_string_utf8(env, string_data, big_string_length, &s) == + napi_ok); + } + delete[] string_data; + + char buf[64]; + size_t len; + assert(napi_get_value_string_utf8(env, *hidden, buf, sizeof(buf), &len) == + napi_ok); + assert(len == strlen(str)); + assert(strcmp(buf, str) == 0); + + delete hidden; + return ok(env); +} + +napi_value test_napi_handle_scope_nesting(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + constexpr const char *str = "this is a long string meow meow meow"; + + // Create an outer handle scope, hidden on the heap (the one created in + // NAPIFunction::call is still on the stack + napi_handle_scope *outer_hs = new napi_handle_scope; + assert(napi_open_handle_scope(env, outer_hs) == napi_ok); + + // Make a handle in the outer scope, on the heap so stack scanning can't see + // it + napi_value *outer_scope_handle = new napi_value; + assert(napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, + outer_scope_handle) == napi_ok); + + // Make a new handle scope on the heap + napi_handle_scope *inner_hs = new napi_handle_scope; + assert(napi_open_handle_scope(env, inner_hs) == napi_ok); + + // Allocate lots of memory to force GC + constexpr size_t big_string_length = 20'000'000; + auto *string_data = new char[big_string_length]; + for (int i = 0; i < 100; i++) { + napi_value s; + memset(string_data, i + 1, big_string_length); + assert(napi_create_string_utf8(env, string_data, big_string_length, &s) == + napi_ok); + } + delete[] string_data; + + // Try to read our first handle. Did the outer handle scope get + // collected now that it's not on the global object? + char buf[64]; + size_t len; + assert(napi_get_value_string_utf8(env, *outer_scope_handle, buf, sizeof(buf), + &len) == napi_ok); + assert(len == strlen(str)); + assert(strcmp(buf, str) == 0); + + // Clean up + assert(napi_close_handle_scope(env, *inner_hs) == napi_ok); + delete inner_hs; + assert(napi_close_handle_scope(env, *outer_hs) == napi_ok); + delete outer_hs; + delete outer_scope_handle; + return ok(env); +} + +napi_value constructor(napi_env env, napi_callback_info info) { + napi_value this_value; + assert(napi_get_cb_info(env, info, nullptr, nullptr, &this_value, nullptr) == + napi_ok); + napi_value property_value; + assert(napi_create_string_utf8(env, "meow", NAPI_AUTO_LENGTH, + &property_value) == napi_ok); + assert(napi_set_named_property(env, this_value, "foo", property_value) == + napi_ok); + napi_value undefined; + assert(napi_get_undefined(env, &undefined) == napi_ok); + return undefined; +} + +napi_value get_class_with_constructor(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value napi_class; + assert(napi_define_class(env, "NapiClass", NAPI_AUTO_LENGTH, constructor, + nullptr, 0, nullptr, &napi_class) == napi_ok); + return napi_class; +} + +struct AsyncWorkData { + int result; + napi_deferred deferred; + napi_async_work work; + + static void execute(napi_env env, void *data) { + AsyncWorkData *async_work_data = reinterpret_cast(data); + async_work_data->result = 42; + } + + static void complete(napi_env env, napi_status status, void *data) { + AsyncWorkData *async_work_data = reinterpret_cast(data); + assert(status == napi_ok); + + napi_value result; + char buf[64] = {0}; + snprintf(buf, sizeof(buf), "the number is %d", async_work_data->result); + assert(napi_create_string_utf8(env, buf, NAPI_AUTO_LENGTH, &result) == + napi_ok); + assert(napi_resolve_deferred(env, async_work_data->deferred, result) == + napi_ok); + assert(napi_delete_async_work(env, async_work_data->work) == napi_ok); + delete async_work_data; + } +}; + +napi_value create_promise(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + auto *data = new AsyncWorkData; + napi_value promise; + + assert(napi_create_promise(env, &data->deferred, &promise) == napi_ok); + + napi_value resource_name; + assert(napi_create_string_utf8(env, "napitests::create_promise", + NAPI_AUTO_LENGTH, &resource_name) == napi_ok); + assert(napi_create_async_work(env, nullptr, resource_name, + AsyncWorkData::execute, AsyncWorkData::complete, + data, &data->work) == napi_ok); + assert(napi_queue_async_work(env, data->work) == napi_ok); + return promise; +} + Napi::Value RunCallback(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); Napi::Function cb = info[0].As(); @@ -174,6 +459,19 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { "test_napi_threadsafe_function_does_not_hang_after_finalize", Napi::Function::New( env, test_napi_threadsafe_function_does_not_hang_after_finalize)); + exports.Set("test_napi_handle_scope_string", + Napi::Function::New(env, test_napi_handle_scope_string)); + exports.Set("test_napi_handle_scope_bigint", + Napi::Function::New(env, test_napi_handle_scope_bigint)); + exports.Set("test_napi_delete_property", + Napi::Function::New(env, test_napi_delete_property)); + exports.Set("test_napi_escapable_handle_scope", + Napi::Function::New(env, test_napi_escapable_handle_scope)); + exports.Set("test_napi_handle_scope_nesting", + Napi::Function::New(env, test_napi_handle_scope_nesting)); + exports.Set("get_class_with_constructor", + Napi::Function::New(env, get_class_with_constructor)); + exports.Set("create_promise", Napi::Function::New(env, create_promise)); return exports; } diff --git a/test/napi/napi-app/main.js b/test/napi/napi-app/main.js index 27bba50a23..b878967932 100644 --- a/test/napi/napi-app/main.js +++ b/test/napi/napi-app/main.js @@ -1,4 +1,4 @@ -const tests = require("./build/Release/napitests.node"); +const tests = require("./module"); if (process.argv[2] === "self") { console.log( tests(function (str) { @@ -11,7 +11,9 @@ const fn = tests[process.argv[2]]; if (typeof fn !== "function") { throw new Error("Unknown test:", process.argv[2]); } -const result = fn.apply(null, JSON.parse(process.argv[3] ?? "[]")); -if (result) { +const result = fn.apply(null, eval(process.argv[3] ?? "[]")); +if (result instanceof Promise) { + result.then(x => console.log("resolved to", x)); +} else if (result) { throw new Error(result); } diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js new file mode 100644 index 0000000000..0ab3896980 --- /dev/null +++ b/test/napi/napi-app/module.js @@ -0,0 +1,9 @@ +const nativeTests = require("./build/Release/napitests.node"); + +nativeTests.test_napi_class_constructor_handle_scope = () => { + const NapiClass = nativeTests.get_class_with_constructor(); + const x = new NapiClass(); + console.log("x.foo =", x.foo); +}; + +module.exports = nativeTests; diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index d06bc0c63c..17828b23c1 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -69,9 +69,56 @@ describe("napi", () => { const result = checkSameOutput("self", []); expect(result).toBe("hello world!"); }); + + describe("handle_scope", () => { + it("keeps strings alive", () => { + checkSameOutput("test_napi_handle_scope_string", []); + }); + it("keeps bigints alive", () => { + checkSameOutput("test_napi_handle_scope_bigint", []); + }, 10000); + it("keeps the parent handle scope alive", () => { + checkSameOutput("test_napi_handle_scope_nesting", []); + }); + it("exists when calling a napi constructor", () => { + checkSameOutput("test_napi_class_constructor_handle_scope", []); + }); + it("exists while calling a napi_async_complete_callback", () => { + checkSameOutput("create_promise", []); + }); + }); + + describe("escapable_handle_scope", () => { + it("keeps the escaped value alive in the outer scope", () => { + checkSameOutput("test_napi_escapable_handle_scope", []); + }); + }); + + describe("napi_delete_property", () => { + it("returns a valid boolean", () => { + checkSameOutput( + "test_napi_delete_property", + // generate a string representing an array around an IIFE which main.js will eval + // we do this as the napi_delete_property test needs an object with an own non-configurable + // property + "[(" + + function () { + const object = { foo: 42 }; + Object.defineProperty(object, "bar", { + get() { + return 1; + }, + configurable: false, + }); + return object; + }.toString() + + ")()]", + ); + }); + }); }); -function checkSameOutput(test: string, args: any[]) { +function checkSameOutput(test: string, args: any[] | string) { const nodeResult = runOn("node", test, args).trim(); let bunResult = runOn(bunExe(), test, args); // remove all debug logs @@ -80,9 +127,9 @@ function checkSameOutput(test: string, args: any[]) { return nodeResult; } -function runOn(executable: string, test: string, args: any[]) { +function runOn(executable: string, test: string, args: any[] | string) { const exec = spawnSync({ - cmd: [executable, join(__dirname, "napi-app/main.js"), test, JSON.stringify(args)], + cmd: [executable, join(__dirname, "napi-app/main.js"), test, typeof args == "string" ? args : JSON.stringify(args)], env: bunEnv, }); const errs = exec.stderr.toString();