diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 418e6cab..1f9be3f6 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -20,9 +20,11 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMMON_FLAGS}") # Arguments set(TARGET_PLATFORM "macos" CACHE STRING "Target platform for the Objective-C bridge") set(TARGET_ENGINE "v8" CACHE STRING "Target JS engine for the NativeScript runtime") +set(NS_GSD_BACKEND "auto" CACHE STRING "Generated signature dispatch backend: auto, v8, jsc, quickjs, hermes, napi, or none") set(METADATA_SIZE 0 CACHE STRING "Size of embedded metadata in bytes") set(BUILD_CLI_BINARY OFF CACHE BOOL "Build the NativeScript CLI binary") set(BUILD_MACOS_NODE_API OFF CACHE BOOL "Build the NativeScript macOS Node API dylib") +set_property(CACHE NS_GSD_BACKEND PROPERTY STRINGS auto v8 jsc quickjs hermes napi none) if (BUILD_MACOS_NODE_API) set(BUILD_FRAMEWORK OFF) @@ -132,6 +134,44 @@ message(STATUS "TARGET_ENGINE = ${TARGET_ENGINE}") message(STATUS "ENABLE_JS_RUNTIME = ${ENABLE_JS_RUNTIME}") message(STATUS "GENERIC_NAPI = ${GENERIC_NAPI}") +if(NS_GSD_BACKEND STREQUAL "auto") + if(TARGET_ENGINE_V8) + set(NS_EFFECTIVE_GSD_BACKEND "v8") + elseif(TARGET_ENGINE_JSC) + set(NS_EFFECTIVE_GSD_BACKEND "jsc") + elseif(TARGET_ENGINE_QUICKJS) + set(NS_EFFECTIVE_GSD_BACKEND "quickjs") + elseif(TARGET_ENGINE_HERMES) + set(NS_EFFECTIVE_GSD_BACKEND "hermes") + else() + set(NS_EFFECTIVE_GSD_BACKEND "napi") + endif() +elseif(NS_GSD_BACKEND STREQUAL "v8" OR + NS_GSD_BACKEND STREQUAL "jsc" OR + NS_GSD_BACKEND STREQUAL "quickjs" OR + NS_GSD_BACKEND STREQUAL "hermes" OR + NS_GSD_BACKEND STREQUAL "napi" OR + NS_GSD_BACKEND STREQUAL "none") + set(NS_EFFECTIVE_GSD_BACKEND "${NS_GSD_BACKEND}") +else() + message(FATAL_ERROR "Unknown NS_GSD_BACKEND: ${NS_GSD_BACKEND}") +endif() + +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8" AND NOT TARGET_ENGINE_V8) + message(FATAL_ERROR "NS_GSD_BACKEND=v8 requires TARGET_ENGINE=v8") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "jsc" AND NOT TARGET_ENGINE_JSC) + message(FATAL_ERROR "NS_GSD_BACKEND=jsc requires TARGET_ENGINE=jsc") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "quickjs" AND NOT TARGET_ENGINE_QUICKJS) + message(FATAL_ERROR "NS_GSD_BACKEND=quickjs requires TARGET_ENGINE=quickjs") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "hermes" AND NOT TARGET_ENGINE_HERMES) + message(FATAL_ERROR "NS_GSD_BACKEND=hermes requires TARGET_ENGINE=hermes") +endif() + +message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND})") + # Set up sources include_directories( ./ @@ -158,6 +198,7 @@ set(SOURCE_FILES ffi/Variable.mm ffi/Object.mm ffi/CFunction.mm + ffi/EngineDirectCall.mm ffi/Interop.mm ffi/InlineFunctions.mm ffi/ClassBuilder.mm @@ -207,6 +248,7 @@ if(ENABLE_JS_RUNTIME) napi/v8/v8-module-loader.cpp napi/v8/jsr.cpp napi/v8/SimpleAllocator.cpp + ffi/V8FastNativeApi.mm ) elseif(TARGET_ENGINE_HERMES) @@ -231,6 +273,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp + ffi/HermesFastNativeApi.mm ) elseif(TARGET_ENGINE_QUICKJS) @@ -263,6 +306,7 @@ if(ENABLE_JS_RUNTIME) # napi napi/quickjs/quickjs-api.c napi/quickjs/jsr.cpp + ffi/QuickJSFastNativeApi.mm ) elseif(TARGET_ENGINE_JSC) @@ -275,6 +319,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/jsc/jsc-api.cpp napi/jsc/jsr.cpp + ffi/JSCFastNativeApi.mm ) endif() else() @@ -345,6 +390,38 @@ if(TARGET_ENGINE_V8 AND TARGET_PLATFORM_IOS) ) endif() +if(ENABLE_JS_RUNTIME) + target_compile_definitions(${NAME} PRIVATE ENABLE_JS_RUNTIME) +endif() + +if(TARGET_PLATFORM_MACOS) + target_compile_definitions(${NAME} PRIVATE TARGET_PLATFORM_MACOS) +endif() + +if(TARGET_ENGINE_HERMES) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_HERMES) +elseif(TARGET_ENGINE_V8) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_V8) +elseif(TARGET_ENGINE_QUICKJS) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_QUICKJS) +elseif(TARGET_ENGINE_JSC) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_JSC) +endif() + +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=1 NS_GSD_BACKEND_JSC=0 NS_GSD_BACKEND_QUICKJS=0 NS_GSD_BACKEND_HERMES=0 NS_GSD_BACKEND_NAPI=0) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "jsc") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_JSC=1 NS_GSD_BACKEND_QUICKJS=0 NS_GSD_BACKEND_HERMES=0 NS_GSD_BACKEND_NAPI=0) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "quickjs") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_JSC=0 NS_GSD_BACKEND_QUICKJS=1 NS_GSD_BACKEND_HERMES=0 NS_GSD_BACKEND_NAPI=0) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "hermes") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_JSC=0 NS_GSD_BACKEND_QUICKJS=0 NS_GSD_BACKEND_HERMES=1 NS_GSD_BACKEND_NAPI=0) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_JSC=0 NS_GSD_BACKEND_QUICKJS=0 NS_GSD_BACKEND_HERMES=0 NS_GSD_BACKEND_NAPI=1) +else() + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_JSC=0 NS_GSD_BACKEND_QUICKJS=0 NS_GSD_BACKEND_HERMES=0 NS_GSD_BACKEND_NAPI=0) +endif() + set(FRAMEWORK_VERSION_VALUE "${VERSION}") if(TARGET_PLATFORM_MACOS) # macOS framework consumers (including Xcode's copy/sign phases) expect diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/Block.h index 97097831..4cd81962 100644 --- a/NativeScript/ffi/Block.h +++ b/NativeScript/ffi/Block.h @@ -1,6 +1,7 @@ #ifndef BLOCK_H #define BLOCK_H +#include #include #include "Cif.h" @@ -15,6 +16,10 @@ class FunctionPointer { metagen::MDSectionOffset offset; Cif* cif; bool ownsCif = false; + bool dispatchLookupCached = false; + uint64_t dispatchLookupSignatureHash = 0; + uint64_t dispatchId = 0; + void* preparedInvoker = nullptr; static napi_value wrap(napi_env env, void* function, metagen::MDSectionOffset offset, bool isBlock); diff --git a/NativeScript/ffi/Block.mm b/NativeScript/ffi/Block.mm index 6d60da6e..612a7e50 100644 --- a/NativeScript/ffi/Block.mm +++ b/NativeScript/ffi/Block.mm @@ -7,6 +7,7 @@ #include #include "Interop.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -65,8 +66,32 @@ inline bool removeCachedBlockJsFunctionEntry(void* blockPtr, BlockJsFunctionEntr } inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) { - nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, - entry.bridgeStateToken, entry.ref); + nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, entry.bridgeStateToken, + entry.ref); +} + +inline nativescript::BlockPreparedInvoker ensureFunctionPointerPreparedInvoker( + nativescript::FunctionPointer* ref, nativescript::SignatureCallKind kind) { + if (ref == nullptr || ref->cif == nullptr || ref->cif->signatureHash == 0) { + return nullptr; + } + + if (!ref->dispatchLookupCached || + ref->dispatchLookupSignatureHash != ref->cif->signatureHash) { + ref->dispatchLookupSignatureHash = ref->cif->signatureHash; + ref->dispatchId = + nativescript::composeSignatureDispatchId(ref->cif->signatureHash, kind, 0); + if (kind == nativescript::SignatureCallKind::BlockInvoke) { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupBlockPreparedInvoker(ref->dispatchId)); + } else { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupCFunctionPreparedInvoker(ref->dispatchId)); + } + ref->dispatchLookupCached = true; + } + + return reinterpret_cast(ref->preparedInvoker); } void block_copy(void* dest, void* src) { @@ -144,8 +169,7 @@ inline void cacheBlockJsFunction(napi_env env, void* blockPtr, napi_value jsFunc entry.ref = nativescript::make_ref(env, jsFunction, 0); entry.env = env; entry.bridgeState = nativescript::ObjCBridgeState::InstanceData(env); - entry.bridgeStateToken = - entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; + entry.bridgeStateToken = entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; entry.jsThreadId = closure != nullptr ? closure->jsThreadId : std::this_thread::get_id(); entry.jsRunLoop = closure != nullptr ? closure->jsRunLoop : CFRunLoopGetCurrent(); g_blockToJsFunction[blockPtr] = entry; @@ -174,7 +198,7 @@ void block_finalize_now(napi_env env, void* data, void* hint) { free(block); } - + void finalizeFunctionPointerNow(napi_env env, void* finalize_data, void* finalize_hint) { auto ref = static_cast(finalize_data); if (ref == nullptr) { @@ -259,14 +283,23 @@ bool isObjCBlockObject(id obj) { return false; } + static thread_local std::unordered_map blockClassCache; + auto cached = blockClassCache.find(cls); + if (cached != blockClassCache.end()) { + return cached->second; + } + const char* className = class_getName(cls); if (className == nullptr) { + blockClassCache.emplace(cls, false); return false; } // Runtime block classes are typically internal names like // __NSGlobalBlock__, __NSMallocBlock__, __NSStackBlock__. - return className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + bool isBlock = className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + blockClassCache.emplace(cls, isBlock); + return isBlock; } const char* getObjCBlockSignature(void* blockPtr) { @@ -444,7 +477,13 @@ bool isObjCBlockObject(id obj) { } } - ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + auto preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::CFunction); + if (preparedInvoker != nullptr) { + preparedInvoker(ref->function, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + } if (shouldFreeAny) { for (unsigned int i = 0; i < cif->argc; i++) { @@ -484,7 +523,14 @@ bool isObjCBlockObject(id obj) { } } - ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + BlockPreparedInvoker preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::BlockInvoke); + + if (preparedInvoker != nullptr) { + preparedInvoker(block->invoke, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + } if (shouldFreeAny) { for (unsigned int i = 0; i < cif->argc; i++) { diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/CFunction.h index d0003f12..42ac39ef 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -2,18 +2,25 @@ #define C_FUNCTION_H #include + #include "Cif.h" namespace nativescript { +class ObjCBridgeState; + class CFunction { public: static napi_value jsCall(napi_env env, napi_callback_info cbinfo); + static napi_value jsCallDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* callArgs); CFunction(void* fnptr) : fnptr(fnptr) {} ~CFunction(); void* fnptr; + ObjCBridgeState* bridgeState = nullptr; Cif* cif = nullptr; uint8_t dispatchFlags = 0; bool dispatchLookupCached = false; @@ -21,6 +28,8 @@ class CFunction { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; + void* v8Invoker = nullptr; }; } // namespace nativescript diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 3f3a21f7..3b1ea423 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -4,6 +4,9 @@ #include #include "Block.h" #include "ClassMember.h" +#include "HermesFastCallbackInfo.h" +#include "HermesFastNativeApi.h" +#include "EngineDirectCall.h" #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -81,13 +84,10 @@ inline napi_value createCompatDispatchQueueWrapperForCFunction(napi_env env, return Pointer::create(env, reinterpret_cast(queue)); } -inline napi_value tryCallCompatLibdispatchFunction(napi_env env, napi_callback_info cbinfo, +inline napi_value tryCallCompatLibdispatchFunction(napi_env env, size_t argc, + const napi_value* argv, const char* functionName) { if (strcmp(functionName, "dispatch_get_global_queue") == 0) { - size_t argc = 2; - napi_value argv[2] = {nullptr, nullptr}; - napi_get_cb_info(env, cbinfo, &argc, argv, nullptr, nullptr); - int64_t identifier = 0; if (argc > 0) { napi_valuetype identifierType = napi_undefined; @@ -142,10 +142,6 @@ inline napi_value tryCallCompatLibdispatchFunction(napi_env env, napi_callback_i } if (strcmp(functionName, "dispatch_async") == 0) { - size_t argc = 2; - napi_value argv[2] = {nullptr, nullptr}; - napi_get_cb_info(env, cbinfo, &argc, argv, nullptr, nullptr); - if (argc < 2) { napi_throw_type_error(env, nullptr, "dispatch_async expects a queue and callback."); return nullptr; @@ -189,6 +185,8 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->dispatchId = 0; function->preparedInvoker = nullptr; function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; } return; } @@ -204,6 +202,11 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->preparedInvoker = reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); function->napiInvoker = reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = + reinterpret_cast(lookupCFunctionEngineDirectInvoker(function->dispatchId)); +#ifdef TARGET_ENGINE_V8 + function->v8Invoker = reinterpret_cast(lookupCFunctionV8Invoker(function->dispatchId)); +#endif function->dispatchLookupCached = true; } @@ -241,6 +244,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { MDFunctionFlag functionFlags = metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); auto cFunction = new CFunction(dlsym(self_dl, metadata->getString(offset))); + cFunction->bridgeState = this; cFunction->cif = getCFunctionCif(env, sigOffset); cFunction->dispatchFlags = (functionFlags & mdFunctionReturnOwned) != 0 ? 1 : 0; cFunctionCache[offset] = cFunction; @@ -249,18 +253,68 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { } napi_value CFunction::jsCall(napi_env env, napi_callback_info cbinfo) { - void* _offset; +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* args = stackArgs; + const size_t actualArgc = fastInfo->argc; + if (actualArgc > 16) { + heapArgs.resize(actualArgc); + args = heapArgs.data(); + } + for (size_t i = 0; i < actualArgc; i++) { + args[i] = HermesFastArg(fastInfo, i); + } - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, &_offset); + bool handledDirect = false; + napi_value directResult = TryCallHermesCFunctionFast( + env, + static_cast(reinterpret_cast(fastInfo->data)), + actualArgc, args, &handledDirect); + if (handledDirect) { + return directResult; + } + + return jsCallDirect( + env, static_cast(reinterpret_cast(fastInfo->data)), + actualArgc, args); + } +#endif + + void* _offset = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, &_offset); - auto bridgeState = ObjCBridgeState::InstanceData(env); MDSectionOffset offset = (MDSectionOffset)((size_t)_offset); + if (actualArgc > 16) { + std::vector dynamicArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, dynamicArgs.data(), nullptr, nullptr); + dynamicArgs.resize(retryArgc); + return jsCallDirect(env, offset, retryArgc, dynamicArgs.data()); + } + + return jsCallDirect(env, offset, actualArgc, stackArgs); +} + +napi_value CFunction::jsCallDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* callArgs) { + auto bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C bridge state."); + return nullptr; + } + auto name = bridgeState->metadata->getString(offset); if (strcmp(name, "dispatch_async") == 0 || strcmp(name, "dispatch_get_current_queue") == 0 || strcmp(name, "dispatch_get_global_queue") == 0) { - return tryCallCompatLibdispatchFunction(env, cbinfo, name); + return tryCallCompatLibdispatchFunction(env, actualArgc, callArgs, name); } auto func = bridgeState->getCFunction(env, offset); @@ -269,28 +323,15 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { ensureCFunctionDispatchLookup(func, cif); auto preparedInvoker = reinterpret_cast(func->preparedInvoker); auto napiInvoker = reinterpret_cast(func->napiInvoker); + auto engineDirectInvoker = + reinterpret_cast(func->engineDirectInvoker); MDFunctionFlag functionFlags = bridgeState->metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); const napi_value* invocationArgs = nullptr; - std::vector dynamicArgs; std::vector paddedArgs; - napi_value stackArgs[16]; if (cif->argc > 0) { - size_t actualArgc = 16; - napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, nullptr); - - const napi_value* callArgs = stackArgs; - if (actualArgc > 16) { - dynamicArgs.resize(actualArgc); - size_t retryArgc = actualArgc; - napi_get_cb_info(env, cbinfo, &retryArgc, dynamicArgs.data(), nullptr, nullptr); - dynamicArgs.resize(retryArgc); - actualArgc = retryArgc; - callArgs = dynamicArgs.data(); - } - invocationArgs = callArgs; if (actualArgc != cif->argc) { napi_value jsUndefined = nullptr; @@ -312,9 +353,14 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; - if (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch && !isMainEntrypoint) { + if ((engineDirectInvoker != nullptr || + (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && + !isMainEntrypoint) { @try { - if (!napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue)) { + bool invoked = engineDirectInvoker != nullptr + ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue) + : napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue); + if (!invoked) { return nullptr; } } @catch (NSException* exception) { @@ -325,6 +371,11 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { return nullptr; } + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, cif->rvalue, + &fastResult)) { + return fastResult; + } return cif->returnType->toJS(env, cif->rvalue, toJSFlags); } @@ -406,6 +457,11 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { } } + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } return cif->returnType->toJS(env, rvalue, toJSFlags); } diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/Cif.h index ee483acf..3614d6ab 100644 --- a/NativeScript/ffi/Cif.h +++ b/NativeScript/ffi/Cif.h @@ -21,6 +21,9 @@ class Cif { bool isVariadic = false; uint64_t signatureHash = 0; bool skipGeneratedNapiDispatch = false; + bool generatedDispatchHasRoundTripCacheArgument = false; + bool generatedDispatchUsesObjectReturnStorage = false; + bool generatedDispatchSetsV8ReturnDirectly = false; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/Cif.mm b/NativeScript/ffi/Cif.mm index 6f98b49f..9d8ba085 100644 --- a/NativeScript/ffi/Cif.mm +++ b/NativeScript/ffi/Cif.mm @@ -75,17 +75,69 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptrskipGeneratedNapiDispatch = false; + cif->generatedDispatchHasRoundTripCacheArgument = false; + cif->generatedDispatchUsesObjectReturnStorage = false; + cif->generatedDispatchSetsV8ReturnDirectly = false; + + if (cif->returnType != nullptr) { + cif->generatedDispatchUsesObjectReturnStorage = + typeKindMayUseRoundTripCache(cif->returnType->kind); + cif->generatedDispatchSetsV8ReturnDirectly = + typeKindCanSetV8ReturnDirectly(cif->returnType->kind); + } + cif->skipGeneratedNapiDispatch = typeRequiresSlowGeneratedNapiDispatch(cif->returnType); if (cif->skipGeneratedNapiDispatch) { return; } for (const auto& argType : cif->argTypes) { + if (argType != nullptr && typeKindMayUseRoundTripCache(argType->kind)) { + cif->generatedDispatchHasRoundTripCacheArgument = true; + } if (typeRequiresSlowGeneratedNapiDispatch(argType)) { cif->skipGeneratedNapiDispatch = true; return; diff --git a/NativeScript/ffi/Class.mm b/NativeScript/ffi/Class.mm index cd435952..f4ab722a 100644 --- a/NativeScript/ffi/Class.mm +++ b/NativeScript/ffi/Class.mm @@ -146,7 +146,7 @@ inline bool tryGetInteropPointerArg(napi_env env, napi_value value, void** out) ClassBuilder* builder = new ClassBuilder(env, argv[0]); // It gets lazily built when a static method is called. // builder->build(); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return nullptr; } @@ -747,8 +747,7 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value if (nativeClass != nil) { napi_wrap(env, constructor, (void*)nativeClass, nil, nil, nil); - bridgeState->classesByPointer[nativeClass] = this; - bridgeState->mdClassesByPointer[nativeClass] = metadataOffset; + bridgeState->registerRuntimeClass(this, nativeClass); } napi_get_named_property(env, constructor, "prototype", &prototype); diff --git a/NativeScript/ffi/ClassBuilder.mm b/NativeScript/ffi/ClassBuilder.mm index f11cfe5e..ac3ac914 100644 --- a/NativeScript/ffi/ClassBuilder.mm +++ b/NativeScript/ffi/ClassBuilder.mm @@ -1016,7 +1016,7 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat // Register the builder in the bridge state bridgeState = ObjCBridgeState::InstanceData(env); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return newConstructor; } diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/ClassMember.h index aea44b72..62ac9628 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -33,6 +33,10 @@ class MethodDescriptor { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; + void* v8Invoker = nullptr; + bool nserrorOutSignatureCached = false; + bool nserrorOutSignature = false; MethodDescriptor() {} @@ -62,7 +66,8 @@ struct ObjCClassMemberOverload { MethodDescriptor method; Cif* cif = nullptr; - ObjCClassMemberOverload(SEL selector, MDSectionOffset offset, uint8_t dispatchFlags) + ObjCClassMemberOverload(SEL selector, MDSectionOffset offset, + uint8_t dispatchFlags) : method(selector, offset) { method.dispatchFlags = dispatchFlags; } @@ -79,6 +84,14 @@ class ObjCClassMember { static napi_value jsGetter(napi_env env, napi_callback_info cbinfo); static napi_value jsReadOnlySetter(napi_env env, napi_callback_info cbinfo); static napi_value jsSetter(napi_env env, napi_callback_info cbinfo); + static napi_value jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* callArgs); + static napi_value jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis); + static napi_value jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value); + static napi_value jsReadOnlySetterDirect(napi_env env); void addOverload(SEL selector, MDSectionOffset offset, uint8_t dispatchFlags); ObjCClassMember(ObjCBridgeState* bridgeState, SEL selector, diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index 59f9f6a2..56896794 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -9,10 +9,14 @@ #include #include #include +#include #include #include "ClassBuilder.h" #include "Closure.h" +#include "EngineDirectCall.h" #include "Interop.h" +#include "HermesFastCallbackInfo.h" +#include "HermesFastNativeApi.h" #include "MetadataReader.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -47,7 +51,8 @@ napi_value JS_NSObject_alloc(napi_env env, napi_callback_info cbinfo) { method->cls->nativeClass != nil) { bool canFallbackToMethodClass = true; napi_valuetype jsType = napi_undefined; - if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && jsType == napi_function) { + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { napi_value definingConstructor = get_ref_value(env, method->cls->constructor); if (definingConstructor != nullptr) { bool isSameConstructor = false; @@ -318,7 +323,7 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho *didInvoke = false; } - if (cif == nullptr || cif->signatureHash == 0 || cif->skipGeneratedNapiDispatch) { + if (cif == nullptr || cif->signatureHash == 0) { return true; } @@ -342,20 +347,36 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); descriptor->napiInvoker = reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif descriptor->dispatchLookupCached = true; } } - auto invoker = descriptor != nullptr - ? reinterpret_cast(descriptor->napiInvoker) - : lookupObjCNapiInvoker(composeSignatureDispatchId( - cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); - if (invoker == nullptr) { + ObjCEngineDirectInvoker engineInvoker = + descriptor != nullptr + ? reinterpret_cast(descriptor->engineDirectInvoker) + : lookupObjCEngineDirectInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); + ObjCNapiInvoker invoker = + engineInvoker == nullptr && !cif->skipGeneratedNapiDispatch + ? (descriptor != nullptr + ? reinterpret_cast(descriptor->napiInvoker) + : lookupObjCNapiInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags))) + : nullptr; + if (engineInvoker == nullptr && invoker == nullptr) { return true; } @try { - if (!invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue)) { + bool invoked = engineInvoker != nullptr + ? engineInvoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue) + : invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue); + if (!invoked) { return false; } } @catch (NSException* exception) { @@ -414,6 +435,12 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); descriptor->napiInvoker = reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = + reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif descriptor->dispatchLookupCached = true; } @@ -598,7 +625,7 @@ inline bool isNSErrorOutMethodSignature(SEL selector, Cif* cif) { } auto lastArgType = cif->argTypes[cif->argc - 1]; - return lastArgType != nullptr && lastArgType->kind == mdTypePointer; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; } inline void throwArgumentsCountError(napi_env env, size_t actualCount, size_t expectedCount) { @@ -961,8 +988,7 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = napi_value definingConstructor = get_ref_value(env, method->cls->constructor); if (definingConstructor != nullptr) { bool isSameConstructor = false; - if (napi_strict_equals(env, jsThis, definingConstructor, &isSameConstructor) == - napi_ok && + if (napi_strict_equals(env, jsThis, definingConstructor, &isSameConstructor) == napi_ok && !isSameConstructor) { shouldUseClassFallback = false; } @@ -1068,6 +1094,12 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = ObjCBridgeState* bridgeState_; }; +inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + namespace { inline size_t alignUpSize(size_t value, size_t alignment) { @@ -1412,21 +1444,66 @@ explicit CifReturnStorage(Cif* cif) { } napi_value ObjCClassMember::jsCall(napi_env env, napi_callback_info cbinfo) { +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* args = stackArgs; + const size_t actualArgc = fastInfo->argc; + if (actualArgc > 16) { + heapArgs.resize(actualArgc); + args = heapArgs.data(); + } + for (size_t i = 0; i < actualArgc; i++) { + args[i] = HermesFastArg(fastInfo, i); + } + + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), actualArgc, args, + EngineDirectMemberKind::Method, &handledDirect); + if (handledDirect) { + return directResult; + } + + return jsCallDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), actualArgc, args); + } +#endif + napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; size_t actualArgc = 16; napi_value stackArgs[16]; napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, &jsThis, (void**)&method); + if (actualArgc > 16) { + std::vector dynamicArgs(actualArgc); + size_t argcRetry = actualArgc; + napi_get_cb_info(env, cbinfo, &argcRetry, dynamicArgs.data(), &jsThis, (void**)&method); + dynamicArgs.resize(argcRetry); + return jsCallDirect(env, method, jsThis, argcRetry, dynamicArgs.data()); + } + + return jsCallDirect(env, method, jsThis, actualArgc, stackArgs); +} + +napi_value ObjCClassMember::jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* rawCallArgs) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C method metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { return nullptr; } - RoundTripCacheFrameGuard roundTripCacheFrame(env, method->bridgeState); - const bool receiverIsClass = object_isClass(self); Class receiverClass = receiverIsClass ? (Class)self : [self class]; auto resolveDescriptorCif = [&](MethodDescriptor* descriptor, Cif** cacheSlot) -> Cif* { @@ -1454,17 +1531,10 @@ explicit CifReturnStorage(Cif* cif) { return resolved; }; - const napi_value* callArgs = stackArgs; + const napi_value* callArgs = actualArgc > 0 ? rawCallArgs : nullptr; std::vector dynamicArgs; - if (actualArgc > 16) { - dynamicArgs.resize(actualArgc); - size_t argcRetry = actualArgc; - napi_get_cb_info(env, cbinfo, &argcRetry, dynamicArgs.data(), &jsThis, (void**)&method); - dynamicArgs.resize(argcRetry); - actualArgc = argcRetry; - callArgs = dynamicArgs.data(); - } else if (!method->overloads.empty()) { - dynamicArgs.assign(stackArgs, stackArgs + actualArgc); + if (!method->overloads.empty() && actualArgc > 0 && rawCallArgs != nullptr) { + dynamicArgs.assign(rawCallArgs, rawCallArgs + actualArgc); callArgs = dynamicArgs.data(); } @@ -1547,6 +1617,11 @@ explicit CifReturnStorage(Cif* cif) { return nullptr; } + std::optional roundTripCacheFrame; + if (generatedDispatchNeedsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + CifReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { napi_throw_error(env, "NativeScriptException", @@ -1636,7 +1711,13 @@ explicit CifReturnStorage(Cif* cif) { } } - return cif->returnType->toJS(env, nativeResult, method->returnOwned ? kReturnOwned : 0); + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, + nativeResult, &fastResult)) { + return fastResult; + } + return cif->returnType->toJS(env, nativeResult, + method->returnOwned ? kReturnOwned : 0); }; bool usesBlockFallback = false; @@ -1654,7 +1735,7 @@ explicit CifReturnStorage(Cif* cif) { } } - if (!hasImplicitNSErrorOutArg && !usesBlockFallback) { + if (!isNSErrorOutMethod && !usesBlockFallback) { bool didDirectInvoke = false; if (!tryObjCNapiDispatch(env, cif, self, receiverIsClass, selectedSelector, selectedMethod, selectedMethod->dispatchFlags, invocationArgs, rvalue, @@ -1690,7 +1771,8 @@ explicit CifReturnStorage(Cif* cif) { const char* blockEncoding = blockEncodingForSelector(selectedSelectorName, i); if (hasImplicitNSErrorOutArg && i == cif->argc - 1) { - *((NSError***)avalues[i + 2]) = &implicitNSError; + NSError** implicitNSErrorOutArg = &implicitNSError; + *reinterpret_cast(avalues[i + 2]) = implicitNSErrorOutArg; continue; } @@ -1748,11 +1830,36 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } napi_value ObjCClassMember::jsGetter(napi_env env, napi_callback_info cbinfo) { +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), 0, nullptr, + EngineDirectMemberKind::Getter, &handledDirect); + if (handledDirect) { + return directResult; + } + return jsGetterDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo)); + } +#endif + napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, nullptr, nullptr, &jsThis, (void**)&method); + return jsGetterDirect(env, method, jsThis); +} + +napi_value ObjCClassMember::jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C getter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1810,21 +1917,61 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa method->returnOwned ? kOwnedObject : kUnownedObject); } + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } return cif->returnType->toJS(env, rvalue, 0); } napi_value ObjCClassMember::jsReadOnlySetter(napi_env env, napi_callback_info cbinfo) { + return jsReadOnlySetterDirect(env); +} + +napi_value ObjCClassMember::jsReadOnlySetterDirect(napi_env env) { napi_throw_error(env, nullptr, "Attempted to assign to readonly property."); return nullptr; } napi_value ObjCClassMember::jsSetter(napi_env env, napi_callback_info cbinfo) { +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value value = nullptr; + if (fastInfo->argc > 0) { + value = HermesFastArg(fastInfo, 0); + } else { + napi_get_undefined(env, &value); + } + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), 1, &value, + EngineDirectMemberKind::Setter, &handledDirect); + if (handledDirect) { + return directResult; + } + return jsSetterDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), value); + } +#endif + napi_value jsThis, argv; size_t argc = 1; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, &argc, &argv, &jsThis, (void**)&method); + return jsSetterDirect(env, method, jsThis, argv); +} + +napi_value ObjCClassMember::jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C setter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1833,16 +1980,19 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa const bool receiverIsClass = object_isClass(self); - RoundTripCacheFrameGuard roundTripCacheFrame(env, method->bridgeState); - Cif* cif = method->setterCif; if (cif == nullptr) { cif = method->setterCif = method->bridgeState->getMethodCif(env, method->setter.signatureOffset); } + std::optional roundTripCacheFrame; + if (generatedDispatchNeedsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + if (cif->argc > 0) { - cif->argv[0] = argv; + cif->argv[0] = value; } bool didDirectInvoke = false; @@ -1867,7 +2017,7 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa void* rvalue = nullptr; bool shouldFree = false; - cif->argTypes[0]->toNative(env, argv, avalues[2], &shouldFree, &shouldFree); + cif->argTypes[0]->toNative(env, value, avalues[2], &shouldFree, &shouldFree); if (!objcNativeCall(env, cif, self, receiverIsClass, &method->setter, method->setter.dispatchFlags, avalues, rvalue)) { diff --git a/NativeScript/ffi/EngineDirectCall.h b/NativeScript/ffi/EngineDirectCall.h new file mode 100644 index 00000000..84e4c02a --- /dev/null +++ b/NativeScript/ffi/EngineDirectCall.h @@ -0,0 +1,53 @@ +#ifndef NS_ENGINE_DIRECT_CALL_H +#define NS_ENGINE_DIRECT_CALL_H + +#include +#include + +#include + +#include "MetadataReader.h" +#include "js_native_api.h" + +namespace nativescript { + +using metagen::MDSectionOffset; + +class CFunction; +class Cif; +class ObjCClassMember; +struct MethodDescriptor; + +enum class EngineDirectMemberKind : uint8_t { + Method, + Getter, + Setter, +}; + +napi_value TryCallObjCMemberEngineDirect(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled); + +napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled); + +bool InvokeObjCMemberEngineDirectDynamic(napi_env env, Cif* cif, id self, + bool receiverIsClass, + MethodDescriptor* descriptor, + uint8_t dispatchFlags, + size_t actualArgc, + const napi_value* rawArgs, + void* rvalue); + +bool InvokeCFunctionEngineDirectDynamic(napi_env env, CFunction* function, + Cif* cif, size_t actualArgc, + const napi_value* rawArgs, + void* rvalue); + +} // namespace nativescript + +#endif // NS_ENGINE_DIRECT_CALL_H diff --git a/NativeScript/ffi/EngineDirectCall.mm b/NativeScript/ffi/EngineDirectCall.mm new file mode 100644 index 00000000..98608ab0 --- /dev/null +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -0,0 +1,1058 @@ +#include "EngineDirectCall.h" + +#import +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "Class.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "NativeScriptException.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" + +namespace nativescript { +namespace { + +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + +class RoundTripCacheFrameGuard { + public: + RoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) + : env_(env), bridgeState_(bridgeState) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~RoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; +}; + +class CifReturnStorage { + public: + explicit CifReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~CifReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +inline size_t alignUpSize(size_t value, size_t alignment) { + if (alignment == 0) { + return value; + } + return ((value + alignment - 1) / alignment) * alignment; +} + +size_t getCifArgumentStorageSize(Cif* cif, unsigned int argumentIndex, + unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->cif.arg_types == nullptr) { + return sizeof(void*); + } + + const unsigned int ffiIndex = argumentIndex + implicitArgumentCount; + if (ffiIndex >= cif->cif.nargs) { + return sizeof(void*); + } + + ffi_type* ffiArgType = cif->cif.arg_types[ffiIndex]; + size_t storageSize = ffiArgType != nullptr ? ffiArgType->size : 0; + return storageSize != 0 ? storageSize : sizeof(void*); +} + +size_t getCifArgumentStorageAlign(Cif* cif, unsigned int argumentIndex, + unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->cif.arg_types == nullptr) { + return alignof(void*); + } + + const unsigned int ffiIndex = argumentIndex + implicitArgumentCount; + if (ffiIndex >= cif->cif.nargs) { + return alignof(void*); + } + + ffi_type* ffiArgType = cif->cif.arg_types[ffiIndex]; + size_t alignment = ffiArgType != nullptr ? ffiArgType->alignment : 0; + return alignment != 0 ? alignment : alignof(void*); +} + +class EngineDirectArgumentStorage { + public: + EngineDirectArgumentStorage(Cif* cif, unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->argc == 0) { + return; + } + + count_ = cif->argc; + if (count_ <= kInlineArgCount) { + buffers_ = inlineBuffers_; + } else { + heapBuffers_.resize(count_, nullptr); + buffers_ = heapBuffers_.data(); + } + + size_t totalSize = 0; + for (unsigned int i = 0; i < count_; i++) { + const size_t storageAlign = + getCifArgumentStorageAlign(cif, i, implicitArgumentCount); + const size_t storageSize = + getCifArgumentStorageSize(cif, i, implicitArgumentCount); + totalSize = alignUpSize(totalSize, storageAlign); + totalSize += storageSize; + } + + if (totalSize == 0) { + totalSize = sizeof(void*); + } + + storageBase_ = totalSize <= kInlineStorageSize + ? static_cast(inlineStorage_) + : std::malloc(totalSize); + if (storageBase_ == nullptr) { + valid_ = false; + return; + } + + std::memset(storageBase_, 0, totalSize); + + size_t offset = 0; + for (unsigned int i = 0; i < count_; i++) { + const size_t storageAlign = + getCifArgumentStorageAlign(cif, i, implicitArgumentCount); + const size_t storageSize = + getCifArgumentStorageSize(cif, i, implicitArgumentCount); + offset = alignUpSize(offset, storageAlign); + buffers_[i] = + static_cast(static_cast(storageBase_) + offset); + offset += storageSize; + } + } + + ~EngineDirectArgumentStorage() { + if (storageBase_ != nullptr && storageBase_ != inlineStorage_) { + std::free(storageBase_); + } + } + + bool valid() const { return valid_; } + + void* at(unsigned int index) const { + return index < count_ ? buffers_[index] : nullptr; + } + + private: + static constexpr unsigned int kInlineArgCount = 16; + static constexpr size_t kInlineStorageSize = 256; + alignas(max_align_t) unsigned char inlineStorage_[kInlineStorageSize]; + void* inlineBuffers_[kInlineArgCount] = {}; + std::vector heapBuffers_; + void** buffers_ = inlineBuffers_; + unsigned int count_ = 0; + void* storageBase_ = nullptr; + bool valid_ = true; +}; + +void reportNativeException(napi_env env, NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); +} + +const napi_value* prepareDynamicInvocationArgs(napi_env env, Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + napi_value* stackArgs, + size_t stackCapacity, + std::vector* heapArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + + napi_value* paddedArgs = stackArgs; + if (cif->argc > stackCapacity) { + heapArgs->assign(cif->argc, jsUndefined); + paddedArgs = heapArgs->data(); + } else { + for (unsigned int i = 0; i < cif->argc; i++) { + paddedArgs[i] = jsUndefined; + } + } + + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + std::memcpy(paddedArgs, rawArgs, copyArgc * sizeof(napi_value)); + } + return paddedArgs; +} + +void freeObjCConvertedArguments(napi_env env, Cif* cif, void** avalues, + bool* shouldFree, bool shouldFreeAny) { + if (!shouldFreeAny || cif == nullptr || avalues == nullptr || + shouldFree == nullptr) { + return; + } + + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *static_cast(avalues[i + 2])); + } + } +} + +void freeCFunctionConvertedArguments(napi_env env, Cif* cif, void** avalues, + bool* shouldFree, bool shouldFreeAny, + void* rvalue) { + if (!shouldFreeAny || cif == nullptr || avalues == nullptr || + shouldFree == nullptr) { + return; + } + + void* returnPointerValue = nullptr; + const bool returnIsPointer = + cif->returnType != nullptr && cif->returnType->type == &ffi_type_pointer; + if (returnIsPointer && rvalue != nullptr) { + returnPointerValue = *static_cast(rvalue); + } + + for (unsigned int i = 0; i < cif->argc; i++) { + if (!shouldFree[i]) { + continue; + } + if (returnPointerValue != nullptr && avalues[i] != nullptr) { + void* argPointerValue = *static_cast(avalues[i]); + if (argPointerValue == returnPointerValue) { + continue; + } + } + cif->argTypes[i]->free(env, *static_cast(avalues[i])); + } +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeNSErrorOutMethodSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty()) { + return false; + } + if (!selectorEndsWith(selector, "error:")) { + return false; + } + + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isNSErrorOutMethodSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeNSErrorOutMethodSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeNSErrorOutMethodSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline void throwArgumentsCountError(napi_env env, size_t actualCount, + size_t expectedCount) { + std::string message = "Actual arguments count: \"" + + std::to_string(actualCount) + "\". Expected: \"" + + std::to_string(expectedCount) + "\"."; + napi_throw_error(env, "NativeScriptException", message.c_str()); +} + +inline bool isBlockFallbackSelector(SEL selector) { + const char* selectorName = sel_getName(selector); + return selectorName != nullptr && + (std::strcmp(selectorName, "methodWithSimpleBlock:") == 0 || + std::strcmp(selectorName, "methodRetainingBlock:") == 0 || + std::strcmp(selectorName, "methodWithBlock:") == 0 || + std::strcmp(selectorName, "methodWithComplexBlock:") == 0); +} + +id resolveSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + napi_status unwrapStatus = + self != nil ? napi_ok : napi_unwrap(env, jsThis, reinterpret_cast(&self)); + + if ((unwrapStatus != napi_ok || self == nil) && jsThis != nullptr) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, jsThis, kNativePointerProperty, + &nativePointerValue) == napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); + if (nativePointer != nullptr && nativePointer->data != nullptr) { + self = static_cast(nativePointer->data); + unwrapStatus = napi_ok; + } + } + } + + if (unwrapStatus == napi_ok && self != nil) { + return self; + } + + bool shouldUseClassFallback = false; + if (method != nullptr && method->cls != nullptr && + method->cls->nativeClass != nil) { + if (method->classMethod) { + shouldUseClassFallback = true; + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + napi_value definingConstructor = get_ref_value(env, method->cls->constructor); + if (definingConstructor != nullptr) { + bool isSameConstructor = false; + if (napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } + } + } + } else { + napi_valuetype jsType = napi_undefined; + if (napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + shouldUseClassFallback = true; + } + } + } + + if (shouldUseClassFallback) { + return static_cast(method->cls->nativeClass); + } + + napi_throw_error(env, "NativeScriptException", + "There was no native counterpart to the JavaScript object. " + "Native API was called with a likely plain object."); + return nil; +} + +Cif* resolveMethodDescriptorCif(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif** cacheSlot, + bool receiverIsClass, Class receiverClass) { + if (env == nullptr || member == nullptr || descriptor == nullptr || + cacheSlot == nullptr) { + return nullptr; + } + + Cif* cached = *cacheSlot; + if (cached != nullptr) { + return cached; + } + + Method runtimeMethod = receiverIsClass + ? class_getClassMethod(receiverClass, descriptor->selector) + : class_getInstanceMethod(receiverClass, descriptor->selector); + Cif* resolved = nullptr; + if (runtimeMethod != nullptr) { + resolved = member->bridgeState->getMethodCif(env, runtimeMethod); + } + if (resolved == nullptr) { + resolved = member->bridgeState->getMethodCif(env, descriptor->signatureOffset); + } + + *cacheSlot = resolved; + return resolved; +} + +inline bool receiverClassRequiresSuperCall(Class receiverClass) { + if (receiverClass == nil) { + return false; + } + + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + class_conformsToProtocol(receiverClass, @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +ObjCEngineDirectInvoker ensureObjCEngineDirectInvoker(Cif* cif, + MethodDescriptor* descriptor, + uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = + reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureCFunctionEngineDirectInvoker(CFunction* function, + Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); +#ifdef TARGET_ENGINE_V8 + function->v8Invoker = + reinterpret_cast(lookupCFunctionV8Invoker(function->dispatchId)); +#endif + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +const napi_value* prepareInvocationArgs(napi_env env, Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + std::vector* paddedArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + paddedArgs->assign(cif->argc, jsUndefined); + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + std::memcpy(paddedArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return paddedArgs->data(); +} + +napi_value convertObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, + id self, bool receiverIsClass, + napi_value jsThis, void* rvalue, + bool propertyAccess) { + if (member == nullptr || descriptor == nullptr || cif == nullptr) { + return nullptr; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + return constructor; + } + + id classObject = receiverIsClass ? self : static_cast(object_getClass(self)); + return member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + + id obj = *reinterpret_cast(rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + + return member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + } + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + return cif->returnType->toJS(env, rvalue, member->returnOwned ? kReturnOwned : 0); +} + +napi_value convertCFunctionReturnValue(napi_env env, CFunction* function, + Cif* cif, void* rvalue) { + if (cif == nullptr) { + return nullptr; + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + return cif->returnType->toJS(env, rvalue, toJSFlags); +} + +bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return true; + } + + const char* name = bridgeState->metadata->getString(offset); + return name == nullptr || + std::strcmp(name, "dispatch_async") == 0 || + std::strcmp(name, "dispatch_get_current_queue") == 0 || + std::strcmp(name, "dispatch_get_global_queue") == 0 || + std::strcmp(name, "UIApplicationMain") == 0 || + std::strcmp(name, "NSApplicationMain") == 0; +} + +} // namespace + +bool InvokeObjCMemberEngineDirectDynamic(napi_env env, Cif* cif, id self, + bool receiverIsClass, + MethodDescriptor* descriptor, + uint8_t dispatchFlags, + size_t actualArgc, + const napi_value* rawArgs, + void* rvalue) { + if (env == nullptr || cif == nullptr || self == nil || + descriptor == nullptr || rvalue == nullptr || cif->isVariadic) { + return false; + } + + Class receiverClass = receiverIsClass ? static_cast(self) + : object_getClass(self); + if (receiverClassRequiresSuperCall(receiverClass)) { + return false; + } + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareDynamicInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + EngineDirectArgumentStorage argStorage(cif, 2); + if (!argStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate argument storage for Objective-C call."); + return false; + } + + void* stackAvalues[32]; + std::vector heapAvalues; + void** avalues = stackAvalues; + if (cif->cif.nargs > 32) { + heapAvalues.resize(cif->cif.nargs); + avalues = heapAvalues.data(); + } + + SEL selector = descriptor->selector; + avalues[0] = static_cast(&self); + avalues[1] = static_cast(&selector); + + bool stackShouldFree[16] = {}; + std::vector heapShouldFree; + if (cif->argc > 16) { + heapShouldFree.assign(cif->argc, 0); + } + + bool shouldFreeAny = false; + for (unsigned int i = 0; i < cif->argc; i++) { + bool shouldFreeArg = false; + avalues[i + 2] = argStorage.at(i); + if (!TryFastConvertEngineArgument(env, cif->argTypes[i]->kind, + invocationArgs[i], avalues[i + 2])) { + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i + 2], + &shouldFreeArg, &shouldFreeAny); + } + if (cif->argc > 16) { + heapShouldFree[i] = shouldFreeArg ? 1 : 0; + } else { + stackShouldFree[i] = shouldFreeArg; + } + } + + bool didInvoke = false; + @try { + auto preparedInvoker = + reinterpret_cast(descriptor->preparedInvoker); + if (preparedInvoker != nullptr) { + preparedInvoker(reinterpret_cast(objc_msgSend), avalues, rvalue); + } else { +#if defined(__x86_64__) + const bool isStret = + cif->returnType != nullptr && cif->returnType->type != nullptr && + cif->returnType->type->size > 16 && + cif->returnType->type->type == FFI_TYPE_STRUCT; + ffi_call(&cif->cif, + isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend), + rvalue, avalues); +#else + ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); +#endif + } + didInvoke = true; + } @catch (NSException* exception) { + reportNativeException(env, exception); + } + + if (cif->argc > 16 && shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (heapShouldFree[i] != 0) { + cif->argTypes[i]->free(env, *static_cast(avalues[i + 2])); + } + } + } else { + freeObjCConvertedArguments(env, cif, avalues, stackShouldFree, + shouldFreeAny); + } + + return didInvoke; +} + +bool InvokeCFunctionEngineDirectDynamic(napi_env env, CFunction* function, + Cif* cif, size_t actualArgc, + const napi_value* rawArgs, + void* rvalue) { + if (env == nullptr || function == nullptr || cif == nullptr || + function->fnptr == nullptr || rvalue == nullptr || cif->isVariadic) { + return false; + } + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareDynamicInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + void* stackAvalues[16]; + std::vector heapAvalues; + void** avalues = stackAvalues; + if (cif->argc > 16) { + heapAvalues.resize(cif->argc); + avalues = heapAvalues.data(); + } + + bool stackShouldFree[16] = {}; + std::vector heapShouldFree; + if (cif->argc > 16) { + heapShouldFree.reserve(cif->argc); + } + bool shouldFreeAny = false; + + for (unsigned int i = 0; i < cif->argc; i++) { + bool shouldFreeArg = false; + avalues[i] = cif->avalues[i]; + if (!TryFastConvertEngineArgument(env, cif->argTypes[i]->kind, + invocationArgs[i], avalues[i])) { + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], + &shouldFreeArg, &shouldFreeAny); + } + if (cif->argc > 16) { + heapShouldFree.push_back(shouldFreeArg ? 1 : 0); + } else { + stackShouldFree[i] = shouldFreeArg; + } + } + + bool didInvoke = false; + @try { + auto preparedInvoker = + reinterpret_cast(function->preparedInvoker); + if (preparedInvoker != nullptr) { + preparedInvoker(function->fnptr, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(function->fnptr), rvalue, avalues); + } + didInvoke = true; + } @catch (NSException* exception) { + reportNativeException(env, exception); + } + + if (cif->argc > 16) { + if (shouldFreeAny) { + void* returnPointerValue = nullptr; + const bool returnIsPointer = + cif->returnType != nullptr && + cif->returnType->type == &ffi_type_pointer; + if (returnIsPointer && rvalue != nullptr) { + returnPointerValue = *static_cast(rvalue); + } + for (unsigned int i = 0; i < cif->argc; i++) { + if (heapShouldFree[i] == 0) { + continue; + } + if (returnPointerValue != nullptr && avalues[i] != nullptr) { + void* argPointerValue = *static_cast(avalues[i]); + if (argPointerValue == returnPointerValue) { + continue; + } + } + cif->argTypes[i]->free(env, *static_cast(avalues[i])); + } + } + } else { + freeCFunctionConvertedArguments(env, cif, avalues, stackShouldFree, + shouldFreeAny, rvalue); + } + + return didInvoke; +} + +napi_value TryCallObjCMemberEngineDirect(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + if (env == nullptr || member == nullptr || member->bridgeState == nullptr) { + return nullptr; + } + + if (kind == EngineDirectMemberKind::Method && !member->overloads.empty()) { + return nullptr; + } + + id self = resolveSelf(env, jsThis, member); + if (self == nil) { + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresSuperCall(receiverClass)) { + return nullptr; + } + + MethodDescriptor* descriptor = nullptr; + Cif** cifSlot = nullptr; + bool propertyAccess = false; + switch (kind) { + case EngineDirectMemberKind::Method: + descriptor = &member->methodOrGetter; + cifSlot = &member->cif; + break; + case EngineDirectMemberKind::Getter: + descriptor = &member->methodOrGetter; + cifSlot = &member->cif; + propertyAccess = true; + break; + case EngineDirectMemberKind::Setter: + descriptor = &member->setter; + cifSlot = &member->setterCif; + propertyAccess = true; + break; + } + + Cif* cif = resolveMethodDescriptorCif(env, member, descriptor, cifSlot, + receiverIsClass, receiverClass); + if (cif == nullptr) { + return nullptr; + } + + if (cif->isVariadic || isBlockFallbackSelector(descriptor->selector)) { + return nullptr; + } + + const bool isNSErrorOutMethod = isNSErrorOutMethodSignature(descriptor, cif); + if (isNSErrorOutMethod) { + if (!cif->isVariadic && + (actualArgc > cif->argc || actualArgc + 1 < cif->argc)) { + throwArgumentsCountError(env, actualArgc, cif->argc); + if (handled != nullptr) { + *handled = true; + } + } + return nullptr; + } + + ObjCEngineDirectInvoker invoker = + ensureObjCEngineDirectInvoker(cif, descriptor, descriptor->dispatchFlags); + + std::vector paddedArgs; + const napi_value* invocationArgs = + prepareInvocationArgs(env, cif, actualArgc, rawArgs, &paddedArgs); + + CifReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate return value storage for Objective-C call."); + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, member->bridgeState); + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, invocationArgs, rvalue); + } else { + didInvoke = InvokeObjCMemberEngineDirectDynamic( + env, cif, self, receiverIsClass, descriptor, descriptor->dispatchFlags, + actualArgc, rawArgs, rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return convertObjCReturnValue(env, member, descriptor, cif, self, + receiverIsClass, jsThis, rvalue, propertyAccess); +} + +napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (env == nullptr || bridgeState == nullptr || + isCompatOrMainCFunction(bridgeState, offset)) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic) { + return nullptr; + } + + CFunctionEngineDirectInvoker invoker = + ensureCFunctionEngineDirectInvoker(function, cif); + + std::vector paddedArgs; + const napi_value* invocationArgs = + prepareInvocationArgs(env, cif, actualArgc, rawArgs, &paddedArgs); + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, bridgeState); + } + + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + } else { + didInvoke = InvokeCFunctionEngineDirectDynamic( + env, function, cif, actualArgc, rawArgs, cif->rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return convertCFunctionReturnValue(env, function, cif, cif->rvalue); +} + +} // namespace nativescript diff --git a/NativeScript/ffi/HermesFastCallbackInfo.h b/NativeScript/ffi/HermesFastCallbackInfo.h new file mode 100644 index 00000000..26718648 --- /dev/null +++ b/NativeScript/ffi/HermesFastCallbackInfo.h @@ -0,0 +1,54 @@ +#ifndef NS_HERMES_FAST_CALLBACK_INFO_H +#define NS_HERMES_FAST_CALLBACK_INFO_H + +#include "js_native_api.h" + +#ifdef TARGET_ENGINE_HERMES + +#include +#include + +namespace nativescript { + +struct HermesFastCallbackInfo { + napi_env env = nullptr; + const uint64_t* thisArg = nullptr; + const uint64_t* argsBase = nullptr; + unsigned int argc = 0; + void* data = nullptr; + const uint64_t* newTarget = nullptr; +}; + +inline const HermesFastCallbackInfo* TryGetHermesFastCallbackInfo( + napi_env env, napi_callback_info cbinfo) { + if (env == nullptr || cbinfo == nullptr) { + return nullptr; + } + + auto* info = reinterpret_cast(cbinfo); + if (info->env != env || info->thisArg == nullptr || info->argsBase == nullptr) { + return nullptr; + } + + return info; +} + +inline napi_value HermesFastThisArg(const HermesFastCallbackInfo* info) { + return reinterpret_cast(const_cast(info->thisArg)); +} + +inline napi_value HermesFastArg(const HermesFastCallbackInfo* info, + size_t index) { + if (index >= info->argc) { + return nullptr; + } + + return reinterpret_cast( + const_cast(info->argsBase - (index + 1))); +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES + +#endif // NS_HERMES_FAST_CALLBACK_INFO_H diff --git a/NativeScript/ffi/HermesFastNativeApi.h b/NativeScript/ffi/HermesFastNativeApi.h new file mode 100644 index 00000000..6a71ab58 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.h @@ -0,0 +1,31 @@ +#ifndef NS_HERMES_FAST_NATIVE_API_H +#define NS_HERMES_FAST_NATIVE_API_H + +#include + +#include "EngineDirectCall.h" +#include "MetadataReader.h" +#include "js_native_api.h" + +#ifdef TARGET_ENGINE_HERMES + +namespace nativescript { + +class ObjCClassMember; + +napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled); + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled); + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES + +#endif // NS_HERMES_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm new file mode 100644 index 00000000..70d7c668 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -0,0 +1,1213 @@ +#include "HermesFastNativeApi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "Class.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "NativeScriptException.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" + +namespace nativescript { +namespace { + +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; +constexpr uint64_t kHermesFirstTaggedValue = 0xfff9000000000000ULL; +constexpr uint64_t kHermesBoolETag = 0x1fff6ULL; +constexpr uint64_t kHermesBoolBit = 1ULL << 46; + +inline bool isHermesNumber(uint64_t raw) { + return raw < kHermesFirstTaggedValue; +} + +inline bool isHermesBool(uint64_t raw) { + return (raw >> 47) == kHermesBoolETag; +} + +inline uint64_t hermesRawValueBits(napi_value value) { + return value != nullptr ? *reinterpret_cast(value) : 0; +} + +inline double hermesRawToDouble(uint64_t raw) { + double value = 0.0; + std::memcpy(&value, &raw, sizeof(value)); + return value; +} + +inline bool readHermesFiniteNumber(napi_value value, double* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = *reinterpret_cast(value); + if (!isHermesNumber(raw)) { + return false; + } + + double converted = hermesRawToDouble(raw); + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *result = converted; + return true; +} + +inline napi_value makeHermesRawValue(uint64_t raw) { + static thread_local uint64_t slots[8] = {}; + static thread_local unsigned int nextSlot = 0; + uint64_t* slot = &slots[nextSlot++ & 7]; + *slot = raw; + return reinterpret_cast(slot); +} + +inline napi_value makeHermesRawNumberValue(double value) { + uint64_t raw = 0; + std::memcpy(&raw, &value, sizeof(raw)); + return makeHermesRawValue(raw); +} + +inline napi_value makeHermesRawBoolValue(bool value) { + return makeHermesRawValue((kHermesBoolETag << 47) | + (value ? kHermesBoolBit : 0)); +} + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +bool tryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + size_t length = 0; + napi_status status = napi_get_value_string_utf8( + env, value, stackBuffer, kStackCapacity, &length); + if (status == napi_ok && length + 1 < kStackCapacity) { + *result = cachedSelectorForName(stackBuffer, length); + return true; + } + + if (status == napi_ok || status == napi_string_expected) { + if (status == napi_string_expected) { + napi_valuetype valueType = napi_undefined; + if (napi_typeof(env, value, &valueType) == napi_ok && + (valueType == napi_null || valueType == napi_undefined)) { + *result = nullptr; + return true; + } + return false; + } + + if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != + napi_ok) { + return false; + } + + std::vector heapBuffer(length + 1, '\0'); + if (napi_get_value_string_utf8(env, value, heapBuffer.data(), + heapBuffer.size(), &length) != napi_ok) { + return false; + } + *result = cachedSelectorForName(heapBuffer.data(), length); + return true; + } + + return false; +} + +bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + void* wrapped = nullptr; + if (napi_unwrap(env, value, &wrapped) != napi_ok || wrapped == nullptr) { + return false; + } + + if (kind == mdTypeClass) { + id nativeObject = static_cast(wrapped); + if (!object_isClass(nativeObject)) { + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id normalizedObject = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (normalizedObject != nil) { + nativeObject = normalizedObject; + } + } + } + if (!object_isClass(nativeObject)) { + return false; + } + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + + *reinterpret_cast(result) = static_cast(wrapped); + return true; +} + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + +class HermesFastRoundTripCacheFrameGuard { + public: + HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) + : env_(env), bridgeState_(bridgeState) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~HermesFastRoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; +}; + +class HermesFastReturnStorage { + public: + explicit HermesFastReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~HermesFastReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +const napi_value* prepareHermesInvocationArgs(napi_env env, Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + napi_value* stackArgs, + size_t stackCapacity, + std::vector* heapArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + + if (cif->argc <= stackCapacity) { + for (unsigned int i = 0; i < cif->argc; i++) { + stackArgs[i] = i < actualArgc && rawArgs != nullptr ? rawArgs[i] : jsUndefined; + } + return stackArgs; + } + + heapArgs->assign(cif->argc, jsUndefined); + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + std::memcpy(heapArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return heapArgs->data(); +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeNSErrorOutMethodSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isNSErrorOutMethodSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeNSErrorOutMethodSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeNSErrorOutMethodSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline void throwArgumentsCountError(napi_env env, size_t actualCount, + size_t expectedCount) { + std::string message = "Actual arguments count: \"" + + std::to_string(actualCount) + "\". Expected: \"" + + std::to_string(expectedCount) + "\"."; + napi_throw_error(env, "NativeScriptException", message.c_str()); +} + +inline bool isBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + + struct ReceiverCacheEntry { + napi_env env = nullptr; + ObjCClassMember* method = nullptr; + uint64_t rawValue = 0; + id self = nil; + bool classObject = false; + }; + + static thread_local ReceiverCacheEntry lastReceiver; + const uint64_t rawThis = hermesRawValueBits(jsThis); + if (rawThis != 0 && lastReceiver.env == env && + lastReceiver.method == method && lastReceiver.rawValue == rawThis && + lastReceiver.self != nil && + (lastReceiver.classObject || + (state != nullptr && state->hasObjectRef(lastReceiver.self)))) { + return lastReceiver.self; + } + + auto rememberReceiver = [&](id resolved) { + if (resolved == nil || rawThis == 0) { + return; + } + + lastReceiver.env = env; + lastReceiver.method = method; + lastReceiver.rawValue = rawThis; + lastReceiver.self = resolved; + lastReceiver.classObject = object_isClass(resolved); + }; + + napi_status unwrapStatus = napi_invalid_arg; + if (jsThis != nullptr) { + unwrapStatus = napi_unwrap(env, jsThis, reinterpret_cast(&self)); + if (unwrapStatus == napi_ok && self != nil) { + rememberReceiver(self); + return self; + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + if (self != nil) { + rememberReceiver(self); + return self; + } + } + + if (self == nil && jsThis != nullptr) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, jsThis, kNativePointerProperty, + &nativePointerValue) == napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); + if (nativePointer != nullptr && nativePointer->data != nullptr) { + self = static_cast(nativePointer->data); + } + } + } + + if (self != nil) { + rememberReceiver(self); + return self; + } + + bool shouldUseClassFallback = false; + if (method != nullptr && method->cls != nullptr && + method->cls->nativeClass != nil) { + if (method->classMethod) { + shouldUseClassFallback = true; + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + napi_value definingConstructor = get_ref_value(env, method->cls->constructor); + if (definingConstructor != nullptr) { + bool isSameConstructor = false; + if (napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } + } + } + } else { + napi_valuetype jsType = napi_undefined; + if (napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + shouldUseClassFallback = true; + } + } + } + + if (shouldUseClassFallback) { + rememberReceiver(static_cast(method->cls->nativeClass)); + return static_cast(method->cls->nativeClass); + } + + napi_throw_error(env, "NativeScriptException", + "There was no native counterpart to the JavaScript object. " + "Native API was called with a likely plain object."); + return nil; +} + +Cif* hermesMemberCif(napi_env env, ObjCClassMember* member, + EngineDirectMemberKind kind, + MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresHermesSuperCall(Class receiverClass) { + if (receiverClass == nil) { + return false; + } + + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + class_conformsToProtocol(receiverClass, @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +ObjCEngineDirectInvoker ensureHermesObjCEngineDirectInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureHermesCFunctionEngineDirectInvoker( + CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, + id self, bool receiverIsClass, + napi_value jsThis, void* rvalue, + bool propertyAccess) { + if (member == nullptr || descriptor == nullptr || cif == nullptr || + cif->returnType == nullptr) { + return nullptr; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + return constructor; + } + + id classObject = receiverIsClass ? self : static_cast(object_getClass(self)); + return member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + + id obj = *reinterpret_cast(rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + + return member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + } + + if (cif->returnType->kind == mdTypeAnyObject && receiverIsClass) { + id obj = *reinterpret_cast(rvalue); + Class receiverClass = static_cast(self); + if (obj != nil && + (receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertHermesReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + return cif->returnType->toJS(env, rvalue, + member->returnOwned ? kReturnOwned : 0); +} + +napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, + Cif* cif, void* rvalue) { + if (cif == nullptr || cif->returnType == nullptr) { + return nullptr; + } + + napi_value fastResult = nullptr; + if (TryFastConvertHermesReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + return cif->returnType->toJS(env, rvalue, toJSFlags); +} + +bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return true; + } + + const char* name = bridgeState->metadata->getString(offset); + return name == nullptr || + std::strcmp(name, "dispatch_async") == 0 || + std::strcmp(name, "dispatch_get_current_queue") == 0 || + std::strcmp(name, "dispatch_get_global_queue") == 0 || + std::strcmp(name, "UIApplicationMain") == 0 || + std::strcmp(name, "NSApplicationMain") == 0; +} + +} // namespace + +bool TryFastConvertHermesBoolArgument(napi_env env, napi_value value, + uint8_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = *reinterpret_cast(value); + if (!isHermesBool(raw)) { + return false; + } + *result = (raw & kHermesBoolBit) != 0 ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertHermesDoubleArgument(napi_env env, napi_value value, + double* result) { + return readHermesFiniteNumber(value, result); +} + +bool TryFastConvertHermesFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + return TryFastConvertNapiUInt16Argument(env, value, result); +} + +bool TryFastConvertHermesInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + + bool lossless = false; + return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result) { + return tryFastConvertHermesSelectorArgument(env, value, result); +} + +bool TryFastConvertHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (tryFastUnwrapHermesObjectArgument(env, kind, value, result)) { + return true; + } + return false; +} + +bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeBool: + return TryFastConvertHermesBoolArgument( + env, value, reinterpret_cast(result)); + case mdTypeChar: + return TryFastConvertHermesInt8Argument( + env, value, reinterpret_cast(result)); + case mdTypeUChar: + case mdTypeUInt8: + return TryFastConvertHermesUInt8Argument( + env, value, reinterpret_cast(result)); + case mdTypeSShort: + return TryFastConvertHermesInt16Argument( + env, value, reinterpret_cast(result)); + case mdTypeUShort: + return TryFastConvertHermesUInt16Argument( + env, value, reinterpret_cast(result)); + case mdTypeSInt: + return TryFastConvertHermesInt32Argument( + env, value, reinterpret_cast(result)); + case mdTypeUInt: + return TryFastConvertHermesUInt32Argument( + env, value, reinterpret_cast(result)); + case mdTypeSLong: + case mdTypeSInt64: + return TryFastConvertHermesInt64Argument( + env, value, reinterpret_cast(result)); + case mdTypeULong: + case mdTypeUInt64: + return TryFastConvertHermesUInt64Argument( + env, value, reinterpret_cast(result)); + case mdTypeFloat: + return TryFastConvertHermesFloatArgument( + env, value, reinterpret_cast(result)); + case mdTypeDouble: + return TryFastConvertHermesDoubleArgument( + env, value, reinterpret_cast(result)); + case mdTypeSelector: + return TryFastConvertHermesSelectorArgument( + env, value, reinterpret_cast(result)); + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (TryFastConvertHermesObjectArgument(env, kind, value, result)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + default: + return false; + } +} + +bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeVoid: + return napi_get_null(env, result) == napi_ok; + + case mdTypeBool: + if (value == nullptr) { + return false; + } + *result = + makeHermesRawBoolValue(*reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: { + if (value == nullptr) { + return false; + } + const int8_t raw = *reinterpret_cast(value); + if (raw == 0 || raw == 1) { + *result = makeHermesRawBoolValue(raw == 1); + return true; + } + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) { + return false; + } + const uint8_t raw = *reinterpret_cast(value); + if (raw == 0 || raw == 1) { + *result = makeHermesRawBoolValue(raw == 1); + return true; + } + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeUShort: { + if (value == nullptr) { + return false; + } + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + return napi_create_string_utf8(env, buffer, NAPI_AUTO_LENGTH, + result) == napi_ok; + } + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) { + return false; + } + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { + return napi_create_bigint_int64(env, raw, result) == napi_ok; + } + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) { + return false; + } + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (raw > kMaxSafeInteger) { + return napi_create_bigint_uint64(env, raw, result) == napi_ok; + } + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue( + *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + if (env == nullptr || member == nullptr || member->bridgeState == nullptr) { + return nullptr; + } + + MethodDescriptor* descriptor = nullptr; + Cif* cif = hermesMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { + return nullptr; + } + + if (isNSErrorOutMethodSignature(descriptor, cif)) { + if (!cif->isVariadic && + (actualArgc > cif->argc || actualArgc + 1 < cif->argc)) { + throwArgumentsCountError(env, actualArgc, cif->argc); + if (handled != nullptr) { + *handled = true; + } + } + return nullptr; + } + + if (isBlockFallbackSelector(descriptor->selector)) { + return nullptr; + } + + ObjCEngineDirectInvoker invoker = + cif->signatureHash != 0 + ? ensureHermesObjCEngineDirectInvoker(cif, descriptor, + descriptor->dispatchFlags) + : nullptr; + + id self = resolveHermesSelf(env, jsThis, member); + if (self == nil) { + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = + receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresHermesSuperCall(receiverClass)) { + return nullptr; + } + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + HermesFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate return value storage for Objective-C call."); + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, member->bridgeState); + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, invocationArgs, rvalue); + } else { + didInvoke = InvokeObjCMemberEngineDirectDynamic( + env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, actualArgc, rawArgs, rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return makeHermesObjCReturnValue( + env, member, descriptor, cif, self, receiverIsClass, jsThis, rvalue, + kind != EngineDirectMemberKind::Method); +} + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (env == nullptr || bridgeState == nullptr || + isCompatOrMainCFunction(bridgeState, offset)) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic || + cif->returnType == nullptr) { + return nullptr; + } + + CFunctionEngineDirectInvoker invoker = + cif->signatureHash != 0 + ? ensureHermesCFunctionEngineDirectInvoker(function, cif) + : nullptr; + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, bridgeState); + } + + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + } else { + didInvoke = InvokeCFunctionEngineDirectDynamic( + env, function, cif, actualArgc, rawArgs, cif->rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return makeHermesCFunctionReturnValue(env, function, cif, cif->rvalue); +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/Interop.mm b/NativeScript/ffi/Interop.mm index 6e5ca5ef..9ff39883 100644 --- a/NativeScript/ffi/Interop.mm +++ b/NativeScript/ffi/Interop.mm @@ -424,11 +424,19 @@ inline bool unwrapKnownNativeHandle(napi_env env, napi_value value, void** out) napi_value nativePointerValue; if (napi_get_named_property(env, value, kNativePointerProperty, &nativePointerValue) == napi_ok) { - void* nativePointer = nullptr; - if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && - nativePointer != nullptr) { - *out = nativePointer; - return true; + if (Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + *out = pointer->data; + return true; + } + } else { + void* nativePointer = nullptr; + if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && + nativePointer != nullptr) { + *out = nativePointer; + return true; + } } } } @@ -582,7 +590,7 @@ napi_value __extends(napi_env env, napi_callback_info info) { if (superClassNative != nullptr) { ClassBuilder* builder = new ClassBuilder(env, constructor); ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); } return nullptr; diff --git a/NativeScript/ffi/JSCFastNativeApi.h b/NativeScript/ffi/JSCFastNativeApi.h new file mode 100644 index 00000000..12e5e004 --- /dev/null +++ b/NativeScript/ffi/JSCFastNativeApi.h @@ -0,0 +1,17 @@ +#ifndef NS_JSC_FAST_NATIVE_API_H +#define NS_JSC_FAST_NATIVE_API_H + +#include "js_native_api.h" + +namespace nativescript { + +#ifdef TARGET_ENGINE_JSC + +bool JSCTryDefineFastNativeProperty(napi_env env, napi_value object, + const napi_property_descriptor* descriptor); + +#endif // TARGET_ENGINE_JSC + +} // namespace nativescript + +#endif // NS_JSC_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm new file mode 100644 index 00000000..2f9ba7de --- /dev/null +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -0,0 +1,1847 @@ +#include "JSCFastNativeApi.h" + +#ifdef TARGET_ENGINE_JSC + +#import + +#include +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "EngineDirectCall.h" +#include "MetadataReader.h" +#include "NativeScriptException.h" +#include "Object.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" +#include "jsc-api.h" + +namespace nativescript { +namespace { + +enum class JSCFastNativeKind : uint8_t { + ObjCMethod = 1, + ObjCGetter = 2, + ObjCSetter = 3, + ObjCReadOnlySetter = 4, + CFunction = 5, +}; + +struct JSCFastNativeBinding { + napi_env env = nullptr; + JSCFastNativeKind kind = JSCFastNativeKind::ObjCMethod; + void* data = nullptr; +}; + +inline JSValueRef ToJSValue(napi_value value) { + return reinterpret_cast(value); +} + +inline napi_value ToNapi(JSValueRef value) { + return reinterpret_cast(const_cast(value)); +} + +class ScopedJSString { + public: + explicit ScopedJSString(const char* value) + : value_(JSStringCreateWithUTF8CString(value != nullptr ? value : "")) {} + + ~ScopedJSString() { + if (value_ != nullptr) { + JSStringRelease(value_); + } + } + + operator JSStringRef() const { return value_; } + + private: + JSStringRef value_ = nullptr; +}; + +bool isCompatCFunction(napi_env env, void* data) { + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || data == nullptr) { + return true; + } + + auto offset = static_cast(reinterpret_cast(data)); + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || + strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || + strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +class JSCFastReturnStorage { + public: + explicit JSCFastReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~JSCFastReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +bool canMakeJSCRawReturnValue(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool makeJSCRawReturnValue(napi_env env, MDTypeKind kind, const void* value, + JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + switch (kind) { + case mdTypeVoid: + *result = JSValueMakeNull(ctx); + return true; + + case mdTypeBool: + if (value == nullptr) return false; + *result = JSValueMakeBoolean( + ctx, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: { + if (value == nullptr) return false; + const int8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) return false; + const uint8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + return true; + } + + case mdTypeSShort: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: { + if (value == nullptr) return false; + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + *result = JSValueMakeString(ctx, ScopedJSString(buffer)); + } else { + *result = JSValueMakeNumber(ctx, raw); + } + return true; + } + + case mdTypeSInt: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) return false; + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_int64(env, raw, &bigint) == napi_ok) { + *result = ToJSValue(bigint); + return true; + } + } + *result = JSValueMakeNumber(ctx, static_cast(raw)); + return true; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) return false; + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (raw > kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_uint64(env, raw, &bigint) == napi_ok) { + *result = ToJSValue(bigint); + return true; + } + } + *result = JSValueMakeNumber(ctx, static_cast(raw)); + return true; + } + + case mdTypeFloat: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeDouble: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +bool makeJSCNSStringValue(napi_env env, NSString* string, JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + if (string == nil) { + *result = JSValueMakeNull(env->context); + return true; + } + + NSUInteger length = [string length]; + std::vector chars(length > 0 ? length : 1); + if (length > 0) { + [string getCharacters:reinterpret_cast(chars.data()) + range:NSMakeRange(0, length)]; + } + + JSStringRef jsString = JSStringCreateWithCharacters( + reinterpret_cast(chars.data()), length); + if (jsString == nullptr) { + return false; + } + *result = JSValueMakeString(env->context, jsString); + JSStringRelease(jsString); + return true; +} + +bool makeJSCBoxedObjectValue(napi_env env, id obj, JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + if (obj == nil || obj == [NSNull null]) { + *result = JSValueMakeNull(env->context); + return true; + } + + if ([obj isKindOfClass:[NSString class]]) { + return makeJSCNSStringValue(env, (NSString*)obj, result); + } + + if ([obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)obj) == CFBooleanGetTypeID()) { + *result = JSValueMakeBoolean(env->context, [obj boolValue]); + } else { + *result = JSValueMakeNumber(env->context, [obj doubleValue]); + } + return true; + } + + return false; +} + +id resolveJSCSelf(napi_env env, napi_value jsThis, ObjCClassMember* member) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + + if (jsThis != nullptr) { + void* wrapped = nullptr; + if (nativescript_jsc_try_unwrap_native(env, jsThis, &wrapped) && + wrapped != nullptr) { + return static_cast(wrapped); + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + if (self == nil && jsThis != nullptr) { + napi_unwrap(env, jsThis, reinterpret_cast(&self)); + } + + if (self != nil) { + return self; + } + + if (member != nullptr && member->cls != nullptr && + member->cls->nativeClass != nil) { + if (member->classMethod) { + return static_cast(member->cls->nativeClass); + } + + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + return static_cast(member->cls->nativeClass); + } + } + + return nil; +} + +Cif* jscMemberCif(napi_env env, ObjCClassMember* member, + EngineDirectMemberKind kind, + MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresJSCSuperCall(Class receiverClass) { + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + receiverClass != nil && + class_conformsToProtocol(receiverClass, + @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeJSCNSErrorOutSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isJSCNSErrorOutSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeJSCNSErrorOutSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeJSCNSErrorOutSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline bool isJSCBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +ObjCEngineDirectInvoker ensureJSCObjCEngineDirectInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureJSCCFunctionEngineDirectInvoker( + CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, + function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +bool makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, napi_value jsThis, + void* rvalue, bool propertyAccess, + JSValueRef* result) { + if (env == nullptr || member == nullptr || descriptor == nullptr || + cif == nullptr || cif->returnType == nullptr || result == nullptr) { + return false; + } + + if (makeJSCRawReturnValue(env, cif->returnType->kind, rvalue, result)) { + return true; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + *result = ToJSValue(constructor); + return true; + } + + id classObject = receiverIsClass ? self : (id)object_getClass(self); + napi_value converted = + member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *reinterpret_cast(rvalue); + napi_value converted = member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + *result = converted != nullptr ? ToJSValue(converted) : JSValueMakeNull(env->context); + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeJSCNSStringValue( + env, *reinterpret_cast(rvalue), result); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + napi_value converted = + member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; + } + } + + if (makeJSCBoxedObjectValue(env, obj, result)) { + return true; + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + *result = ToJSValue(fastResult); + return true; + } + + napi_value converted = + cif->returnType->toJS(env, rvalue, member->returnOwned ? kReturnOwned : 0); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; +} + +bool makeJSCCFunctionReturnValue(napi_env env, CFunction* function, Cif* cif, + void* rvalue, JSValueRef* result) { + if (env == nullptr || cif == nullptr || cif->returnType == nullptr || + result == nullptr) { + return false; + } + + if (makeJSCRawReturnValue(env, cif->returnType->kind, rvalue, result)) { + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeJSCNSStringValue( + env, *reinterpret_cast(rvalue), result); + } + if (cif->returnType->kind == mdTypeAnyObject && + makeJSCBoxedObjectValue(env, *reinterpret_cast(rvalue), result)) { + return true; + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + *result = ToJSValue(fastResult); + return true; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + napi_value converted = cif->returnType->toJS(env, rvalue, toJSFlags); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; +} + +bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t argc, + const napi_value* argv, + EngineDirectMemberKind kind, + JSValueRef* result) { + if (env == nullptr || member == nullptr || member->bridgeState == nullptr || + result == nullptr) { + return false; + } + + MethodDescriptor* descriptor = nullptr; + Cif* cif = jscMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { + return false; + } + + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && argc == cif->argc; + ObjCEngineDirectInvoker invoker = canUseGeneratedInvoker + ? ensureJSCObjCEngineDirectInvoker(cif, descriptor, + descriptor->dispatchFlags) + : nullptr; + + if (isJSCNSErrorOutSignature(descriptor, cif) || + isJSCBlockFallbackSelector(descriptor->selector)) { + return false; + } + + id self = resolveJSCSelf(env, jsThis, member); + if (self == nil) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresJSCSuperCall(receiverClass)) { + return false; + } + + JSCFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, argv, rvalue); + } else { + didInvoke = InvokeObjCMemberEngineDirectDynamic( + env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, argc, argv, rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return false; + } + + return didInvoke && + makeJSCObjCReturnValue(env, member, descriptor, cif, self, + receiverIsClass, jsThis, rvalue, + kind != EngineDirectMemberKind::Method, result); +} + +bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, + size_t argc, const napi_value* argv, + JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || + isCompatCFunction(env, reinterpret_cast( + static_cast(offset)))) { + return false; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic || + cif->returnType == nullptr) { + return false; + } + + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && argc == cif->argc; + CFunctionEngineDirectInvoker invoker = canUseGeneratedInvoker + ? ensureJSCCFunctionEngineDirectInvoker(function, cif) + : nullptr; + + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + } else { + didInvoke = InvokeCFunctionEngineDirectDynamic( + env, function, cif, argc, argv, cif->rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return false; + } + + return didInvoke && + makeJSCCFunctionReturnValue(env, function, cif, cif->rvalue, result); +} + +void initializeFastFunction(JSContextRef ctx, JSObjectRef object) { + JSObjectRef global = JSContextGetGlobalObject(ctx); + JSValueRef functionCtorValue = + JSObjectGetProperty(ctx, global, ScopedJSString("Function"), nullptr); + JSObjectRef functionCtor = JSValueToObject(ctx, functionCtorValue, nullptr); + if (functionCtor == nullptr) { + return; + } + + JSValueRef functionPrototype = + JSObjectGetProperty(ctx, functionCtor, ScopedJSString("prototype"), nullptr); + JSObjectRef functionPrototypeObject = + JSValueToObject(ctx, functionPrototype, nullptr); + if (functionPrototypeObject != nullptr) { + JSObjectSetPrototype(ctx, object, functionPrototype); + for (const char* name : {"bind", "call", "apply"}) { + ScopedJSString propertyName(name); + JSValueRef property = + JSObjectGetProperty(ctx, functionPrototypeObject, propertyName, nullptr); + if (property != nullptr && !JSValueIsUndefined(ctx, property)) { + JSObjectSetProperty(ctx, object, propertyName, property, + kJSPropertyAttributeDontEnum, nullptr); + } + } + } +} + +JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, + JSObjectRef thisObject, size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + auto* binding = + static_cast(JSObjectGetPrivate(function)); + if (binding == nullptr || binding->env == nullptr) { + return JSValueMakeUndefined(ctx); + } + + napi_env env = binding->env; + env->last_error.error_code = napi_ok; + env->last_error.engine_error_code = 0; + env->last_error.engine_reserved = nullptr; + + JSValueRef effectiveThis = + thisObject != nullptr ? thisObject : JSContextGetGlobalObject(ctx); + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* argv = stackArgs; + if (argumentCount > 16) { + heapArgs.resize(argumentCount); + argv = heapArgs.data(); + } + for (size_t i = 0; i < argumentCount; i++) { + argv[i] = ToNapi(arguments[i]); + } + napi_value jsThis = ToNapi(effectiveThis); + JSValueRef directResult = nullptr; + bool didUseDirectResult = false; + switch (binding->kind) { + case JSCFastNativeKind::ObjCMethod: + didUseDirectResult = tryCallJSCObjCEngineDirect( + env, static_cast(binding->data), jsThis, + argumentCount, argv, EngineDirectMemberKind::Method, + &directResult); + break; + case JSCFastNativeKind::ObjCGetter: + didUseDirectResult = tryCallJSCObjCEngineDirect( + env, static_cast(binding->data), jsThis, 0, + nullptr, EngineDirectMemberKind::Getter, &directResult); + break; + case JSCFastNativeKind::ObjCSetter: { + JSValueRef undefined = JSValueMakeUndefined(ctx); + napi_value value = + argumentCount > 0 ? ToNapi(arguments[0]) : ToNapi(undefined); + didUseDirectResult = tryCallJSCObjCEngineDirect( + env, static_cast(binding->data), jsThis, 1, + &value, EngineDirectMemberKind::Setter, &directResult); + break; + } + case JSCFastNativeKind::CFunction: + didUseDirectResult = tryCallJSCCFunctionEngineDirect( + env, + static_cast( + reinterpret_cast(binding->data)), + argumentCount, argv, &directResult); + break; + default: + break; + } + + if (didUseDirectResult) { + if (env->last_exception != nullptr) { + if (exception != nullptr) { + *exception = env->last_exception; + } + env->last_exception = nullptr; + return JSValueMakeUndefined(ctx); + } + return directResult != nullptr ? directResult : JSValueMakeUndefined(ctx); + } + + napi_value result = nullptr; + + switch (binding->kind) { + case JSCFastNativeKind::ObjCMethod: + result = ObjCClassMember::jsCallDirect( + env, static_cast(binding->data), jsThis, + argumentCount, argv); + break; + + case JSCFastNativeKind::ObjCGetter: + result = ObjCClassMember::jsGetterDirect( + env, static_cast(binding->data), jsThis); + break; + + case JSCFastNativeKind::ObjCSetter: { + JSValueRef undefined = JSValueMakeUndefined(ctx); + napi_value value = + argumentCount > 0 ? ToNapi(arguments[0]) : ToNapi(undefined); + result = ObjCClassMember::jsSetterDirect( + env, static_cast(binding->data), jsThis, value); + break; + } + + case JSCFastNativeKind::ObjCReadOnlySetter: + result = ObjCClassMember::jsReadOnlySetterDirect(env); + break; + + case JSCFastNativeKind::CFunction: + result = CFunction::jsCallDirect( + env, static_cast( + reinterpret_cast(binding->data)), + argumentCount, argv); + break; + } + + if (env->last_exception != nullptr) { + if (exception != nullptr) { + *exception = env->last_exception; + } + env->last_exception = nullptr; + return JSValueMakeUndefined(ctx); + } + + return result != nullptr ? ToJSValue(result) : JSValueMakeUndefined(ctx); +} + +void finalizeFastFunction(JSObjectRef object) { + delete static_cast(JSObjectGetPrivate(object)); +} + +JSClassRef fastFunctionClass() { + static JSClassRef cls = [] { + JSClassDefinition definition = kJSClassDefinitionEmpty; + definition.className = "NativeScriptFastNativeFunction"; + definition.initialize = initializeFastFunction; + definition.callAsFunction = callFastFunction; + definition.finalize = finalizeFastFunction; + return JSClassCreate(&definition); + }(); + return cls; +} + +JSObjectRef makeFastFunction(napi_env env, JSCFastNativeKind kind, + void* data) { + auto* binding = new JSCFastNativeBinding{env, kind, data}; + JSObjectRef function = JSObjectMake(env->context, fastFunctionClass(), binding); + if (function == nullptr) { + delete binding; + } + return function; +} + +bool setDescriptorValue(JSContextRef ctx, JSObjectRef descriptor, + const char* name, JSValueRef value) { + JSValueRef exception = nullptr; + JSObjectSetProperty(ctx, descriptor, ScopedJSString(name), value, + kJSPropertyAttributeNone, &exception); + return exception == nullptr; +} + +bool defineProperty(napi_env env, napi_value object, + const napi_property_descriptor* descriptor, + JSValueRef propertyName, JSObjectRef value, + JSObjectRef getter, JSObjectRef setter) { + JSContextRef ctx = env->context; + JSValueRef objectValue = ToJSValue(object); + if (!JSValueIsObject(ctx, objectValue)) { + return false; + } + + JSObjectRef jsObject = JSValueToObject(ctx, objectValue, nullptr); + JSObjectRef propertyDescriptor = JSObjectMake(ctx, nullptr, nullptr); + if (propertyDescriptor == nullptr) { + return false; + } + + if (!setDescriptorValue(ctx, propertyDescriptor, "configurable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_configurable) != 0)) || + !setDescriptorValue(ctx, propertyDescriptor, "enumerable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_enumerable) != 0))) { + return false; + } + + if (getter != nullptr || setter != nullptr) { + if (getter != nullptr && + !setDescriptorValue(ctx, propertyDescriptor, "get", getter)) { + return false; + } + if (setter != nullptr && + !setDescriptorValue(ctx, propertyDescriptor, "set", setter)) { + return false; + } + } else if (value != nullptr) { + if (!setDescriptorValue(ctx, propertyDescriptor, "writable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_writable) != 0)) || + !setDescriptorValue(ctx, propertyDescriptor, "value", value)) { + return false; + } + } else { + return false; + } + + JSObjectRef global = JSContextGetGlobalObject(ctx); + JSValueRef objectCtorValue = + JSObjectGetProperty(ctx, global, ScopedJSString("Object"), nullptr); + JSObjectRef objectCtor = JSValueToObject(ctx, objectCtorValue, nullptr); + if (objectCtor == nullptr) { + return false; + } + + JSValueRef definePropertyValue = + JSObjectGetProperty(ctx, objectCtor, ScopedJSString("defineProperty"), + nullptr); + JSObjectRef definePropertyFunction = + JSValueToObject(ctx, definePropertyValue, nullptr); + if (definePropertyFunction == nullptr) { + return false; + } + + JSValueRef args[] = {jsObject, propertyName, propertyDescriptor}; + JSValueRef exception = nullptr; + JSObjectCallAsFunction(ctx, definePropertyFunction, objectCtor, 3, args, + &exception); + return exception == nullptr; +} + +bool makePropertyName(napi_env env, const napi_property_descriptor* descriptor, + JSValueRef* propertyName) { + if (descriptor->utf8name != nullptr) { + *propertyName = + JSValueMakeString(env->context, ScopedJSString(descriptor->utf8name)); + return true; + } + if (descriptor->name != nullptr) { + *propertyName = ToJSValue(descriptor->name); + return true; + } + return false; +} + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +bool readJSCStringUTF8(napi_env env, JSValueRef jsValue, const char** out, + size_t* outLength, char* stackBuffer, + size_t stackCapacity, std::vector* heapBuffer) { + if (env == nullptr || jsValue == nullptr || out == nullptr || + outLength == nullptr || stackBuffer == nullptr || heapBuffer == nullptr) { + return false; + } + + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(env->context, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return false; + } + + const size_t maxLength = JSStringGetMaximumUTF8CStringSize(str); + char* buffer = stackBuffer; + size_t capacity = stackCapacity; + if (maxLength > stackCapacity) { + heapBuffer->assign(maxLength, '\0'); + buffer = heapBuffer->data(); + capacity = heapBuffer->size(); + } + + const size_t copied = JSStringGetUTF8CString(str, buffer, capacity); + JSStringRelease(str); + if (copied == 0) { + return false; + } + + *out = buffer; + *outLength = copied - 1; + return true; +} + +NSString* makeNSStringFromJSCString(napi_env env, JSValueRef jsValue, + bool mutableString) { + if (env == nullptr || jsValue == nullptr) { + return nil; + } + + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(env->context, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return nil; + } + + const size_t length = JSStringGetLength(str); + const JSChar* chars = JSStringGetCharactersPtr(str); + NSString* result = + [[[NSString alloc] initWithCharacters:reinterpret_cast(chars) + length:length] autorelease]; + JSStringRelease(str); + if (result == nil) { + result = @""; + } + if (mutableString) { + return [[[NSMutableString alloc] initWithString:result] autorelease]; + } + return result; +} + +id normalizeWrappedNativeObject(napi_env env, MDTypeKind kind, void* wrapped) { + if (wrapped == nullptr) { + return nil; + } + + auto bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id cachedNative = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (cachedNative != nil) { + return cachedNative; + } + + for (const auto& entry : bridgeState->classes) { + ObjCClass* bridgedClass = entry.second; + if (bridgedClass == wrapped && bridgedClass->nativeClass != nil) { + return (id)bridgedClass->nativeClass; + } + } + + if (kind == mdTypeProtocolObject || kind == mdTypeAnyObject) { + for (const auto& entry : bridgeState->protocols) { + ObjCProtocol* bridgedProtocol = entry.second; + if (bridgedProtocol != wrapped) { + continue; + } + + Protocol* runtimeProtocol = objc_getProtocol(bridgedProtocol->name.c_str()); + if (runtimeProtocol != nil) { + return (id)runtimeProtocol; + } + break; + } + } + } + + return static_cast(wrapped); +} + +bool tryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, + JSValueRef jsValue, void* result) { + if (env == nullptr || jsValue == nullptr || result == nullptr) { + return false; + } + + if (JSValueIsNull(env->context, jsValue) || + JSValueIsUndefined(env->context, jsValue)) { + if (kind == mdTypeClass) { + *reinterpret_cast(result) = Nil; + } else { + *reinterpret_cast(result) = nil; + } + return true; + } + + if (JSValueIsString(env->context, jsValue) && + (kind == mdTypeAnyObject || kind == mdTypeNSStringObject || + kind == mdTypeNSMutableStringObject)) { + *reinterpret_cast(result) = makeNSStringFromJSCString( + env, jsValue, kind == mdTypeNSMutableStringObject); + return true; + } + + if (kind == mdTypeAnyObject && JSValueIsBoolean(env->context, jsValue)) { + *reinterpret_cast(result) = + [NSNumber numberWithBool:JSValueToBoolean(env->context, jsValue)]; + return true; + } + + if (kind == mdTypeAnyObject && JSValueIsNumber(env->context, jsValue)) { + JSValueRef exception = nullptr; + double converted = JSValueToNumber(env->context, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + *reinterpret_cast(result) = [NSNumber numberWithDouble:converted]; + return true; + } + + if (!JSValueIsObject(env->context, jsValue)) { + return false; + } + + void* wrapped = nullptr; + if (!nativescript_jsc_try_unwrap_native(env, ToNapi(jsValue), &wrapped) || + wrapped == nullptr) { + return false; + } + + if (kind == mdTypeClass) { + id nativeObject = static_cast(wrapped); + if (!object_isClass(nativeObject)) { + nativeObject = normalizeWrappedNativeObject(env, kind, wrapped); + } + if (!object_isClass(nativeObject)) { + return false; + } + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + + *reinterpret_cast(result) = + normalizeWrappedNativeObject(env, kind, wrapped); + return true; +} + +} // namespace + +bool TryFastConvertJSCBoolArgument(napi_env env, napi_value value, + uint8_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (!JSValueIsBoolean(env->context, jsValue)) { + return false; + } + *result = JSValueToBoolean(env->context, jsValue) ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertJSCDoubleArgument(napi_env env, napi_value value, + double* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (!JSValueIsNumber(env->context, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + double converted = JSValueToNumber(env->context, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *result = converted; + return true; +} + +bool TryFastConvertJSCFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsString(ctx, jsValue)) { + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(ctx, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return false; + } + const size_t length = JSStringGetLength(str); + if (length != 1) { + JSStringRelease(str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *result = static_cast(JSStringGetCharactersPtr(str)[0]); + JSStringRelease(str); + return true; + } + + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNumber(ctx, jsValue)) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; + } + + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + *result = JSValueToInt64(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + + bool lossless = false; + return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertJSCUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNumber(ctx, jsValue)) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; + } + + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + *result = JSValueToUInt64(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertJSCSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNull(env->context, jsValue) || + JSValueIsUndefined(env->context, jsValue)) { + *result = nullptr; + return true; + } + if (!JSValueIsString(env->context, jsValue)) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + std::vector heapBuffer; + const char* selectorName = nullptr; + size_t selectorLength = 0; + if (!readJSCStringUTF8(env, jsValue, &selectorName, &selectorLength, + stackBuffer, kStackCapacity, &heapBuffer)) { + return false; + } + *result = cachedSelectorForName(selectorName, selectorLength); + return true; +} + +bool TryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return tryFastConvertJSCObjectArgument(env, kind, ToJSValue(value), result); +} + +bool TryFastConvertJSCArgument(napi_env env, MDTypeKind kind, napi_value value, + void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + switch (kind) { + case mdTypeBool: + if (!JSValueIsBoolean(ctx, jsValue)) { + return false; + } + *reinterpret_cast(result) = + JSValueToBoolean(ctx, jsValue) ? static_cast(1) : static_cast(0); + return true; + + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeFloat: + case mdTypeDouble: { + if (!JSValueIsNumber(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + switch (kind) { + case mdTypeChar: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeUChar: + case mdTypeUInt8: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeSShort: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeSInt: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeUInt: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeFloat: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeDouble: + *reinterpret_cast(result) = converted; + break; + default: + break; + } + return true; + } + + case mdTypeUShort: + if (JSValueIsString(ctx, jsValue)) { + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(ctx, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return false; + } + const size_t length = JSStringGetLength(str); + if (length != 1) { + JSStringRelease(str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *reinterpret_cast(result) = + static_cast(JSStringGetCharactersPtr(str)[0]); + JSStringRelease(str); + return true; + } + if (JSValueIsNumber(ctx, jsValue)) { + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + return false; + + case mdTypeSLong: + case mdTypeSInt64: + case mdTypeULong: + case mdTypeUInt64: + if (JSValueIsNumber(ctx, jsValue)) { + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + *reinterpret_cast(result) = static_cast(converted); + } else { + *reinterpret_cast(result) = static_cast(converted); + } + return true; + } + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + *reinterpret_cast(result) = + JSValueToInt64(ctx, jsValue, &exception); + } else { + *reinterpret_cast(result) = + JSValueToUInt64(ctx, jsValue, &exception); + } + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + bool lossless = false; + return napi_get_value_bigint_int64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + { + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + + case mdTypeSelector: { + SEL* selector = reinterpret_cast(result); + if (JSValueIsNull(ctx, jsValue) || JSValueIsUndefined(ctx, jsValue)) { + *selector = nullptr; + return true; + } + if (!JSValueIsString(ctx, jsValue)) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + std::vector heapBuffer; + const char* selectorName = nullptr; + size_t selectorLength = 0; + if (!readJSCStringUTF8(env, jsValue, &selectorName, &selectorLength, + stackBuffer, kStackCapacity, &heapBuffer)) { + return false; + } + *selector = cachedSelectorForName(selectorName, selectorLength); + return true; + } + + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (tryFastConvertJSCObjectArgument(env, kind, jsValue, result)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + default: + return false; + } +} + +bool TryFastConvertJSCReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = nullptr; + switch (kind) { + case mdTypeVoid: + jsValue = JSValueMakeNull(ctx); + break; + + case mdTypeBool: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeBoolean( + ctx, *reinterpret_cast(value) != 0); + break; + + case mdTypeChar: { + if (value == nullptr) { + return false; + } + const int8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 + ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + break; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) { + return false; + } + const uint8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 + ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + break; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeUShort: { + if (value == nullptr) { + return false; + } + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + jsValue = JSValueMakeString(ctx, ScopedJSString(buffer)); + } else { + jsValue = JSValueMakeNumber(ctx, raw); + } + break; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) { + return false; + } + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_int64(env, raw, &bigint) == napi_ok) { + *result = bigint; + return true; + } + } + jsValue = JSValueMakeNumber(ctx, static_cast(raw)); + break; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) { + return false; + } + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (raw > kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_uint64(env, raw, &bigint) == napi_ok) { + *result = bigint; + return true; + } + } + jsValue = JSValueMakeNumber(ctx, static_cast(raw)); + break; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + default: + return false; + } + + *result = ToNapi(jsValue); + return true; +} + +bool JSCTryDefineFastNativeProperty( + napi_env env, napi_value object, + const napi_property_descriptor* descriptor) { + if (env == nullptr || object == nullptr || descriptor == nullptr) { + return false; + } + + JSValueRef propertyName = nullptr; + if (!makePropertyName(env, descriptor, &propertyName)) { + return false; + } + + if (descriptor->method == ObjCClassMember::jsCall && + descriptor->data != nullptr) { + JSObjectRef function = makeFastFunction( + env, JSCFastNativeKind::ObjCMethod, descriptor->data); + return function != nullptr && + defineProperty(env, object, descriptor, propertyName, function, + nullptr, nullptr); + } + + if (descriptor->method == CFunction::jsCall && descriptor->data != nullptr && + !isCompatCFunction(env, descriptor->data)) { + JSObjectRef function = makeFastFunction( + env, JSCFastNativeKind::CFunction, descriptor->data); + return function != nullptr && + defineProperty(env, object, descriptor, propertyName, function, + nullptr, nullptr); + } + + if (descriptor->getter == ObjCClassMember::jsGetter && + descriptor->data != nullptr) { + JSObjectRef getter = makeFastFunction( + env, JSCFastNativeKind::ObjCGetter, descriptor->data); + JSObjectRef setter = nullptr; + if (descriptor->setter == ObjCClassMember::jsSetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCSetter, + descriptor->data); + } else if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCReadOnlySetter, + descriptor->data); + } else if (descriptor->setter != nullptr) { + return false; + } + + return getter != nullptr && + (descriptor->setter == nullptr || setter != nullptr) && + defineProperty(env, object, descriptor, propertyName, nullptr, + getter, setter); + } + + return false; +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index d2e81b2a..b2be9cef 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -97,6 +97,48 @@ class ObjCBridgeState { #endif } + inline void registerRuntimeClass(ObjCClass* bridgedClass, + Class runtimeClass) { + if (bridgedClass == nullptr || runtimeClass == nil) { + return; + } + + bridgedClass->nativeClass = runtimeClass; + classesByPointer[runtimeClass] = bridgedClass; + nativeObjectsByBridgeWrapper[bridgedClass] = (id)runtimeClass; + if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { + mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; + } + } + + inline void registerProtocolMetadata(Protocol* runtimeProtocol, + MDSectionOffset metadataOffset) { + if (runtimeProtocol == nil || metadataOffset == MD_SECTION_OFFSET_NULL) { + return; + } + + mdProtocolsByPointer[runtimeProtocol] = metadataOffset; + } + + inline void registerRuntimeProtocol(ObjCProtocol* bridgedProtocol, + Protocol* runtimeProtocol) { + if (bridgedProtocol == nullptr || runtimeProtocol == nil) { + return; + } + + nativeObjectsByBridgeWrapper[bridgedProtocol] = (id)runtimeProtocol; + registerProtocolMetadata(runtimeProtocol, bridgedProtocol->metadataOffset); + } + + inline id nativeObjectForBridgeWrapper(void* wrapped) const { + if (wrapped == nullptr) { + return nil; + } + + auto cached = nativeObjectsByBridgeWrapper.find(wrapped); + return cached != nativeObjectsByBridgeWrapper.end() ? cached->second : nil; + } + void registerVarGlobals(napi_env env, napi_value global); void registerEnumGlobals(napi_env env, napi_value global); void registerStructGlobals(napi_env env, napi_value global); @@ -182,10 +224,10 @@ class ObjCBridgeState { } uintptr_t objectKey = NormalizeHandleKey((void*)object); - Class objectClass = object_getClass(object); + uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (const auto& entry : objectRefs) { if (NormalizeHandleKey((void*)entry.first) == objectKey && - object_getClass(entry.first) == objectClass) { + NormalizeHandleKey((void*)object_getClass(entry.first)) == objectClassKey) { return true; } } @@ -412,19 +454,6 @@ class ObjCBridgeState { return name; }; - auto registerResolvedRuntimeClass = [&](ObjCClass* bridgedClass, - Class runtimeClass) { - if (bridgedClass == nullptr || runtimeClass == nil) { - return; - } - - bridgedClass->nativeClass = runtimeClass; - classesByPointer[runtimeClass] = bridgedClass; - if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { - mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; - } - }; - auto matchesConstructor = [&](ObjCClass* bridgedClass, ObjCClass** unresolvedMatch) -> bool { if (bridgedClass == nullptr || bridgedClass->constructor == nullptr) { @@ -485,7 +514,7 @@ class ObjCBridgeState { Class runtimeClass = objc_lookUpClass(candidateName.c_str()); if (runtimeClass != nil) { if (unresolvedConstructorMatch != nullptr) { - registerResolvedRuntimeClass(unresolvedConstructorMatch, runtimeClass); + registerRuntimeClass(unresolvedConstructorMatch, runtimeClass); } *out = runtimeClass; return true; @@ -703,6 +732,7 @@ class ObjCBridgeState { std::unordered_map classesByPointer; std::unordered_map mdClassesByPointer; std::unordered_map mdProtocolsByPointer; + std::unordered_map nativeObjectsByBridgeWrapper; std::unordered_map constructorsByPointer; std::unordered_map cifs; @@ -734,10 +764,10 @@ class ObjCBridgeState { } uintptr_t objectKey = NormalizeHandleKey((void*)object); - Class objectClass = object_getClass(object); + uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (const auto& entry : objectRefs) { if (NormalizeHandleKey((void*)entry.first) != objectKey || - object_getClass(entry.first) != objectClass) { + NormalizeHandleKey((void*)object_getClass(entry.first)) != objectClassKey) { continue; } diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index bb304ea7..6fa741c3 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -949,13 +949,21 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_value ObjCBridgeState::proxyNativeObject(napi_env env, napi_value object, id nativeObject) { NAPI_PREAMBLE - napi_value factory = get_ref_value(env, createNativeProxy); - napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); - napi_value result, global; - napi_value args[3] = {object, nullptr, transferOwnershipFunc}; - napi_get_boolean(env, [nativeObject isKindOfClass:NSArray.class], &args[1]); - napi_get_global(env, &global); - napi_call_function(env, global, factory, 3, args, &result); +#ifdef TARGET_ENGINE_V8 + napi_value result = object; +#else + napi_value result = object; + const bool nativeIsArray = [nativeObject isKindOfClass:NSArray.class]; + if (nativeIsArray) { + napi_value factory = get_ref_value(env, createNativeProxy); + napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); + napi_value global; + napi_value args[3] = {object, nullptr, transferOwnershipFunc}; + napi_get_boolean(env, true, &args[1]); + napi_get_global(env, &global); + napi_call_function(env, global, factory, 3, args, &result); + } +#endif napi_value nativePointer = Pointer::create(env, nativeObject); if (nativePointer != nullptr) { napi_set_named_property(env, result, kNativePointerProperty, nativePointer); @@ -978,6 +986,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); + cacheHandleObject(env, nativeObject, result); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/Object.h b/NativeScript/ffi/Object.h index 2290468f..3174bb91 100644 --- a/NativeScript/ffi/Object.h +++ b/NativeScript/ffi/Object.h @@ -7,6 +7,7 @@ namespace nativescript { void initProxyFactory(napi_env env, ObjCBridgeState* bridgeState); void attachObjectLifecycleAssociation(napi_env env, id object); +void transferOwnershipToNative(napi_env env, napi_value value, id object); } // namespace nativescript diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/Object.mm index 10f043c9..79cbc585 100644 --- a/NativeScript/ffi/Object.mm +++ b/NativeScript/ffi/Object.mm @@ -3,6 +3,9 @@ #include "Interop.h" #include "JSObject.h" #include "ObjCBridge.h" +#ifdef TARGET_ENGINE_V8 +#include "V8FastNativeApi.h" +#endif #include "js_native_api.h" #include "node_api_util.h" @@ -128,6 +131,14 @@ napi_value JS_transferOwnershipToNative(napi_env env, napi_callback_info cbinfo) napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, id object, Class cls = nil); +void transferOwnershipToNative(napi_env env, napi_value value, id object) { + if (env == nullptr || value == nullptr || object == nil) { + return; + } + + [JSWrapperObjectAssociation transferOwnership:env of:value toNative:object]; +} + namespace { constexpr const char* kNativePointerProperty = "__ns_native_ptr"; @@ -186,14 +197,18 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return target.class().superclass(); } - if (name in target) { - const value = target[name]; + const value = target[name]; + if (value !== undefined || name in target) { if (typeof value === "function" && name !== "constructor") { + if (value.__ns_proxy_bound === true) { + return value; + } + + let wrapper; if ((name === "isKindOfClass" || name === "isMemberOfClass")) { - return function (cls, ...args) { + wrapper = function (cls, a1, a2, a3) { let resolvedClass = cls; - if (resolvedClass != null && - (typeof resolvedClass === "object" || typeof resolvedClass === "function")) { + if (resolvedClass != null && typeof resolvedClass === "object") { try { const runtimeName = typeof NSStringFromClass === "function" ? NSStringFromClass(resolvedClass) @@ -210,23 +225,37 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt } } - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, [resolvedClass, ...args]); - } finally { - value.__ns_bound_receiver = undefined; + switch (arguments.length) { + case 0: + case 1: + return value.call(target, resolvedClass); + case 2: + return value.call(target, resolvedClass, a1); + case 3: + return value.call(target, resolvedClass, a1, a2); + case 4: + return value.call(target, resolvedClass, a1, a2, a3); + default: { + const args = Array.prototype.slice.call(arguments); + args[0] = resolvedClass; + return Reflect.apply(value, target, args); + } } }; + } else { + wrapper = value.bind(target); } - return function (...args) { - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, args); - } finally { - value.__ns_bound_receiver = undefined; - } - }; + Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; } return value; } @@ -371,10 +400,18 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { return nullptr; } +#ifdef TARGET_ENGINE_V8 + result = CreateV8NativeWrapperObject(env); + if (result == nullptr) { + napi_throw_error(env, "NativeScriptException", "Unable to create V8 native wrapper object."); + return nullptr; + } +#else NAPI_GUARD(napi_create_object(env, &result)) { NAPI_THROW_LAST_ERROR return nullptr; } +#endif napi_value global; napi_value objectCtor; @@ -443,6 +480,24 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { NormalizeHandleKey(wrapped) == NormalizeHandleKey((void*)obj)) { return handleCached; } + + bool hasNativePointer = false; + if (napi_has_named_property(env, handleCached, kNativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, handleCached, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && + NormalizeHandleKey(pointer->data) == NormalizeHandleKey((void*)obj)) { + return handleCached; + } + } + } + + return handleCached; } if (napi_value existing = getNormalizedObjectRef(env, obj); existing != nullptr) { diff --git a/NativeScript/ffi/Protocol.mm b/NativeScript/ffi/Protocol.mm index 1bbe911d..7cb65571 100644 --- a/NativeScript/ffi/Protocol.mm +++ b/NativeScript/ffi/Protocol.mm @@ -64,7 +64,7 @@ // protocolOffsets[name] = originalOffset; auto objcProtocol = resolveRuntimeProtocol(name); if (objcProtocol != nil) { - mdProtocolsByPointer[objcProtocol] = originalOffset; + registerProtocolMetadata(objcProtocol, originalOffset); } while (next) { @@ -163,6 +163,7 @@ nameOffset &= ~mdSectionOffsetNext; name = bridgeState->metadata->resolveString(nameOffset); + bridgeState->registerRuntimeProtocol(this, resolveRuntimeProtocol(name.c_str())); napi_value constructor; napi_define_class(env, name.c_str(), NAPI_AUTO_LENGTH, ObjCProtocol::jsConstructor, nullptr, 0, diff --git a/NativeScript/ffi/QuickJSFastNativeApi.h b/NativeScript/ffi/QuickJSFastNativeApi.h new file mode 100644 index 00000000..042a8761 --- /dev/null +++ b/NativeScript/ffi/QuickJSFastNativeApi.h @@ -0,0 +1,20 @@ +#ifndef NS_QUICKJS_FAST_NATIVE_API_H +#define NS_QUICKJS_FAST_NATIVE_API_H + +#include + +#include "js_native_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +bool nativescript_quickjs_try_define_fast_native_property( + napi_env env, napi_value object, + const napi_property_descriptor* descriptor); + +#ifdef __cplusplus +} +#endif + +#endif // NS_QUICKJS_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm new file mode 100644 index 00000000..5c24af8c --- /dev/null +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -0,0 +1,1822 @@ +#include "QuickJSFastNativeApi.h" + +#ifdef TARGET_ENGINE_QUICKJS + +#import + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "EngineDirectCall.h" +#include "MetadataReader.h" +#include "NativeScriptException.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" +#include "mimalloc.h" +#include "quicks-runtime.h" + +#ifndef SLIST_FOREACH_SAFE +#define SLIST_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = SLIST_FIRST((head)); \ + (var) && ((tvar) = SLIST_NEXT((var), field), 1); (var) = (tvar)) +#endif + +enum QuickJSFastHandleType { + kQuickJSFastHandleStackAllocated, + kQuickJSFastHandleHeapAllocated, +}; + +struct QuickJSFastHandle { + JSValue value; + SLIST_ENTRY(QuickJSFastHandle) node; + QuickJSFastHandleType type; +}; + +struct napi_handle_scope__ { + LIST_ENTRY(napi_handle_scope__) node; + SLIST_HEAD(, QuickJSFastHandle) handleList; + bool escapeCalled; + QuickJSFastHandle stackHandles[8]; + int handleCount; + QuickJSFastHandleType type; +}; + +struct napi_ref__ { + JSValue value; + LIST_ENTRY(napi_ref__) node; + uint8_t referenceCount; +}; + +struct QuickJSFastExternalInfo { + void* data; + void* finalizeHint; + napi_finalize finalizeCallback; +}; + +struct QuickJSFastAtoms { + JSAtom napi_external; + JSAtom registerFinalizer; + JSAtom constructor; + JSAtom prototype; + JSAtom napi_buffer; + JSAtom NAPISymbolFor; + JSAtom object; + JSAtom freeze; + JSAtom seal; + JSAtom Symbol; + JSAtom length; + JSAtom is; + JSAtom byteLength; + JSAtom buffer; + JSAtom byteOffset; + JSAtom name; + JSAtom napi_typetag; + JSAtom weakref; +}; + +struct napi_runtime__ { + JSRuntime* runtime; + JSClassID constructorClassId; + JSClassID functionClassId; + JSClassID externalClassId; + JSClassID napiHostObjectClassId; + JSClassID napiObjectClassId; +}; + +struct napi_env__ { + JSValue referenceSymbolValue; + napi_runtime runtime; + JSContext* context; + LIST_HEAD(, napi_handle_scope__) handleScopeList; + LIST_HEAD(, napi_ref__) referencesList; + bool isThrowNull; + QuickJSFastExternalInfo* instanceData; + JSValue finalizationRegistry; + napi_extended_error_info last_error; + QuickJSFastAtoms atoms; + QuickJSFastExternalInfo* gcBefore; + QuickJSFastExternalInfo* gcAfter; + int js_enter_state; + int64_t usedMemory; +}; + +namespace { + +enum QuickJSFastNativeKind : int { + kQuickJSFastObjCMethod = 1, + kQuickJSFastObjCGetter = 2, + kQuickJSFastObjCSetter = 3, + kQuickJSFastObjCReadOnlySetter = 4, + kQuickJSFastCFunction = 5, +}; + +inline JSValue ToJSValue(napi_value value) { + return value != nullptr ? *reinterpret_cast(value) : JS_UNDEFINED; +} + +bool tryFastUnwrapQuickJSNativeObject(napi_env env, JSValue jsValue, + void** result) { + if (env == nullptr || result == nullptr || !JS_IsObject(jsValue)) { + return false; + } + + *result = nullptr; + auto* directInfo = static_cast( + JS_GetOpaque(jsValue, env->runtime->napiObjectClassId)); + if (directInfo != nullptr && directInfo->data != nullptr) { + *result = directInfo->data; + return true; + } + + JSPropertyDescriptor descriptor{}; + int wrapped = JS_GetOwnProperty(env->context, &descriptor, jsValue, + env->atoms.napi_external); + if (wrapped <= 0) { + return false; + } + + auto* externalInfo = static_cast( + JS_GetOpaque(descriptor.value, env->runtime->externalClassId)); + if (externalInfo != nullptr && externalInfo->data != nullptr) { + *result = externalInfo->data; + } + + JS_FreeValue(env->context, descriptor.value); + return *result != nullptr; +} + +class QuickJSFastReturnStorage { + public: + explicit QuickJSFastReturnStorage(nativescript::Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + memset(data_, 0, size); + return; + } + + data_ = malloc(size); + if (data_ != nullptr) { + memset(data_, 0, size); + } + } + + ~QuickJSFastReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +class QuickJSFastStackHandleScope { + public: + explicit QuickJSFastStackHandleScope(napi_env env) : env_(env) { + scope_.type = kQuickJSFastHandleStackAllocated; + scope_.handleCount = 0; + scope_.escapeCalled = false; + SLIST_INIT(&scope_.handleList); + LIST_INSERT_HEAD(&env_->handleScopeList, &scope_, node); + } + + ~QuickJSFastStackHandleScope() { close(); } + + void close() { + if (closed_) { + return; + } + + assert(LIST_FIRST(&env_->handleScopeList) == &scope_ && + "QuickJS fast native handle scope should follow FILO rule."); + QuickJSFastHandle *handle, *tempHandle; + SLIST_FOREACH_SAFE(handle, &scope_.handleList, node, tempHandle) { + JS_FreeValue(env_->context, handle->value); + handle->value = JS_UNDEFINED; + SLIST_REMOVE(&scope_.handleList, handle, QuickJSFastHandle, node); + if (handle->type == kQuickJSFastHandleHeapAllocated) { + mi_free(handle); + } + } + LIST_REMOVE(&scope_, node); + closed_ = true; + } + + private: + napi_env env_ = nullptr; + napi_handle_scope__ scope_{}; + bool closed_ = false; +}; + +bool makeQuickJSRawReturnValue(JSContext* context, MDTypeKind kind, + const void* value, JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeVoid: + *result = JS_NULL; + return true; + + case mdTypeBool: + if (value == nullptr) return false; + *result = JS_NewBool(context, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: { + if (value == nullptr) return false; + const int8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewInt32(context, raw); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) return false; + const uint8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewUint32(context, raw); + return true; + } + + case mdTypeSShort: + if (value == nullptr) return false; + *result = JS_NewInt32(context, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: { + if (value == nullptr) return false; + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[1] = {static_cast(raw)}; + *result = JS_NewStringLen(context, buffer, 1); + } else { + *result = JS_NewUint32(context, raw); + } + return !JS_IsException(*result); + } + + case mdTypeSInt: + if (value == nullptr) return false; + *result = JS_NewInt32(context, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + if (value == nullptr) return false; + *result = JS_NewUint32(context, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) return false; + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + *result = raw > kMaxSafeInteger || raw < -kMaxSafeInteger + ? JS_NewBigInt64(context, raw) + : JS_NewInt64(context, raw); + return !JS_IsException(*result); + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) return false; + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + *result = raw > kMaxSafeInteger + ? JS_NewBigUint64(context, raw) + : JS_NewInt64(context, static_cast(raw)); + return !JS_IsException(*result); + } + + case mdTypeFloat: + if (value == nullptr) return false; + *result = JS_NewFloat64(context, *reinterpret_cast(value)); + return !JS_IsException(*result); + + case mdTypeDouble: + if (value == nullptr) return false; + *result = JS_NewFloat64(context, *reinterpret_cast(value)); + return !JS_IsException(*result); + + default: + return false; + } +} + +bool makeQuickJSNSStringValue(JSContext* context, NSString* string, + JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + if (string == nil) { + *result = JS_NULL; + return true; + } + + NSUInteger length = [string length]; + std::vector chars(length > 0 ? length : 1); + if (length > 0) { + [string getCharacters:reinterpret_cast(chars.data()) + range:NSMakeRange(0, length)]; + } + + *result = JS_NewString16(context, + reinterpret_cast(chars.data()), + static_cast(length)); + return !JS_IsException(*result); +} + +bool makeQuickJSBoxedObjectValue(JSContext* context, id obj, JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + if (obj == nil || obj == [NSNull null]) { + *result = JS_NULL; + return true; + } + + if ([obj isKindOfClass:[NSString class]]) { + return makeQuickJSNSStringValue(context, (NSString*)obj, result); + } + + if ([obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)obj) == CFBooleanGetTypeID()) { + *result = JS_NewBool(context, [obj boolValue]); + } else { + *result = JS_NewFloat64(context, [obj doubleValue]); + } + return !JS_IsException(*result); + } + + return false; +} + +bool duplicateQuickJSNapiResult(JSContext* context, napi_value value, + JSValue* result) { + if (context == nullptr || value == nullptr || result == nullptr) { + return false; + } + + *result = JS_DupValue(context, ToJSValue(value)); + return true; +} + +bool canMakeQuickJSRawReturnValue(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool isCompatCFunction(napi_env env, void* data) { + auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || data == nullptr) { + return true; + } + + auto offset = static_cast(reinterpret_cast(data)); + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || + strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || + strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +id resolveQuickJSSelf(napi_env env, napi_value jsThis, + nativescript::ObjCClassMember* member) { + id self = nil; + auto* state = nativescript::ObjCBridgeState::InstanceData(env); + + if (jsThis != nullptr) { + void* wrapped = nullptr; + if (tryFastUnwrapQuickJSNativeObject(env, ToJSValue(jsThis), &wrapped) && + wrapped != nullptr) { + return static_cast(wrapped); + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + if (self == nil && jsThis != nullptr) { + napi_unwrap(env, jsThis, reinterpret_cast(&self)); + } + + if (self != nil) { + return self; + } + + if (member != nullptr && member->cls != nullptr && + member->cls->nativeClass != nil) { + if (member->classMethod) { + return static_cast(member->cls->nativeClass); + } + + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + return static_cast(member->cls->nativeClass); + } + } + + return nil; +} + +nativescript::Cif* quickJSMemberCif( + napi_env env, nativescript::ObjCClassMember* member, + nativescript::EngineDirectMemberKind kind, + nativescript::MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case nativescript::EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case nativescript::EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case nativescript::EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresQuickJSSuperCall(Class receiverClass) { + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + receiverClass != nil && + class_conformsToProtocol(receiverClass, + @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeQuickJSNSErrorOutSignature(SEL selector, + nativescript::Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isQuickJSNSErrorOutSignature( + nativescript::MethodDescriptor* descriptor, nativescript::Cif* cif) { + if (descriptor == nullptr) { + return computeQuickJSNSErrorOutSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeQuickJSNSErrorOutSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline bool isQuickJSBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +nativescript::ObjCEngineDirectInvoker ensureQuickJSObjCEngineDirectInvoker( + nativescript::Cif* cif, nativescript::MethodDescriptor* descriptor, + uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = nativescript::composeSignatureDispatchId( + cif->signatureHash, nativescript::SignatureCallKind::ObjCMethod, + dispatchFlags); + descriptor->preparedInvoker = reinterpret_cast( + nativescript::lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = reinterpret_cast( + nativescript::lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = reinterpret_cast( + nativescript::lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +nativescript::CFunctionEngineDirectInvoker +ensureQuickJSCFunctionEngineDirectInvoker(nativescript::CFunction* function, + nativescript::Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = nativescript::composeSignatureDispatchId( + cif->signatureHash, nativescript::SignatureCallKind::CFunction, + function->dispatchFlags); + function->preparedInvoker = reinterpret_cast( + nativescript::lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = reinterpret_cast( + nativescript::lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + nativescript::lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +bool makeQuickJSObjCReturnValue( + JSContext* context, napi_env env, nativescript::ObjCClassMember* member, + nativescript::MethodDescriptor* descriptor, nativescript::Cif* cif, + id self, bool receiverIsClass, napi_value jsThis, void* rvalue, + bool propertyAccess, JSValue* result) { + if (context == nullptr || env == nullptr || member == nullptr || + descriptor == nullptr || cif == nullptr || cif->returnType == nullptr || + result == nullptr) { + return false; + } + + if (makeQuickJSRawReturnValue(context, cif->returnType->kind, rvalue, + result)) { + return true; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && strcmp(selectorName, "class") == 0) { + QuickJSFastStackHandleScope scope(env); + napi_value converted = nullptr; + if (!propertyAccess && !receiverIsClass) { + converted = jsThis; + napi_get_named_property(env, jsThis, "constructor", &converted); + } else { + id classObject = receiverIsClass ? self : (id)object_getClass(self); + converted = + member->bridgeState->getObject(env, classObject, + nativescript::kUnownedObject, 0, nullptr); + } + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + QuickJSFastStackHandleScope scope(env); + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *reinterpret_cast(rvalue); + napi_value converted = member->bridgeState->getObject( + env, obj, constructor, + member->returnOwned ? nativescript::kOwnedObject + : nativescript::kUnownedObject); + bool ok = false; + if (converted != nullptr) { + ok = duplicateQuickJSNapiResult(context, converted, result); + } else { + *result = JS_NULL; + ok = true; + } + scope.close(); + return ok; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeQuickJSNSStringValue( + context, *reinterpret_cast(rvalue), result); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (strcmp(selectorName, "string") == 0 || + strcmp(selectorName, "stringWithString:") == 0 || + strcmp(selectorName, "stringWithCapacity:") == 0)) { + QuickJSFastStackHandleScope scope(env); + napi_value converted = + member->bridgeState->getObject(env, obj, jsThis, + nativescript::kUnownedObject); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; + } + } + + if (makeQuickJSBoxedObjectValue(context, obj, result)) { + return true; + } + } + + QuickJSFastStackHandleScope scope(env); + napi_value fastResult = nullptr; + if (nativescript::TryFastConvertEngineReturnValue( + env, cif->returnType->kind, rvalue, &fastResult)) { + bool ok = duplicateQuickJSNapiResult(context, fastResult, result); + scope.close(); + return ok; + } + + napi_value converted = cif->returnType->toJS( + env, rvalue, member->returnOwned ? nativescript::kReturnOwned : 0); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; +} + +bool makeQuickJSCFunctionReturnValue(JSContext* context, napi_env env, + nativescript::CFunction* function, + nativescript::Cif* cif, void* rvalue, + JSValue* result) { + if (context == nullptr || env == nullptr || cif == nullptr || + cif->returnType == nullptr || result == nullptr) { + return false; + } + + if (makeQuickJSRawReturnValue(context, cif->returnType->kind, rvalue, + result)) { + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeQuickJSNSStringValue( + context, *reinterpret_cast(rvalue), result); + } + if (cif->returnType->kind == mdTypeAnyObject && + makeQuickJSBoxedObjectValue(context, *reinterpret_cast(rvalue), + result)) { + return true; + } + + QuickJSFastStackHandleScope scope(env); + napi_value fastResult = nullptr; + if (nativescript::TryFastConvertEngineReturnValue( + env, cif->returnType->kind, rvalue, &fastResult)) { + bool ok = duplicateQuickJSNapiResult(context, fastResult, result); + scope.close(); + return ok; + } + + uint32_t toJSFlags = nativescript::kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= nativescript::kReturnOwned; + } + napi_value converted = cif->returnType->toJS(env, rvalue, toJSFlags); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; +} + +bool tryCallQuickJSObjCEngineDirect( + JSContext* context, napi_env env, nativescript::ObjCClassMember* member, + napi_value jsThis, int argc, const napi_value* argv, + nativescript::EngineDirectMemberKind kind, JSValue* result) { + if (context == nullptr || env == nullptr || member == nullptr || + member->bridgeState == nullptr || argc < 0 || result == nullptr) { + return false; + } + + nativescript::MethodDescriptor* descriptor = nullptr; + nativescript::Cif* cif = quickJSMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { + return false; + } + + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && static_cast(argc) == cif->argc; + auto invoker = canUseGeneratedInvoker + ? ensureQuickJSObjCEngineDirectInvoker( + cif, descriptor, descriptor->dispatchFlags) + : nullptr; + + if (isQuickJSNSErrorOutSignature(descriptor, cif) || + isQuickJSBlockFallbackSelector(descriptor->selector)) { + return false; + } + + id self = resolveQuickJSSelf(env, jsThis, member); + if (self == nil) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresQuickJSSuperCall(receiverClass)) { + return false; + } + + QuickJSFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, argv, rvalue); + } else { + didInvoke = nativescript::InvokeObjCMemberEngineDirectDynamic( + env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, static_cast(argc), argv, rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return false; + } + + return didInvoke && + makeQuickJSObjCReturnValue( + context, env, member, descriptor, cif, self, receiverIsClass, + jsThis, rvalue, kind != nativescript::EngineDirectMemberKind::Method, + result); +} + +bool tryCallQuickJSCFunctionEngineDirect(JSContext* context, napi_env env, + MDSectionOffset offset, int argc, + const napi_value* argv, + JSValue* result) { + if (context == nullptr || env == nullptr || argc < 0 || result == nullptr) { + return false; + } + + auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || isCompatCFunction(env, reinterpret_cast( + static_cast(offset)))) { + return false; + } + + auto* function = bridgeState->getCFunction(env, offset); + auto* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic || + cif->returnType == nullptr) { + return false; + } + + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && static_cast(argc) == cif->argc; + auto invoker = canUseGeneratedInvoker + ? ensureQuickJSCFunctionEngineDirectInvoker(function, cif) + : nullptr; + + bool didInvoke = false; + @try { + if (invoker != nullptr) { + didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + } else { + didInvoke = nativescript::InvokeCFunctionEngineDirectDynamic( + env, function, cif, static_cast(argc), argv, cif->rvalue); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return false; + } + + return didInvoke && + makeQuickJSCFunctionReturnValue(context, env, function, cif, + cif->rvalue, result); +} + +JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, + JSValueConst* argv, int magic, JSValue* funcData) { + napi_env env = static_cast(JS_GetContextOpaque(context)); + if (env == nullptr) { + return JS_UNDEFINED; + } + + auto* externalInfo = static_cast( + JS_GetOpaque(funcData[0], env->runtime->externalClassId)); + void* data = externalInfo != nullptr ? externalInfo->data : nullptr; + if (data == nullptr) { + return JS_UNDEFINED; + } + + bool useGlobalValue = false; + JSValue effectiveThis = thisValue; + if (JS_IsUndefined(effectiveThis)) { + useGlobalValue = true; + effectiveThis = JS_GetGlobalObject(context); + } + + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* napiArgs = stackArgs; + if (argc > 16) { + heapArgs.resize(static_cast(argc)); + napiArgs = heapArgs.data(); + } + for (int i = 0; i < argc; i++) { + napiArgs[i] = reinterpret_cast(&argv[i]); + } + + napi_value jsThis = reinterpret_cast(&effectiveThis); + JSValue directReturn = JS_UNDEFINED; + bool didUseDirectReturn = false; + switch (magic) { + case kQuickJSFastObjCMethod: + didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + context, env, static_cast(data), + jsThis, argc, napiArgs, + nativescript::EngineDirectMemberKind::Method, &directReturn); + break; + case kQuickJSFastObjCGetter: + didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + context, env, static_cast(data), + jsThis, 0, nullptr, + nativescript::EngineDirectMemberKind::Getter, &directReturn); + break; + case kQuickJSFastObjCSetter: { + JSValue undefined = JS_UNDEFINED; + napi_value value = + argc > 0 ? reinterpret_cast(&argv[0]) + : reinterpret_cast(&undefined); + didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + context, env, static_cast(data), + jsThis, 1, &value, + nativescript::EngineDirectMemberKind::Setter, &directReturn); + break; + } + case kQuickJSFastCFunction: + didUseDirectReturn = tryCallQuickJSCFunctionEngineDirect( + context, env, + static_cast(reinterpret_cast(data)), + argc, napiArgs, &directReturn); + break; + default: + break; + } + + if (didUseDirectReturn) { + if (useGlobalValue) { + JS_FreeValue(context, effectiveThis); + } + if (JS_HasException(context)) { + JS_FreeValue(context, directReturn); + return JS_Throw(context, JS_GetException(context)); + } + return directReturn; + } + + QuickJSFastStackHandleScope scope(env); + + napi_value result = nullptr; + switch (magic) { + case kQuickJSFastObjCMethod: + result = nativescript::ObjCClassMember::jsCallDirect( + env, static_cast(data), jsThis, + static_cast(argc), napiArgs); + break; + + case kQuickJSFastObjCGetter: + result = nativescript::ObjCClassMember::jsGetterDirect( + env, static_cast(data), jsThis); + break; + + case kQuickJSFastObjCSetter: { + JSValue undefined = JS_UNDEFINED; + napi_value value = + argc > 0 ? reinterpret_cast(&argv[0]) + : reinterpret_cast(&undefined); + result = nativescript::ObjCClassMember::jsSetterDirect( + env, static_cast(data), jsThis, + value); + break; + } + + case kQuickJSFastObjCReadOnlySetter: + result = nativescript::ObjCClassMember::jsReadOnlySetterDirect(env); + break; + + case kQuickJSFastCFunction: + result = nativescript::CFunction::jsCallDirect( + env, + static_cast(reinterpret_cast(data)), + static_cast(argc), napiArgs); + break; + + default: + break; + } + + JSValue returnValue = JS_UNDEFINED; + if (result != nullptr) { + returnValue = JS_DupValue(context, ToJSValue(result)); + } + + scope.close(); + + if (useGlobalValue) { + JS_FreeValue(context, effectiveThis); + } + + if (JS_HasException(context)) { + JS_FreeValue(context, returnValue); + return JS_Throw(context, JS_GetException(context)); + } + + return returnValue; +} + +JSValue makeFastFunction(napi_env env, int kind, void* data) { + if (env == nullptr || env->context == nullptr) { + return JS_EXCEPTION; + } + + JSContext* context = env->context; + auto* externalInfo = static_cast( + mi_malloc(sizeof(QuickJSFastExternalInfo))); + if (externalInfo == nullptr) { + return JS_EXCEPTION; + } + externalInfo->data = data; + externalInfo->finalizeHint = nullptr; + externalInfo->finalizeCallback = nullptr; + + JSValue dataValue = + JS_NewObjectClass(context, static_cast(env->runtime->externalClassId)); + if (JS_IsException(dataValue)) { + mi_free(externalInfo); + return JS_EXCEPTION; + } + if (JS_SetOpaque(dataValue, externalInfo) != 0) { + mi_free(externalInfo); + JS_FreeValue(context, dataValue); + return JS_EXCEPTION; + } + + JSValue functionValue = + JS_NewCFunctionData(context, callFastNative, 0, kind, 1, &dataValue); + JS_FreeValue(context, dataValue); + return functionValue; +} + +bool defineFastProperty(napi_env env, napi_value object, + const napi_property_descriptor* descriptor, + JSValue value, JSValue getter, JSValue setter) { + JSContext* context = qjs_get_context(env); + if (context == nullptr || object == nullptr || descriptor == nullptr) { + return false; + } + + JSAtom key = 0; + if (descriptor->name != nullptr) { + key = JS_ValueToAtom(context, ToJSValue(descriptor->name)); + } else if (descriptor->utf8name != nullptr) { + key = JS_NewAtom(context, descriptor->utf8name); + } else { + return false; + } + + JSValue jsObject = ToJSValue(object); + if (!JS_IsObject(jsObject)) { + JS_FreeAtom(context, key); + return false; + } + + int flags = + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE | JS_PROP_HAS_CONFIGURABLE; + if ((descriptor->attributes & napi_writable) != 0 || + !JS_IsUndefined(getter) || !JS_IsUndefined(setter)) { + flags |= JS_PROP_WRITABLE; + } + if ((descriptor->attributes & napi_enumerable) != 0) { + flags |= JS_PROP_ENUMERABLE; + } + if ((descriptor->attributes & napi_configurable) != 0) { + flags |= JS_PROP_CONFIGURABLE; + } + + if (!JS_IsUndefined(value)) { + flags |= JS_PROP_HAS_VALUE; + } + if (!JS_IsUndefined(getter)) { + flags |= JS_PROP_HAS_GET; + } + if (!JS_IsUndefined(setter)) { + flags |= JS_PROP_HAS_SET; + } + + int status = JS_DefineProperty(context, jsObject, key, value, getter, setter, + flags); + JS_FreeAtom(context, key); + return status >= 0; +} + +} // namespace + +namespace nativescript { + +namespace { + +inline bool readQuickJSNumber(JSValue value, double* result) { + if (result == nullptr) { + return false; + } + + const int tag = JS_VALUE_GET_NORM_TAG(value); + if (tag == JS_TAG_INT) { + *result = static_cast(JS_VALUE_GET_INT(value)); + return true; + } + if (tag == JS_TAG_FLOAT64) { + *result = JS_VALUE_GET_FLOAT64(value); + return true; + } + return false; +} + +inline bool readQuickJSFiniteNumber(JSValue value, double* result) { + if (!readQuickJSNumber(value, result)) { + return false; + } + if (std::isnan(*result) || std::isinf(*result)) { + *result = 0.0; + } + return true; +} + +inline bool readQuickJSInt64(JSContext* context, JSValue value, + int64_t* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + const int tag = JS_VALUE_GET_NORM_TAG(value); + if (tag == JS_TAG_INT) { + *result = static_cast(JS_VALUE_GET_INT(value)); + return true; + } + if (tag == JS_TAG_FLOAT64) { + const double converted = JS_VALUE_GET_FLOAT64(value); + if (std::isnan(converted) || std::isinf(converted)) { + *result = 0; + return true; + } + *result = static_cast(converted); + return true; + } + if (JS_IsBigInt(context, value)) { + return JS_ToBigInt64(context, result, value) == 0; + } + return false; +} + +inline bool readQuickJSUInt64(JSContext* context, JSValue value, + uint64_t* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + const int tag = JS_VALUE_GET_NORM_TAG(value); + if (tag == JS_TAG_INT) { + *result = static_cast( + static_cast(JS_VALUE_GET_INT(value))); + return true; + } + if (tag == JS_TAG_FLOAT64) { + const double converted = JS_VALUE_GET_FLOAT64(value); + if (std::isnan(converted) || std::isinf(converted)) { + *result = 0; + return true; + } + *result = static_cast(converted); + return true; + } + if (JS_IsBigInt(context, value)) { + return JS_ToBigUint64(context, result, value) == 0; + } + return false; +} + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +id normalizeWrappedNativeObject(napi_env env, MDTypeKind kind, void* wrapped) { + if (wrapped == nullptr) { + return nil; + } + + auto* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id cachedNative = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (cachedNative != nil) { + return cachedNative; + } + + for (const auto& entry : bridgeState->classes) { + ObjCClass* bridgedClass = entry.second; + if (bridgedClass == wrapped && bridgedClass->nativeClass != nil) { + return (id)bridgedClass->nativeClass; + } + } + + if (kind == mdTypeProtocolObject || kind == mdTypeAnyObject) { + for (const auto& entry : bridgeState->protocols) { + ObjCProtocol* bridgedProtocol = entry.second; + if (bridgedProtocol != wrapped) { + continue; + } + + Protocol* runtimeProtocol = + objc_getProtocol(bridgedProtocol->name.c_str()); + if (runtimeProtocol != nil) { + return (id)runtimeProtocol; + } + break; + } + } + } + + return static_cast(wrapped); +} + +bool tryFastUnwrapQuickJSNativeObject(napi_env env, JSValue jsValue, + void** result) { + if (env == nullptr || result == nullptr || !JS_IsObject(jsValue)) { + return false; + } + + *result = nullptr; + auto* directInfo = static_cast( + JS_GetOpaque(jsValue, env->runtime->napiObjectClassId)); + if (directInfo != nullptr && directInfo->data != nullptr) { + *result = directInfo->data; + return true; + } + + JSPropertyDescriptor descriptor{}; + int wrapped = JS_GetOwnProperty(env->context, &descriptor, jsValue, + env->atoms.napi_external); + if (wrapped <= 0) { + return false; + } + + auto* externalInfo = static_cast( + JS_GetOpaque(descriptor.value, env->runtime->externalClassId)); + if (externalInfo != nullptr && externalInfo->data != nullptr) { + *result = externalInfo->data; + } + + JS_FreeValue(env->context, descriptor.value); + return *result != nullptr; +} + +bool tryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, + JSValue jsValue, void* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + if (JS_IsNull(jsValue) || JS_IsUndefined(jsValue)) { + if (kind == mdTypeClass) { + *reinterpret_cast(result) = Nil; + } else { + *reinterpret_cast(result) = nil; + } + return true; + } + + if (JS_IsString(jsValue) && + (kind == mdTypeAnyObject || kind == mdTypeNSStringObject || + kind == mdTypeNSMutableStringObject)) { + size_t length = 0; + const char* chars = JS_ToCStringLen(env->context, &length, jsValue); + if (chars == nullptr) { + return false; + } + + NSString* string = + [[[NSString alloc] initWithBytes:chars + length:length + encoding:NSUTF8StringEncoding] autorelease]; + JS_FreeCString(env->context, chars); + if (string == nil) { + string = @""; + } + if (kind == mdTypeNSMutableStringObject) { + string = [[[NSMutableString alloc] initWithString:string] autorelease]; + } + *reinterpret_cast(result) = string; + return true; + } + + if (kind == mdTypeAnyObject && JS_IsBool(jsValue)) { + *reinterpret_cast(result) = + [NSNumber numberWithBool:JS_VALUE_GET_BOOL(jsValue)]; + return true; + } + + if (kind == mdTypeAnyObject) { + double number = 0.0; + if (readQuickJSNumber(jsValue, &number)) { + *reinterpret_cast(result) = [NSNumber numberWithDouble:number]; + return true; + } + } + + void* wrapped = nullptr; + if (!tryFastUnwrapQuickJSNativeObject(env, jsValue, &wrapped)) { + return false; + } + + if (kind == mdTypeClass) { + id nativeObject = static_cast(wrapped); + if (!object_isClass(nativeObject)) { + nativeObject = normalizeWrappedNativeObject(env, kind, wrapped); + } + if (!object_isClass(nativeObject)) { + return false; + } + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + + *reinterpret_cast(result) = + normalizeWrappedNativeObject(env, kind, wrapped); + return true; +} + +} // namespace + +bool TryFastConvertQuickJSBoolArgument(napi_env env, napi_value value, + uint8_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValue jsValue = ToJSValue(value); + if (!JS_IsBool(jsValue)) { + return false; + } + *result = JS_VALUE_GET_BOOL(jsValue) ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertQuickJSDoubleArgument(napi_env env, napi_value value, + double* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSFiniteNumber(ToJSValue(value), result); +} + +bool TryFastConvertQuickJSFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValue jsValue = ToJSValue(value); + if (JS_IsString(jsValue)) { + size_t length = 0; + const char* str = JS_ToCStringLen(env->context, &length, jsValue); + if (str == nullptr) { + return false; + } + if (length != 1) { + JS_FreeCString(env->context, str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *result = static_cast(str[0]); + JS_FreeCString(env->context, str); + return true; + } + + double converted = 0.0; + if (!readQuickJSFiniteNumber(jsValue, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSInt64(env->context, ToJSValue(value), result); +} + +bool TryFastConvertQuickJSUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSUInt64(env->context, ToJSValue(value), result); +} + +bool TryFastConvertQuickJSSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValue jsValue = ToJSValue(value); + if (JS_IsNull(jsValue) || JS_IsUndefined(jsValue)) { + *result = nullptr; + return true; + } + if (!JS_IsString(jsValue)) { + return false; + } + + size_t length = 0; + const char* selectorName = JS_ToCStringLen(env->context, &length, jsValue); + if (selectorName == nullptr) { + return false; + } + *result = cachedSelectorForName(selectorName, length); + JS_FreeCString(env->context, selectorName); + return true; +} + +bool TryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return tryFastConvertQuickJSObjectArgument(env, kind, ToJSValue(value), + result); +} + +bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeBool: + return TryFastConvertQuickJSBoolArgument( + env, value, reinterpret_cast(result)); + case mdTypeChar: + return TryFastConvertQuickJSInt8Argument( + env, value, reinterpret_cast(result)); + case mdTypeUChar: + case mdTypeUInt8: + return TryFastConvertQuickJSUInt8Argument( + env, value, reinterpret_cast(result)); + case mdTypeSShort: + return TryFastConvertQuickJSInt16Argument( + env, value, reinterpret_cast(result)); + case mdTypeUShort: + return TryFastConvertQuickJSUInt16Argument( + env, value, reinterpret_cast(result)); + case mdTypeSInt: + return TryFastConvertQuickJSInt32Argument( + env, value, reinterpret_cast(result)); + case mdTypeUInt: + return TryFastConvertQuickJSUInt32Argument( + env, value, reinterpret_cast(result)); + case mdTypeSLong: + case mdTypeSInt64: + return TryFastConvertQuickJSInt64Argument( + env, value, reinterpret_cast(result)); + case mdTypeULong: + case mdTypeUInt64: + return TryFastConvertQuickJSUInt64Argument( + env, value, reinterpret_cast(result)); + case mdTypeFloat: + return TryFastConvertQuickJSFloatArgument( + env, value, reinterpret_cast(result)); + case mdTypeDouble: + return TryFastConvertQuickJSDoubleArgument( + env, value, reinterpret_cast(result)); + case mdTypeSelector: + return TryFastConvertQuickJSSelectorArgument( + env, value, reinterpret_cast(result)); + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (TryFastConvertQuickJSObjectArgument(env, kind, value, result)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + default: + return false; + } +} + +bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + JSContext* context = qjs_get_context(env); + if (context == nullptr) { + return false; + } + + JSValue jsValue = JS_UNDEFINED; + switch (kind) { + case mdTypeVoid: + jsValue = JS_NULL; + break; + + case mdTypeBool: + if (value == nullptr) { + return false; + } + jsValue = JS_NewBool(context, *reinterpret_cast(value) != 0); + break; + + case mdTypeChar: { + if (value == nullptr) { + return false; + } + const int8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewInt32(context, raw); + break; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) { + return false; + } + const uint8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewUint32(context, raw); + break; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + jsValue = JS_NewInt32(context, *reinterpret_cast(value)); + break; + + case mdTypeUShort: { + if (value == nullptr) { + return false; + } + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[1] = {static_cast(raw)}; + jsValue = JS_NewStringLen(context, buffer, 1); + } else { + jsValue = JS_NewUint32(context, raw); + } + break; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + jsValue = JS_NewInt32(context, *reinterpret_cast(value)); + break; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + jsValue = JS_NewUint32(context, *reinterpret_cast(value)); + break; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) { + return false; + } + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + jsValue = raw > kMaxSafeInteger || raw < -kMaxSafeInteger + ? JS_NewBigInt64(context, raw) + : JS_NewInt64(context, raw); + break; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) { + return false; + } + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + jsValue = raw > kMaxSafeInteger + ? JS_NewBigUint64(context, raw) + : JS_NewInt64(context, static_cast(raw)); + break; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + jsValue = JS_NewFloat64(context, *reinterpret_cast(value)); + break; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + jsValue = JS_NewFloat64(context, *reinterpret_cast(value)); + break; + + default: + return false; + } + + if (JS_IsException(jsValue)) { + return false; + } + + return qjs_create_scoped_value(env, jsValue, result) == napi_ok; +} + +} // namespace nativescript + +extern "C" bool nativescript_quickjs_try_define_fast_native_property( + napi_env env, napi_value object, + const napi_property_descriptor* descriptor) { + if (env == nullptr || object == nullptr || descriptor == nullptr) { + return false; + } + + JSContext* context = qjs_get_context(env); + if (context == nullptr) { + return false; + } + + if (descriptor->method == nativescript::ObjCClassMember::jsCall && + descriptor->data != nullptr) { + JSValue function = + makeFastFunction(env, kQuickJSFastObjCMethod, descriptor->data); + return !JS_IsException(function) && + defineFastProperty(env, object, descriptor, function, + JS_UNDEFINED, JS_UNDEFINED); + } + + if (descriptor->method == nativescript::CFunction::jsCall && + descriptor->data != nullptr && + !isCompatCFunction(env, descriptor->data)) { + JSValue function = + makeFastFunction(env, kQuickJSFastCFunction, descriptor->data); + return !JS_IsException(function) && + defineFastProperty(env, object, descriptor, function, + JS_UNDEFINED, JS_UNDEFINED); + } + + if (descriptor->getter == nativescript::ObjCClassMember::jsGetter && + descriptor->data != nullptr) { + JSValue getter = + makeFastFunction(env, kQuickJSFastObjCGetter, descriptor->data); + if (JS_IsException(getter)) { + return false; + } + + JSValue setter = JS_UNDEFINED; + if (descriptor->setter == nativescript::ObjCClassMember::jsSetter) { + setter = + makeFastFunction(env, kQuickJSFastObjCSetter, descriptor->data); + if (JS_IsException(setter)) { + return false; + } + } else if (descriptor->setter == + nativescript::ObjCClassMember::jsReadOnlySetter) { + setter = makeFastFunction(env, kQuickJSFastObjCReadOnlySetter, + descriptor->data); + if (JS_IsException(setter)) { + return false; + } + } else if (descriptor->setter != nullptr) { + return false; + } + + return defineFastProperty(env, object, descriptor, JS_UNDEFINED, getter, + setter); + } + + return false; +} + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 14367c8b..017eb5ac 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -11,22 +11,48 @@ #include "Cif.h" #include "js_native_api.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif namespace nativescript { enum class SignatureCallKind : uint8_t { ObjCMethod = 1, CFunction = 2, + BlockInvoke = 3, }; using ObjCPreparedInvoker = void (*)(void* fnptr, void** avalues, void* rvalue); using CFunctionPreparedInvoker = void (*)(void* fnptr, void** avalues, void* rvalue); +using BlockPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); using ObjCNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, SEL selector, const napi_value* argv, void* rvalue); using CFunctionNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, const napi_value* argv, void* rvalue); +using ObjCEngineDirectInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, + id self, SEL selector, + const napi_value* argv, + void* rvalue); +using CFunctionEngineDirectInvoker = bool (*)(napi_env env, Cif* cif, + void* fnptr, + const napi_value* argv, + void* rvalue); + +#ifdef TARGET_ENGINE_V8 +using ObjCV8Invoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, + SEL selector, void* bridgeState, bool returnOwned, + bool receiverIsClass, bool propertyAccess, + const v8::FunctionCallbackInfo& info, + void* rvalue, bool* didSetReturnValue); +using CFunctionV8Invoker = + bool (*)(napi_env env, Cif* cif, void* fnptr, + const v8::FunctionCallbackInfo& info, void* rvalue, + bool* didSetReturnValue); +#endif struct ObjCDispatchEntry { uint64_t dispatchId; @@ -38,6 +64,11 @@ struct CFunctionDispatchEntry { CFunctionPreparedInvoker invoker; }; +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + struct ObjCNapiDispatchEntry { uint64_t dispatchId; ObjCNapiInvoker invoker; @@ -48,6 +79,28 @@ struct CFunctionNapiDispatchEntry { CFunctionNapiInvoker invoker; }; +struct ObjCEngineDirectDispatchEntry { + uint64_t dispatchId; + ObjCEngineDirectInvoker invoker; +}; + +struct CFunctionEngineDirectDispatchEntry { + uint64_t dispatchId; + CFunctionEngineDirectInvoker invoker; +}; + +#ifdef TARGET_ENGINE_V8 +struct ObjCV8DispatchEntry { + uint64_t dispatchId; + ObjCV8Invoker invoker; +}; + +struct CFunctionV8DispatchEntry { + uint64_t dispatchId; + CFunctionV8Invoker invoker; +}; +#endif + inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; @@ -71,8 +124,100 @@ inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); } +#ifdef TARGET_ENGINE_V8 +static_assert(sizeof(v8::Local) == sizeof(napi_value), + "Cannot convert between v8::Local and napi_value"); + +inline napi_value v8LocalValueToNapiValue(v8::Local local) { + return reinterpret_cast(*local); +} + +inline void setV8DispatchInt64ReturnValue( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + int64_t value) { + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (value > kMaxSafeInteger || value < -kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::New(isolate, value)); + } else { + info.GetReturnValue().Set(static_cast(value)); + } +} + +inline void setV8DispatchUInt64ReturnValue( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + uint64_t value) { + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (value > kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, value)); + } else { + info.GetReturnValue().Set(static_cast(value)); + } +} + +bool TryFastSetV8GeneratedObjCObjectReturnValue( + napi_env env, const v8::FunctionCallbackInfo& info, + Cif* cif, void* bridgeState, id self, SEL selector, id value, + bool returnOwned, bool receiverIsClass, bool propertyAccess); +#endif + } // namespace nativescript +#ifndef NS_GSD_BACKEND_V8 +#ifdef TARGET_ENGINE_V8 +#define NS_GSD_BACKEND_V8 1 +#else +#define NS_GSD_BACKEND_V8 0 +#endif +#endif + +#ifndef NS_GSD_BACKEND_JSC +#ifdef TARGET_ENGINE_JSC +#define NS_GSD_BACKEND_JSC 1 +#else +#define NS_GSD_BACKEND_JSC 0 +#endif +#endif + +#ifndef NS_GSD_BACKEND_QUICKJS +#ifdef TARGET_ENGINE_QUICKJS +#define NS_GSD_BACKEND_QUICKJS 1 +#else +#define NS_GSD_BACKEND_QUICKJS 0 +#endif +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#ifdef TARGET_ENGINE_HERMES +#define NS_GSD_BACKEND_HERMES 1 +#else +#define NS_GSD_BACKEND_HERMES 0 +#endif +#endif + +#define NS_GSD_BACKEND_ENGINE_DIRECT \ + (NS_GSD_BACKEND_JSC || NS_GSD_BACKEND_QUICKJS || NS_GSD_BACKEND_HERMES) + +#ifndef NS_GSD_BACKEND_NAPI +#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_ENGINE_DIRECT +#define NS_GSD_BACKEND_NAPI 0 +#else +#define NS_GSD_BACKEND_NAPI 1 +#endif +#endif + +#if NS_GSD_BACKEND_V8 && !defined(TARGET_ENGINE_V8) +#error "NS_GSD_BACKEND_V8 requires TARGET_ENGINE_V8" +#endif +#if NS_GSD_BACKEND_JSC && !defined(TARGET_ENGINE_JSC) +#error "NS_GSD_BACKEND_JSC requires TARGET_ENGINE_JSC" +#endif +#if NS_GSD_BACKEND_QUICKJS && !defined(TARGET_ENGINE_QUICKJS) +#error "NS_GSD_BACKEND_QUICKJS requires TARGET_ENGINE_QUICKJS" +#endif +#if NS_GSD_BACKEND_HERMES && !defined(TARGET_ENGINE_HERMES) +#error "NS_GSD_BACKEND_HERMES requires TARGET_ENGINE_HERMES" +#endif + #ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH #define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 #endif @@ -81,6 +226,14 @@ inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, #define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 0 #endif +#ifndef NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 0 +#endif + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" @@ -93,6 +246,8 @@ inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { {0, nullptr}}; inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; } // namespace nativescript #endif @@ -105,6 +260,24 @@ inline constexpr CFunctionNapiDispatchEntry } // namespace nativescript #endif +#if !NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH +namespace nativescript { +inline constexpr ObjCEngineDirectDispatchEntry + kGeneratedObjCEngineDirectDispatchEntries[] = {{0, nullptr}}; +inline constexpr CFunctionEngineDirectDispatchEntry + kGeneratedCFunctionEngineDirectDispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + +#if defined(TARGET_ENGINE_V8) && !NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH +namespace nativescript { +inline constexpr ObjCV8DispatchEntry kGeneratedObjCV8DispatchEntries[] = { + {0, nullptr}}; +inline constexpr CFunctionV8DispatchEntry + kGeneratedCFunctionV8DispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + namespace nativescript { template @@ -156,10 +329,19 @@ inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( if (!isGeneratedDispatchEnabled()) { return nullptr; } - return lookupDispatchInvoker( + return lookupDispatchInvoker( kGeneratedCFunctionDispatchEntries, dispatchId); } +inline BlockPreparedInvoker lookupBlockPreparedInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedBlockDispatchEntries, dispatchId); +} + inline ObjCNapiInvoker lookupObjCNapiInvoker(uint64_t dispatchId) { if (!isGeneratedDispatchEnabled()) { return nullptr; @@ -172,10 +354,49 @@ inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { if (!isGeneratedDispatchEnabled()) { return nullptr; } - return lookupDispatchInvoker( + return lookupDispatchInvoker( kGeneratedCFunctionNapiDispatchEntries, dispatchId); } +inline ObjCEngineDirectInvoker lookupObjCEngineDirectInvoker( + uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCEngineDirectDispatchEntries, dispatchId); +} + +inline CFunctionEngineDirectInvoker lookupCFunctionEngineDirectInvoker( + uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionEngineDirectDispatchEntries, dispatchId); +} + +#ifdef TARGET_ENGINE_V8 +inline ObjCV8Invoker lookupObjCV8Invoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCV8DispatchEntries, dispatchId); +} + +inline CFunctionV8Invoker lookupCFunctionV8Invoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionV8DispatchEntries, dispatchId); +} +#endif + } // namespace nativescript #endif // NS_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index 4e6dfcee..7f97f4e0 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -7,6 +7,9 @@ #include "ffi.h" #include "js_native_api.h" #include "objc/runtime.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif using namespace metagen; @@ -56,6 +59,152 @@ bool TryFastConvertNapiArgument(napi_env env, MDTypeKind kind, napi_value value, bool TryFastConvertNapiUInt16Argument(napi_env env, napi_value value, uint16_t* result); +#ifdef TARGET_ENGINE_V8 +// V8-only variants used by generated dispatch wrappers. These skip the +// Node-API callback/argument layer for primitive conversions and fall back to +// the regular TypeConv path for complex values. +bool TryFastConvertV8Argument(napi_env env, MDTypeKind kind, + v8::Local value, void* result); +bool TryFastConvertV8UInt16Argument(napi_env env, v8::Local value, + uint16_t* result); +bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, + const void* value, + v8::Local* result); +#endif + +#ifdef TARGET_ENGINE_JSC +// JSC-only conversion used by generated dispatch wrappers. The value is the +// JavaScriptCore JSValueRef carried through the fast-native callback, not a +// value copied through napi_get_cb_info. +bool TryFastConvertJSCArgument(napi_env env, MDTypeKind kind, napi_value value, + void* result); +bool TryFastConvertJSCBoolArgument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertJSCInt8Argument(napi_env env, napi_value value, + int8_t* result); +bool TryFastConvertJSCUInt8Argument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertJSCInt16Argument(napi_env env, napi_value value, + int16_t* result); +bool TryFastConvertJSCUInt16Argument(napi_env env, napi_value value, + uint16_t* result); +bool TryFastConvertJSCInt32Argument(napi_env env, napi_value value, + int32_t* result); +bool TryFastConvertJSCUInt32Argument(napi_env env, napi_value value, + uint32_t* result); +bool TryFastConvertJSCInt64Argument(napi_env env, napi_value value, + int64_t* result); +bool TryFastConvertJSCUInt64Argument(napi_env env, napi_value value, + uint64_t* result); +bool TryFastConvertJSCFloatArgument(napi_env env, napi_value value, + float* result); +bool TryFastConvertJSCDoubleArgument(napi_env env, napi_value value, + double* result); +bool TryFastConvertJSCSelectorArgument(napi_env env, napi_value value, + SEL* result); +bool TryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); +bool TryFastConvertJSCReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result); +#endif + +#ifdef TARGET_ENGINE_QUICKJS +// QuickJS-only conversion used by generated dispatch wrappers. The value is +// the raw JSValue slot passed to the QuickJS C callback. +bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); +bool TryFastConvertQuickJSBoolArgument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertQuickJSInt8Argument(napi_env env, napi_value value, + int8_t* result); +bool TryFastConvertQuickJSUInt8Argument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertQuickJSInt16Argument(napi_env env, napi_value value, + int16_t* result); +bool TryFastConvertQuickJSUInt16Argument(napi_env env, napi_value value, + uint16_t* result); +bool TryFastConvertQuickJSInt32Argument(napi_env env, napi_value value, + int32_t* result); +bool TryFastConvertQuickJSUInt32Argument(napi_env env, napi_value value, + uint32_t* result); +bool TryFastConvertQuickJSInt64Argument(napi_env env, napi_value value, + int64_t* result); +bool TryFastConvertQuickJSUInt64Argument(napi_env env, napi_value value, + uint64_t* result); +bool TryFastConvertQuickJSFloatArgument(napi_env env, napi_value value, + float* result); +bool TryFastConvertQuickJSDoubleArgument(napi_env env, napi_value value, + double* result); +bool TryFastConvertQuickJSSelectorArgument(napi_env env, napi_value value, + SEL* result); +bool TryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); +bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result); +#endif + +#ifdef TARGET_ENGINE_HERMES +// Hermes-only conversion used by generated dispatch wrappers. The value points +// at the PinnedHermesValue slot supplied by Hermes' native trampoline. +bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); +bool TryFastConvertHermesBoolArgument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertHermesInt8Argument(napi_env env, napi_value value, + int8_t* result); +bool TryFastConvertHermesUInt8Argument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertHermesInt16Argument(napi_env env, napi_value value, + int16_t* result); +bool TryFastConvertHermesUInt16Argument(napi_env env, napi_value value, + uint16_t* result); +bool TryFastConvertHermesInt32Argument(napi_env env, napi_value value, + int32_t* result); +bool TryFastConvertHermesUInt32Argument(napi_env env, napi_value value, + uint32_t* result); +bool TryFastConvertHermesInt64Argument(napi_env env, napi_value value, + int64_t* result); +bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, + uint64_t* result); +bool TryFastConvertHermesFloatArgument(napi_env env, napi_value value, + float* result); +bool TryFastConvertHermesDoubleArgument(napi_env env, napi_value value, + double* result); +bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result); +bool TryFastConvertHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); +bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result); +#endif + +inline bool TryFastConvertEngineReturnValue(napi_env env, MDTypeKind kind, + const void* value, + napi_value* result) { +#ifdef TARGET_ENGINE_JSC + return TryFastConvertJSCReturnValue(env, kind, value, result); +#elif defined(TARGET_ENGINE_QUICKJS) + return TryFastConvertQuickJSReturnValue(env, kind, value, result); +#elif defined(TARGET_ENGINE_HERMES) + return TryFastConvertHermesReturnValue(env, kind, value, result); +#else + return false; +#endif +} + +inline bool TryFastConvertEngineArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { +#ifdef TARGET_ENGINE_JSC + return TryFastConvertJSCArgument(env, kind, value, result); +#elif defined(TARGET_ENGINE_QUICKJS) + return TryFastConvertQuickJSArgument(env, kind, value, result); +#elif defined(TARGET_ENGINE_HERMES) + return TryFastConvertHermesArgument(env, kind, value, result); +#else + return false; +#endif +} + // Cleanup function to clear thread-local struct type caches void clearStructTypeCaches(); diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/TypeConv.mm index f82c8bb3..66decdcb 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/TypeConv.mm @@ -215,13 +215,22 @@ static id resolveCachedHandleObject(napi_env env, void* handle) { if (napi_has_named_property(env, cachedValue, "__ns_native_ptr", &hasNativePointer) == napi_ok && hasNativePointer) { napi_value nativePointerValue = nullptr; - void* nativePointer = nullptr; if (napi_get_named_property(env, cachedValue, "__ns_native_ptr", &nativePointerValue) == - napi_ok && - napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && - nativePointer != nullptr) { - bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); - return static_cast(nativePointer); + napi_ok) { + if (nativescript::Pointer::isInstance(env, nativePointerValue)) { + nativescript::Pointer* pointer = nativescript::Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(pointer->data), cachedValue); + return static_cast(pointer->data); + } + } else { + void* nativePointer = nullptr; + if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && + nativePointer != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); + return static_cast(nativePointer); + } + } } } @@ -1223,8 +1232,9 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { MDSectionOffset metadataOffset = findProtocolMetadataOffset(bridgeState->metadata, runtimeName); if (metadataOffset != MD_SECTION_OFFSET_NULL) { - bridgeState->mdProtocolsByPointer[runtimeProto] = metadataOffset; + bridgeState->registerProtocolMetadata(runtimeProto, metadataOffset); auto proto = bridgeState->getProtocol(env, metadataOffset); + bridgeState->registerRuntimeProtocol(proto, runtimeProto); if (proto != nullptr) { ::free(protocols); return get_ref_value(env, proto->constructor); @@ -1272,6 +1282,21 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, void* wrapped = nullptr; napi_status unwrapStatus = napi_unwrap(env, input, &wrapped); if (unwrapStatus != napi_ok) { + bool hasNativePointer = false; + if (napi_has_named_property(env, input, "__ns_native_ptr", &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, input, "__ns_native_ptr", &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + *out = pointer->data; + return true; + } + } + } return false; } @@ -2130,15 +2155,18 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { return get_ref_value(env, proto->constructor); } } else { + const uintptr_t objPtr = reinterpret_cast((void*)obj); const uintptr_t objNormalized = normalizePtr((void*)obj); - for (const auto& entry : bridgeState->mdProtocolsByPointer) { - if (normalizePtr((void*)entry.first) != objNormalized) { - continue; - } + if (objNormalized != objPtr) { + for (const auto& entry : bridgeState->mdProtocolsByPointer) { + if (normalizePtr((void*)entry.first) != objNormalized) { + continue; + } - auto proto = bridgeState->getProtocol(env, entry.second); - if (proto != nullptr) { - return get_ref_value(env, proto->constructor); + auto proto = bridgeState->getProtocol(env, entry.second); + if (proto != nullptr) { + return get_ref_value(env, proto->constructor); + } } } } diff --git a/NativeScript/ffi/V8FastNativeApi.h b/NativeScript/ffi/V8FastNativeApi.h new file mode 100644 index 00000000..8175f24a --- /dev/null +++ b/NativeScript/ffi/V8FastNativeApi.h @@ -0,0 +1,22 @@ +#ifndef V8_FAST_NATIVE_API_H +#define V8_FAST_NATIVE_API_H + +#ifdef TARGET_ENGINE_V8 + +#include + +#include "js_native_api.h" + +namespace nativescript { + +napi_value CreateV8NativeWrapperObject(napi_env env); + +bool V8TryDefineFastNativeProperty(napi_env env, v8::Local object, + v8::Local propertyName, + const napi_property_descriptor* descriptor); + +} // namespace nativescript + +#endif // TARGET_ENGINE_V8 + +#endif // V8_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm new file mode 100644 index 00000000..feb5fa9a --- /dev/null +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -0,0 +1,2290 @@ +#include "V8FastNativeApi.h" + +#ifdef TARGET_ENGINE_V8 + +#import +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "Object.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" +#include "ffi/NativeScriptException.h" +#include "v8-api.h" + +namespace nativescript { +namespace { + +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; +constexpr int kNativeWrapperReferenceField = 0; +constexpr int kNativeWrapperMarkerField = 1; +constexpr int kNativeWrapperFieldCount = 2; + +#if V8_MAJOR_VERSION >= 14 +#define NS_V8_INTERCEPTED v8::Intercepted +#define NS_V8_RETURN_YES return v8::Intercepted::kYes +#define NS_V8_RETURN_NO return v8::Intercepted::kNo +#define NS_V8_SETTER_INFO v8::PropertyCallbackInfo +#else +#define NS_V8_INTERCEPTED void +#define NS_V8_RETURN_YES return +#define NS_V8_RETURN_NO return +#define NS_V8_SETTER_INFO v8::PropertyCallbackInfo +#endif + +struct V8CFunctionBinding { + ObjCBridgeState* bridgeState = nullptr; + MDSectionOffset offset = 0; + CFunction* function = nullptr; +}; + +id tryReadWrappedReference(napi_env env, v8::Local object); +bool TryFastConvertV8FoundationObject(napi_env env, id value, v8::Local* result); +bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, + v8::Local* result); + +napi_env envFromHandlerData(v8::Local data) { + if (data.IsEmpty() || !data->IsExternal()) { + return nullptr; + } + + return static_cast(data.As()->Value()); +} + +void* nativeWrapperMarker() { + static uintptr_t marker; + return ▮ +} + +bool isV8NativeWrapperObject(v8::Local object) { + return !object.IsEmpty() && object->InternalFieldCount() > kNativeWrapperMarkerField && + object->GetAlignedPointerFromInternalField(kNativeWrapperMarkerField) == + nativeWrapperMarker(); +} + +void throwV8Error(v8::Isolate* isolate, const char* message) { + if (isolate == nullptr) { + return; + } + + v8::Local errorMessage; + if (!v8::String::NewFromUtf8(isolate, message != nullptr ? message : "", + v8::NewStringType::kNormal) + .ToLocal(&errorMessage)) { + return; + } + + isolate->ThrowException(v8::Exception::Error(errorMessage)); +} + +void throwNativeScriptExceptionToV8(napi_env env, v8::Isolate* isolate, + NativeScriptException& exception) { + if (env == nullptr || isolate == nullptr) { + return; + } + + napi_value error = nullptr; + exception.ReThrowToJS(env, &error); + if (error != nullptr) { + isolate->ThrowException(v8impl::V8LocalValueFromJsValue(error)); + return; + } + + throwV8Error(isolate, exception.Description().c_str()); +} + +thread_local bool isDefiningNativeWrapperProperty = false; + +class NativeWrapperPropertyDefinitionGuard { + public: + NativeWrapperPropertyDefinitionGuard() : previous_(isDefiningNativeWrapperProperty) { + isDefiningNativeWrapperProperty = true; + } + + ~NativeWrapperPropertyDefinitionGuard() { + isDefiningNativeWrapperProperty = previous_; + } + + private: + bool previous_; +}; + +bool definePlainValueProperty(v8::Local context, v8::Local object, + v8::Local property, v8::Local value) { + NativeWrapperPropertyDefinitionGuard guard; + return object->CreateDataProperty(context, property, value).FromMaybe(false); +} + +bool isInternalNativeProperty(v8::Isolate* isolate, v8::Local property) { + if (property.IsEmpty() || !property->IsString()) { + return true; + } + + v8::String::Utf8Value name(isolate, property); + if (*name == nullptr) { + return true; + } + + return strcmp(*name, "napi_external") == 0 || strcmp(*name, "napi_typetag") == 0 || + strcmp(*name, kNativePointerProperty) == 0; +} + +NS_V8_INTERCEPTED nativeWrapperNamedSetter(v8::Local property, + v8::Local value, + const NS_V8_SETTER_INFO& info) { + if (isDefiningNativeWrapperProperty) { + NS_V8_RETURN_NO; + } + + napi_env env = envFromHandlerData(info.Data()); + v8::Local holder = info.Holder(); + + if (env != nullptr && !isInternalNativeProperty(info.GetIsolate(), property)) { + id nativeObject = tryReadWrappedReference(env, holder); + if (nativeObject != nil) { + transferOwnershipToNative(env, v8impl::JsValueFromV8LocalValue(holder), nativeObject); + } + } + + definePlainValueProperty(info.GetIsolate()->GetCurrentContext(), holder, property, value); + NS_V8_RETURN_YES; +} + +NS_V8_INTERCEPTED nativeWrapperIndexedGetter( + uint32_t index, const v8::PropertyCallbackInfo& info) { + napi_env env = envFromHandlerData(info.Data()); + if (env == nullptr) { + NS_V8_RETURN_NO; + } + + id nativeObject = tryReadWrappedReference(env, info.Holder()); + if (nativeObject == nil || ![nativeObject isKindOfClass:[NSArray class]]) { + NS_V8_RETURN_NO; + } + + @try { + id value = reinterpret_cast(objc_msgSend)( + nativeObject, @selector(objectAtIndex:), static_cast(index)); + if (value == nil) { + info.GetReturnValue().Set(v8::Null(info.GetIsolate())); + NS_V8_RETURN_YES; + } + + v8::Local fastValue; + if (TryFastConvertV8FoundationObject(env, value, &fastValue)) { + info.GetReturnValue().Set(fastValue); + NS_V8_RETURN_YES; + } + + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + napi_value result = nullptr; + if (state != nullptr) { + result = state->findCachedObjectWrapper(env, value); + if (result == nullptr) { + result = state->getObject(env, value, kUnownedObject, 0, nullptr); + } + } + if (result != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + NS_V8_RETURN_YES; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + NS_V8_RETURN_YES; + } + + NS_V8_RETURN_NO; +} + +NS_V8_INTERCEPTED nativeWrapperIndexedSetter(uint32_t index, v8::Local value, + const NS_V8_SETTER_INFO& info) { + napi_env env = envFromHandlerData(info.Data()); + if (env == nullptr) { + NS_V8_RETURN_NO; + } + + id nativeObject = tryReadWrappedReference(env, info.Holder()); + if (nativeObject == nil || + ![nativeObject respondsToSelector:@selector(setObject:atIndexedSubscript:)]) { + NS_V8_RETURN_NO; + } + + id nativeValue = nil; + if (!TryFastConvertV8Argument(env, mdTypeAnyObject, value, &nativeValue)) { + NS_V8_RETURN_NO; + } + + @try { + reinterpret_cast(objc_msgSend)( + nativeObject, @selector(setObject:atIndexedSubscript:), nativeValue, + static_cast(index)); + NS_V8_RETURN_YES; + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + NS_V8_RETURN_YES; + } +} + +v8::Local nativeWrapperObjectTemplate(napi_env env) { + v8::Isolate* isolate = env->isolate; + static thread_local v8::Persistent objectTemplate; + static thread_local v8::Isolate* templateIsolate = nullptr; + static thread_local napi_env templateEnv = nullptr; + + if (objectTemplate.IsEmpty() || templateIsolate != isolate || templateEnv != env) { + objectTemplate.Reset(); + templateIsolate = isolate; + templateEnv = env; + v8::Local created = v8::ObjectTemplate::New(isolate); + created->SetInternalFieldCount(kNativeWrapperFieldCount); + v8::Local envData = v8::External::New(isolate, env); + v8::PropertyHandlerFlags namedFlags = static_cast( + static_cast(v8::PropertyHandlerFlags::kNonMasking) | + static_cast(v8::PropertyHandlerFlags::kOnlyInterceptStrings)); + created->SetHandler(v8::NamedPropertyHandlerConfiguration( + nullptr, nativeWrapperNamedSetter, nullptr, nullptr, nullptr, envData, namedFlags)); + created->SetHandler(v8::IndexedPropertyHandlerConfiguration( + nativeWrapperIndexedGetter, nativeWrapperIndexedSetter, nullptr, nullptr, nullptr, + envData)); + objectTemplate.Reset(isolate, created); + } + + return v8::Local::New(isolate, objectTemplate); +} + +} // namespace + +napi_value CreateV8NativeWrapperObject(napi_env env) { + if (env == nullptr) { + return nullptr; + } + + v8::EscapableHandleScope scope(env->isolate); + v8::Local object; + if (!nativeWrapperObjectTemplate(env)->NewInstance(env->context()).ToLocal(&object)) { + return nullptr; + } + object->SetAlignedPointerInInternalField(kNativeWrapperMarkerField, nativeWrapperMarker()); + + return v8impl::JsValueFromV8LocalValue(scope.Escape(object)); +} + +namespace { + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +bool TryFastConvertV8SelectorArgument(napi_env env, v8::Local value, SEL* selector) { + if (env == nullptr || selector == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *selector = nullptr; + return true; + } + + if (!value->IsString()) { + return false; + } + + v8::Local string = value.As(); + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + char* buffer = stackBuffer; + size_t length = 0; + size_t capacity = 0; + + if (string->IsOneByte() || string->ContainsOnlyOneByte()) { + length = static_cast(string->Length()); + capacity = length + 1; + if (capacity > kStackCapacity) { + buffer = static_cast(malloc(capacity)); + if (buffer == nullptr) { + return false; + } + } + string->WriteOneByteV2(env->isolate, 0, static_cast(length), + reinterpret_cast(buffer), + v8::String::WriteFlags::kNullTerminate); + } else { + length = string->Utf8LengthV2(env->isolate); + capacity = length + 1; + if (capacity > kStackCapacity) { + buffer = static_cast(malloc(capacity)); + if (buffer == nullptr) { + return false; + } + } + + size_t written = + string->WriteUtf8V2(env->isolate, buffer, capacity, v8::String::WriteFlags::kNullTerminate); + if (written == 0) { + if (buffer != stackBuffer) { + free(buffer); + } + return false; + } + length = buffer[written - 1] == '\0' ? written - 1 : written; + } + + buffer[length] = '\0'; + *selector = cachedSelectorForName(buffer, length); + if (buffer != stackBuffer) { + free(buffer); + } + return true; +} + +id tryUnwrapV8NativeObject(napi_env env, v8::Local value); + +v8::Local nativePointerPropertyName(v8::Isolate* isolate) { + static thread_local v8::Persistent name; + static thread_local v8::Isolate* nameIsolate = nullptr; + + if (name.IsEmpty() || nameIsolate != isolate) { + name.Reset(); + nameIsolate = isolate; + name.Reset(isolate, v8::String::NewFromUtf8(isolate, kNativePointerProperty, + v8::NewStringType::kInternalized) + .ToLocalChecked()); + } + + return v8::Local::New(isolate, name); +} + +bool hasV8NativePointerProperty(napi_env env, v8::Local object) { + if (env == nullptr || object.IsEmpty()) { + return false; + } + + return object->HasOwnProperty(env->context(), nativePointerPropertyName(env->isolate)) + .FromMaybe(false); +} + +id resolveCachedHandleObject(napi_env env, void* handle) { + if (env == nullptr || handle == nullptr) { + return nil; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr) { + return nil; + } + + napi_value cachedValue = bridgeState->getCachedHandleObject(env, handle); + if (cachedValue == nullptr) { + return nil; + } + + void* wrapped = nullptr; + if (napi_unwrap(env, cachedValue, &wrapped) == napi_ok && wrapped != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(wrapped), cachedValue); + return static_cast(wrapped); + } + + bool hasNativePointer = false; + if (napi_has_named_property(env, cachedValue, kNativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, cachedValue, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(pointer->data), cachedValue); + return static_cast(pointer->data); + } + } + } + + return nil; +} + +bool TryFastUnwrapV8PointerLikeObjectArgument(napi_env env, v8::Local value, + id* result) { + if (env == nullptr || result == nullptr || value.IsEmpty() || !value->IsObject()) { + return false; + } + + napi_value jsValue = v8impl::JsValueFromV8LocalValue(value); + void* data = nullptr; + if (Pointer::isInstance(env, jsValue)) { + Pointer* pointer = Pointer::unwrap(env, jsValue); + data = pointer != nullptr ? pointer->data : nullptr; + } else if (Reference::isInstance(env, jsValue)) { + Reference* reference = Reference::unwrap(env, jsValue); + data = reference != nullptr ? reference->data : nullptr; + } else { + return false; + } + + if (id cachedObject = resolveCachedHandleObject(env, data); cachedObject != nil) { + *result = cachedObject; + } else { + *result = static_cast(data); + } + return true; +} + +bool TryFastUnwrapV8ObjectArgument(napi_env env, v8::Local value, id* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *result = nil; + return true; + } + + if (!value->IsObject()) { + return false; + } + + v8::Local object = value.As(); + if (isV8NativeWrapperObject(object)) { + id nativeObject = tryReadWrappedReference(env, object); + if (nativeObject != nil) { + *result = nativeObject; + return true; + } + } + + if (TryFastUnwrapV8PointerLikeObjectArgument(env, value, result)) { + return true; + } + + if (hasV8NativePointerProperty(env, object)) { + id nativeObject = tryUnwrapV8NativeObject(env, value); + if (nativeObject != nil) { + *result = nativeObject; + return true; + } + } + + return false; +} + +bool TryFastUnwrapV8ClassArgument(napi_env env, v8::Local value, Class* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *result = Nil; + return true; + } + + if (!value->IsObject()) { + return false; + } + + id nativeObject = tryUnwrapV8NativeObject(env, value); + if (nativeObject == nil || !object_isClass(nativeObject)) { + return false; + } + + *result = (Class)nativeObject; + return true; +} + +} // namespace + +bool TryFastConvertV8UInt16Argument(napi_env env, v8::Local value, uint16_t* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsString()) { + v8::String::Value chars(env->isolate, value); + if (chars.length() != 1) { + throwV8Error(env->isolate, "Expected a single-character string."); + *result = 0; + return false; + } + + *result = static_cast((*chars)[0]); + return true; + } + + uint32_t converted = 0; + if (!value->Uint32Value(env->context()).To(&converted)) { + return false; + } + + *result = static_cast(converted); + return true; +} + +bool TryFastConvertV8Argument(napi_env env, MDTypeKind kind, v8::Local value, + void* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + switch (kind) { + case mdTypeChar: { + int32_t converted = 0; + if (!value->Int32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + uint32_t converted = 0; + if (!value->Uint32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeSShort: { + int32_t converted = 0; + if (!value->Int32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeUShort: + return TryFastConvertV8UInt16Argument(env, value, reinterpret_cast(result)); + + case mdTypeSInt: + return value->Int32Value(env->context()).To(reinterpret_cast(result)); + + case mdTypeUInt: + return value->Uint32Value(env->context()).To(reinterpret_cast(result)); + + case mdTypeSLong: + case mdTypeSInt64: + if (value->IsBigInt()) { + bool lossless = false; + *reinterpret_cast(result) = value.As()->Int64Value(&lossless); + return true; + } + return value->IntegerValue(env->context()).To(reinterpret_cast(result)); + + case mdTypeULong: + case mdTypeUInt64: + if (value->IsBigInt()) { + bool lossless = false; + *reinterpret_cast(result) = + value.As()->Uint64Value(&lossless); + return true; + } else { + int64_t converted = 0; + if (!value->IntegerValue(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeFloat: { + double converted = 0.0; + if (!value->NumberValue(env->context()).To(&converted)) { + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeDouble: { + double converted = 0.0; + if (!value->NumberValue(env->context()).To(&converted)) { + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = converted; + return true; + } + + case mdTypeBool: + if (!value->IsBoolean()) { + return false; + } + *reinterpret_cast(result) = + value->BooleanValue(env->isolate) ? static_cast(1) : static_cast(0); + return true; + + case mdTypeSelector: { + return TryFastConvertV8SelectorArgument(env, value, reinterpret_cast(result)); + } + + case mdTypeClass: + if (TryFastUnwrapV8ClassArgument(env, value, reinterpret_cast(result))) { + return true; + } + return TryFastConvertNapiArgument(env, kind, v8impl::JsValueFromV8LocalValue(value), result); + + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (TryFastUnwrapV8ObjectArgument(env, value, reinterpret_cast(result))) { + return true; + } + return TryFastConvertNapiArgument(env, kind, v8impl::JsValueFromV8LocalValue(value), result); + + default: + return false; + } +} + +bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, const void* value, + v8::Local* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + v8::Isolate* isolate = env->isolate; + switch (kind) { + case mdTypeVoid: + *result = v8::Undefined(isolate); + return true; + + case mdTypeBool: + *result = v8::Boolean::New(isolate, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUChar: + case mdTypeUInt8: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSShort: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSInt: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + int64_t nativeValue = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (nativeValue > kMaxSafeInteger || nativeValue < -kMaxSafeInteger) { + *result = v8::BigInt::New(isolate, nativeValue); + } else { + *result = v8::Number::New(isolate, static_cast(nativeValue)); + } + return true; + } + + case mdTypeULong: + case mdTypeUInt64: { + uint64_t nativeValue = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (nativeValue > kMaxSafeInteger) { + *result = v8::BigInt::NewFromUnsigned(isolate, nativeValue); + } else { + *result = v8::Number::New(isolate, static_cast(nativeValue)); + } + return true; + } + + case mdTypeFloat: + *result = v8::Number::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeDouble: + *result = v8::Number::New(isolate, *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +namespace { + +bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, + v8::Local* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + NSString* str = *reinterpret_cast(value); + v8::Isolate* isolate = env->isolate; + if (str == nil) { + *result = v8::Null(isolate); + return true; + } + + const NSUInteger length = [str length]; + if (length == 0) { + *result = v8::String::Empty(isolate); + return true; + } + + if (length > static_cast(std::numeric_limits::max())) { + return false; + } + + const UniChar* directChars = CFStringGetCharactersPtr((CFStringRef)str); + if (directChars != nullptr) { + v8::Local stringValue; + if (!v8::String::NewFromTwoByte( + isolate, reinterpret_cast(directChars), v8::NewStringType::kNormal, + static_cast(length)) + .ToLocal(&stringValue)) { + return false; + } + *result = stringValue; + return true; + } + + constexpr NSUInteger kStackCapacity = 256; + UniChar stackBuffer[kStackCapacity]; + UniChar* buffer = length <= kStackCapacity + ? stackBuffer + : static_cast(malloc(length * sizeof(UniChar))); + if (buffer == nullptr) { + return false; + } + + [str getCharacters:buffer range:NSMakeRange(0, length)]; + + v8::Local stringValue; + bool converted = v8::String::NewFromTwoByte( + isolate, reinterpret_cast(buffer), + v8::NewStringType::kNormal, static_cast(length)) + .ToLocal(&stringValue); + if (buffer != stackBuffer) { + free(buffer); + } + + if (!converted) { + return false; + } + + *result = stringValue; + return true; +} + +bool TryFastConvertV8FoundationObject(napi_env env, id value, v8::Local* result) { + if (env == nullptr || value == nil || result == nullptr) { + return false; + } + + v8::Isolate* isolate = env->isolate; + if ([value isKindOfClass:[NSNull class]]) { + *result = v8::Null(isolate); + return true; + } + + if ([value isKindOfClass:[NSNumber class]] && ![value isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)value) == CFBooleanGetTypeID()) { + *result = v8::Boolean::New(isolate, [value boolValue] == YES); + return true; + } + + *result = v8::Number::New(isolate, [value doubleValue]); + return true; + } + + if ([value isKindOfClass:[NSString class]]) { + NSString* str = (NSString*)value; + return TryFastConvertV8NSStringReturnValue(env, &str, result); + } + + return false; +} + +bool TryFastSetV8ObjectReturnValue(napi_env env, + const v8::FunctionCallbackInfo& info, + ObjCBridgeState* bridgeState, id value, + ObjectOwnership ownership) { + if (env == nullptr || bridgeState == nullptr) { + return false; + } + + v8::Isolate* isolate = info.GetIsolate(); + if (value == nil) { + info.GetReturnValue().Set(v8::Null(isolate)); + return true; + } + + v8::Local fastValue; + if (TryFastConvertV8FoundationObject(env, value, &fastValue)) { + info.GetReturnValue().Set(fastValue); + return true; + } + + napi_value cached = bridgeState->getCachedHandleObject(env, (void*)value); + if (cached == nullptr) { + cached = bridgeState->findCachedObjectWrapper(env, value); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return true; + } + + napi_value result = bridgeState->getObject(env, value, ownership, 0, nullptr); + if (result == nullptr) { + return false; + } + + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return true; +} + +} // namespace + +bool TryFastSetV8GeneratedObjCObjectReturnValue( + napi_env env, const v8::FunctionCallbackInfo& info, Cif* cif, + void* bridgeState, id self, SEL selector, id value, bool returnOwned, + bool receiverIsClass, bool propertyAccess) { + (void)propertyAccess; + auto* state = static_cast(bridgeState); + if (env == nullptr || state == nullptr || cif == nullptr || cif->returnType == nullptr) { + return false; + } + + if (selector == @selector(class)) { + return false; + } + + switch (cif->returnType->kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeNSStringObject: + break; + default: + return false; + } + + if (receiverIsClass && value != nil) { + Class receiverClass = (Class)self; + if ((receiverClass == [NSString class] || receiverClass == [NSMutableString class]) && + (selector == @selector(string) || + selector == @selector(stringWithString:) || + selector == @selector(stringWithCapacity:))) { + return false; + } + } + + return TryFastSetV8ObjectReturnValue( + env, info, state, value, returnOwned ? kOwnedObject : kUnownedObject); +} + +namespace { + +inline size_t alignUpSize(size_t value, size_t alignment) { + if (alignment == 0) { + return value; + } + return ((value + alignment - 1) / alignment) * alignment; +} + +size_t getCifArgumentStorageSize(Cif* cif, unsigned int argumentIndex, + unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->cif.arg_types == nullptr) { + return sizeof(void*); + } + + const unsigned int ffiIndex = argumentIndex + implicitArgumentCount; + if (ffiIndex >= cif->cif.nargs) { + return sizeof(void*); + } + + ffi_type* ffiArgType = cif->cif.arg_types[ffiIndex]; + size_t storageSize = ffiArgType != nullptr ? ffiArgType->size : 0; + return storageSize != 0 ? storageSize : sizeof(void*); +} + +size_t getCifArgumentStorageAlign(Cif* cif, unsigned int argumentIndex, + unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->cif.arg_types == nullptr) { + return alignof(void*); + } + + const unsigned int ffiIndex = argumentIndex + implicitArgumentCount; + if (ffiIndex >= cif->cif.nargs) { + return alignof(void*); + } + + ffi_type* ffiArgType = cif->cif.arg_types[ffiIndex]; + size_t alignment = ffiArgType != nullptr ? ffiArgType->alignment : 0; + return alignment != 0 ? alignment : alignof(void*); +} + +class V8CifArgumentStorage { + public: + V8CifArgumentStorage(Cif* cif, unsigned int implicitArgumentCount) { + if (cif == nullptr || cif->argc == 0) { + return; + } + + buffers_.resize(cif->argc, nullptr); + + size_t totalSize = 0; + for (unsigned int i = 0; i < cif->argc; i++) { + const size_t storageAlign = getCifArgumentStorageAlign(cif, i, implicitArgumentCount); + const size_t storageSize = getCifArgumentStorageSize(cif, i, implicitArgumentCount); + totalSize = alignUpSize(totalSize, storageAlign); + totalSize += storageSize; + } + + if (totalSize == 0) { + totalSize = sizeof(void*); + } + + storageBase_ = totalSize <= kInlineSize ? inlineBuffer_ : malloc(totalSize); + if (storageBase_ == nullptr) { + valid_ = false; + return; + } + + memset(storageBase_, 0, totalSize); + + size_t offset = 0; + for (unsigned int i = 0; i < cif->argc; i++) { + const size_t storageAlign = getCifArgumentStorageAlign(cif, i, implicitArgumentCount); + const size_t storageSize = getCifArgumentStorageSize(cif, i, implicitArgumentCount); + offset = alignUpSize(offset, storageAlign); + buffers_[i] = static_cast(static_cast(storageBase_) + offset); + offset += storageSize; + } + } + + ~V8CifArgumentStorage() { + if (storageBase_ != nullptr && storageBase_ != inlineBuffer_) { + free(storageBase_); + } + } + + bool valid() const { return valid_; } + + void* at(unsigned int index) const { + if (index >= buffers_.size()) { + return nullptr; + } + return buffers_[index]; + } + + private: + static constexpr size_t kInlineSize = 256; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* storageBase_ = nullptr; + bool valid_ = true; + std::vector buffers_; +}; + +class V8CifReturnStorage { + public: + explicit V8CifReturnStorage(Cif* cif) { + size_ = 0; + if (cif != nullptr) { + size_ = cif->rvalueLength; + if (size_ == 0 && cif->cif.rtype != nullptr) { + size_ = cif->cif.rtype->size; + } + } + if (size_ == 0) { + size_ = sizeof(void*); + } + + data_ = size_ <= kInlineSize ? inlineBuffer_ : malloc(size_); + } + + ~V8CifReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; + size_t size_ = 0; +}; + +class RoundTripCacheFrameGuard { + public: + RoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) + : env_(env), bridgeState_(bridgeState) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~RoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_; + ObjCBridgeState* bridgeState_; +}; + +inline napi_env envFromCurrentContext(v8::Isolate* isolate) { + (void)isolate; + return nullptr; +} + +v8::Local napiPrivateKey(v8::Isolate* isolate) { + static thread_local v8::Persistent key; + static thread_local v8::Isolate* keyIsolate = nullptr; + + if (key.IsEmpty() || keyIsolate != isolate) { + key.Reset(); + keyIsolate = isolate; + key.Reset(isolate, v8::Private::ForApi( + isolate, v8::String::NewFromUtf8Literal(isolate, "napi_private"))); + } + + return v8::Local::New(isolate, key); +} + +v8::Local prototypePropertyName(v8::Isolate* isolate) { + static thread_local v8::Persistent name; + static thread_local v8::Isolate* nameIsolate = nullptr; + + if (name.IsEmpty() || nameIsolate != isolate) { + name.Reset(); + nameIsolate = isolate; + name.Reset(isolate, v8::String::NewFromUtf8Literal(isolate, "prototype")); + } + + return v8::Local::New(isolate, name); +} + +id tryReadWrappedReference(napi_env env, v8::Local object) { + if (env == nullptr || object.IsEmpty()) { + return nil; + } + + if (object->InternalFieldCount() > 0) { + auto* reference = + static_cast( + object->GetAlignedPointerFromInternalField(kNativeWrapperReferenceField)); + if (reference != nullptr) { + return static_cast(reference->Data()); + } + } + + v8::Local wrappedReference; + if (!object->GetPrivate(env->context(), napiPrivateKey(env->isolate)).ToLocal(&wrappedReference) || + !wrappedReference->IsExternal()) { + return nil; + } + + auto* reference = static_cast(wrappedReference.As()->Value()); + return reference != nullptr ? static_cast(reference->Data()) : nil; +} + +id tryUnwrapV8NativeObject(napi_env env, v8::Local value) { + if (env == nullptr || value.IsEmpty() || !value->IsObject()) { + return nil; + } + + v8::Local object = value.As(); + if (isV8NativeWrapperObject(object)) { + id nativeObject = tryReadWrappedReference(env, object); + if (nativeObject != nil) { + return nativeObject; + } + } + + v8::Local context = env->context(); + if (object->IsProxy()) { + v8::Local proxy = object.As(); + v8::Local target = proxy->GetTarget(); + if (target->IsObject()) { + id nativeObject = tryReadWrappedReference(env, target.As()); + if (nativeObject != nil) { + return nativeObject; + } + } + } + + id nativeObject = tryReadWrappedReference(env, object); + if (nativeObject != nil) { + return nativeObject; + } + + if (object->IsFunction()) { + v8::MaybeLocal maybePrototype = + object->Get(context, prototypePropertyName(env->isolate)); + v8::Local prototype; + if (maybePrototype.ToLocal(&prototype) && prototype->IsObject()) { + object = prototype.As(); + nativeObject = tryReadWrappedReference(env, object); + if (nativeObject != nil) { + return nativeObject; + } + } + } + + return nil; +} + +id resolveSelf(napi_env env, v8::Local jsThisValue, ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = + method != nullptr ? method->bridgeState : ObjCBridgeState::InstanceData(env); + + if (!jsThisValue.IsEmpty() && jsThisValue->IsObject()) { + v8::Local jsThisObject = jsThisValue.As(); + if (isV8NativeWrapperObject(jsThisObject)) { + self = tryReadWrappedReference(env, jsThisObject); + if (self != nil) { + return self; + } + } + } + + self = tryUnwrapV8NativeObject(env, jsThisValue); + if (self != nil) { + return self; + } + + napi_value jsThis = v8impl::JsValueFromV8LocalValue(jsThisValue); + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + if (self == nil && jsThis != nullptr) { + void* unwrapped = nullptr; + if (napi_unwrap(env, jsThis, &unwrapped) == napi_ok) { + self = static_cast(unwrapped); + } + } + + if (self == nil && jsThis != nullptr) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, jsThis, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); + if (nativePointer != nullptr && nativePointer->data != nullptr) { + self = static_cast(nativePointer->data); + } + } + } + + if (self != nil) { + return self; + } + + bool shouldUseClassFallback = false; + if (method != nullptr && method->cls != nullptr && method->cls->nativeClass != nil) { + if (method->classMethod) { + shouldUseClassFallback = true; + } else if (!jsThisValue.IsEmpty() && jsThisValue->IsFunction()) { + shouldUseClassFallback = true; + } + } + + if (shouldUseClassFallback) { + return (id)method->cls->nativeClass; + } + + throwV8Error(env != nullptr ? env->isolate : nullptr, + "There was no native counterpart to the JavaScript object. Native API was " + "called with a likely plain object."); + return nil; +} + +bool receiverClassRequiresSuperCall(Class receiverClass); + +bool receiverRequiresSuperCall(id self, bool classMethod) { + if (self == nil) { + return false; + } + + Class receiverClass = classMethod ? (Class)self : object_getClass(self); + return receiverClassRequiresSuperCall(receiverClass); +} + +ObjCV8Invoker ensureObjCV8Invoker(Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0 || + cif->skipGeneratedNapiDispatch) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->v8Invoker = reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast(descriptor->v8Invoker); +} + +CFunctionV8Invoker ensureCFunctionV8Invoker(CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0 || + cif->skipGeneratedNapiDispatch) { + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->v8Invoker = reinterpret_cast(lookupCFunctionV8Invoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast(function->v8Invoker); +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + size_t selectorLength = strlen(selectorName); + size_t suffixLength = strlen(suffix); + if (selectorLength < suffixLength) { + return false; + } + + return strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeNSErrorOutMethodSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty()) { + return false; + } + + if (!selectorEndsWith(selector, "error:")) { + return false; + } + + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isNSErrorOutMethodSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeNSErrorOutMethodSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeNSErrorOutMethodSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline void throwArgumentsCountError(v8::Isolate* isolate, size_t actualCount, + size_t expectedCount) { + std::string message = "Actual arguments count: \"" + std::to_string(actualCount) + + "\". Expected: \"" + std::to_string(expectedCount) + "\"."; + throwV8Error(isolate, message.c_str()); +} + +bool canConvertV8ValueToType(napi_env env, v8::Local value, + std::shared_ptr typeConv) { + if (env == nullptr || typeConv == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + return true; + } + + switch (typeConv->kind) { + case mdTypeBool: + return value->IsBoolean() || value->IsNumber(); + + case mdTypeChar: + case mdTypeUChar: + return value->IsBoolean() || value->IsNumber() || value->IsBigInt(); + + case mdTypeSShort: + return value->IsNumber() || value->IsBigInt(); + + case mdTypeUShort: + if (value->IsString()) { + return value.As()->Length() == 1; + } + return value->IsNumber() || value->IsBigInt(); + + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return value->IsNumber() || value->IsBigInt(); + + case mdTypeString: + return value->IsString() || value->IsObject(); + + case mdTypeAnyObject: + return value->IsObject() || value->IsFunction() || value->IsString() || value->IsNumber() || + value->IsBoolean() || value->IsBigInt(); + + case mdTypeClass: + case mdTypeClassObject: + case mdTypeProtocolObject: + return value->IsFunction() || value->IsObject(); + + case mdTypeInstanceObject: + return value->IsObject() || value->IsFunction() || value->IsString() || value->IsNumber() || + value->IsBoolean() || value->IsBigInt(); + + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return value->IsString() || value->IsObject(); + + case mdTypeSelector: + return value->IsString(); + + case mdTypePointer: + case mdTypeOpaquePointer: + return value->IsObject() || value->IsFunction() || value->IsBigInt() || value->IsString(); + + case mdTypeStruct: + return value->IsObject(); + + case mdTypeBlock: + case mdTypeFunctionPointer: + return value->IsFunction() || value->IsNullOrUndefined(); + + default: + return false; + } +} + +int scoreV8ValueForType(v8::Local value, std::shared_ptr typeConv) { + if (typeConv == nullptr || value.IsEmpty()) { + return 0; + } + + switch (typeConv->kind) { + case mdTypeBool: + return value->IsBoolean() ? 2 : 0; + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return value->IsNumber() || value->IsBigInt() ? 2 : 0; + case mdTypeString: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return value->IsString() ? 2 : 0; + default: + return 1; + } +} + +Cif* resolveMethodDescriptorCif(napi_env env, ObjCClassMember* method, + MethodDescriptor* descriptor, Cif** cacheSlot, + bool receiverIsClass, Class receiverClass) { + if (env == nullptr || method == nullptr || descriptor == nullptr || cacheSlot == nullptr) { + return nullptr; + } + + Cif* cached = *cacheSlot; + if (cached != nullptr) { + return cached; + } + + Method runtimeMethod = receiverIsClass + ? class_getClassMethod(receiverClass, descriptor->selector) + : class_getInstanceMethod(receiverClass, descriptor->selector); + Cif* resolved = nullptr; + if (runtimeMethod != nullptr) { + resolved = method->bridgeState->getMethodCif(env, runtimeMethod); + } + if (resolved == nullptr) { + resolved = method->bridgeState->getMethodCif(env, descriptor->signatureOffset); + } + + *cacheSlot = resolved; + return resolved; +} + +bool selectV8MethodOverload(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, id self, MethodDescriptor** selectedMethod, + Cif** selectedCif) { + if (env == nullptr || method == nullptr || self == nil || selectedMethod == nullptr || + selectedCif == nullptr) { + return false; + } + + *selectedMethod = &method->methodOrGetter; + + if (method->overloads.empty() && method->cif != nullptr) { + *selectedCif = method->cif; + return true; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? (Class)self : object_getClass(self); + *selectedCif = resolveMethodDescriptorCif(env, method, &method->methodOrGetter, &method->cif, + receiverIsClass, receiverClass); + + if (method->overloads.empty()) { + return *selectedCif != nullptr; + } + + struct Candidate { + MethodDescriptor* descriptor; + Cif* cif; + int score; + }; + + std::vector candidates; + const size_t actualArgc = static_cast(info.Length()); + auto tryAddCandidate = [&](MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr || cif == nullptr || cif->argc != actualArgc) { + return; + } + + int score = 0; + for (size_t i = 0; i < actualArgc; i++) { + if (!canConvertV8ValueToType(env, info[static_cast(i)], cif->argTypes[i])) { + return; + } + score += scoreV8ValueForType(info[static_cast(i)], cif->argTypes[i]); + } + + candidates.push_back(Candidate{descriptor, cif, score}); + }; + + tryAddCandidate(&method->methodOrGetter, *selectedCif); + for (auto& overload : method->overloads) { + Cif* overloadCif = + resolveMethodDescriptorCif(env, method, &overload.method, &overload.cif, receiverIsClass, + receiverClass); + tryAddCandidate(&overload.method, overloadCif); + } + + if (!candidates.empty()) { + Candidate* best = &candidates[0]; + for (auto& candidate : candidates) { + if (candidate.score > best->score) { + best = &candidate; + } + } + *selectedMethod = best->descriptor; + *selectedCif = best->cif; + } + + return *selectedCif != nullptr; +} + +bool receiverClassRequiresSuperCall(Class receiverClass) { + if (receiverClass == nil) { + return false; + } + + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + bool requiresSuperCall = + class_conformsToProtocol(receiverClass, @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +bool invokeObjCPreparedOrFfi(napi_env env, Cif* cif, id self, bool classMethod, + MethodDescriptor* descriptor, uint8_t dispatchFlags, void** avalues, + void* rvalue) { + if (cif == nullptr || descriptor == nullptr) { + return false; + } + + Class receiverClass = classMethod ? (Class)self : object_getClass(self); + const bool supercall = receiverClassRequiresSuperCall(receiverClass); + if (supercall && classMethod) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + ClassBuilder* builder = + state != nullptr ? static_cast(state->classesByPointer[self]) : nullptr; + if (builder != nullptr && !builder->isFinal) { + builder->build(); + } + } + +#if defined(__x86_64__) + bool isStret = cif->returnType->type->size > 16 && cif->returnType->type->type == FFI_TYPE_STRUCT; +#endif + + @try { + if (!supercall) { + auto invoker = ensureObjCV8Invoker(cif, descriptor, dispatchFlags); + auto preparedInvoker = + descriptor != nullptr ? reinterpret_cast(descriptor->preparedInvoker) + : nullptr; + if (preparedInvoker != nullptr) { + preparedInvoker((void*)objc_msgSend, avalues, rvalue); + return true; + } + +#if defined(__x86_64__) + ffi_call(&cif->cif, isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend), rvalue, + avalues); +#else + ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); +#endif + (void)invoker; + } else { + Class superClass = classMethod ? class_getSuperclass(object_getClass((id)receiverClass)) + : class_getSuperclass(receiverClass); + struct objc_super superobj = {self, superClass}; + auto superobjPtr = &superobj; + avalues[0] = (void*)&superobjPtr; +#if defined(__x86_64__) + ffi_call(&cif->cif, isStret ? FFI_FN(objc_msgSendSuper_stret) : FFI_FN(objc_msgSendSuper), + rvalue, avalues); +#else + ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); +#endif + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, env != nullptr ? env->isolate : nullptr, + nativeScriptException); + return false; + } + + return true; +} + +void setObjCReturnValue(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, void* rvalue, bool propertyAccess) { + if (cif == nullptr || method == nullptr || descriptor == nullptr) { + return; + } + + if (cif->returnType->kind == mdTypeVoid) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + return; + } + + v8::Local fastResult; + if (TryFastConvertV8ReturnValue(env, cif->returnType->kind, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + if (cif->returnType->kind == mdTypeNSStringObject && + TryFastConvertV8NSStringReturnValue(env, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + napi_value jsThis = v8impl::JsValueFromV8LocalValue(info.This()); + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(constructor)); + return; + } + + id classObject = receiverIsClass ? self : (id)object_getClass(self); + napi_value result = + method->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *((id*)rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, (void*)obj); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return; + } + } + } + napi_value result = method->bridgeState->getObject( + env, obj, constructor, method->returnOwned ? kOwnedObject : kUnownedObject); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + + if (cif->returnType->kind == mdTypeAnyObject && receiverIsClass) { + id obj = *((id*)rvalue); + Class receiverClass = (Class)self; + if (obj != nil && + (receiverClass == [NSString class] || receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (strcmp(selectorName, "string") == 0 || strcmp(selectorName, "stringWithString:") == 0 || + strcmp(selectorName, "stringWithCapacity:") == 0)) { + napi_value result = method->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeInstanceObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *((id*)rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, (void*)obj); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return; + } + } + } + } + + napi_value result = cif->returnType->toJS(env, rvalue, method->returnOwned ? kReturnOwned : 0); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); +} + +bool invokeObjCSlow(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, bool propertyAccess) { + V8CifReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + throwV8Error(info.GetIsolate(), + "Unable to allocate return value storage for Objective-C call."); + return false; + } + + V8CifArgumentStorage argStorage(cif, 2); + if (!argStorage.valid()) { + throwV8Error(info.GetIsolate(), + "Unable to allocate argument storage for Objective-C call."); + return false; + } + + void* avalues[cif->cif.nargs]; + avalues[0] = (void*)&self; + avalues[1] = (void*)&descriptor->selector; + + const size_t actualArgc = static_cast(info.Length()); + const bool hasImplicitNSErrorOutArg = + !cif->isVariadic && isNSErrorOutMethodSignature(descriptor, cif) && + actualArgc + 1 == cif->argc; + NSError* implicitNSError = nil; + + bool shouldFreeAny = false; + bool shouldFree[cif->argc]; + v8::Local undefinedValue = v8::Undefined(info.GetIsolate()); + + for (unsigned int i = 0; i < cif->argc; i++) { + shouldFree[i] = false; + avalues[i + 2] = argStorage.at(i); + if (hasImplicitNSErrorOutArg && i == cif->argc - 1) { + NSError** implicitNSErrorOutArg = &implicitNSError; + *reinterpret_cast(avalues[i + 2]) = implicitNSErrorOutArg; + continue; + } + + v8::Local argValue = i < actualArgc ? info[i] : undefinedValue; + if (!TryFastConvertV8Argument(env, cif->argTypes[i]->kind, argValue, avalues[i + 2])) { + cif->argTypes[i]->toNative(env, v8impl::JsValueFromV8LocalValue(argValue), avalues[i + 2], + &shouldFree[i], &shouldFreeAny); + } + } + + void* rvalue = rvalueStorage.get(); + const bool didInvoke = invokeObjCPreparedOrFfi(env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, avalues, rvalue); + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i + 2])); + } + } + } + + if (!didInvoke) { + return false; + } + + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessage + : "Unknown NSError"); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return false; + } + + setObjCReturnValue(env, info, method, descriptor, cif, self, receiverIsClass, rvalue, + propertyAccess); + return true; +} + +bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool propertyAccess) { + if (env == nullptr || method == nullptr || descriptor == nullptr || cif == nullptr) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? (Class)self : object_getClass(self); + const bool requiresSuperCall = receiverClassRequiresSuperCall(receiverClass); + const size_t actualArgc = static_cast(info.Length()); + const bool isNSErrorOutMethod = isNSErrorOutMethodSignature(descriptor, cif); + if (!cif->isVariadic && isNSErrorOutMethod) { + if (actualArgc > cif->argc || actualArgc + 1 < cif->argc) { + throwArgumentsCountError(info.GetIsolate(), actualArgc, cif->argc); + return false; + } + } + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && !cif->isVariadic && actualArgc + 1 == cif->argc; + const bool canUseGeneratedDispatch = !isNSErrorOutMethod && !requiresSuperCall; + ObjCV8Invoker invoker = + canUseGeneratedDispatch ? ensureObjCV8Invoker(cif, descriptor, descriptor->dispatchFlags) + : nullptr; + if (invoker == nullptr) { + return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, + propertyAccess); + } + + const bool generatedDispatchSetsReturnDirectly = + cif->generatedDispatchSetsV8ReturnDirectly; + const bool generatedDispatchUsesObjectReturnStorage = + !generatedDispatchSetsReturnDirectly && cif->generatedDispatchUsesObjectReturnStorage; + const bool needsRoundTripCache = + generatedDispatchUsesObjectReturnStorage && + cif->generatedDispatchHasRoundTripCacheArgument; + std::optional roundTripCacheFrame; + if (needsRoundTripCache) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + + std::optional rvalueStorage; + id objectRvalue = nil; + void* rvalue = nullptr; + if (generatedDispatchUsesObjectReturnStorage) { + rvalue = &objectRvalue; + } else if (!generatedDispatchSetsReturnDirectly) { + rvalueStorage.emplace(cif); + if (!rvalueStorage->valid()) { + throwV8Error(info.GetIsolate(), + "Unable to allocate return value storage for Objective-C call."); + return false; + } + rvalue = rvalueStorage->get(); + } + + bool didInvoke = false; + bool didSetReturnValue = false; + @try { + didInvoke = invoker(env, cif, (void*)objc_msgSend, self, descriptor->selector, + method->bridgeState, method->returnOwned, receiverIsClass, + propertyAccess, info, rvalue, &didSetReturnValue); + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return false; + } + + if (!didInvoke) { + return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, + propertyAccess); + } + + if (!didSetReturnValue) { + setObjCReturnValue(env, info, method, descriptor, cif, self, receiverIsClass, rvalue, + propertyAccess); + } + return true; +} + +void v8ObjCMethodCallback(const v8::FunctionCallbackInfo& info) { + auto* method = static_cast(info.Data().As()->Value()); + napi_env env = method != nullptr && method->bridgeState != nullptr + ? method->bridgeState->env + : envFromCurrentContext(info.GetIsolate()); + if (env == nullptr || method == nullptr) { + return; + } + + id self = resolveSelf(env, info.This(), method); + if (self == nil) { + return; + } + + MethodDescriptor* descriptor = nullptr; + Cif* cif = nullptr; + if (!selectV8MethodOverload(env, info, method, self, &descriptor, &cif)) { + throwV8Error(info.GetIsolate(), "Unable to resolve native call signature."); + return; + } + + invokeObjCFast(env, info, method, descriptor, cif, self, false); +} + +void v8ObjCGetterCallback(const v8::FunctionCallbackInfo& info) { + auto* method = static_cast(info.Data().As()->Value()); + napi_env env = method != nullptr && method->bridgeState != nullptr + ? method->bridgeState->env + : envFromCurrentContext(info.GetIsolate()); + if (env == nullptr || method == nullptr) { + return; + } + + id self = resolveSelf(env, info.This(), method); + if (self == nil) { + return; + } + + Cif* cif = method->cif; + if (cif == nullptr) { + cif = method->cif = + method->bridgeState->getMethodCif(env, method->methodOrGetter.signatureOffset); + } + + invokeObjCFast(env, info, method, &method->methodOrGetter, cif, self, true); +} + +void v8ObjCSetterCallback(const v8::FunctionCallbackInfo& info) { + auto* method = static_cast(info.Data().As()->Value()); + napi_env env = method != nullptr && method->bridgeState != nullptr + ? method->bridgeState->env + : envFromCurrentContext(info.GetIsolate()); + if (env == nullptr || method == nullptr) { + return; + } + + id self = resolveSelf(env, info.This(), method); + if (self == nil) { + return; + } + + Cif* cif = method->setterCif; + if (cif == nullptr) { + cif = method->setterCif = + method->bridgeState->getMethodCif(env, method->setter.signatureOffset); + } + + invokeObjCFast(env, info, method, &method->setter, cif, self, true); +} + +void v8ReadOnlySetterCallback(const v8::FunctionCallbackInfo& info) { + auto* method = info.Data().IsEmpty() + ? nullptr + : static_cast(info.Data().As()->Value()); + napi_env env = method != nullptr && method->bridgeState != nullptr + ? method->bridgeState->env + : envFromCurrentContext(info.GetIsolate()); + throwV8Error(info.GetIsolate(), "Attempted to assign to readonly property."); +} + +void setCFunctionReturnValue(napi_env env, const v8::FunctionCallbackInfo& info, + CFunction* function, Cif* cif, void* rvalue) { + if (cif == nullptr) { + return; + } + + if (cif->returnType->kind == mdTypeVoid) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + return; + } + + v8::Local fastResult; + if (TryFastConvertV8ReturnValue(env, cif->returnType->kind, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + if (cif->returnType->kind == mdTypeNSStringObject && + TryFastConvertV8NSStringReturnValue(env, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + + napi_value result = cif->returnType->toJS(env, rvalue, toJSFlags); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); +} + +bool invokeCFunctionSlow(napi_env env, const v8::FunctionCallbackInfo& info, + CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr) { + return false; + } + + void* avalues[cif->argc]; + bool shouldFreeAny = false; + bool shouldFree[cif->argc]; + v8::Local undefinedValue = v8::Undefined(info.GetIsolate()); + + for (unsigned int i = 0; i < cif->argc; i++) { + shouldFree[i] = false; + avalues[i] = cif->avalues[i]; + v8::Local argValue = + i < static_cast(info.Length()) ? info[i] : undefinedValue; + if (!TryFastConvertV8Argument(env, cif->argTypes[i]->kind, argValue, avalues[i])) { + cif->argTypes[i]->toNative(env, v8impl::JsValueFromV8LocalValue(argValue), avalues[i], + &shouldFree[i], &shouldFreeAny); + } + } + + auto preparedInvoker = reinterpret_cast(function->preparedInvoker); + + @try { + if (preparedInvoker != nullptr) { + preparedInvoker(function->fnptr, avalues, cif->rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(function->fnptr), cif->rvalue, avalues); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return false; + } + + if (shouldFreeAny) { + void* returnPointerValue = nullptr; + const bool returnIsPointer = + cif->returnType != nullptr && cif->returnType->type == &ffi_type_pointer; + if (returnIsPointer && cif->rvalue != nullptr) { + returnPointerValue = *((void**)cif->rvalue); + } + + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + if (returnPointerValue != nullptr && avalues[i] != nullptr) { + void* argPointerValue = *((void**)avalues[i]); + if (argPointerValue == returnPointerValue) { + continue; + } + } + cif->argTypes[i]->free(env, *((void**)avalues[i])); + } + } + } + + setCFunctionReturnValue(env, info, function, cif, cif->rvalue); + return true; +} + +void v8CFunctionCallback(const v8::FunctionCallbackInfo& info) { + auto* binding = info.Data().IsEmpty() + ? nullptr + : static_cast(info.Data().As()->Value()); + ObjCBridgeState* bridgeState = binding != nullptr ? binding->bridgeState : nullptr; + napi_env env = bridgeState != nullptr ? bridgeState->env : envFromCurrentContext(info.GetIsolate()); + CFunction* function = binding != nullptr ? binding->function : nullptr; + if (function == nullptr && bridgeState != nullptr && binding != nullptr) { + function = bridgeState->getCFunction(env, binding->offset); + binding->function = function; + } + if (env == nullptr || function == nullptr) { + return; + } + + Cif* cif = function != nullptr ? function->cif : nullptr; + CFunctionV8Invoker invoker = ensureCFunctionV8Invoker(function, cif); + + bool didInvoke = false; + bool didSetReturnValue = false; + if (invoker != nullptr) { + @try { + didInvoke = invoker(env, cif, function->fnptr, info, cif->rvalue, &didSetReturnValue); + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return; + } + } + + if (!didInvoke) { + invokeCFunctionSlow(env, info, function, cif); + return; + } + + if (!didSetReturnValue) { + setCFunctionReturnValue(env, info, function, cif, cif->rvalue); + } +} + +bool isCompatLibdispatchFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return false; + } + + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +bool defineV8FunctionProperty(napi_env env, v8::Local object, + v8::Local propertyName, v8::Local function, + napi_property_attributes attributes) { + v8::PropertyDescriptor descriptor(function, (attributes & napi_writable) != 0); + descriptor.set_enumerable((attributes & napi_enumerable) != 0); + descriptor.set_configurable((attributes & napi_configurable) != 0); + + return object->DefineProperty(env->context(), propertyName, descriptor).FromMaybe(false); +} + +bool defineV8AccessorProperty(napi_env env, v8::Local object, + v8::Local propertyName, v8::Local getter, + v8::Local setter, napi_property_attributes attributes) { + v8::PropertyDescriptor descriptor(getter, setter); + descriptor.set_enumerable((attributes & napi_enumerable) != 0); + descriptor.set_configurable((attributes & napi_configurable) != 0); + + return object->DefineProperty(env->context(), propertyName, descriptor).FromMaybe(false); +} + +} // namespace + +bool V8TryDefineFastNativeProperty(napi_env env, v8::Local object, + v8::Local propertyName, + const napi_property_descriptor* descriptor) { +#if !NS_GSD_BACKEND_V8 + return false; +#else + if (env == nullptr || descriptor == nullptr) { + return false; + } + + v8::Local context = env->context(); + + if (descriptor->method == ObjCClassMember::jsCall) { + auto* method = static_cast(descriptor->data); + if (method == nullptr || !method->overloads.empty()) { + return false; + } + + Cif* cif = method->cif; + if (cif == nullptr) { + cif = method->cif = + method->bridgeState->getMethodCif(env, method->methodOrGetter.signatureOffset); + } + + v8::Local function; + if (!v8::Function::New(context, v8ObjCMethodCallback, v8::External::New(env->isolate, method)) + .ToLocal(&function)) { + return false; + } + + return defineV8FunctionProperty(env, object, propertyName, function, descriptor->attributes); + } + + if (descriptor->method == CFunction::jsCall) { + auto offset = static_cast(reinterpret_cast(descriptor->data)); + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (isCompatLibdispatchFunction(bridgeState, offset)) { + return false; + } + + if (bridgeState == nullptr) { + return false; + } + + auto* binding = new V8CFunctionBinding{bridgeState, offset, nullptr}; + v8::Local functionValue; + if (!v8::Function::New(context, v8CFunctionCallback, v8::External::New(env->isolate, binding)) + .ToLocal(&functionValue)) { + delete binding; + return false; + } + + return defineV8FunctionProperty(env, object, propertyName, functionValue, + descriptor->attributes); + } + + if (descriptor->getter == ObjCClassMember::jsGetter && descriptor->data != nullptr) { + auto* method = static_cast(descriptor->data); + Cif* getterCif = method->cif; + if (getterCif == nullptr) { + getterCif = method->cif = + method->bridgeState->getMethodCif(env, method->methodOrGetter.signatureOffset); + } + + v8::Local getter; + if (!v8::Function::New(context, v8ObjCGetterCallback, v8::External::New(env->isolate, method)) + .ToLocal(&getter)) { + return false; + } + + v8::Local setter; + if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { + if (!v8::Function::New(context, v8ReadOnlySetterCallback, + v8::External::New(env->isolate, method)) + .ToLocal(&setter)) { + return false; + } + } else if (descriptor->setter == ObjCClassMember::jsSetter) { + Cif* setterCif = method->setterCif; + if (setterCif == nullptr) { + setterCif = method->setterCif = + method->bridgeState->getMethodCif(env, method->setter.signatureOffset); + } + if (!v8::Function::New(context, v8ObjCSetterCallback, v8::External::New(env->isolate, method)) + .ToLocal(&setter)) { + return false; + } + } else if (descriptor->setter != nullptr) { + return false; + } + + return defineV8AccessorProperty(env, object, propertyName, getter, setter, + descriptor->attributes); + } + + return false; +#endif +} + +} // namespace nativescript + +#undef NS_V8_INTERCEPTED +#undef NS_V8_RETURN_YES +#undef NS_V8_RETURN_NO +#undef NS_V8_SETTER_INFO + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 50fcbf14..49d1a76e 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -15,6 +15,8 @@ #include #include +#include "ffi/JSCFastNativeApi.h" + struct napi_callback_info__ { napi_value newTarget; napi_value thisArg; @@ -791,6 +793,14 @@ class WrapperInfo : public BaseInfoT { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); WrapperInfo* info{}; + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo != env->wrapper_info_cache.end()) { + info = static_cast(cachedInfo->second); + RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + *result = info; + return napi_ok; + } + bool hasOwnProperty = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol) != nullptr; @@ -798,6 +808,7 @@ class WrapperInfo : public BaseInfoT { if (hasOwnProperty) { CHECK_NAPI(Unwrap(env, object, &info)); RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + env->wrapper_info_cache[object] = info; *result = info; return napi_ok; } @@ -811,6 +822,13 @@ class WrapperInfo : public BaseInfoT { NativeInfo::SetNativeInfoKey(env->context, ToJSObject(env, object), info->_class, env->wrapper_info_symbol, info); + info->AddFinalizer([object](WrapperInfo* info) { + napi_env env = info->Env(); + if (env != nullptr) { + env->wrapper_info_cache.erase(object); + } + }); + env->wrapper_info_cache[object] = info; } *result = info; @@ -820,6 +838,12 @@ class WrapperInfo : public BaseInfoT { static napi_status Unwrap(napi_env env, napi_value object, WrapperInfo** result) { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo != env->wrapper_info_cache.end()) { + *result = static_cast(cachedInfo->second); + return napi_ok; + } + *result = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol); return napi_ok; @@ -1282,6 +1306,10 @@ napi_status napi_define_properties(napi_env env, napi_value object, for (size_t i = 0; i < property_count; i++) { const napi_property_descriptor* p{properties + i}; + if (nativescript::JSCTryDefineFastNativeProperty(env, object, p)) { + continue; + } + napi_value descriptor{}; CHECK_NAPI(napi_create_object(env, &descriptor)); @@ -2104,6 +2132,28 @@ napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) { return napi_ok; } +extern "C" bool nativescript_jsc_try_unwrap_native(napi_env env, + napi_value value, + void** result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + *result = nullptr; + if (!IsJSObjectValue(env, value)) { + return false; + } + + WrapperInfo* info{}; + if (WrapperInfo::Unwrap(env, value, &info) != napi_ok || + info == nullptr || info->Data() == nullptr) { + return false; + } + + *result = info->Data(); + return true; +} + napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) { CHECK_ENV(env); diff --git a/NativeScript/napi/jsc/jsc-api.h b/NativeScript/napi/jsc/jsc-api.h index 8aa4b969..36e91f1a 100644 --- a/NativeScript/napi/jsc/jsc-api.h +++ b/NativeScript/napi/jsc/jsc-api.h @@ -10,16 +10,22 @@ #include #include #include +#include #include #include "js_native_api.h" #include "js_native_api_types.h" +extern "C" bool nativescript_jsc_try_unwrap_native(napi_env env, + napi_value value, + void** result); + struct napi_env__ { JSGlobalContextRef context{}; JSValueRef last_exception{}; napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_set active_ref_values{}; + std::unordered_map wrapper_info_cache{}; std::list strong_refs{}; void* instance_data{}; napi_finalize instance_data_finalize_cb; diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index 4213296a..c1d8ce0a 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -4,6 +4,7 @@ #include #include "js_native_api.h" +#include "ffi/QuickJSFastNativeApi.h" #include "libbf.h" #include "quicks-runtime.h" @@ -2844,6 +2845,11 @@ napi_status napi_delete_element(napi_env env, napi_value object, uint32_t index, static inline void napi_set_property_descriptor( napi_env env, napi_value object, napi_property_descriptor descriptor) { + if (nativescript_quickjs_try_define_fast_native_property(env, object, + &descriptor)) { + return; + } + JSAtom key; if (descriptor.name) { @@ -3414,6 +3420,10 @@ napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, return napi_set_last_error(env, napi_pending_exception, NULL, 0, NULL); } + if (JS_GetClassID(jsValue) == env->runtime->napiObjectClassId) { + JS_SetOpaque(jsValue, externalInfo); + } + if (result) { napi_ref ref; napi_create_reference(env, js_object, 0, &ref); @@ -3434,6 +3444,13 @@ napi_status napi_unwrap(napi_env env, napi_value jsObject, void** result) { return napi_set_last_error(env, napi_object_expected, NULL, 0, NULL); } + ExternalInfo* directInfo = + (ExternalInfo*)JS_GetOpaque(jsValue, env->runtime->napiObjectClassId); + if (directInfo && directInfo->data) { + *result = directInfo->data; + return napi_clear_last_error(env); + } + JSPropertyDescriptor descriptor; int isWrapped = JS_GetOwnProperty(env->context, &descriptor, jsValue, @@ -3496,6 +3513,9 @@ napi_status napi_remove_wrap(napi_env env, napi_value jsObject, void** result) { if (externalInfo) { *result = externalInfo->data; } + if (JS_GetClassID(jsValue) == env->runtime->napiObjectClassId) { + JS_SetOpaque(jsValue, NULL); + } mi_free(externalInfo); JS_SetOpaque(external, NULL); } diff --git a/NativeScript/napi/v8/v8-api.cpp b/NativeScript/napi/v8/v8-api.cpp index dc6bfeda..eec2772d 100644 --- a/NativeScript/napi/v8/v8-api.cpp +++ b/NativeScript/napi/v8/v8-api.cpp @@ -9,6 +9,7 @@ #define NAPI_EXPERIMENTAL +#include "ffi/V8FastNativeApi.h" #include "js_native_api.h" #include "v8-api.h" #include "v8-module-loader.h" @@ -1378,6 +1379,11 @@ napi_define_properties(napi_env env, napi_value object, size_t property_count, v8::Local property_name; STATUS_CALL(v8impl::V8NameFromPropertyDescriptor(env, p, &property_name)); + if (nativescript::V8TryDefineFastNativeProperty(env, obj, property_name, + p)) { + continue; + } + if (p->getter != nullptr || p->setter != nullptr) { v8::Local local_getter; v8::Local local_setter; diff --git a/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md new file mode 100644 index 00000000..6a2951bd --- /dev/null +++ b/benchmarks/objc-dispatch/README.md @@ -0,0 +1,45 @@ +# Objective-C Dispatch Benchmarks + +This benchmark compares hot Objective-C dispatch shapes across the generated +signature dispatch runtime and the PR #366 AOT direct-call runtime. + +The benchmark body is plain NativeScript JavaScript: + +- `objc-dispatch-benchmarks.js` + +The runner can execute it in three modes: + +- `napi-node`: fastest smoke run using the packaged macOS Node-API runtime. +- `napi-ios`: builds a temporary iOS app from the packaged `@nativescript/ios` + template and runs it in Simulator. +- `legacy-ios`: temporarily injects the benchmark into the PR branch + `TestRunner` app, builds it, runs it in Simulator, then restores the app + entry point. + +For V8 builds, `gsd-off` still uses the V8-native callback/marshalling path, +but generated signature dispatch lookup is disabled so Objective-C calls fall +back to the dynamic prepared/`ffi_call` path. This keeps the comparison focused +on the generated dispatch win instead of accidentally measuring a hand-written +direct `objc_msgSend` fast path. + +For JSC, QuickJS, and Hermes builds, `gsd-off` follows the same rule: the +engine-native callback and marshalling layer remains active, while only the +generated typed invoker lookup is disabled. + +Examples: + +```sh +npm run benchmark:objc-dispatch -- --runtime napi-node --iterations 100000 +npm run benchmark:objc-dispatch -- --runtime napi-ios,legacy-ios --iterations 250000 +npm run benchmark:objc-dispatch -- --runtime all --include-napi-gsd-off +``` + +Useful options: + +```sh +--legacy-repo /path/to/NativeScript/ios +--destination "platform=iOS Simulator,id=" +--napi-package-tgz /path/to/nativescript-ios.tgz +--iterations 250000 +--include-napi-gsd-off +``` diff --git a/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js new file mode 100644 index 00000000..1778a344 --- /dev/null +++ b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js @@ -0,0 +1,215 @@ +(function () { + "use strict"; + + var marker = "NS_BENCH_RESULT:"; + var runtime = globalThis.__NS_BENCHMARK_RUNTIME || "unknown"; + var variant = globalThis.__NS_BENCHMARK_VARIANT || "default"; + var options = globalThis.__NS_BENCHMARK_OPTIONS__ || {}; + var baseIterations = Math.max(1, Number(options.iterations || 250000) | 0); + var warmupIterations = Math.max(0, Number(options.warmupIterations || Math.min(10000, baseIterations / 10)) | 0); + var sink = 0; + + function nowMs() { + if (globalThis.performance && typeof globalThis.performance.now === "function") { + return globalThis.performance.now(); + } + return Date.now(); + } + + function consume(value) { + var n = 0; + switch (typeof value) { + case "number": + n = value | 0; + break; + case "boolean": + n = value ? 1 : 0; + break; + case "string": + n = value.length; + break; + case "object": + case "function": + if (value === null || value === undefined) { + n = 0; + } else if (typeof value.length === "number") { + n = value.length | 0; + } else if (typeof value.count === "number") { + n = value.count | 0; + } else { + n = 1; + } + break; + default: + n = value ? 1 : 0; + break; + } + + sink = ((sink << 5) - sink + n) | 0; + } + + function runLoop(iterations, fn) { + for (var i = 0; i < iterations; i++) { + consume(fn(i)); + } + } + + function bench(name, factor, fn) { + var iterations = Math.max(1, Math.floor(baseIterations * factor)); + var warmup = Math.min(warmupIterations, iterations); + runLoop(warmup, fn); + + var started = nowMs(); + runLoop(iterations, fn); + var elapsedMs = nowMs() - started; + + return { + name: name, + iterations: iterations, + ms: elapsedMs, + nsPerOp: elapsedMs * 1000000 / iterations + }; + } + + function emit(payload) { + console.log(marker + JSON.stringify(payload)); + } + + function addCase(cases, name, factor, fn) { + try { + consume(fn(0)); + cases.push({ name: name, factor: factor, fn: fn }); + } catch (error) { + cases.push({ + name: name, + skip: true, + error: error && error.message ? error.message : String(error) + }); + } + } + + function buildCases() { + var cases = []; + var object = NSObject.alloc().init(); + var otherObject = NSObject.alloc().init(); + var string = NSString.stringWithString("NativeScript dispatch benchmark"); + var compareString = NSString.stringWithString("NativeScript dispatch baseline"); + var prefix = NSString.stringWithString("NativeScript"); + var key = NSString.stringWithString("benchmark-key"); + var array = NSMutableArray.alloc().init(); + array.addObject(object); + array.addObject(otherObject); + array.addObject(string); + + var immutableArray = NSArray.arrayWithArray([object, otherObject, string, object]); + var dictionary = NSMutableDictionary.alloc().init(); + var date = NSDate.dateWithTimeIntervalSince1970(123456); + + addCase(cases, "js.loop.baseline", 1, function (i) { + return i; + }); + + addCase(cases, "NSObject.respondsToSelector", 1, function () { + return object.respondsToSelector("description"); + }); + + addCase(cases, "NSObject.isKindOfClass", 1, function () { + return object.isKindOfClass(NSObject); + }); + + addCase(cases, "NSObject.description.getter", 0.25, function () { + return object.description; + }); + + addCase(cases, "NSObject.hash.getter", 1, function () { + return object.hash; + }); + + addCase(cases, "NSString.length.getter", 1, function () { + return string.length; + }); + + addCase(cases, "NSString.characterAtIndex", 1, function (i) { + return string.characterAtIndex(i & 7); + }); + + addCase(cases, "NSString.compare", 1, function () { + return string.compare(compareString); + }); + + addCase(cases, "NSString.hasPrefix", 1, function () { + return string.hasPrefix(prefix); + }); + + addCase(cases, "NSArray.objectAtIndex", 1, function (i) { + return immutableArray.objectAtIndex(i & 3); + }); + + addCase(cases, "NSMutableArray.count.getter", 1, function () { + return array.count; + }); + + addCase(cases, "NSMutableArray.addRemoveObject", 0.5, function () { + array.addObject(object); + array.removeObjectAtIndex(array.count - 1); + return array.count; + }); + + addCase(cases, "NSMutableDictionary.setRemoveObject", 0.5, function () { + dictionary.setObjectForKey(object, key); + dictionary.removeObjectForKey(key); + return dictionary.count; + }); + + addCase(cases, "NSDate.timeIntervalSince1970", 1, function () { + return date.timeIntervalSince1970; + }); + + if (typeof CGPointMake === "function") { + addCase(cases, "CoreGraphics.CGPointMake", 0.5, function (i) { + return CGPointMake(i & 255, (i + 1) & 255).x; + }); + } + + return cases; + } + + var startedAt = nowMs(); + var results = []; + var skipped = []; + var cases = buildCases(); + + for (var i = 0; i < cases.length; i++) { + var item = cases[i]; + if (item.skip) { + var skippedCase = { name: item.name, error: item.error }; + skipped.push(skippedCase); + emit({ kind: "skip", name: skippedCase.name, error: skippedCase.error }); + continue; + } + var result = bench(item.name, item.factor, item.fn); + results.push(result); + emit({ + kind: "case", + name: result.name, + iterations: result.iterations, + ms: result.ms, + nsPerOp: result.nsPerOp + }); + } + + var report = { + kind: "done", + version: 1, + runtime: runtime, + variant: variant, + baseIterations: baseIterations, + warmupIterations: warmupIterations, + totalMs: nowMs() - startedAt, + sink: sink, + resultCount: results.length, + skippedCount: skipped.length + }; + + emit(report); +}()); diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js new file mode 100644 index 00000000..62760a34 --- /dev/null +++ b/benchmarks/objc-dispatch/run.js @@ -0,0 +1,939 @@ +#!/usr/bin/env node +"use strict"; + +const childProcess = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../.."); +const benchmarkFile = path.join(__dirname, "objc-dispatch-benchmarks.js"); +const marker = "NS_BENCH_RESULT:"; +const defaultLegacyRepo = "/Users/dj/.codex/worktrees/0a0e/ios"; +const defaultMetadataPath = path.join( + repoRoot, + "build/derived-data/macos-tests/Build/Products/Debug/metadata-arm64.bin" +); +const defaultWorkRoot = path.join(repoRoot, "build/benchmarks/objc-dispatch"); + +function parseArgs(argv) { + const args = { + runtime: "all", + iterations: 250000, + warmupIterations: undefined, + includeNapiGsdOff: false, + includeLegacyAotOff: false, + legacyRepo: process.env.NS_LEGACY_IOS_REPO || defaultLegacyRepo, + metadataPath: process.env.METADATA_PATH || defaultMetadataPath, + destination: process.env.IOS_DESTINATION || "", + workRoot: defaultWorkRoot, + timeoutMs: 120000, + buildTimeoutMs: 15 * 60 * 1000, + napiPackageTgz: "", + napiVariantLabel: "", + skipBuild: false, + compareResults: "" + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + const next = () => argv[++i]; + + if (arg === "--runtime") args.runtime = next(); + else if (arg.startsWith("--runtime=")) args.runtime = arg.slice("--runtime=".length); + else if (arg === "--iterations") args.iterations = Number(next()); + else if (arg.startsWith("--iterations=")) args.iterations = Number(arg.slice("--iterations=".length)); + else if (arg === "--warmup") args.warmupIterations = Number(next()); + else if (arg.startsWith("--warmup=")) args.warmupIterations = Number(arg.slice("--warmup=".length)); + else if (arg === "--legacy-repo") args.legacyRepo = path.resolve(next()); + else if (arg.startsWith("--legacy-repo=")) args.legacyRepo = path.resolve(arg.slice("--legacy-repo=".length)); + else if (arg === "--metadata-path") args.metadataPath = path.resolve(next()); + else if (arg.startsWith("--metadata-path=")) args.metadataPath = path.resolve(arg.slice("--metadata-path=".length)); + else if (arg === "--destination") args.destination = next(); + else if (arg.startsWith("--destination=")) args.destination = arg.slice("--destination=".length); + else if (arg === "--work-root") args.workRoot = path.resolve(next()); + else if (arg.startsWith("--work-root=")) args.workRoot = path.resolve(arg.slice("--work-root=".length)); + else if (arg === "--timeout-ms") args.timeoutMs = Number(next()); + else if (arg.startsWith("--timeout-ms=")) args.timeoutMs = Number(arg.slice("--timeout-ms=".length)); + else if (arg === "--build-timeout-ms") args.buildTimeoutMs = Number(next()); + else if (arg.startsWith("--build-timeout-ms=")) args.buildTimeoutMs = Number(arg.slice("--build-timeout-ms=".length)); + else if (arg === "--napi-package-tgz") args.napiPackageTgz = path.resolve(next()); + else if (arg.startsWith("--napi-package-tgz=")) args.napiPackageTgz = path.resolve(arg.slice("--napi-package-tgz=".length)); + else if (arg === "--napi-variant-label") args.napiVariantLabel = next(); + else if (arg.startsWith("--napi-variant-label=")) args.napiVariantLabel = arg.slice("--napi-variant-label=".length); + else if (arg === "--include-napi-gsd-off") args.includeNapiGsdOff = true; + else if (arg === "--include-legacy-aot-off") args.includeLegacyAotOff = true; + else if (arg === "--skip-build") args.skipBuild = true; + else if (arg === "--compare-results") args.compareResults = path.resolve(next()); + else if (arg.startsWith("--compare-results=")) args.compareResults = path.resolve(arg.slice("--compare-results=".length)); + else if (arg === "--help" || arg === "-h") { + printUsage(); + process.exit(0); + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!Number.isFinite(args.iterations) || args.iterations <= 0) { + throw new Error("--iterations must be a positive number"); + } + if (args.warmupIterations !== undefined && + (!Number.isFinite(args.warmupIterations) || args.warmupIterations < 0)) { + throw new Error("--warmup must be a non-negative number"); + } + + return args; +} + +function printUsage() { + console.log(`Usage: node benchmarks/objc-dispatch/run.js [options] + +Options: + --runtime all|napi-node|napi-ios|legacy-ios + --iterations N + --warmup N + --legacy-repo PATH Default: ${defaultLegacyRepo} + --metadata-path PATH Used by napi-node. Default: ${defaultMetadataPath} + --destination DEST_OR_UDID iOS simulator destination or UDID + --napi-package-tgz PATH @nativescript/ios package tgz for napi-ios + --napi-variant-label LABEL Prefix N-API iOS report variants with an engine/backend label + --include-napi-gsd-off Also run N-API with generated signature dispatch disabled + --include-legacy-aot-off Also run legacy iOS V8 with AOT disabled + --skip-build Reuse existing derived-data app builds + --compare-results PATH Print report and comparison tables from a saved result JSON +`); +} + +function run(command, args, options = {}) { + const result = childProcess.spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + ...options + }); + + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); + throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}\n${details}`); + } + return result; +} + +function runInherited(command, args, options = {}) { + const result = childProcess.spawnSync(command, args, { + stdio: "inherit", + ...options + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}`); + } +} + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function rmrf(target) { + fs.rmSync(target, { recursive: true, force: true }); +} + +function copyDirectoryContents(sourceDir, destDir) { + rmrf(destDir); + ensureDir(destDir); + fs.cpSync(sourceDir, destDir, { recursive: true }); +} + +function writeJsonRunner(targetPath, runtime, variant, options) { + const payload = JSON.stringify({ + iterations: options.iterations, + warmupIterations: options.warmupIterations + }); + fs.writeFileSync( + targetPath, + [ + `global.__NS_BENCHMARK_RUNTIME = ${JSON.stringify(runtime)};`, + `global.__NS_BENCHMARK_VARIANT = ${JSON.stringify(variant)};`, + `global.__NS_BENCHMARK_OPTIONS__ = ${payload};`, + `require("./${path.basename(benchmarkFile)}");`, + "" + ].join("\n") + ); +} + +function parseBenchmarkOutput(output) { + let position = 0; + const results = []; + const skipped = []; + let done = null; + let sawMarker = false; + + while (position < output.length) { + const index = output.indexOf(marker, position); + if (index === -1) { + break; + } + sawMarker = true; + const parsed = parseJsonAfterMarker(output, index); + position = parsed.nextPosition; + if (!parsed.value) { + continue; + } + + const value = parsed.value; + if (value.kind === "case") { + results.push({ + name: value.name, + iterations: value.iterations, + ms: value.ms, + nsPerOp: value.nsPerOp + }); + } else if (value.kind === "skip") { + skipped.push({ name: value.name, error: value.error }); + } else if (value.kind === "done") { + done = value; + } else if (value.results) { + return value; + } + } + + if (done) { + return { + version: done.version, + runtime: done.runtime, + variant: done.variant, + baseIterations: done.baseIterations, + warmupIterations: done.warmupIterations, + totalMs: done.totalMs, + sink: done.sink, + results, + skipped + }; + } + + if (!sawMarker) { + throw new Error(`Benchmark marker not found in output:\n${output.slice(-4000)}`); + } + + throw new Error(`Benchmark done marker not found in output:\n${output.slice(-4000)}`); +} + +function parseJsonAfterMarker(output, index) { + const afterMarker = output.slice(index + marker.length); + const jsonStart = afterMarker.indexOf("{"); + if (jsonStart === -1) { + return { value: null, nextPosition: index + marker.length }; + } + + let depth = 0; + let inString = false; + let escaped = false; + for (let i = jsonStart; i < afterMarker.length; i++) { + const ch = afterMarker[i]; + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === "\"") { + inString = false; + } + continue; + } + + if (ch === "\"") { + inString = true; + } else if (ch === "{") { + depth++; + } else if (ch === "}") { + depth--; + if (depth === 0) { + try { + return { + value: JSON.parse(afterMarker.slice(jsonStart, i + 1)), + nextPosition: index + marker.length + i + 1 + }; + } catch (_) { + return { value: null, nextPosition: index + marker.length + i + 1 }; + } + } + } + } + + return { value: null, nextPosition: index + marker.length }; +} + +function printReport(report) { + console.log(`\n${report.runtime} (${report.variant})`); + console.log("case".padEnd(40) + "ops".padStart(12) + "ms".padStart(12) + "ns/op".padStart(14)); + for (const result of report.results) { + console.log( + result.name.padEnd(40) + + String(result.iterations).padStart(12) + + result.ms.toFixed(2).padStart(12) + + result.nsPerOp.toFixed(1).padStart(14) + ); + } + if (report.skipped && report.skipped.length > 0) { + console.log("skipped: " + report.skipped.map((item) => item.name).join(", ")); + } +} + +function reportLabel(report) { + return `${report.runtime} (${report.variant})`; +} + +function labeledNapiVariant(options, variant) { + return options.napiVariantLabel ? `${options.napiVariantLabel} ${variant}` : variant; +} + +function napiVariantGroup(variant) { + const match = String(variant).match(/^(?:(.*)\s+)?(gsd-on|gsd-off)$/); + if (!match) { + return null; + } + return { + label: match[1] || "", + kind: match[2] + }; +} + +function resultMap(report) { + return new Map(report.results.map((result) => [result.name, result])); +} + +function formatSigned(value, digits = 2) { + const fixed = Math.abs(value).toFixed(digits); + if (value > 0) { + return `+${fixed}`; + } + if (value < 0) { + return `-${fixed}`; + } + return fixed; +} + +function formatPercent(value) { + return `${formatSigned(value, 1)}%`; +} + +function comparisonWinner(deltaNsPerOp) { + if (Math.abs(deltaNsPerOp) < 0.05) { + return "tie"; + } + return deltaNsPerOp < 0 ? "comparison" : "baseline"; +} + +function printTotalsComparison(reports, baseline) { + console.log(`\nTotal comparison, baseline ${reportLabel(baseline)}`); + console.log("| runtime | total ms | delta ms | ratio | relative |"); + console.log("|---|---:|---:|---:|---:|"); + for (const report of reports) { + const deltaMs = report.totalMs - baseline.totalMs; + const ratio = report.totalMs / baseline.totalMs; + const relative = (baseline.totalMs / report.totalMs) * 100; + console.log( + `| ${reportLabel(report)} | ${report.totalMs.toFixed(2)} | ${formatSigned(deltaMs)} | ${ratio.toFixed(2)}x | ${relative.toFixed(1)}% |` + ); + } +} + +function printPairComparison(baseline, comparison) { + const baselineResults = resultMap(baseline); + const comparisonResults = resultMap(comparison); + console.log(`\n${reportLabel(comparison)} vs ${reportLabel(baseline)}`); + console.log("| case | baseline ms | comparison ms | delta ms | baseline ns/op | comparison ns/op | delta ns/op | delta % | winner |"); + console.log("|---|---:|---:|---:|---:|---:|---:|---:|---|"); + for (const baselineResult of baseline.results) { + const comparisonResult = comparisonResults.get(baselineResult.name); + if (!comparisonResult) { + continue; + } + const deltaMs = comparisonResult.ms - baselineResult.ms; + const deltaNsPerOp = comparisonResult.nsPerOp - baselineResult.nsPerOp; + const deltaPercent = (deltaNsPerOp / baselineResult.nsPerOp) * 100; + console.log( + `| ${baselineResult.name} | ${baselineResult.ms.toFixed(2)} | ${comparisonResult.ms.toFixed(2)} | ${formatSigned(deltaMs)} | ${baselineResult.nsPerOp.toFixed(1)} | ${comparisonResult.nsPerOp.toFixed(1)} | ${formatSigned(deltaNsPerOp, 1)} | ${formatPercent(deltaPercent)} | ${comparisonWinner(deltaNsPerOp)} |` + ); + } +} + +function printComparisons(reports) { + if (!Array.isArray(reports) || reports.length < 2) { + return; + } + + const baseline = reports[0]; + printTotalsComparison(reports, baseline); + + const napiGroups = new Map(); + for (const report of reports) { + if (report.runtime !== "napi-ios") { + continue; + } + const group = napiVariantGroup(report.variant); + if (!group) { + continue; + } + const key = group.label; + if (!napiGroups.has(key)) { + napiGroups.set(key, new Map()); + } + napiGroups.get(key).set(group.kind, report); + } + + let napiGsdOn = null; + for (const group of napiGroups.values()) { + const gsdOn = group.get("gsd-on"); + const gsdOff = group.get("gsd-off"); + if (gsdOn && !napiGsdOn) { + napiGsdOn = gsdOn; + } + if (gsdOn && gsdOff) { + printPairComparison(gsdOn, gsdOff); + } + } + + const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); + if (napiGsdOn && legacyAotOn) { + printPairComparison(napiGsdOn, legacyAotOn); + } + + const legacyAotOff = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-off"); + if (napiGsdOn && legacyAotOff) { + printPairComparison(napiGsdOn, legacyAotOff); + } +} + +function printSavedResultsComparison(resultsPath) { + const parsed = JSON.parse(fs.readFileSync(resultsPath, "utf8")); + const reports = parsed.reports || []; + for (const report of reports) { + printReport(report); + } + printComparisons(reports); +} + +function runNapiNode(options, variant) { + if (!fs.existsSync(options.metadataPath)) { + throw new Error(`Metadata file not found: ${options.metadataPath}`); + } + + const runnerDir = path.join(options.workRoot, "node"); + ensureDir(runnerDir); + fs.copyFileSync(benchmarkFile, path.join(runnerDir, path.basename(benchmarkFile))); + + const runnerPath = path.join(runnerDir, `run-${variant}.cjs`); + const benchmarkOptions = { + iterations: options.iterations, + warmupIterations: options.warmupIterations + }; + fs.writeFileSync( + runnerPath, + [ + `global.__NS_BENCHMARK_RUNTIME = "napi-node";`, + `global.__NS_BENCHMARK_VARIANT = ${JSON.stringify(variant)};`, + `global.__NS_BENCHMARK_OPTIONS__ = ${JSON.stringify(benchmarkOptions)};`, + `import(${JSON.stringify(pathToFileUrl(path.join(repoRoot, "packages/macos-node-api/index.mjs")))}).then(() => {`, + ` require(${JSON.stringify(path.join(runnerDir, path.basename(benchmarkFile)))});`, + `}).catch((error) => { console.error(error && error.stack || error); process.exit(1); });`, + "" + ].join("\n") + ); + + const env = { ...process.env, METADATA_PATH: options.metadataPath }; + if (variant === "gsd-off") { + // Current runtime disables generated signature dispatch when this value is exactly "0". + env.NS_DISABLE_GSD = "0"; + } else { + delete env.NS_DISABLE_GSD; + } + + const result = run(process.execPath, [runnerPath], { cwd: repoRoot, env, timeout: options.timeoutMs }); + return parseBenchmarkOutput(result.stdout + result.stderr); +} + +function pathToFileUrl(filePath) { + return new URL(`file://${filePath}`).href; +} + +function destinationToUdid(destination) { + if (!destination) { + return ""; + } + const idMatch = destination.match(/id=([0-9A-Fa-f-]{36})/); + if (idMatch) { + return idMatch[1]; + } + if (/^[0-9A-Fa-f-]{36}$/.test(destination)) { + return destination; + } + return ""; +} + +function pickSimulator(destination) { + const explicit = destinationToUdid(destination); + if (explicit) { + return explicit; + } + + const result = run("xcrun", ["simctl", "list", "devices", "available", "--json"]); + const parsed = JSON.parse(result.stdout); + const devices = []; + for (const runtimeName of Object.keys(parsed.devices || {})) { + for (const device of parsed.devices[runtimeName]) { + if (device.isAvailable && /iPhone/.test(device.name)) { + devices.push(device); + } + } + } + + const booted = devices.find((device) => device.state === "Booted"); + const preferred = booted || devices.find((device) => /Pro/.test(device.name)) || devices[0]; + if (!preferred) { + throw new Error("No available iPhone simulator found"); + } + return preferred.udid; +} + +function bootSimulator(udid) { + const boot = childProcess.spawnSync("xcrun", ["simctl", "boot", udid], { encoding: "utf8" }); + if (boot.status !== 0 && !/Unable to boot device in current state: Booted/.test(boot.stderr || "")) { + throw new Error(`Unable to boot simulator ${udid}:\n${boot.stderr || boot.stdout}`); + } + runInherited("xcrun", ["simctl", "bootstatus", udid, "-b"], { timeout: 180000 }); +} + +function findBuiltApp(derivedDataPath, appName) { + const productsRoot = path.join(derivedDataPath, "Build/Products"); + const queue = [productsRoot]; + while (queue.length > 0) { + const current = queue.pop(); + if (!fs.existsSync(current)) { + continue; + } + const stats = fs.statSync(current); + if (stats.isDirectory() && path.basename(current) === `${appName}.app`) { + return current; + } + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + throw new Error(`Built app not found under ${productsRoot}`); +} + +function launchAndCollect(udid, bundleId, options, env = {}) { + return new Promise((resolve, reject) => { + let output = ""; + let settled = false; + const children = []; + const launchEnv = { ...process.env }; + for (const [key, value] of Object.entries(env)) { + launchEnv[`SIMCTL_CHILD_${key}`] = value; + } + + const logChild = childProcess.spawn( + "xcrun", + [ + "simctl", "spawn", udid, + "log", "stream", + "--style", "compact", + "--level", "debug", + "--predicate", `eventMessage CONTAINS "${marker}"` + ], + { env: process.env } + ); + children.push(logChild); + + const child = childProcess.spawn( + "xcrun", + ["simctl", "launch", "--console", "--terminate-running-process", udid, bundleId], + { env: launchEnv } + ); + children.push(child); + + const timeout = setTimeout(() => { + if (settled) { + return; + } + settled = true; + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + reject(new Error(`Timed out waiting for benchmark marker from ${bundleId}`)); + }, options.timeoutMs); + + function settleWithReport(report) { + settled = true; + clearTimeout(timeout); + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + resolve(report); + } + + function onData(data) { + const text = data.toString(); + output += text; + process.stdout.write(text); + if (!settled && output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + } catch (_) { + // log stream prints the predicate itself before app logs; wait for the + // actual console message containing marker JSON. + } + } + } + + logChild.stdout.on("data", onData); + logChild.stderr.on("data", onData); + child.stdout.on("data", onData); + child.stderr.on("data", onData); + for (const activeChild of children) { + activeChild.on("error", (error) => { + if (!settled) { + settled = true; + clearTimeout(timeout); + reject(error); + } + }); + } + child.on("exit", (code) => { + if (!settled) { + if (output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + } catch (_) { + // The unified log stream may still deliver the actual message after + // simctl launch exits. + } + } + } + }); + }); +} + +function xcodebuild(args, cwd, timeoutMs) { + runInherited("xcodebuild", args, { + cwd, + timeout: timeoutMs, + env: { + ...process.env, + PATH: ["/opt/homebrew/bin", "/usr/local/bin", process.env.PATH || ""].join(":"), + NSUnbufferedIO: "YES" + } + }); +} + +function installApp(udid, appPath, bundleId) { + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + childProcess.spawnSync("xcrun", ["simctl", "uninstall", udid, bundleId], { stdio: "ignore" }); + runInherited("xcrun", ["simctl", "install", udid, appPath], { timeout: 120000 }); +} + +function readAppBundleId(appPath, fallback) { + const plistPath = path.join(appPath, "Info.plist"); + if (!fs.existsSync(plistPath)) { + return fallback; + } + const result = childProcess.spawnSync( + "/usr/libexec/PlistBuddy", + ["-c", "Print :CFBundleIdentifier", plistPath], + { encoding: "utf8" } + ); + if (result.status === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + return fallback; +} + +async function runLegacyIOS(options, variant = "aot-on") { + const appName = "TestRunner"; + let bundleId = "com.descendra.TestRunner"; + const appDir = path.join(options.legacyRepo, "TestRunner/app"); + const indexPath = path.join(appDir, "index.js"); + const copiedBenchmarkPath = path.join(appDir, path.basename(benchmarkFile)); + const originalIndex = fs.readFileSync(indexPath, "utf8"); + const derivedDataPath = path.join(options.workRoot, "derived-data/legacy-ios"); + const udid = pickSimulator(options.destination); + + ensureDir(path.dirname(copiedBenchmarkPath)); + fs.copyFileSync(benchmarkFile, copiedBenchmarkPath); + writeJsonRunner(indexPath, "legacy-ios", variant, options); + + try { + bootSimulator(udid); + if (!options.skipBuild) { + xcodebuild([ + "-project", "v8ios.xcodeproj", + "-scheme", "TestRunner", + "-configuration", "Release", + "-destination", `platform=iOS Simulator,id=${udid}`, + "-derivedDataPath", derivedDataPath, + "CODE_SIGNING_ALLOWED=NO", + "CODE_SIGNING_REQUIRED=NO", + "CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION=NO", + "CLANG_WARN_NULLABILITY_COMPLETENESS=NO", + "OTHER_CPLUSPLUSFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "OTHER_CFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "build", + "-quiet" + ], options.legacyRepo, options.buildTimeoutMs); + } + let appPath; + try { + appPath = findBuiltApp(derivedDataPath, appName); + } catch (_) { + appPath = path.join(options.legacyRepo, "build/Release-iphonesimulator", `${appName}.app`); + if (!fs.existsSync(appPath)) { + throw _; + } + } + if (options.skipBuild) { + copyDirectoryContents(appDir, path.join(appPath, "app")); + } + bundleId = readAppBundleId(appPath, bundleId); + installApp(udid, appPath, bundleId); + const launchEnv = variant === "aot-off" ? { NS_DISABLE_AOT: "1" } : {}; + return await launchAndCollect(udid, bundleId, options, launchEnv); + } finally { + fs.writeFileSync(indexPath, originalIndex); + rmrf(copiedBenchmarkPath); + } +} + +function findDefaultNapiPackage() { + const distDir = path.join(repoRoot, "packages/ios/dist"); + const names = fs.readdirSync(distDir) + .filter((name) => /^nativescript-ios-.*\.tgz$/.test(name)) + .sort(); + if (names.length === 0) { + throw new Error(`No @nativescript/ios package tgz found in ${distDir}`); + } + return path.join(distDir, names[names.length - 1]); +} + +function replaceInTextFiles(root, search, replacement) { + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + const stats = fs.lstatSync(current); + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + continue; + } + if (!stats.isFile()) { + continue; + } + const buffer = fs.readFileSync(current); + if (buffer.includes(0)) { + continue; + } + const text = buffer.toString("utf8"); + if (text.includes(search)) { + fs.writeFileSync(current, text.split(search).join(replacement)); + } + } +} + +function renamePlaceholderPaths(root, search, replacement) { + const entries = []; + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + entries.push(current); + if (fs.lstatSync(current).isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + + entries.sort((a, b) => b.length - a.length); + for (const current of entries) { + const base = path.basename(current); + if (!base.includes(search) || !fs.existsSync(current)) { + continue; + } + fs.renameSync(current, path.join(path.dirname(current), base.split(search).join(replacement))); + } +} + +function scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant = variant) { + const appName = "NativeScriptDispatchBench"; + const bundleId = "org.nativescript.bench.dispatch.napi"; + const tgz = packageTgz || options.napiPackageTgz || findDefaultNapiPackage(); + const root = path.join(options.workRoot, "apps", `napi-ios-${variant}`); + rmrf(root); + ensureDir(root); + run("tar", ["-xzf", tgz, "-C", root]); + + const frameworkRoot = path.join(root, "package/framework"); + const projectPath = path.join(frameworkRoot, `${appName}.xcodeproj`); + const appSourceRoot = path.join(frameworkRoot, appName); + + fs.renameSync( + path.join(frameworkRoot, "__PROJECT_NAME__.xcodeproj"), + projectPath + ); + fs.renameSync( + path.join(frameworkRoot, "__PROJECT_NAME__"), + appSourceRoot + ); + + for (const name of fs.readdirSync(appSourceRoot)) { + if (name.includes("__PROJECT_NAME__")) { + fs.renameSync( + path.join(appSourceRoot, name), + path.join(appSourceRoot, name.replaceAll("__PROJECT_NAME__", appName)) + ); + } + } + + renamePlaceholderPaths(frameworkRoot, "__PROJECT_NAME__", appName); + replaceInTextFiles(frameworkRoot, "__PROJECT_NAME__", appName); + replaceInTextFiles(frameworkRoot, "config.LogToSystemConsole = isDebug;", "config.LogToSystemConsole = YES;"); + fs.writeFileSync(path.join(frameworkRoot, "plugins-debug.xcconfig"), "\n"); + fs.writeFileSync(path.join(frameworkRoot, "plugins-release.xcconfig"), "\n"); + writeInfoPlist(path.join(appSourceRoot, `${appName}-Info.plist`)); + + const appDir = path.join(appSourceRoot, "app"); + ensureDir(appDir); + fs.writeFileSync(path.join(appDir, "package.json"), JSON.stringify({ main: "index" }, null, 2) + "\n"); + fs.copyFileSync(benchmarkFile, path.join(appDir, path.basename(benchmarkFile))); + writeJsonRunner(path.join(appDir, "index.js"), "napi-ios", reportVariant, options); + + const zipPath = path.join(frameworkRoot, "internal/XCFrameworks.zip"); + run("unzip", ["-q", "-o", zipPath, "-d", path.join(frameworkRoot, "internal")]); + + return { appName, bundleId, frameworkRoot, projectPath, appDir }; +} + +function writeInfoPlist(plistPath) { + fs.writeFileSync(plistPath, ` + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + +`); +} + +async function runNapiIOS(options, variant, packageTgz, reportVariant = variant) { + const app = scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant); + const derivedDataPath = path.join(options.workRoot, `derived-data/napi-ios-${variant}`); + const udid = pickSimulator(options.destination); + + bootSimulator(udid); + if (!options.skipBuild) { + xcodebuild([ + "-project", app.projectPath, + "-scheme", app.appName, + "-configuration", "Release", + "-destination", `platform=iOS Simulator,id=${udid}`, + "-derivedDataPath", derivedDataPath, + "CODE_SIGNING_ALLOWED=NO", + "CODE_SIGNING_REQUIRED=NO", + `PRODUCT_BUNDLE_IDENTIFIER=${app.bundleId}`, + "ARCHS=arm64", + "ONLY_ACTIVE_ARCH=YES", + "EXCLUDED_ARCHS=", + "build", + "-quiet" + ], app.frameworkRoot, options.buildTimeoutMs); + } + + const appPath = findBuiltApp(derivedDataPath, app.appName); + if (options.skipBuild) { + copyDirectoryContents(app.appDir, path.join(appPath, "app")); + } + installApp(udid, appPath, app.bundleId); + const launchEnv = variant === "gsd-off" ? { NS_DISABLE_GSD: "0" } : {}; + return await launchAndCollect(udid, app.bundleId, options, launchEnv); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + if (options.compareResults) { + printSavedResultsComparison(options.compareResults); + return; + } + + ensureDir(options.workRoot); + + const reports = []; + const runtimes = options.runtime === "all" + ? ["napi-node", "napi-ios", "legacy-ios"] + : options.runtime.split(",").map((item) => item.trim()).filter(Boolean); + + for (const runtime of runtimes) { + if (runtime === "napi-node") { + reports.push(runNapiNode(options, "gsd-on")); + if (options.includeNapiGsdOff) { + reports.push(runNapiNode(options, "gsd-off")); + } + } else if (runtime === "napi-ios") { + reports.push(await runNapiIOS(options, "gsd-on", undefined, labeledNapiVariant(options, "gsd-on"))); + if (options.includeNapiGsdOff) { + reports.push(await runNapiIOS(options, "gsd-off", undefined, labeledNapiVariant(options, "gsd-off"))); + } + } else if (runtime === "legacy-ios") { + reports.push(await runLegacyIOS(options, "aot-on")); + if (options.includeLegacyAotOff) { + reports.push(await runLegacyIOS({ ...options, skipBuild: true }, "aot-off")); + } + } else { + throw new Error(`Unknown runtime: ${runtime}`); + } + } + + for (const report of reports) { + printReport(report); + } + printComparisons(reports); + + const outPath = path.join(options.workRoot, `results-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); + fs.writeFileSync(outPath, JSON.stringify({ createdAt: new Date().toISOString(), reports }, null, 2) + "\n"); + console.log(`\nWrote ${outPath}`); +} + +main().catch((error) => { + console.error(error && error.stack ? error.stack : error); + process.exit(1); +}); diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 94069b62..3a24c0ce 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -17,6 +17,7 @@ namespace { enum class DispatchKind : uint8_t { ObjCMethod = 1, CFunction = 2, + BlockInvoke = 3, }; struct SignatureUse { @@ -385,8 +386,73 @@ std::string toBase36(size_t value) { std::string makeNapiWrapperName(DispatchKind kind, size_t index) { std::ostringstream stream; - stream << "d" << (kind == DispatchKind::ObjCMethod ? "o" : "c") - << toBase36(index); + stream << "dn"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +std::string makeV8WrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dv"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +std::string makeEngineDirectWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "de"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +std::string makePreparedWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dp"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); return stream.str(); } @@ -427,6 +493,102 @@ bool isFastManagedNapiKind(MDTypeKind kind) { } } +bool fastV8ArgConversionNeedsContext(MDTypeKind kind) { + switch (kind) { + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canSetV8ReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canTrySetV8ObjectReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, + const std::string& valueExpr) { + switch (kind) { + case mdTypeBool: + out << " info.GetReturnValue().Set(" << valueExpr << " != 0);\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeUShort: + case mdTypeUInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " setV8DispatchInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " setV8DispatchUInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + default: + break; + } +} + bool argKindMayNeedCleanup(MDTypeKind kind) { switch (kind) { case mdTypeAnyObject: @@ -603,43 +765,601 @@ void writeFastNapiArgConversion(std::ostringstream& out, const MDTypeInfo* type, << ");\n"; break; } - case mdTypeDouble: { - out << " if (napi_get_value_double(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; + case mdTypeDouble: { + out << " if (napi_get_value_double(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index + << ")) {\n"; + out << " arg" << index << " = 0.0;\n"; + out << " }\n"; + break; + } + case mdTypeBool: { + out << " bool boolValue" << index << " = false;\n"; + out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(boolValue" << index + << " ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} + +void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, + size_t index, bool hasCleanupArgs) { + const char* failCleanup = hasCleanupArgs ? " cleanupManagedArgs();\n" : ""; + if (type == nullptr) { + out << failCleanup; + out << " return false;\n"; + return; + } + + switch (type->kind) { + case mdTypeChar: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUChar: + case mdTypeUInt8: { + out << " uint32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Uint32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeSShort: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUShort: { + out << " if (!TryFastConvertV8UInt16Argument(env, info[" << index + << "], &arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSInt: { + out << " if (!info[" << index << "]->Int32Value(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeUInt: { + out << " if (!info[" << index << "]->Uint32Value(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSLong: + case mdTypeSInt64: { + out << " if (info[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = info[" << index + << "].As()->Int64Value(&lossless" << index << ");\n"; + out << " } else if (!info[" << index + << "]->IntegerValue(context).To(&arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeULong: + case mdTypeUInt64: { + out << " if (info[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = info[" << index + << "].As()->Uint64Value(&lossless" << index << ");\n"; + out << " } else {\n"; + out << " int64_t signedValue" << index << " = 0;\n"; + out << " if (!info[" << index + << "]->IntegerValue(context).To(&signedValue" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(signedValue" + << index << ");\n"; + out << " }\n"; + break; + } + case mdTypeFloat: { + out << " double tmpArg" << index << " = 0.0;\n"; + out << " if (!info[" << index << "]->NumberValue(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeDouble: { + out << " if (!info[" << index << "]->NumberValue(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index + << ")) {\n"; + out << " arg" << index << " = 0.0;\n"; + out << " }\n"; + break; + } + case mdTypeBool: { + out << " if (!info[" << index << "]->IsBoolean()) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(info[" << index + << "]->BooleanValue(info.GetIsolate()) ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} + +const char* engineDirectConverterMacroForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT"; + case mdTypeChar: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT"; + case mdTypeUChar: + case mdTypeUInt8: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT"; + case mdTypeSShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT"; + case mdTypeUShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT"; + case mdTypeSInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT"; + case mdTypeUInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT"; + case mdTypeSLong: + case mdTypeSInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT"; + case mdTypeULong: + case mdTypeUInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT"; + case mdTypeFloat: + return "NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT"; + case mdTypeDouble: + return "NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT"; + case mdTypeSelector: + return "NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT"; + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return "NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT"; + default: + return "NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"; + } +} + +bool engineDirectConverterTakesKind(MDTypeKind kind) { + switch (kind) { + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return engineDirectConverterMacroForKind(kind) == + std::string("NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"); + } +} + +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index) { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + const char* converter = engineDirectConverterMacroForKind(type->kind); + out << " if (!" << converter << "(env, "; + if (engineDirectConverterTakesKind(type->kind)) { + out << "static_cast(" << static_cast(type->kind) + << "), "; + } + out << "argv[" << index << "], &arg" << index << ")) {\n"; + if (argKindMayNeedCleanup(type->kind)) { + out << " cif->argTypes[" << index << "]->toNative(env, argv[" << index + << "], &arg" << index << ", &shouldFree" << index + << ", &shouldFreeAny);\n"; + } else { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, argv[" << index + << "], &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; +} + +void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, "; + } + out << "const napi_value* argv, void* rvalue) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + std::vector cleanupArgIndexes; + std::vector noCleanupManagedArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + noCleanupManagedArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (!isFastDirectNapiKind(argTypeInfos[i]->kind)) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } else { + noCleanupManagedArgIndexes.push_back(i); + } + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (!noCleanupManagedArgIndexes.empty()) { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (!isFastDirectNapiKind(argTypeInfos[i]->kind) && + argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " // Returning an argument pointer keeps ownership " + "with " + "the return value.\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { + writeFastNapiArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + out << " if (!TryFastConvertNapiArgument(env, static_cast(" + << static_cast(argTypeInfos[i]->kind) << "), argv[" << i + << "], &arg" << i << ")) {\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; + } else { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + } + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + + out << " return true;\n"; + out << "}\n\n"; +} + +void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind == DispatchKind::BlockInvoke) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, "; + } + out << "const napi_value* argv, void* rvalue) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + + std::vector cleanupArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; } - out << " return false;\n"; - out << " }\n"; - out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index - << ")) {\n"; - out << " arg" << index << " = 0.0;\n"; - out << " }\n"; - break; + out << " }\n"; } - case mdTypeBool: { - out << " bool boolValue" << index << " = false;\n"; - out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(boolValue" << index - << " ? 1 : 0);\n"; - break; + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + writeEngineDirectArgConversion(out, argTypeInfos[i], i); + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; } - default: - out << failCleanup; - out << " return false;\n"; - break; + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return true;\n"; + out << "}\n\n"; } -void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, - const std::string& wrapperName, - const MDSignature* signature) { +void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind == DispatchKind::BlockInvoke) { + return; + } + std::string returnType; if (!mapTypeToCpp(signature->returnType, &returnType, true)) { return; @@ -661,9 +1381,26 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, out << "static inline bool " << wrapperName << "(napi_env env, Cif* cif, void* fnptr, "; if (kind == DispatchKind::ObjCMethod) { - out << "id self, SEL selector, "; + out << "id self, SEL selector, void* bridgeState, bool returnOwned, " + "bool receiverIsClass, bool propertyAccess, "; + } + out << "const v8::FunctionCallbackInfo& info, void* rvalue, " + "bool* didSetReturnValue) {\n"; + if (!argTypes.empty()) { + out << " if (info.Length() < " << argTypes.size() << ") {\n"; + out << " return false;\n"; + out << " }\n"; + } + bool needsContext = false; + for (const auto* arg : argTypeInfos) { + if (arg != nullptr && fastV8ArgConversionNeedsContext(arg->kind)) { + needsContext = true; + break; + } + } + if (needsContext) { + out << " v8::Local context = info.GetIsolate()->GetCurrentContext();\n"; } - out << "const napi_value* argv, void* rvalue) {\n"; out << " using Fn = " << returnType << " (*)("; bool first = true; @@ -680,6 +1417,7 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, } out << ");\n"; out << " auto fn = reinterpret_cast(fnptr);\n"; + std::vector cleanupArgIndexes; std::vector noCleanupManagedArgIndexes; cleanupArgIndexes.reserve(argTypes.size()); @@ -694,6 +1432,11 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, } } const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + const bool setsReturnDirectly = + canSetV8ReturnDirectly(signature->returnType->kind); + const bool triesObjectReturnDirectly = + kind == DispatchKind::ObjCMethod && + canTrySetV8ObjectReturnDirectly(signature->returnType->kind); if (hasCleanupArgs) { out << " bool shouldFreeAny = false;\n"; } @@ -730,9 +1473,6 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, out << " if (returnPointerValue != nullptr && " "*reinterpret_cast(&arg" << i << ") == returnPointerValue) {\n"; - out << " // Returning an argument pointer keeps ownership " - "with " - "the return value.\n"; out << " } else {\n"; out << " cif->argTypes[" << i << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; @@ -749,31 +1489,37 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, for (size_t i = 0; i < argTypes.size(); i++) { if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { - writeFastNapiArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { - out << " if (!TryFastConvertNapiArgument(env, static_cast(" - << static_cast(argTypeInfos[i]->kind) << "), argv[" << i + out << " if (!TryFastConvertV8Argument(env, static_cast(" + << static_cast(argTypeInfos[i]->kind) << "), info[" << i << "], &arg" << i << ")) {\n"; if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; } else { out << " ignoredShouldFree = false;\n"; out << " ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; } out << " }\n"; } else { if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; } else { out << " ignoredShouldFree = false;\n"; out << " ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; } } @@ -797,6 +1543,19 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, if (returnType == "void") { out << " " << callExpr.str() << ";\n"; + out << " *didSetReturnValue = true;\n"; + } else if (setsReturnDirectly) { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeV8DirectReturnValue(out, signature->returnType->kind, "nativeResult"); + out << " *didSetReturnValue = true;\n"; + } else if (triesObjectReturnDirectly) { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + out << " if (TryFastSetV8GeneratedObjCObjectReturnValue(env, info, cif, bridgeState, self, " + "selector, nativeResult, returnOwned, receiverIsClass, propertyAccess)) {\n"; + out << " *didSetReturnValue = true;\n"; + out << " }\n"; } else { out << " nativeResult = " << callExpr.str() << ";\n"; out << " *reinterpret_cast<" << returnType @@ -810,6 +1569,127 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, out << "}\n\n"; } +void writePreparedWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind != DispatchKind::BlockInvoke) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypes.push_back(argType); + } + + out << "static inline void " << wrapperName + << "(void* fnptr, void** avalues, void* rvalue) {\n"; + out << " using Fn = " << returnType << " (*)(void*"; + for (const auto& argType : argTypes) { + out << ", " << argType; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + out << " void* block = *reinterpret_cast(avalues[0]);\n"; + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << " = *reinterpret_cast<" + << argTypes[i] << "*>(avalues[" << (i + 1) << "]);\n"; + } + + std::ostringstream callExpr; + callExpr << "fn(block"; + for (size_t i = 0; i < argTypes.size(); i++) { + callExpr << ", arg" << i; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = " << callExpr.str() << ";\n"; + } + out << "}\n\n"; +} + +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses); + +void collectBlockUsesFromType(const MDTypeInfo* type, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses) { + if (type == nullptr || active == nullptr || uses == nullptr) { + return; + } + + switch (type->kind) { + case mdTypeArray: + case mdTypeVector: + case mdTypeExtVector: + case mdTypeComplex: + collectBlockUsesFromType(type->elementType, signatures, active, uses); + break; + + case mdTypePointer: + collectBlockUsesFromType(type->pointeeType, signatures, active, uses); + break; + + case mdTypeBlock: + if (type->signatureOffset != MD_SECTION_OFFSET_NULL) { + uses->push_back({DispatchKind::BlockInvoke, type->signatureOffset, 0}); + collectBlockUsesFromSignature(type->signatureOffset, signatures, active, + uses); + } + break; + + case mdTypeFunctionPointer: + if (type->signatureOffset != MD_SECTION_OFFSET_NULL) { + collectBlockUsesFromSignature(type->signatureOffset, signatures, active, + uses); + } + break; + + default: + break; + } +} + +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses) { + if (active == nullptr || uses == nullptr || + signatureOffset == MD_SECTION_OFFSET_NULL || + active->find(signatureOffset) != active->end()) { + return; + } + + auto it = signatures.find(signatureOffset); + if (it == signatures.end() || it->second == nullptr) { + return; + } + + active->insert(signatureOffset); + const MDSignature* signature = it->second; + collectBlockUsesFromType(signature->returnType, signatures, active, uses); + for (const auto* arg : signature->arguments) { + collectBlockUsesFromType(arg, signatures, active, uses); + } + active->erase(signatureOffset); +} + void collectMethodUses(const std::vector& members, std::vector* uses) { if (uses == nullptr) { @@ -879,10 +1759,24 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collectMethodUses(protocol->members, &signatureUses); } + const auto rootSignatureUses = signatureUses; + std::unordered_set activeBlockSignatures; + for (const auto& use : rootSignatureUses) { + collectBlockUsesFromSignature(use.signatureOffset, writer.signatures, + &activeBlockSignatures, &signatureUses); + } + std::unordered_map> wrappersByKey; + std::unordered_map> + preparedWrappersByKey; std::unordered_map objcNapiEntries; std::unordered_map cFunctionNapiEntries; + std::unordered_map objcEngineDirectEntries; + std::unordered_map cFunctionEngineDirectEntries; + std::unordered_map objcV8Entries; + std::unordered_map cFunctionV8Entries; + std::unordered_map blockPreparedEntries; std::unordered_map dispatchEncoding; std::unordered_set collidedDispatchIds; @@ -912,6 +1806,11 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collidedDispatchIds.insert(dispatchId); objcNapiEntries.erase(dispatchId); cFunctionNapiEntries.erase(dispatchId); + objcEngineDirectEntries.erase(dispatchId); + cFunctionEngineDirectEntries.erase(dispatchId); + objcV8Entries.erase(dispatchId); + cFunctionV8Entries.erase(dispatchId); + blockPreparedEntries.erase(dispatchId); dispatchEncoding.erase(dispatchId); continue; } @@ -924,12 +1823,21 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, if (wrapperKey.empty()) { continue; } - wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); if (use.kind == DispatchKind::ObjCMethod) { + wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); objcNapiEntries.emplace(dispatchId, wrapperKey); - } else { + objcEngineDirectEntries.emplace(dispatchId, wrapperKey); + objcV8Entries.emplace(dispatchId, wrapperKey); + } else if (use.kind == DispatchKind::CFunction) { + wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); cFunctionNapiEntries.emplace(dispatchId, wrapperKey); + cFunctionEngineDirectEntries.emplace(dispatchId, wrapperKey); + cFunctionV8Entries.emplace(dispatchId, wrapperKey); + } else if (use.kind == DispatchKind::BlockInvoke) { + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + blockPreparedEntries.emplace(dispatchId, wrapperKey); } } @@ -940,6 +1848,14 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, wrappers.begin(), wrappers.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector< + std::pair>> + preparedWrappers(preparedWrappersByKey.begin(), + preparedWrappersByKey.end()); + std::sort( + preparedWrappers.begin(), preparedWrappers.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::unordered_map wrapperNameByKey; wrapperNameByKey.reserve(wrappers.size()); size_t wrapperIndex = 0; @@ -949,6 +1865,34 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, makeNapiWrapperName(wrapper.second.first, wrapperIndex++)); } + std::unordered_map v8WrapperNameByKey; + v8WrapperNameByKey.reserve(wrappers.size()); + size_t v8WrapperIndex = 0; + for (const auto& wrapper : wrappers) { + v8WrapperNameByKey.emplace( + wrapper.first, + makeV8WrapperName(wrapper.second.first, v8WrapperIndex++)); + } + + std::unordered_map engineDirectWrapperNameByKey; + engineDirectWrapperNameByKey.reserve(wrappers.size()); + size_t engineDirectWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + engineDirectWrapperNameByKey.emplace( + wrapper.first, + makeEngineDirectWrapperName(wrapper.second.first, + engineDirectWrapperIndex++)); + } + + std::unordered_map preparedWrapperNameByKey; + preparedWrapperNameByKey.reserve(preparedWrappers.size()); + size_t preparedWrapperIndex = 0; + for (const auto& wrapper : preparedWrappers) { + preparedWrapperNameByKey.emplace( + wrapper.first, + makePreparedWrapperName(wrapper.second.first, preparedWrapperIndex++)); + } + std::vector> sortedObjCNapiEntries( objcNapiEntries.begin(), objcNapiEntries.end()); std::sort( @@ -961,20 +1905,241 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, sortedCFunctionNapiEntries.begin(), sortedCFunctionNapiEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector> sortedObjCEngineDirectEntries( + objcEngineDirectEntries.begin(), objcEngineDirectEntries.end()); + std::sort(sortedObjCEngineDirectEntries.begin(), + sortedObjCEngineDirectEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionEngineDirectEntries(cFunctionEngineDirectEntries.begin(), + cFunctionEngineDirectEntries.end()); + std::sort(sortedCFunctionEngineDirectEntries.begin(), + sortedCFunctionEngineDirectEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> sortedObjCV8Entries( + objcV8Entries.begin(), objcV8Entries.end()); + std::sort( + sortedObjCV8Entries.begin(), sortedObjCV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> sortedCFunctionV8Entries( + cFunctionV8Entries.begin(), cFunctionV8Entries.end()); + std::sort( + sortedCFunctionV8Entries.begin(), sortedCFunctionV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> sortedBlockPreparedEntries( + blockPreparedEntries.begin(), blockPreparedEntries.end()); + std::sort( + sortedBlockPreparedEntries.begin(), sortedBlockPreparedEntries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::ostringstream generated; generated << "#ifndef NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; generated << "#define NS_GENERATED_SIGNATURE_DISPATCH_INC\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_NAPI\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 1\n\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_V8\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 1\n"; + generated << "#endif\n\n"; generated << "namespace nativescript {\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + for (const auto& wrapper : preparedWrappers) { + writePreparedWrapper(generated, wrapper.second.first, + preparedWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_NAPI\n"; for (const auto& wrapper : wrappers) { writeNapiWrapper(generated, wrapper.second.first, wrapperNameByKey.at(wrapper.first), wrapper.second.second); } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#if NS_GSD_BACKEND_JSC\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertJSCArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertJSCBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertJSCInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertJSCUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertJSCInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertJSCUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertJSCInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertJSCUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertJSCInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertJSCUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertJSCFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertJSCDoubleArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertJSCSelectorArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertJSCObjectArgument\n"; + generated << "#elif NS_GSD_BACKEND_QUICKJS\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertQuickJSArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertQuickJSBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertQuickJSInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertQuickJSUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertQuickJSInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertQuickJSUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertQuickJSInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertQuickJSUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertQuickJSInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertQuickJSUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertQuickJSFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertQuickJSDoubleArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertQuickJSSelectorArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertQuickJSObjectArgument\n"; + generated << "#elif NS_GSD_BACKEND_HERMES\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertHermesArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertHermesBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesDoubleArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertHermesSelectorArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertHermesObjectArgument\n"; + generated << "#else\n"; + generated << "#error \"No generated signature engine-direct converter selected\"\n"; + generated << "#endif\n"; + for (const auto& wrapper : wrappers) { + writeEngineDirectWrapper(generated, wrapper.second.first, + engineDirectWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT\n"; + generated << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8\n"; + for (const auto& wrapper : wrappers) { + writeV8Wrapper(generated, wrapper.second.first, + v8WrapperNameByKey.at(wrapper.first), wrapper.second.second); + } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "inline constexpr ObjCDispatchEntry " + "kGeneratedObjCDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + generated << "};\n\n"; + + generated << "inline constexpr CFunctionDispatchEntry " + "kGeneratedCFunctionDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + generated << "};\n\n"; + + generated << "inline constexpr BlockDispatchEntry " + "kGeneratedBlockDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedBlockPreparedEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << preparedWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "inline constexpr ObjCEngineDirectDispatchEntry " + "kGeneratedObjCEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionEngineDirectDispatchEntry " + "kGeneratedCFunctionEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_NAPI\n"; generated << "inline constexpr ObjCNapiDispatchEntry " "kGeneratedObjCNapiDispatchEntries[] = {\n"; generated << " {0, nullptr},\n"; @@ -992,6 +2157,27 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, << wrapperNameByKey.at(entry.second) << "},\n"; } generated << "};\n\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8\n"; + generated << "inline constexpr ObjCV8DispatchEntry " + "kGeneratedObjCV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionV8DispatchEntry " + "kGeneratedCFunctionV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; generated << "} // namespace nativescript\n\n"; generated << "#endif // NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; diff --git a/package.json b/package.json index f0d5032b..a67d5d18 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "build:macos-napi": "./scripts/build_nativescript.sh --no-iphone --no-simulator --macos-napi", "nsr": "./dist/nsr", "test:macos": "node ./scripts/run-tests-macos.js ./build/test-results/macos-junit.xml", - "test:ios": "node ./scripts/run-tests-ios.js" + "test:ios": "node ./scripts/run-tests-ios.js", + "benchmark:objc-dispatch": "node ./benchmarks/objc-dispatch/run.js" }, "license": "Apache-2.0", "devDependencies": { diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index 47edbfa5..cd263298 100755 --- a/scripts/build_all_ios.sh +++ b/scripts/build_all_ios.sh @@ -19,7 +19,7 @@ for arg in $@; do --no-iphone|--no-device) BUILD_IPHONE=false ;; --macos) BUILD_MACOS=true ;; --no-macos) BUILD_MACOS=false ;; - --no-engine) TARGET_ENGINE=none ;; + --no-engine|--generic-napi) TARGET_ENGINE=none ;; --embed-metadata) EMBED_METADATA=true ;; *) ;; esac @@ -51,9 +51,23 @@ if $EMBED_METADATA; then fi checkpoint "... All metadata generated!" +elif [[ "$TARGET_ENGINE" != "none" ]]; then + GSD_PLATFORM= + if $BUILD_SIMULATOR; then + GSD_PLATFORM=ios-sim + elif $BUILD_IPHONE; then + GSD_PLATFORM=ios + elif $BUILD_MACOS; then + GSD_PLATFORM=macos + fi + + if [ -n "$GSD_PLATFORM" ]; then + checkpoint "Generating signature dispatch bindings for $GSD_PLATFORM..." + npm run metagen "$GSD_PLATFORM" + fi fi -"$SCRIPT_DIR/build_nativescript.sh" --no-vision $1 $2 $3 $4 $5 $6 $7 $8 $9 +"$SCRIPT_DIR/build_nativescript.sh" --no-vision "$@" if [[ "$TARGET_ENGINE" == "none" ]]; then # If you're building *with* --no-engine, you're trying to make an npm release diff --git a/scripts/build_all_macos.sh b/scripts/build_all_macos.sh index d333880d..9b09f9c9 100755 --- a/scripts/build_all_macos.sh +++ b/scripts/build_all_macos.sh @@ -10,6 +10,7 @@ if [ -z "$NO_UPDATE_VERSION" ]; then fi "$SCRIPT_DIR/build_metadata_generator.sh" +npm run metagen macos "$SCRIPT_DIR/build_nativescript.sh" --no-catalyst --no-iphone --no-sim --macos "$SCRIPT_DIR/build_tklivesync.sh" --no-catalyst --no-iphone --no-sim --no-vision --macos "$SCRIPT_DIR/prepare_dSYMs.sh" diff --git a/scripts/build_all_node_api.sh b/scripts/build_all_node_api.sh index 2a7c9422..18fa0b0b 100755 --- a/scripts/build_all_node_api.sh +++ b/scripts/build_all_node_api.sh @@ -54,7 +54,7 @@ fi checkpoint "... All metadata generated!" -"$SCRIPT_DIR/build_nativescript.sh" --no-vision --no-engine $1 $2 $3 $4 $5 $6 $7 $8 $9 +"$SCRIPT_DIR/build_nativescript.sh" --no-vision --no-engine "$@" "$SCRIPT_DIR/prepare_dSYMs.sh" "$SCRIPT_DIR/build_npm_node_api.sh" diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 7de343ac..ade3fb57 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -14,7 +14,9 @@ EMBED_METADATA=$(to_bool ${EMBED_METADATA:=false}) CONFIG_BUILD=RelWithDebInfo TARGET_ENGINE=${TARGET_ENGINE:=v8} # default to v8 for compat +NS_GSD_BACKEND=${NS_GSD_BACKEND:=auto} METADATA_SIZE=${METADATA_SIZE:=0} +GENERATED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/GeneratedSignatureDispatch.inc}} for arg in $@; do case $arg in @@ -39,6 +41,13 @@ for arg in $@; do --embed-metadata) EMBED_METADATA=true ;; --hermes) TARGET_ENGINE=hermes ;; --no-engine|--generic-napi) TARGET_ENGINE=none ;; + --gsd-v8) NS_GSD_BACKEND=v8 ;; + --gsd-jsc) NS_GSD_BACKEND=jsc ;; + --gsd-quickjs) NS_GSD_BACKEND=quickjs ;; + --gsd-hermes) NS_GSD_BACKEND=hermes ;; + --gsd-napi) NS_GSD_BACKEND=napi ;; + --gsd-none) NS_GSD_BACKEND=none ;; + --gsd-backend=*) NS_GSD_BACKEND="${arg#--gsd-backend=}" ;; *) ;; esac done @@ -61,6 +70,70 @@ if ! $VERBOSE; then QUIET=-quiet fi +function effective_gsd_backend () { + case "$NS_GSD_BACKEND" in + auto) + if [ "$TARGET_ENGINE" == "none" ]; then + echo none + elif [ "$TARGET_ENGINE" == "v8" ]; then + echo v8 + elif [ "$TARGET_ENGINE" == "jsc" ]; then + echo jsc + elif [ "$TARGET_ENGINE" == "quickjs" ]; then + echo quickjs + elif [ "$TARGET_ENGINE" == "hermes" ]; then + echo hermes + else + echo napi + fi + ;; + *) + echo "$NS_GSD_BACKEND" + ;; + esac +} + +function signature_dispatch_platform () { + if $BUILD_SIMULATOR; then + echo ios-sim + elif $BUILD_IPHONE; then + echo ios + elif $BUILD_MACOS || $BUILD_MACOS_CLI || $BUILD_MACOS_NODE_API; then + echo macos + elif $BUILD_VISION; then + echo visionos-sim + elif $BUILD_CATALYST; then + echo catalyst + fi +} + +function ensure_signature_dispatch_bindings () { + local backend + backend=$(effective_gsd_backend) + if [ "$TARGET_ENGINE" == "none" ] || [ "$backend" == "none" ]; then + return + fi + + if [ -f "$GENERATED_SIGNATURE_DISPATCH" ]; then + return + fi + + local platform + platform=$(signature_dispatch_platform) + if [ -z "$platform" ]; then + return + fi + + if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi + + checkpoint "Generating signature dispatch bindings for $platform..." + npm run metagen "$platform" +} + +ensure_signature_dispatch_bindings + DEV_TEAM=${DEVELOPMENT_TEAM:-} DIST=$(PWD)/dist mkdir -p $DIST @@ -102,10 +175,20 @@ function cmake_build () { local cache_file="$build_dir/CMakeCache.txt" if [ -f "$cache_file" ]; then + local needs_reconfigure=false local cached_engine - cached_engine=$(grep '^TARGET_ENGINE:STRING=' "$cache_file" | sed 's/^TARGET_ENGINE:STRING=//') + cached_engine=$(grep '^TARGET_ENGINE:STRING=' "$cache_file" | sed 's/^TARGET_ENGINE:STRING=//' || true) if [ -n "$cached_engine" ] && [ "$cached_engine" != "$TARGET_ENGINE" ]; then echo "Reconfiguring $platform build directory for engine '$TARGET_ENGINE' (was '$cached_engine')." + needs_reconfigure=true + fi + local cached_gsd_backend + cached_gsd_backend=$(grep '^NS_GSD_BACKEND:STRING=' "$cache_file" | sed 's/^NS_GSD_BACKEND:STRING=//' || true) + if [ -n "$cached_gsd_backend" ] && [ "$cached_gsd_backend" != "$NS_GSD_BACKEND" ]; then + echo "Reconfiguring $platform build directory for GSD backend '$NS_GSD_BACKEND' (was '$cached_gsd_backend')." + needs_reconfigure=true + fi + if $needs_reconfigure; then rm -rf "$build_dir" fi fi @@ -122,7 +205,7 @@ function cmake_build () { fi - cmake -S=./NativeScript -B="$build_dir" -GXcode -DTARGET_PLATFORM=$platform -DTARGET_ENGINE=$TARGET_ENGINE -DMETADATA_SIZE=$METADATA_SIZE -DBUILD_CLI_BINARY=$is_macos_cli -DBUILD_MACOS_NODE_API=$is_macos_napi + cmake -S=./NativeScript -B="$build_dir" -GXcode -DTARGET_PLATFORM=$platform -DTARGET_ENGINE=$TARGET_ENGINE -DNS_GSD_BACKEND=$NS_GSD_BACKEND -DMETADATA_SIZE=$METADATA_SIZE -DBUILD_CLI_BINARY=$is_macos_cli -DBUILD_MACOS_NODE_API=$is_macos_napi cmake --build "$build_dir" --config $CONFIG_BUILD -- \ CODE_SIGN_STYLE=Manual \ diff --git a/scripts/metagen.js b/scripts/metagen.js index bf3bc286..ac8b8712 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -313,9 +313,14 @@ async function main() { const typesDir = path.resolve(__dirname, "..", "packages", sdkName, "types"); const metadataDir = path.resolve(__dirname, "..", "metadata-generator", "metadata"); + const signatureBindingsPath = + process.env.NS_SIGNATURE_BINDINGS_CPP_PATH || + process.env.TNS_SIGNATURE_BINDINGS_CPP_PATH || + path.resolve(__dirname, "..", "NativeScript", "ffi", "GeneratedSignatureDispatch.inc"); await fsp.rm(typesDir, { recursive: true, force: true }); await fsp.mkdir(typesDir, { recursive: true }); await fsp.mkdir(metadataDir, { recursive: true }); + await fsp.mkdir(path.dirname(signatureBindingsPath), { recursive: true }); for (const arch of Object.keys(sdk.targets)) { // Use the matching arch binary when available, falling back to arm64. @@ -367,6 +372,8 @@ async function main() { metadataDir, `metadata.${sdkName}.${arch}.h`, ), + "-output-signature-bindings-cpp", + signatureBindingsPath, "Xclang", "-isysroot", sdk.path,