diff --git a/.gitignore b/.gitignore index dc351670..51e665b5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,10 @@ SwiftBindgen # Generated Objective-C/C dispatch wrappers NativeScript/ffi/GeneratedSignatureDispatch.inc +NativeScript/ffi/GeneratedSignatureDispatch.inc.stamp + +# React Native TurboModule package staging +packages/react-native/dist/ +packages/react-native/ios/vendor/ +packages/react-native/metadata/ +packages/react-native/native-api-jsi/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 418e6cab..c8eadce6 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,8 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp + ffi/jsi/NativeApiJsi.mm + ffi/HermesFastNativeApi.mm ) elseif(TARGET_ENGINE_QUICKJS) @@ -263,6 +307,7 @@ if(ENABLE_JS_RUNTIME) # napi napi/quickjs/quickjs-api.c napi/quickjs/jsr.cpp + ffi/QuickJSFastNativeApi.mm ) elseif(TARGET_ENGINE_JSC) @@ -275,6 +320,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 +391,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/NativeScript-Prefix.pch b/NativeScript/NativeScript-Prefix.pch index e84500d5..9f55361f 100644 --- a/NativeScript/NativeScript-Prefix.pch +++ b/NativeScript/NativeScript-Prefix.pch @@ -1,6 +1,6 @@ #ifndef NativeScript_Prefix_pch #define NativeScript_Prefix_pch -#define NATIVESCRIPT_VERSION "9.0.0-napi-v8.0" +#define NATIVESCRIPT_VERSION "0.0.1" #endif /* NativeScript_Prefix_pch */ diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/Block.h index 97097831..cab8dc2c 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,11 @@ class FunctionPointer { metagen::MDSectionOffset offset; Cif* cif; bool ownsCif = false; + bool dispatchLookupCached = false; + uint64_t dispatchLookupSignatureHash = 0; + uint64_t dispatchId = 0; + void* preparedInvoker = nullptr; + void* hermesFrameDirectReturnInvoker = 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..f7835163 100644 --- a/NativeScript/ffi/Block.mm +++ b/NativeScript/ffi/Block.mm @@ -1,12 +1,19 @@ #include "Block.h" #import +#include #include #include #include +#include #include #include +#include +#include "HermesFastCallbackInfo.h" #include "Interop.h" +#include "NativeScriptException.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -65,10 +72,315 @@ 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) { + if (ref != nullptr) { + ref->dispatchLookupCached = true; + ref->dispatchLookupSignatureHash = 0; + ref->dispatchId = 0; + ref->preparedInvoker = nullptr; +#ifdef TARGET_ENGINE_HERMES + ref->hermesFrameDirectReturnInvoker = nullptr; +#endif + } + 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)); +#ifdef TARGET_ENGINE_HERMES + ref->hermesFrameDirectReturnInvoker = reinterpret_cast( + nativescript::lookupBlockHermesFrameDirectReturnInvoker(ref->dispatchId)); +#endif + } else { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupCFunctionPreparedInvoker(ref->dispatchId)); +#ifdef TARGET_ENGINE_HERMES + ref->hermesFrameDirectReturnInvoker = reinterpret_cast( + nativescript::lookupCFunctionHermesFrameDirectReturnInvoker(ref->dispatchId)); +#endif + } + ref->dispatchLookupCached = true; + } + + return reinterpret_cast(ref->preparedInvoker); +} + +inline const napi_value* prepareFunctionPointerInvocationArgs(napi_env env, nativescript::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) { + memcpy(heapArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return heapArgs->data(); +} + +#ifdef TARGET_ENGINE_HERMES +inline void copyHermesFunctionPointerFrameArgs(const uint64_t* argsBase, size_t argc, + napi_value* args) { + if (argsBase == nullptr || args == nullptr) { + return; + } + for (size_t i = 0; i < argc; i++) { + args[i] = nativescript::hermesDispatchFrameArg(argsBase, i); + } +} +#endif + +inline napi_value tryFastConvertFunctionPointerReturn(napi_env env, nativescript::Cif* cif, + void* rvalue) { + napi_value fastResult = nullptr; + if (cif != nullptr && cif->returnType != nullptr && + nativescript::TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + return nullptr; +} + +napi_value callFunctionPointerAsCFunctionDirect(napi_env env, nativescript::FunctionPointer* ref, + size_t actualArgc, + const napi_value* rawArgs) { + if (ref == nullptr || ref->cif == nullptr || ref->function == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native function pointer."); + return nullptr; + } + + auto cif = ref->cif; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + const napi_value* invocationArgs = prepareFunctionPointerInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, &heapInvocationArgs); + + void* stackAValues[16]; + std::vector heapAValues; + void** avalues = stackAValues; + if (cif->argc > 16) { + heapAValues.resize(cif->argc); + avalues = heapAValues.data(); + } + + bool shouldFreeAny = false; + uint8_t stackShouldFree[16] = {}; + std::vector heapShouldFree; + uint8_t* shouldFree = stackShouldFree; + if (cif->argc > 16) { + heapShouldFree.assign(cif->argc, false); + shouldFree = heapShouldFree.data(); + } + + for (unsigned int i = 0; i < cif->argc; i++) { + avalues[i] = cif->avalues[i]; + bool shouldFreeArg = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], &shouldFreeArg, + &shouldFreeAny); + shouldFree[i] = shouldFreeArg ? 1 : 0; + } + + void* rvalue = cif->rvalue; + auto preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, nativescript::SignatureCallKind::CFunction); + + @try { + if (preparedInvoker != nullptr) { + preparedInvoker(ref->function, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i])); + } + } + } + + if (napi_value fastResult = tryFastConvertFunctionPointerReturn(env, cif, rvalue)) { + return fastResult; + } + return cif->returnType->toJS(env, rvalue); +} + +napi_value callFunctionPointerAsBlockDirect(napi_env env, nativescript::FunctionPointer* ref, + size_t actualArgc, + const napi_value* rawArgs) { + if (ref == nullptr || ref->cif == nullptr || ref->function == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native block pointer."); + return nullptr; + } + + auto block = static_cast(ref->function); + if (block == nullptr || block->invoke == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native block invoke pointer."); + return nullptr; + } + + auto cif = ref->cif; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + const napi_value* invocationArgs = prepareFunctionPointerInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, &heapInvocationArgs); + + void* stackAValues[17]; + std::vector heapAValues; + void** avalues = stackAValues; + if (cif->cif.nargs > 17) { + heapAValues.resize(cif->cif.nargs); + avalues = heapAValues.data(); + } + + bool shouldFreeAny = false; + uint8_t stackShouldFree[16] = {}; + std::vector heapShouldFree; + uint8_t* shouldFree = stackShouldFree; + if (cif->argc > 16) { + heapShouldFree.assign(cif->argc, false); + shouldFree = heapShouldFree.data(); + } + + avalues[0] = █ + for (unsigned int i = 0; i < cif->argc; i++) { + avalues[i + 1] = cif->avalues[i]; + bool shouldFreeArg = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i + 1], &shouldFreeArg, + &shouldFreeAny); + shouldFree[i] = shouldFreeArg ? 1 : 0; + } + + void* rvalue = cif->rvalue; + nativescript::BlockPreparedInvoker preparedInvoker = ensureFunctionPointerPreparedInvoker( + ref, nativescript::SignatureCallKind::BlockInvoke); + + @try { + if (preparedInvoker != nullptr) { + preparedInvoker(block->invoke, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i + 1])); + } + } + } + + if (napi_value fastResult = tryFastConvertFunctionPointerReturn(env, cif, rvalue)) { + return fastResult; + } + return cif->returnType->toJS(env, rvalue); +} + +#ifdef TARGET_ENGINE_HERMES +napi_value tryCallHermesFunctionPointerFastFromFrame( + napi_env env, nativescript::FunctionPointer* ref, bool isBlock, size_t actualArgc, + const uint64_t* argsBase, bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + auto cif = ref != nullptr ? ref->cif : nullptr; + if (env == nullptr || ref == nullptr || cif == nullptr || ref->function == nullptr || + cif->isVariadic || cif->returnType == nullptr || argsBase == nullptr || + actualArgc != cif->argc || cif->signatureHash == 0) { + return nullptr; + } + + ensureFunctionPointerPreparedInvoker( + ref, isBlock ? nativescript::SignatureCallKind::BlockInvoke + : nativescript::SignatureCallKind::CFunction); + auto frameInvoker = ref->hermesFrameDirectReturnInvoker; + if (frameInvoker == nullptr) { + return nullptr; + } + + napi_value directResult = nullptr; + @try { + if (isBlock) { + auto block = static_cast(ref->function); + if (block == nullptr || block->invoke == nullptr) { + return nullptr; + } + auto invoker = reinterpret_cast( + frameInvoker); + if (invoker(env, cif, block->invoke, block, argsBase, &directResult)) { + if (handled != nullptr) { + *handled = true; + } + return directResult; + } + } else { + auto invoker = reinterpret_cast( + frameInvoker); + if (invoker(env, cif, ref->function, argsBase, &directResult)) { + if (handled != nullptr) { + *handled = true; + } + return directResult; + } + } + } @catch (NSException* exception) { + if (handled != nullptr) { + *handled = true; + } + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + return nullptr; +} +#endif + void block_copy(void* dest, void* src) { auto dst = static_cast(dest); auto source = static_cast(src); @@ -144,8 +456,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 +485,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 +570,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) { @@ -421,80 +741,83 @@ bool isObjCBlockObject(id obj) { } napi_value FunctionPointer::jsCallAsCFunction(napi_env env, napi_callback_info cbinfo) { - FunctionPointer* ref; - - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, (void**)&ref); - - auto cif = ref->cif; - - size_t argc = cif->argc; - napi_get_cb_info(env, cbinfo, &argc, cif->argv, nullptr, nullptr); - - void* avalues[cif->argc]; - void* rvalue = cif->rvalue; - - bool shouldFreeAny = false; - bool shouldFree[cif->argc]; +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + auto ref = static_cast(HermesFastData(fastInfo)); + const size_t actualArgc = HermesFastArgc(fastInfo); + bool handledDirect = false; + napi_value directResult = tryCallHermesFunctionPointerFastFromFrame( + env, ref, false, actualArgc, HermesFastArgsBase(fastInfo), &handledDirect); + if (handledDirect) { + return directResult; + } - if (cif->argc > 0) { - for (unsigned int i = 0; i < cif->argc; i++) { - shouldFree[i] = false; - avalues[i] = cif->avalues[i]; - cif->argTypes[i]->toNative(env, cif->argv[i], avalues[i], &shouldFree[i], &shouldFreeAny); + napi_value stackArgs[16]; + if (actualArgc <= 16) { + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, stackArgs); + return callFunctionPointerAsCFunctionDirect(env, ref, actualArgc, stackArgs); } + + std::vector heapArgs(actualArgc); + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, + heapArgs.data()); + return callFunctionPointerAsCFunctionDirect(env, ref, actualArgc, heapArgs.data()); } +#endif - ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i])); - } - } + if (actualArgc > 16) { + std::vector heapArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, heapArgs.data(), nullptr, nullptr); + return callFunctionPointerAsCFunctionDirect(env, ref, retryArgc, heapArgs.data()); } - return cif->returnType->toJS(env, rvalue); + return callFunctionPointerAsCFunctionDirect(env, ref, actualArgc, stackArgs); } napi_value FunctionPointer::jsCallAsBlock(napi_env env, napi_callback_info cbinfo) { - FunctionPointer* ref; - - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, (void**)&ref); - - Block_literal_1* block = (Block_literal_1*)ref->function; - auto cif = ref->cif; - - size_t argc = cif->argc; - napi_get_cb_info(env, cbinfo, &argc, cif->argv, nullptr, nullptr); - - void* avalues[cif->cif.nargs]; - void* rvalue = cif->rvalue; - - bool shouldFreeAny = false; - bool shouldFree[cif->argc]; - - avalues[0] = █ +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + auto ref = static_cast(HermesFastData(fastInfo)); + const size_t actualArgc = HermesFastArgc(fastInfo); + bool handledDirect = false; + napi_value directResult = tryCallHermesFunctionPointerFastFromFrame( + env, ref, true, actualArgc, HermesFastArgsBase(fastInfo), &handledDirect); + if (handledDirect) { + return directResult; + } - if (cif->argc > 0) { - for (unsigned int i = 0; i < cif->argc; i++) { - shouldFree[i] = false; - avalues[i + 1] = cif->avalues[i]; - cif->argTypes[i]->toNative(env, cif->argv[i], avalues[i + 1], &shouldFree[i], &shouldFreeAny); + napi_value stackArgs[16]; + if (actualArgc <= 16) { + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, stackArgs); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, stackArgs); } + + std::vector heapArgs(actualArgc); + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, + heapArgs.data()); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, heapArgs.data()); } +#endif - ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i + 1])); - } - } + if (actualArgc > 16) { + std::vector heapArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, heapArgs.data(), nullptr, nullptr); + return callFunctionPointerAsBlockDirect(env, ref, retryArgc, heapArgs.data()); } - return cif->returnType->toJS(env, rvalue); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, stackArgs); } } // namespace nativescript diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/CFunction.h index d0003f12..2584cdaa 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -2,25 +2,37 @@ #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 skipEngineDirectFastPath = false; bool dispatchLookupCached = false; uint64_t dispatchLookupSignatureHash = 0; uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; + void* v8Invoker = nullptr; + void* hermesDirectReturnInvoker = nullptr; + void* hermesFrameDirectReturnInvoker = nullptr; }; } // namespace nativescript diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 3f3a21f7..8c528dc3 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -3,7 +3,11 @@ #include #include #include "Block.h" +#include "CallbackThreading.h" #include "ClassMember.h" +#include "HermesFastCallbackInfo.h" +#include "HermesFastNativeApi.h" +#include "EngineDirectCall.h" #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -17,6 +21,15 @@ namespace { +inline bool isCompatOrMainCFunctionName(const char* name) { + return name == nullptr || + 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; +} + inline bool unwrapCompatNativeHandleForCFunction(napi_env env, napi_value value, void** out) { if (value == nullptr || out == nullptr) { return false; @@ -81,13 +94,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 +152,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 +195,10 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->dispatchId = 0; function->preparedInvoker = nullptr; function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = nullptr; } return; } @@ -204,6 +214,17 @@ 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 +#ifdef TARGET_ENGINE_HERMES + function->hermesDirectReturnInvoker = + reinterpret_cast(lookupCFunctionHermesDirectReturnInvoker(function->dispatchId)); + function->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupCFunctionHermesFrameDirectReturnInvoker(function->dispatchId)); +#endif function->dispatchLookupCached = true; } @@ -240,27 +261,80 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { metadata->signaturesOffset + metadata->getOffset(offset + sizeof(MDSectionOffset)); MDFunctionFlag functionFlags = metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); - auto cFunction = new CFunction(dlsym(self_dl, metadata->getString(offset))); + const char* name = metadata->getString(offset); + auto cFunction = new CFunction(dlsym(self_dl, name)); + cFunction->bridgeState = this; cFunction->cif = getCFunctionCif(env, sigOffset); cFunction->dispatchFlags = (functionFlags & mdFunctionReturnOwned) != 0 ? 1 : 0; + cFunction->skipEngineDirectFastPath = isCompatOrMainCFunctionName(name); cFunctionCache[offset] = cFunction; return cFunction; } napi_value CFunction::jsCall(napi_env env, napi_callback_info cbinfo) { - void* _offset; +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + const size_t actualArgc = HermesFastArgc(fastInfo); + const auto offset = + static_cast(reinterpret_cast(HermesFastData(fastInfo))); + bool handledDirect = false; + napi_value directResult = TryCallHermesCFunctionFastFromFrame( + env, offset, actualArgc, HermesFastArgsBase(fastInfo), &handledDirect); + if (handledDirect) { + return directResult; + } + + napi_value stackArgs[16]; + if (actualArgc <= 16) { + for (size_t i = 0; i < actualArgc; i++) { + stackArgs[i] = HermesFastArg(fastInfo, i); + } - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, &_offset); + return jsCallDirect(env, offset, actualArgc, stackArgs); + } + + std::vector heapArgs(actualArgc); + for (size_t i = 0; i < actualArgc; i++) { + heapArgs[i] = HermesFastArg(fastInfo, i); + } + return jsCallDirect(env, offset, actualArgc, heapArgs.data()); + } +#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 +343,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 +373,15 @@ 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)) { + NativeCallRuntimeUnlockScope unlockRuntime(env); + 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 +392,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); } @@ -373,6 +445,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { #endif @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (preparedInvoker != nullptr) { preparedInvoker(func->fnptr, avalues, rvalue); } else { @@ -406,6 +479,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/CallbackThreading.h b/NativeScript/ffi/CallbackThreading.h new file mode 100644 index 00000000..6a3d3b1a --- /dev/null +++ b/NativeScript/ffi/CallbackThreading.h @@ -0,0 +1,163 @@ +#ifndef CALLBACK_THREADING_H +#define CALLBACK_THREADING_H + +#include "js_native_api.h" + +#include +#include + +#if defined(ENABLE_JS_RUNTIME) +#include "jsr.h" +#endif + +namespace nativescript { + +namespace detail { + +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) +inline std::atomic native_call_unlocked_runtime_count{0}; +inline thread_local int native_caller_thread_callback_depth = 0; +#endif + +} // namespace detail + +inline bool shouldInvokeCallbackOnNativeCallerThread() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return true; +#else + return false; +#endif +} + +inline bool isNativeCallRuntimeUnlockedForCallbacks() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return detail::native_call_unlocked_runtime_count.load( + std::memory_order_acquire) > 0; +#else + return false; +#endif +} + +inline bool isNativeCallerThreadCallbackActive() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return detail::native_caller_thread_callback_depth > 0; +#else + return false; +#endif +} + +class NativeCallRuntimeUnlockScope final { + public: + explicit NativeCallRuntimeUnlockScope(napi_env env) { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (isNativeCallerThreadCallbackActive()) { + return; + } + + auto it = JSR::env_to_jsr_cache.find(env); + if (it == JSR::env_to_jsr_cache.end() || it->second == nullptr) { + return; + } + + jsr_ = it->second; + unlockedDepth_ = js_current_env_lock_depth(env); + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->unlock(); + } + if (unlockedDepth_ == 0 && jsr_->runtime != nullptr) { + runtime_ = jsr_->runtime.get(); + runtime_->unlock(); + unlockedRuntime_ = true; + } + if (unlockedDepth_ > 0 || unlockedRuntime_) { + didUnlock_ = true; + detail::native_call_unlocked_runtime_count.fetch_add( + 1, std::memory_order_release); + } +#else + (void)env; +#endif + } + + ~NativeCallRuntimeUnlockScope() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (didUnlock_) { + detail::native_call_unlocked_runtime_count.fetch_sub( + 1, std::memory_order_release); + } + if (jsr_ != nullptr) { + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->lock(); + } + } + if (unlockedRuntime_ && runtime_ != nullptr) { + runtime_->lock(); + } +#endif + } + + NativeCallRuntimeUnlockScope(const NativeCallRuntimeUnlockScope&) = delete; + NativeCallRuntimeUnlockScope& operator=(const NativeCallRuntimeUnlockScope&) = + delete; + + private: +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + JSR* jsr_ = nullptr; + facebook::jsi::ThreadSafeRuntime* runtime_ = nullptr; +#endif + int unlockedDepth_ = 0; + bool unlockedRuntime_ = false; + bool didUnlock_ = false; +}; + +class NativeCallbackScope final { + public: + explicit NativeCallbackScope(napi_env env) : env_(env) { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (isNativeCallRuntimeUnlockedForCallbacks()) { + auto it = JSR::env_to_jsr_cache.find(env_); + if (it != JSR::env_to_jsr_cache.end() && it->second != nullptr) { + jsr_ = it->second; + jsr_->js_mutex.lock(); + detail::native_caller_thread_callback_depth += 1; + napi_open_handle_scope(env_, &napiHandleScope_); + return; + } + } +#endif +#if defined(ENABLE_JS_RUNTIME) + napiScope_ = std::make_unique(env_); +#else + (void)env_; +#endif + } + + ~NativeCallbackScope() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (napiHandleScope_ != nullptr) { + napi_close_handle_scope(env_, napiHandleScope_); + } + if (jsr_ != nullptr) { + detail::native_caller_thread_callback_depth -= 1; + jsr_->js_mutex.unlock(); + } +#endif + } + + NativeCallbackScope(const NativeCallbackScope&) = delete; + NativeCallbackScope& operator=(const NativeCallbackScope&) = delete; + + private: + napi_env env_; +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + JSR* jsr_ = nullptr; + napi_handle_scope napiHandleScope_ = nullptr; +#endif +#if defined(ENABLE_JS_RUNTIME) + std::unique_ptr napiScope_; +#endif +}; + +} // namespace nativescript + +#endif // CALLBACK_THREADING_H diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/Cif.h index ee483acf..2ab69483 100644 --- a/NativeScript/ffi/Cif.h +++ b/NativeScript/ffi/Cif.h @@ -21,6 +21,10 @@ class Cif { bool isVariadic = false; uint64_t signatureHash = 0; bool skipGeneratedNapiDispatch = false; + bool generatedDispatchHasRoundTripCacheArgument = false; + bool generatedDispatchUsesObjectReturnStorage = false; + bool generatedDispatchSetsV8ReturnDirectly = false; + uint64_t hermesRawReturnSlot = 0; 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..de9a87b8 100644 --- a/NativeScript/ffi/ClassBuilder.mm +++ b/NativeScript/ffi/ClassBuilder.mm @@ -128,6 +128,58 @@ napi_env resolveClassBuilderEnv(id self) { }) )"; +const char* kInstallClassFromStringAliasSource = R"( + (function (global) { + if (global.__nsClassFromStringAliasInstalled === true) { + return; + } + + const original = global.NSClassFromString; + if (typeof original !== "function") { + return; + } + + Object.defineProperty(global, "__nsOriginalNSClassFromString", { + configurable: true, + enumerable: false, + writable: false, + value: original + }); + + Object.defineProperty(global, "NSClassFromString", { + configurable: true, + enumerable: true, + writable: true, + value: function (name) { + const cls = original.call(this, name); + if (cls != null) { + try { + const registry = global.__nsConstructorsByObjCClassName; + const stringFromClass = global.NSStringFromClass; + if (registry != null && typeof stringFromClass === "function") { + const runtimeName = stringFromClass(cls); + const constructor = registry[runtimeName]; + if (constructor != null) { + return constructor; + } + } + } catch (_) { + } + } + + return cls; + } + }); + + Object.defineProperty(global, "__nsClassFromStringAliasInstalled", { + configurable: true, + enumerable: false, + writable: false, + value: true + }); + }) +)"; + const char* NSFastEnumerationMethodEncoding() { static const char* encoding = nullptr; if (encoding == nullptr) { @@ -856,24 +908,6 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat } } - bool hasOwnOverrides = false; - napi_value overridePropertyNames = nullptr; - napi_get_all_property_names(env, args[0], napi_key_own_only, napi_key_skip_symbols, - napi_key_numbers_to_strings, &overridePropertyNames); - uint32_t overridePropertyCount = 0; - napi_get_array_length(env, overridePropertyNames, &overridePropertyCount); - hasOwnOverrides = overridePropertyCount > 0; - - bool hasExposedMethodsOption = false; - bool hasProtocolsOption = false; - if (hasOptionsObject) { - napi_has_named_property(env, options, "exposedMethods", &hasExposedMethodsOption); - napi_has_named_property(env, options, "protocols", &hasProtocolsOption); - } - - bool shouldReuseExistingClass = false; - Class existingExternalClass = nullptr; - // Create a class name. napi_value baseClassName; napi_get_named_property(env, thisArg, "name", &baseClassName); @@ -902,22 +936,23 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat } std::string newClassName; + bool shouldAliasRequestedName = false; if (!requestedName.empty()) { newClassName = requestedName; Class existingClass = objc_lookUpClass(newClassName.c_str()); - if (existingClass != nullptr && - class_conformsToProtocol(existingClass, @protocol(ObjCBridgeClassBuilderProtocol))) { + auto nextAvailableClassName = [](const std::string& baseName) { size_t suffix = 1; std::string candidate; do { - candidate = requestedName + std::to_string(suffix++); + candidate = baseName + std::to_string(suffix++); } while (objc_lookUpClass(candidate.c_str()) != nullptr); - newClassName = candidate; - } else if (existingClass != nullptr && !hasOwnOverrides && !hasExposedMethodsOption && - !hasProtocolsOption) { - // Name-only extensions should resolve to an existing external class when present. - shouldReuseExistingClass = true; - existingExternalClass = existingClass; + return candidate; + }; + if (existingClass != nullptr) { + newClassName = nextAvailableClassName(requestedName); + shouldAliasRequestedName = + !class_conformsToProtocol(existingClass, + @protocol(ObjCBridgeClassBuilderProtocol)); } } else { newClassName = baseClassNameBuf; @@ -1000,14 +1035,27 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat napi_get_named_property(env, registryGlobal, "__nsConstructorsByObjCClassName", &classRegistry); } - if (classRegistry != nullptr) { - napi_set_named_property(env, classRegistry, newClassName.c_str(), newConstructor); + napi_value installClassFromStringAliasScript = nullptr; + napi_value installClassFromStringAlias = nullptr; + if (napi_create_string_utf8(env, kInstallClassFromStringAliasSource, NAPI_AUTO_LENGTH, + &installClassFromStringAliasScript) == napi_ok && + napi_run_script(env, installClassFromStringAliasScript, &installClassFromStringAlias) == + napi_ok && + installClassFromStringAlias != nullptr) { + napi_value aliasArgs[] = {registryGlobal}; + if (napi_call_function(env, registryGlobal, installClassFromStringAlias, 1, aliasArgs, + nullptr) != napi_ok) { + clearPendingException(env); + } + } else { + clearPendingException(env); } - if (shouldReuseExistingClass && existingExternalClass != nullptr) { - napi_remove_wrap(env, newConstructor, nullptr); - napi_wrap(env, newConstructor, (void*)existingExternalClass, nullptr, nullptr, nullptr); - return newConstructor; + if (classRegistry != nullptr) { + napi_set_named_property(env, classRegistry, newClassName.c_str(), newConstructor); + if (shouldAliasRequestedName && !requestedName.empty()) { + napi_set_named_property(env, classRegistry, requestedName.c_str(), newConstructor); + } } // Use ClassBuilder to create the native class and bridge the methods @@ -1016,7 +1064,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..feb81482 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -33,6 +33,16 @@ class MethodDescriptor { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; + void* v8Invoker = nullptr; + void* hermesDirectReturnInvoker = nullptr; + void* hermesFrameDirectReturnInvoker = nullptr; + bool nserrorOutSignatureCached = false; + bool nserrorOutSignature = false; +#ifdef TARGET_ENGINE_HERMES + bool hermesBlockFallbackCached = false; + bool hermesBlockFallback = false; +#endif MethodDescriptor() {} @@ -62,7 +72,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 +90,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, @@ -115,6 +134,15 @@ class ObjCClassMember { bool classMethod; ObjCClass* cls; std::vector overloads; +#ifdef TARGET_ENGINE_HERMES + napi_env hermesReceiverCacheEnv = nullptr; + uint64_t hermesReceiverCacheRawThis = 0; + id hermesReceiverCacheSelf = nil; + bool hermesReceiverCacheReceiverIsClass = false; + Class hermesReceiverCacheReceiverClass = nil; + bool hermesReceiverCacheRequiresSuperCall = false; + uint64_t hermesReceiverCacheObjectRefsGeneration = 0; +#endif }; } // namespace nativescript diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index 59f9f6a2..e81efb10 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -9,10 +9,15 @@ #include #include #include +#include #include #include "ClassBuilder.h" +#include "CallbackThreading.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 +52,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 +324,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 +348,43 @@ 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 +#ifdef TARGET_ENGINE_HERMES + descriptor->hermesDirectReturnInvoker = + reinterpret_cast(lookupObjCHermesDirectReturnInvoker(descriptor->dispatchId)); + descriptor->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupObjCHermesFrameDirectReturnInvoker(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)) { + NativeCallRuntimeUnlockScope unlockRuntime(env); + 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 +443,18 @@ 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 +#ifdef TARGET_ENGINE_HERMES + descriptor->hermesDirectReturnInvoker = + reinterpret_cast(lookupObjCHermesDirectReturnInvoker(descriptor->dispatchId)); + descriptor->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupObjCHermesFrameDirectReturnInvoker(descriptor->dispatchId)); +#endif descriptor->dispatchLookupCached = true; } @@ -422,6 +463,7 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, : lookupObjCPreparedInvoker(composeSignatureDispatchId( cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); if (invoker != nullptr) { + NativeCallRuntimeUnlockScope unlockRuntime(env); invoker((void*)objc_msgSend, avalues, rvalue); return true; } @@ -429,11 +471,14 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, #if defined(__x86_64__) if (isStret) { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend_stret), rvalue, avalues); } else { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); } #else + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); #endif } else { @@ -444,11 +489,14 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, avalues[0] = (void*)&superobjPtr; #if defined(__x86_64__) if (isStret) { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper_stret), rvalue, avalues); } else { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); } #else + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); #endif } @@ -598,7 +646,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 +1009,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 +1115,10 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = ObjCBridgeState* bridgeState_; }; +inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + namespace { inline size_t alignUpSize(size_t value, size_t alignment) { @@ -1412,21 +1463,69 @@ 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)) { + const size_t actualArgc = HermesFastArgc(fastInfo); + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFastFromFrame( + env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, HermesFastArgsBase(fastInfo), + EngineDirectMemberKind::Method, &handledDirect); + if (handledDirect) { + return directResult; + } + + napi_value stackArgs[16]; + if (actualArgc <= 16) { + for (size_t i = 0; i < actualArgc; i++) { + stackArgs[i] = HermesFastArg(fastInfo, i); + } + + return jsCallDirect(env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, stackArgs); + } + + std::vector heapArgs(actualArgc); + for (size_t i = 0; i < actualArgc; i++) { + heapArgs[i] = HermesFastArg(fastInfo, i); + } + return jsCallDirect(env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, heapArgs.data()); + } +#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 +1553,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 +1639,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", @@ -1617,6 +1714,15 @@ explicit CifReturnStorage(Cif* cif) { napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *((id*)nativeResult); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } return method->bridgeState->getObject(env, obj, constructor, method->returnOwned ? kOwnedObject : kUnownedObject); } @@ -1636,7 +1742,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 +1766,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 +1802,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 +1861,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(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), 0, nullptr, + EngineDirectMemberKind::Getter, &handledDirect); + if (handledDirect) { + return directResult; + } + return jsGetterDirect(env, static_cast(HermesFastData(fastInfo)), + 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) { @@ -1806,25 +1944,76 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *((id*)rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } return method->bridgeState->getObject(env, obj, constructor, 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)) { + const size_t actualArgc = HermesFastArgc(fastInfo); + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFastFromFrame( + env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, HermesFastArgsBase(fastInfo), + EngineDirectMemberKind::Setter, &handledDirect); + if (handledDirect) { + return directResult; + } + + napi_value value = nullptr; + if (actualArgc > 0) { + value = HermesFastArg(fastInfo, 0); + } else { + napi_get_undefined(env, &value); + } + return jsSetterDirect(env, static_cast(HermesFastData(fastInfo)), + 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 +2022,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 +2059,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/Closure.mm b/NativeScript/ffi/Closure.mm index aea75385..621ddca0 100644 --- a/NativeScript/ffi/Closure.mm +++ b/NativeScript/ffi/Closure.mm @@ -1,5 +1,6 @@ #include "Closure.h" #include "AutoreleasePool.h" +#include "CallbackThreading.h" #include "Metadata.h" #include "MetadataReader.h" #include "ObjCBridge.h" @@ -262,7 +263,7 @@ void JSFunctionCallback(ffi_cif* cif, void* ret, void* args[], void* data) { napi_env env = closure->env; #ifdef ENABLE_JS_RUNTIME - NapiScope scope(env); + NativeCallbackScope scope(env); #endif napi_value func = get_ref_value(env, closure->func); @@ -314,7 +315,9 @@ void JSFunctionCallback(ffi_cif* cif, void* ret, void* args[], void* data) { JSCallbackInner(closure, func, thisArg, argv, ctx->cif->nargs - 1, nullptr, ctx->ret); #ifdef TARGET_ENGINE_HERMES - js_execute_pending_jobs(env); + if (!isNativeCallerThreadCallbackActive()) { + js_execute_pending_jobs(env); + } #endif if (ctx->useCondvar) { { @@ -347,9 +350,9 @@ void JSBlockCallback(ffi_cif* cif, void* ret, void* args[], void* data) { ctx.done = false; ctx.useCondvar = false; - if (currentThreadId == closure->jsThreadId) { + if (currentThreadId == closure->jsThreadId || shouldInvokeCallbackOnNativeCallerThread()) { #ifdef ENABLE_JS_RUNTIME - NapiScope scope(env); + NativeCallbackScope scope(env); #endif Closure::callBlockFromMainThread(env, get_ref_value(env, closure->func), closure, &ctx); } else { 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..f3d2262f --- /dev/null +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -0,0 +1,1050 @@ +#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; +} + +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 +#ifdef TARGET_ENGINE_HERMES + descriptor->hermesDirectReturnInvoker = + reinterpret_cast(lookupObjCHermesDirectReturnInvoker(descriptor->dispatchId)); + descriptor->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupObjCHermesFrameDirectReturnInvoker(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; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = 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 +#ifdef TARGET_ENGINE_HERMES + function->hermesDirectReturnInvoker = + reinterpret_cast(lookupCFunctionHermesDirectReturnInvoker(function->dispatchId)); + function->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupCFunctionHermesFrameDirectReturnInvoker(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) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + 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) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + 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); +} + +} // 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) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || function->skipEngineDirectFastPath || + 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..a9934172 --- /dev/null +++ b/NativeScript/ffi/HermesFastCallbackInfo.h @@ -0,0 +1,79 @@ +#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 { + struct HostFunctionContext { + napi_env env = nullptr; + napi_callback callback = nullptr; + void* data = nullptr; + }; + + struct CallbackFrame { + const uint64_t* frameStart = nullptr; + const uint64_t* thisArgAndArgsBase = nullptr; + unsigned int argc = 0; + }; + + const HostFunctionContext* context = nullptr; + const CallbackFrame* frame = 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->context == nullptr || info->frame == nullptr || + info->context->env != env || info->frame->thisArgAndArgsBase == nullptr) { + return nullptr; + } + + return info; +} + +inline size_t HermesFastArgc(const HermesFastCallbackInfo* info) { + return info != nullptr && info->frame != nullptr ? info->frame->argc : 0; +} + +inline void* HermesFastData(const HermesFastCallbackInfo* info) { + return info != nullptr && info->context != nullptr ? info->context->data + : nullptr; +} + +inline napi_value HermesFastThisArg(const HermesFastCallbackInfo* info) { + return reinterpret_cast( + const_cast(info->frame->thisArgAndArgsBase)); +} + +inline const uint64_t* HermesFastArgsBase(const HermesFastCallbackInfo* info) { + return info != nullptr && info->frame != nullptr + ? info->frame->thisArgAndArgsBase + : nullptr; +} + +inline napi_value HermesFastArg(const HermesFastCallbackInfo* info, + size_t index) { + if (index >= HermesFastArgc(info)) { + return nullptr; + } + + return reinterpret_cast( + const_cast(info->frame->thisArgAndArgsBase - (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..576c62b2 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.h @@ -0,0 +1,41 @@ +#ifndef NS_HERMES_FAST_NATIVE_API_H +#define NS_HERMES_FAST_NATIVE_API_H + +#include +#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 TryCallHermesObjCMemberFastFromFrame( + napi_env env, ObjCClassMember* member, napi_value jsThis, + size_t actualArgc, const uint64_t* argsBase, + EngineDirectMemberKind kind, bool* handled); + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled); + +napi_value TryCallHermesCFunctionFastFromFrame( + napi_env env, MDSectionOffset offset, size_t actualArgc, + const uint64_t* argsBase, 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..4067ce01 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -0,0 +1,2340 @@ +#include "HermesFastNativeApi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "CallbackThreading.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 hermesRawDoubleIsFinite(uint64_t raw) { + constexpr uint64_t kExponentMask = 0x7ff0000000000000ULL; + return (raw & kExponentMask) != kExponentMask; +} + +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; + } + + *result = hermesRawDoubleIsFinite(raw) ? hermesRawToDouble(raw) : 0.0; + return true; +} + +inline napi_value makeHermesRawValue(Cif* cif, uint64_t raw) { + if (cif != nullptr) { + cif->hermesRawReturnSlot = raw; + return reinterpret_cast(&cif->hermesRawReturnSlot); + } + + static thread_local uint64_t slots[64] = {}; + static thread_local unsigned int nextSlot = 0; + uint64_t* slot = &slots[nextSlot++ & 63]; + *slot = raw; + return reinterpret_cast(slot); +} + +inline napi_value makeHermesRawNumberValue(Cif* cif, double value) { + uint64_t raw = 0; + std::memcpy(&raw, &value, sizeof(raw)); + return makeHermesRawValue(cif, raw); +} + +inline napi_value makeHermesRawBoolValue(Cif* cif, bool value) { + return makeHermesRawValue(cif, (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; + } + + struct SelectorArgumentCacheEntry { + napi_env env = nullptr; + uint64_t rawValue = 0; + SEL selector = nullptr; + }; + + static thread_local SelectorArgumentCacheEntry lastSelectorArgument; + const uint64_t rawValue = hermesRawValueBits(value); + if (rawValue != 0 && lastSelectorArgument.env == env && + lastSelectorArgument.rawValue == rawValue && + lastSelectorArgument.selector != nullptr) { + *result = lastSelectorArgument.selector; + return true; + } + + 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) { + SEL selector = cachedSelectorForName(stackBuffer, length); + lastSelectorArgument = {env, rawValue, selector}; + *result = selector; + 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; + } + SEL selector = cachedSelectorForName(heapBuffer.data(), length); + lastSelectorArgument = {env, rawValue, selector}; + *result = selector; + return true; + } + + return false; +} + +bool tryFastConvertHermesStringToNSStringArgument(napi_env env, + napi_value value, + id* result, + bool mutableString) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + if (mutableString) { + constexpr size_t kStackUtf16Capacity = 128; + char16_t utf16Stack[kStackUtf16Capacity]; + char16_t* utf16Buffer = utf16Stack; + size_t utf16Capacity = kStackUtf16Capacity; + size_t utf16Length = 0; + if (napi_get_value_string_utf16(env, value, utf16Buffer, utf16Capacity, + &utf16Length) != napi_ok) { + return false; + } + + std::vector utf16Heap; + if (utf16Length + 1 >= utf16Capacity) { + if (napi_get_value_string_utf16(env, value, nullptr, 0, &utf16Length) != + napi_ok) { + return false; + } + utf16Heap.resize(utf16Length + 1, 0); + utf16Buffer = utf16Heap.data(); + utf16Capacity = utf16Heap.size(); + if (napi_get_value_string_utf16(env, value, utf16Buffer, utf16Capacity, + &utf16Length) != napi_ok) { + return false; + } + } + + *result = + [[NSMutableString alloc] + initWithCharacters:reinterpret_cast(utf16Buffer) + length:utf16Length]; + return true; + } + + constexpr size_t kStackUtf8Capacity = 256; + char utf8Stack[kStackUtf8Capacity]; + char* utf8Buffer = utf8Stack; + size_t utf8Capacity = kStackUtf8Capacity; + size_t utf8Length = 0; + if (napi_get_value_string_utf8(env, value, utf8Buffer, utf8Capacity, + &utf8Length) != napi_ok) { + return false; + } + + std::vector utf8Heap; + if (utf8Length + 1 >= utf8Capacity) { + if (napi_get_value_string_utf8(env, value, nullptr, 0, &utf8Length) != + napi_ok) { + return false; + } + utf8Heap.resize(utf8Length + 1, '\0'); + utf8Buffer = utf8Heap.data(); + utf8Capacity = utf8Heap.size(); + if (napi_get_value_string_utf8(env, value, utf8Buffer, utf8Capacity, + &utf8Length) != napi_ok) { + return false; + } + } + + id stringValue = [[[NSString alloc] initWithBytes:utf8Buffer + length:utf8Length + encoding:NSUTF8StringEncoding] + autorelease]; + *result = stringValue != nil ? stringValue : [NSString string]; + return true; +} + +id resolveCachedHermesHandleObject(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) { + if (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); + } + } 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); + } + } + } + } + + return nil; +} + +bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + struct ObjectArgumentCacheEntry { + napi_env env = nullptr; + uint64_t rawValue = 0; + id nativeObject = nil; + bool classObject = false; + ObjCBridgeState* bridgeState = nullptr; + uint64_t objectRefsGeneration = 0; + }; + + static thread_local ObjectArgumentCacheEntry objectArgumentCache[8]; + static thread_local unsigned int nextObjectArgumentCacheSlot = 0; + static thread_local ObjectArgumentCacheEntry lastObjectArgument; + + const uint64_t rawValue = hermesRawValueBits(value); + if (rawValue != 0) { + if (lastObjectArgument.env == env && + lastObjectArgument.rawValue == rawValue && + lastObjectArgument.nativeObject != nil) { + bool lastValid = lastObjectArgument.classObject; + if (!lastValid && lastObjectArgument.bridgeState != nullptr && + lastObjectArgument.objectRefsGeneration != 0 && + lastObjectArgument.bridgeState->currentObjectRefsGeneration() == + lastObjectArgument.objectRefsGeneration) { + lastValid = true; + } + + if (lastValid) { + if (kind == mdTypeClass) { + if (!lastObjectArgument.classObject) { + return false; + } + *reinterpret_cast(result) = + static_cast(lastObjectArgument.nativeObject); + return true; + } + + *reinterpret_cast(result) = lastObjectArgument.nativeObject; + return true; + } + lastObjectArgument.rawValue = 0; + } + + for (auto& entry : objectArgumentCache) { + if (entry.env != env || entry.rawValue != rawValue || + entry.nativeObject == nil) { + continue; + } + + if (!entry.classObject) { + if (entry.bridgeState == nullptr || + entry.objectRefsGeneration == 0 || + entry.bridgeState->currentObjectRefsGeneration() != + entry.objectRefsGeneration) { + entry.rawValue = 0; + continue; + } + } + + if (kind == mdTypeClass) { + if (!entry.classObject) { + return false; + } + lastObjectArgument = entry; + *reinterpret_cast(result) = + static_cast(entry.nativeObject); + return true; + } + + lastObjectArgument = entry; + *reinterpret_cast(result) = entry.nativeObject; + return true; + } + } + + auto rememberObjectArgument = [&](id nativeObject, + ObjCBridgeState* bridgeState) { + if (nativeObject == nil || rawValue == 0) { + return; + } + + const bool classObject = object_isClass(nativeObject); + uint64_t objectRefsGeneration = 0; + if (!classObject) { + if (bridgeState == nullptr) { + bridgeState = ObjCBridgeState::InstanceData(env); + } + if (bridgeState == nullptr) { + return; + } + if (!bridgeState->hasObjectRef(nativeObject)) { + return; + } + objectRefsGeneration = bridgeState->currentObjectRefsGeneration(); + } + + auto& entry = objectArgumentCache[nextObjectArgumentCacheSlot++ & 7]; + entry.env = env; + entry.rawValue = rawValue; + entry.nativeObject = nativeObject; + entry.classObject = classObject; + entry.bridgeState = bridgeState; + entry.objectRefsGeneration = objectRefsGeneration; + lastObjectArgument = entry; + }; + + auto setPointerLikeObject = [&](void* data) -> bool { + id nativeObject = nil; + if (id cachedObject = resolveCachedHermesHandleObject(env, data); + cachedObject != nil) { + nativeObject = cachedObject; + rememberObjectArgument(nativeObject, nullptr); + } else { + nativeObject = static_cast(data); + } + + if (kind == mdTypeClass) { + if (nativeObject == nil || !object_isClass(nativeObject)) { + return false; + } + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + + *reinterpret_cast(result) = nativeObject; + return true; + }; + + ObjCBridgeState* bridgeState = nullptr; + if (kind == mdTypeClass) { + bridgeState = ObjCBridgeState::InstanceData(env); + Class bridgedClass = nil; + if (bridgeState != nullptr && + bridgeState->tryResolveBridgedClassConstructor(env, value, + &bridgedClass) && + bridgedClass != nil) { + rememberObjectArgument(static_cast(bridgedClass), bridgeState); + *reinterpret_cast(result) = bridgedClass; + return true; + } + } else { + bridgeState = ObjCBridgeState::InstanceData(env); + id bridgedType = nil; + if (bridgeState != nullptr && + bridgeState->tryResolveBridgedTypeConstructor(env, value, + &bridgedType) && + bridgedType != nil) { + rememberObjectArgument(bridgedType, bridgeState); + *reinterpret_cast(result) = bridgedType; + return true; + } + } + + if (Pointer::isInstance(env, value)) { + Pointer* pointer = Pointer::unwrap(env, value); + return setPointerLikeObject(pointer != nullptr ? pointer->data : nullptr); + } + + if (Reference::isInstance(env, value)) { + Reference* reference = Reference::unwrap(env, value); + return setPointerLikeObject(reference != nullptr ? reference->data : nullptr); + } + + void* wrapped = nullptr; + if (napi_unwrap(env, value, &wrapped) != napi_ok || wrapped == nullptr) { + return false; + } + + if (kind == mdTypeClass) { + id nativeObject = static_cast(wrapped); + ObjCBridgeState* bridgeState = nullptr; + if (!object_isClass(nativeObject)) { + bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id normalizedObject = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (normalizedObject != nil) { + nativeObject = normalizedObject; + } + } + } + if (!object_isClass(nativeObject)) { + return false; + } + rememberObjectArgument(nativeObject, bridgeState); + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + + id nativeObject = static_cast(wrapped); + if (bridgeState == nullptr) { + bridgeState = ObjCBridgeState::InstanceData(env); + } + if (bridgeState != nullptr && bridgeState->hasRoundTripCacheFrame()) { + bridgeState->cacheRoundTripObject(env, nativeObject, value); + } + rememberObjectArgument(nativeObject, nullptr); + *reinterpret_cast(result) = nativeObject; + return true; +} + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + +class HermesFastRoundTripCacheFrameGuard { + public: + HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState, + bool enabled = true) + : env_(enabled ? env : nullptr), + bridgeState_(enabled ? bridgeState : nullptr) { + 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(); +} + +void copyHermesFrameArgs(const uint64_t* argsBase, size_t argc, + napi_value* args) { + if (argsBase == nullptr || args == nullptr) { + return; + } + for (size_t i = 0; i < argc; i++) { + args[i] = hermesDispatchFrameArg(argsBase, 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() || + !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 computeBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +inline bool isBlockFallbackSelector(MethodDescriptor* descriptor) { + if (descriptor == nullptr) { + return false; + } + if (!descriptor->hermesBlockFallbackCached) { + descriptor->hermesBlockFallback = + computeBlockFallbackSelector(descriptor->selector); + descriptor->hermesBlockFallbackCached = true; + } + return descriptor->hermesBlockFallback; +} + +struct HermesResolvedSelf { + id self = nil; + bool receiverIsClass = false; + Class receiverClass = nil; + bool requiresSuperCall = false; +}; + +bool receiverClassRequiresHermesSuperCall(Class receiverClass); + +HermesResolvedSelf resolveHermesSelf(napi_env env, napi_value jsThis, + ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = + method != nullptr ? method->bridgeState : ObjCBridgeState::InstanceData(env); + + struct ReceiverCacheEntry { + napi_env env = nullptr; + ObjCClassMember* method = nullptr; + uint64_t rawValue = 0; + HermesResolvedSelf resolved; + uint64_t objectRefsGeneration = 0; + }; + + static thread_local ReceiverCacheEntry lastReceiver; + const uint64_t rawThis = hermesRawValueBits(jsThis); + + auto makeResolvedSelfFromFields = + [](id self, bool receiverIsClass, Class receiverClass, + bool requiresSuperCall) { + HermesResolvedSelf result; + result.self = self; + result.receiverIsClass = receiverIsClass; + result.receiverClass = receiverClass; + result.requiresSuperCall = requiresSuperCall; + return result; + }; + + auto receiverValuesAreValid = [&](id self, bool receiverIsClass, + uint64_t objectRefsGeneration) { + return self != nil && + (receiverIsClass || + (state != nullptr && objectRefsGeneration != 0 && + state->currentObjectRefsGeneration() == objectRefsGeneration)); + }; + + auto receiverCacheEntryIsValid = [&](const ReceiverCacheEntry& entry) { + return receiverValuesAreValid(entry.resolved.self, + entry.resolved.receiverIsClass, + entry.objectRefsGeneration); + }; + + if (rawThis != 0 && method != nullptr && + method->hermesReceiverCacheEnv == env && + method->hermesReceiverCacheRawThis == rawThis && + receiverValuesAreValid(method->hermesReceiverCacheSelf, + method->hermesReceiverCacheReceiverIsClass, + method->hermesReceiverCacheObjectRefsGeneration)) { + return makeResolvedSelfFromFields( + method->hermesReceiverCacheSelf, + method->hermesReceiverCacheReceiverIsClass, + method->hermesReceiverCacheReceiverClass, + method->hermesReceiverCacheRequiresSuperCall); + } + + if (rawThis != 0 && lastReceiver.env == env && + lastReceiver.method == method && lastReceiver.rawValue == rawThis && + receiverCacheEntryIsValid(lastReceiver)) { + return lastReceiver.resolved; + } + + auto makeResolvedSelf = [](id resolved) { + HermesResolvedSelf result; + if (resolved == nil) { + return result; + } + + result.self = resolved; + result.receiverIsClass = object_isClass(resolved); + result.receiverClass = + result.receiverIsClass ? static_cast(resolved) + : object_getClass(resolved); + result.requiresSuperCall = + receiverClassRequiresHermesSuperCall(result.receiverClass); + return result; + }; + + auto rememberReceiver = [&](const HermesResolvedSelf& resolved) { + if (resolved.self == nil || rawThis == 0) { + return; + } + + id nativeSelf = resolved.self; + const bool classObject = resolved.receiverIsClass; + uint64_t objectRefsGeneration = 0; + if (!classObject) { + if (state == nullptr || !state->hasObjectRef(nativeSelf)) { + return; + } + objectRefsGeneration = state->currentObjectRefsGeneration(); + } + + lastReceiver.env = env; + lastReceiver.method = method; + lastReceiver.rawValue = rawThis; + lastReceiver.resolved = resolved; + lastReceiver.objectRefsGeneration = objectRefsGeneration; + + if (method != nullptr) { + method->hermesReceiverCacheEnv = env; + method->hermesReceiverCacheRawThis = rawThis; + method->hermesReceiverCacheSelf = resolved.self; + method->hermesReceiverCacheReceiverIsClass = resolved.receiverIsClass; + method->hermesReceiverCacheReceiverClass = resolved.receiverClass; + method->hermesReceiverCacheRequiresSuperCall = resolved.requiresSuperCall; + method->hermesReceiverCacheObjectRefsGeneration = objectRefsGeneration; + } + }; + + auto finishReceiver = [&](id resolved) { + HermesResolvedSelf result = makeResolvedSelf(resolved); + rememberReceiver(result); + return result; + }; + + napi_status unwrapStatus = napi_invalid_arg; + if (jsThis != nullptr) { + unwrapStatus = napi_unwrap(env, jsThis, reinterpret_cast(&self)); + if (unwrapStatus == napi_ok && self != nil) { + return finishReceiver(self); + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + if (self != nil) { + return finishReceiver(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) { + return finishReceiver(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 finishReceiver(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 {}; +} + +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->hermesDirectReturnInvoker = reinterpret_cast( + lookupObjCHermesDirectReturnInvoker(descriptor->dispatchId)); + descriptor->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupObjCHermesFrameDirectReturnInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +ObjCHermesDirectReturnInvoker ensureHermesObjCDirectReturnInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + ensureHermesObjCEngineDirectInvoker(cif, descriptor, dispatchFlags); + return descriptor != nullptr + ? reinterpret_cast( + descriptor->hermesDirectReturnInvoker) + : nullptr; +} + +ObjCHermesFrameDirectReturnInvoker ensureHermesObjCFrameDirectReturnInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + ensureHermesObjCEngineDirectInvoker(cif, descriptor, dispatchFlags); + return descriptor != nullptr + ? reinterpret_cast( + descriptor->hermesFrameDirectReturnInvoker) + : nullptr; +} + +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; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = 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->hermesDirectReturnInvoker = reinterpret_cast( + lookupCFunctionHermesDirectReturnInvoker(function->dispatchId)); + function->hermesFrameDirectReturnInvoker = reinterpret_cast( + lookupCFunctionHermesFrameDirectReturnInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +CFunctionHermesDirectReturnInvoker ensureHermesCFunctionDirectReturnInvoker( + CFunction* function, Cif* cif) { + ensureHermesCFunctionEngineDirectInvoker(function, cif); + return function != nullptr + ? reinterpret_cast( + function->hermesDirectReturnInvoker) + : nullptr; +} + +CFunctionHermesFrameDirectReturnInvoker +ensureHermesCFunctionFrameDirectReturnInvoker(CFunction* function, Cif* cif) { + ensureHermesCFunctionEngineDirectInvoker(function, cif); + return function != nullptr + ? reinterpret_cast( + function->hermesFrameDirectReturnInvoker) + : nullptr; +} + +bool tryFastConvertHermesNSStringReturnValue(napi_env env, NSString* str, + napi_value* result) { + if (env == nullptr || result == nullptr || str == nil) { + return false; + } + + const NSUInteger length = [str length]; + constexpr NSUInteger kStackCapacity = 256; + char16_t stackBuffer[kStackCapacity]; + char16_t* buffer = stackBuffer; + + if (length > kStackCapacity) { + buffer = static_cast( + std::malloc(sizeof(char16_t) * static_cast(length))); + if (buffer == nullptr) { + return false; + } + } + + if (length > 0) { + [str getCharacters:reinterpret_cast(buffer) + range:NSMakeRange(0, length)]; + } + + napi_status status = napi_create_string_utf16( + env, buffer, static_cast(length), result); + if (buffer != stackBuffer) { + std::free(buffer); + } + + return status == napi_ok; +} + +bool tryFastConvertHermesBoxedPrimitiveReturnValue( + napi_env env, Cif* cif, id value, napi_value* result, + bool* recognizedFoundationObject = nullptr) { + if (recognizedFoundationObject != nullptr) { + *recognizedFoundationObject = false; + } + if (env == nullptr || result == nullptr || value == nil) { + return false; + } + + Class valueClass = object_getClass(value); + static thread_local Class lastNonBoxedPrimitiveClasses[8] = {}; + static thread_local unsigned int nextNonBoxedPrimitiveClassSlot = 0; + for (Class cachedClass : lastNonBoxedPrimitiveClasses) { + if (cachedClass == valueClass) { + return false; + } + } + + if ([value isKindOfClass:[NSNumber class]]) { + if (recognizedFoundationObject != nullptr) { + *recognizedFoundationObject = true; + } + if ([value isKindOfClass:[NSDecimalNumber class]]) { + return false; + } + if (CFGetTypeID((CFTypeRef)value) == CFBooleanGetTypeID()) { + *result = makeHermesRawBoolValue(cif, [value boolValue] == YES); + return true; + } + *result = makeHermesRawNumberValue(cif, [value doubleValue]); + return true; + } + + if ([value isKindOfClass:[NSNull class]]) { + if (recognizedFoundationObject != nullptr) { + *recognizedFoundationObject = true; + } + return napi_get_null(env, result) == napi_ok; + } + + if (valueClass != nil) { + lastNonBoxedPrimitiveClasses[nextNonBoxedPrimitiveClassSlot++ & 7] = + valueClass; + } + + return false; +} + +bool tryFastConvertHermesFoundationObject(napi_env env, Cif* cif, id value, + napi_value* result, + bool* recognizedFoundationObject = nullptr) { + if (recognizedFoundationObject != nullptr) { + *recognizedFoundationObject = false; + } + if (env == nullptr || result == nullptr || value == nil) { + return false; + } + + Class valueClass = object_getClass(value); + static thread_local Class lastNonFoundationObjectClasses[8] = {}; + static thread_local unsigned int nextNonFoundationObjectClassSlot = 0; + for (Class cachedClass : lastNonFoundationObjectClasses) { + if (cachedClass == valueClass) { + return false; + } + } + + if ([value isKindOfClass:[NSString class]]) { + if (recognizedFoundationObject != nullptr) { + *recognizedFoundationObject = true; + } + return tryFastConvertHermesNSStringReturnValue( + env, static_cast(value), result); + } + + if (tryFastConvertHermesBoxedPrimitiveReturnValue( + env, cif, value, result, recognizedFoundationObject)) { + return true; + } + + if (recognizedFoundationObject == nullptr || + !*recognizedFoundationObject) { + lastNonFoundationObjectClasses[nextNonFoundationObjectClassSlot++ & 7] = + valueClass; + } + + return false; +} + +inline bool isHermesNSStringFactorySelector(SEL selector) { + return selector == @selector(string) || + selector == @selector(stringWithString:) || + selector == @selector(stringWithCapacity:); +} + +inline bool isHermesNSStringFactoryClass(Class cls) { + return cls == [NSString class] || cls == [NSMutableString class]; +} + +inline bool shouldWrapHermesNSStringFactoryReturn(SEL selector, + bool classMethod, + bool receiverIsClass, + id self, + Class declaredClass) { + if (!classMethod || !isHermesNSStringFactorySelector(selector)) { + return false; + } + + if (isHermesNSStringFactoryClass(declaredClass)) { + return true; + } + + if (!receiverIsClass || self == nil) { + return false; + } + + return isHermesNSStringFactoryClass(static_cast(self)); +} + +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; + } + + napi_value fastResult = nullptr; + if (TryFastConvertHermesReturnValue(env, cif, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + if (descriptor->selector == @selector(class)) { + 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 == mdTypeAnyObject || + cif->returnType->kind == mdTypeNSStringObject || + cif->returnType->kind == mdTypeNSMutableStringObject) { + id obj = *reinterpret_cast(rvalue); + Class declaredClass = member->cls != nullptr ? member->cls->nativeClass : nil; + if (obj != nil && shouldWrapHermesNSStringFactoryReturn( + descriptor->selector, member->classMethod, + receiverIsClass, self, declaredClass)) { + return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + } + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + id obj = *reinterpret_cast(rvalue); + napi_value boxedPrimitiveResult = nullptr; + if (tryFastConvertHermesBoxedPrimitiveReturnValue( + env, cif, obj, &boxedPrimitiveResult)) { + return boxedPrimitiveResult; + } + + if (obj != nil) { + ObjCBridgeState* state = member->bridgeState; + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } + + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + + return member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + napi_value foundationResult = nullptr; + bool recognizedFoundationObject = false; + if (tryFastConvertHermesFoundationObject( + env, cif, obj, &foundationResult, &recognizedFoundationObject)) { + return foundationResult; + } + + if (obj != nil && !recognizedFoundationObject) { + ObjCBridgeState* state = member->bridgeState; + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + NSString* str = *reinterpret_cast(rvalue); + napi_value stringResult = nullptr; + if (tryFastConvertHermesNSStringReturnValue(env, str, &stringResult)) { + return stringResult; + } + } + + 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, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + NSString* str = *reinterpret_cast(rvalue); + napi_value stringResult = nullptr; + if (tryFastConvertHermesNSStringReturnValue(env, str, &stringResult)) { + return stringResult; + } + } else if (cif->returnType->kind == mdTypeInstanceObject) { + id obj = *reinterpret_cast(rvalue); + napi_value boxedPrimitiveResult = nullptr; + if (tryFastConvertHermesBoxedPrimitiveReturnValue( + env, cif, obj, &boxedPrimitiveResult)) { + return boxedPrimitiveResult; + } + } else if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + napi_value foundationResult = nullptr; + if (tryFastConvertHermesFoundationObject(env, cif, obj, &foundationResult)) { + return foundationResult; + } + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + return cif->returnType->toJS(env, rvalue, toJSFlags); +} + +} // namespace + +bool TryFastSetHermesGeneratedObjCObjectReturnValue( + napi_env env, Cif* cif, const HermesObjCReturnContext* context, + SEL selector, MDTypeKind kind, id value, napi_value* result) { + if (env == nullptr || cif == nullptr || cif->returnType == nullptr || + context == nullptr || result == nullptr) { + return false; + } + + ObjCBridgeState* bridgeState = + static_cast(context->bridgeState); + if (bridgeState == nullptr) { + return false; + } + + if (value == nil && selector != @selector(class)) { + return napi_get_null(env, result) == napi_ok; + } + + if (selector == @selector(class)) { + if (!context->propertyAccess && !context->receiverIsClass && + context->jsThis != nullptr) { + napi_value constructor = context->jsThis; + napi_get_named_property(env, context->jsThis, "constructor", &constructor); + *result = constructor; + return true; + } + + id classObject = context->receiverIsClass + ? context->self + : static_cast(object_getClass(context->self)); + *result = bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + return *result != nullptr; + } + + if (kind == mdTypeInstanceObject) { + napi_value boxedPrimitiveResult = nullptr; + if (tryFastConvertHermesBoxedPrimitiveReturnValue( + env, cif, value, &boxedPrimitiveResult)) { + *result = boxedPrimitiveResult; + return true; + } + + if (value != nil) { + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + cached != nullptr) { + *result = cached; + return true; + } + } + + napi_value constructor = context->jsThis; + if (!context->receiverIsClass && context->jsThis != nullptr) { + napi_get_named_property(env, context->jsThis, "constructor", &constructor); + } + + *result = bridgeState->getObject( + env, value, constructor, + context->returnOwned ? kOwnedObject : kUnownedObject); + return *result != nullptr; + } + + if ((kind == mdTypeAnyObject || kind == mdTypeNSStringObject || + kind == mdTypeNSMutableStringObject) && + value != nil && shouldWrapHermesNSStringFactoryReturn( + selector, context->classMethod, + context->receiverIsClass, context->self, + context->declaredClass)) { + *result = + bridgeState->getObject(env, value, context->jsThis, kUnownedObject); + return *result != nullptr; + } + + if (kind == mdTypeNSStringObject) { + return tryFastConvertHermesNSStringReturnValue( + env, static_cast(value), result); + } + + if (kind == mdTypeAnyObject || kind == mdTypeProtocolObject || + kind == mdTypeClassObject) { + napi_value foundationResult = nullptr; + bool recognizedFoundationObject = false; + if (tryFastConvertHermesFoundationObject( + env, cif, value, &foundationResult, &recognizedFoundationObject)) { + *result = foundationResult; + return true; + } + + if (value != nil && !recognizedFoundationObject) { + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + cached != nullptr) { + *result = cached; + return true; + } + } + } + + uint32_t toJSFlags = context->returnOwned ? kReturnOwned : 0; + *result = cif->returnType->toJS(env, &value, toJSFlags); + return *result != nullptr; +} + +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 (value == nullptr || result == nullptr) { + return false; + } + + if (kind != mdTypeClass) { + const uint64_t raw = hermesRawValueBits(value); + if (isHermesBool(raw)) { + *reinterpret_cast(result) = + [NSNumber numberWithBool:(raw & kHermesBoolBit) != 0]; + return true; + } + if (isHermesNumber(raw)) { + *reinterpret_cast(result) = + [NSNumber numberWithDouble:hermesRawToDouble(raw)]; + return true; + } + } + + if (tryFastUnwrapHermesObjectArgument(env, kind, value, result)) { + return true; + } + return false; +} + +bool TryFastConvertHermesPointerArgument(napi_env env, MDTypeKind kind, + napi_value value, void** result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + napi_valuetype valueType = napi_undefined; + if (napi_typeof(env, value, &valueType) != napi_ok) { + return false; + } + + if (kind == mdTypeBlock) { + if (valueType == napi_null || valueType == napi_undefined) { + *result = nullptr; + return true; + } + return false; + } + + switch (valueType) { + case napi_null: + case napi_undefined: + *result = nullptr; + return true; + + case napi_bigint: { + uint64_t raw = 0; + bool lossless = false; + if (napi_get_value_bigint_uint64(env, value, &raw, &lossless) != + napi_ok) { + return false; + } + *result = reinterpret_cast(raw); + return true; + } + + case napi_external: + return napi_get_value_external(env, value, result) == napi_ok; + + case napi_function: + case napi_object: { + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id bridgedType = nil; + if (bridgeState->tryResolveBridgedTypeConstructor(env, value, + &bridgedType) && + bridgedType != nil) { + *result = static_cast(bridgedType); + return true; + } + } + + if (Pointer::isInstance(env, value)) { + Pointer* pointer = Pointer::unwrap(env, value); + *result = pointer != nullptr ? pointer->data : nullptr; + return true; + } + + if (Reference::isInstance(env, value)) { + Reference* reference = Reference::unwrap(env, value); + if (reference == nullptr || reference->data == nullptr) { + return false; + } + *result = reference->data; + return true; + } + + void* wrapped = nullptr; + if (napi_unwrap(env, value, &wrapped) == napi_ok && wrapped != nullptr) { + if (bridgeState != nullptr) { + id nativeObject = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (nativeObject != nil) { + *result = static_cast(nativeObject); + return true; + } + } + + *result = wrapped; + return true; + } + + bool hasNativePointer = false; + if (valueType == napi_object && + napi_has_named_property(env, value, kNativePointerProperty, + &hasNativePointer) == napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, value, kNativePointerProperty, + &nativePointerValue) == napi_ok && + nativePointerValue != nullptr) { + if (Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + *result = pointer != nullptr ? pointer->data : nullptr; + return true; + } + return napi_get_value_external(env, nativePointerValue, result) == + napi_ok; + } + } + + return false; + } + + default: + 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; + } + if (kind != mdTypeClass && + tryFastConvertHermesStringToNSStringArgument( + env, value, reinterpret_cast(result), + kind == mdTypeNSMutableStringObject)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + case mdTypePointer: + case mdTypeOpaquePointer: + case mdTypeBlock: + case mdTypeFunctionPointer: + return TryFastConvertHermesPointerArgument( + env, kind, value, reinterpret_cast(result)); + + default: + return false; + } +} + +bool TryFastConvertHermesReturnValue(napi_env env, Cif* cif, 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( + cif, *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(cif, raw == 1); + return true; + } + *result = makeHermesRawNumberValue(cif, 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(cif, raw == 1); + return true; + } + *result = makeHermesRawNumberValue(cif, static_cast(raw)); + return true; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue(cif, + 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, 1, result) == napi_ok; + } + *result = makeHermesRawNumberValue(cif, static_cast(raw)); + return true; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue(cif, + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue(cif, + 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(cif, 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(cif, static_cast(raw)); + return true; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue(cif, + static_cast(*reinterpret_cast(value))); + return true; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + *result = makeHermesRawNumberValue(cif, + *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + return TryFastConvertHermesReturnValue(env, nullptr, kind, value, result); +} + +napi_value TryCallHermesObjCMemberFastImpl( + napi_env env, ObjCClassMember* member, napi_value jsThis, + size_t actualArgc, const napi_value* rawArgs, + const uint64_t* hermesArgsBase, + 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)) { + return nullptr; + } + + HermesResolvedSelf resolvedSelf = resolveHermesSelf(env, jsThis, member); + id self = resolvedSelf.self; + if (self == nil) { + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + const bool receiverIsClass = resolvedSelf.receiverIsClass; + if (resolvedSelf.requiresSuperCall) { + return nullptr; + } + + const bool hasExactHermesFrameArgs = + hermesArgsBase != nullptr && actualArgc == cif->argc; + const bool needsObjectReturnContext = cif->generatedDispatchUsesObjectReturnStorage; + const bool needsRoundTripFrame = needsRoundTripCacheFrame(cif); + ObjCHermesFrameDirectReturnInvoker frameDirectReturnInvoker = + hasExactHermesFrameArgs && cif->signatureHash != 0 + ? ensureHermesObjCFrameDirectReturnInvoker( + cif, descriptor, descriptor->dispatchFlags) + : nullptr; + + if (frameDirectReturnInvoker != nullptr) { + if (handled != nullptr) { + *handled = true; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, needsRoundTripFrame); + + napi_value directResult = nullptr; + HermesObjCReturnContext returnContextStorage; + const HermesObjCReturnContext* returnContext = nullptr; + if (needsObjectReturnContext) { + Class declaredClass = + member->cls != nullptr ? member->cls->nativeClass : nil; + returnContextStorage = HermesObjCReturnContext{ + member->bridgeState, jsThis, self, declaredClass, + member->returnOwned, receiverIsClass, member->classMethod, + kind != EngineDirectMemberKind::Method}; + returnContext = &returnContextStorage; + } + @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (frameDirectReturnInvoker( + env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, returnContext, hermesArgsBase, + &directResult)) { + return directResult; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + } + + const bool hasExactInvocationArgs = + cif->argc == 0 || (rawArgs != nullptr && actualArgc == cif->argc); + const napi_value* exactInvocationArgs = cif->argc == 0 ? nullptr : rawArgs; + const napi_value* preparedInvocationArgs = + hasExactInvocationArgs ? exactInvocationArgs : nullptr; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + std::vector frameRawArgs; + auto getPreparedInvocationArgs = [&]() -> const napi_value* { + if (preparedInvocationArgs == nullptr && cif->argc != 0) { + if (rawArgs == nullptr && hermesArgsBase != nullptr) { + if (actualArgc <= 16) { + copyHermesFrameArgs(hermesArgsBase, actualArgc, stackInvocationArgs); + rawArgs = stackInvocationArgs; + } else { + frameRawArgs.resize(actualArgc); + copyHermesFrameArgs(hermesArgsBase, actualArgc, + frameRawArgs.data()); + rawArgs = frameRawArgs.data(); + } + } + preparedInvocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, + &heapInvocationArgs); + } + return preparedInvocationArgs; + }; + + ObjCHermesDirectReturnInvoker directReturnInvoker = + cif->signatureHash != 0 + ? ensureHermesObjCDirectReturnInvoker(cif, descriptor, + descriptor->dispatchFlags) + : nullptr; + + if (directReturnInvoker != nullptr) { + if (handled != nullptr) { + *handled = true; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, needsRoundTripFrame); + + napi_value directResult = nullptr; + const napi_value* directArgs = + hasExactInvocationArgs ? exactInvocationArgs + : getPreparedInvocationArgs(); + HermesObjCReturnContext returnContextStorage; + const HermesObjCReturnContext* returnContext = nullptr; + if (needsObjectReturnContext) { + Class declaredClass = + member->cls != nullptr ? member->cls->nativeClass : nil; + returnContextStorage = HermesObjCReturnContext{ + member->bridgeState, jsThis, self, declaredClass, + member->returnOwned, receiverIsClass, member->classMethod, + kind != EngineDirectMemberKind::Method}; + returnContext = &returnContextStorage; + } + @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (directReturnInvoker( + env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, returnContext, directArgs, + &directResult)) { + return directResult; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + } + + 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; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, needsRoundTripFrame); + + ObjCEngineDirectInvoker invoker = + cif->signatureHash != 0 + ? ensureHermesObjCEngineDirectInvoker(cif, descriptor, + descriptor->dispatchFlags) + : nullptr; + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + const napi_value* invocationArgs = getPreparedInvocationArgs(); + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (invoker != nullptr) { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, invocationArgs, rvalue); + } else { + const napi_value* dynamicArgs = + rawArgs != nullptr ? rawArgs : invocationArgs; + const size_t dynamicArgc = rawArgs != nullptr ? actualArgc : cif->argc; + didInvoke = InvokeObjCMemberEngineDirectDynamic( + env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, dynamicArgc, dynamicArgs, 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 TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled) { + return TryCallHermesObjCMemberFastImpl(env, member, jsThis, + actualArgc, rawArgs, nullptr, kind, + handled); +} + +napi_value TryCallHermesObjCMemberFastFromFrame( + napi_env env, ObjCClassMember* member, napi_value jsThis, + size_t actualArgc, const uint64_t* argsBase, + EngineDirectMemberKind kind, bool* handled) { + return TryCallHermesObjCMemberFastImpl(env, member, jsThis, actualArgc, + nullptr, argsBase, kind, handled); +} + +napi_value TryCallHermesCFunctionFastImpl( + napi_env env, MDSectionOffset offset, size_t actualArgc, + const napi_value* rawArgs, const uint64_t* hermesArgsBase, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (env == nullptr || bridgeState == nullptr) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || function->skipEngineDirectFastPath || + cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { + return nullptr; + } + + const bool hasExactHermesFrameArgs = + hermesArgsBase != nullptr && actualArgc == cif->argc; + CFunctionHermesFrameDirectReturnInvoker frameDirectReturnInvoker = + hasExactHermesFrameArgs && cif->signatureHash != 0 + ? ensureHermesCFunctionFrameDirectReturnInvoker(function, cif) + : nullptr; + + if (frameDirectReturnInvoker != nullptr) { + if (handled != nullptr) { + *handled = true; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, needsRoundTripCacheFrame(cif)); + + napi_value directResult = nullptr; + @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (frameDirectReturnInvoker(env, cif, function->fnptr, hermesArgsBase, + &directResult)) { + return directResult; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + } + + const bool hasExactInvocationArgs = + cif->argc == 0 || (rawArgs != nullptr && actualArgc == cif->argc); + const napi_value* exactInvocationArgs = cif->argc == 0 ? nullptr : rawArgs; + const napi_value* preparedInvocationArgs = + hasExactInvocationArgs ? exactInvocationArgs : nullptr; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + std::vector frameRawArgs; + auto getPreparedInvocationArgs = [&]() -> const napi_value* { + if (preparedInvocationArgs == nullptr && cif->argc != 0) { + if (rawArgs == nullptr && hermesArgsBase != nullptr) { + if (actualArgc <= 16) { + copyHermesFrameArgs(hermesArgsBase, actualArgc, stackInvocationArgs); + rawArgs = stackInvocationArgs; + } else { + frameRawArgs.resize(actualArgc); + copyHermesFrameArgs(hermesArgsBase, actualArgc, + frameRawArgs.data()); + rawArgs = frameRawArgs.data(); + } + } + preparedInvocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, + &heapInvocationArgs); + } + return preparedInvocationArgs; + }; + + CFunctionHermesDirectReturnInvoker directReturnInvoker = + cif->signatureHash != 0 + ? ensureHermesCFunctionDirectReturnInvoker(function, cif) + : nullptr; + + if (directReturnInvoker != nullptr) { + if (handled != nullptr) { + *handled = true; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, needsRoundTripCacheFrame(cif)); + + napi_value directResult = nullptr; + @try { + const napi_value* directArgs = + hasExactInvocationArgs ? exactInvocationArgs + : getPreparedInvocationArgs(); + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (directReturnInvoker(env, cif, function->fnptr, directArgs, + &directResult)) { + return directResult; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + } + + if (handled != nullptr) { + *handled = true; + } + + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, needsRoundTripCacheFrame(cif)); + + bool didInvoke = false; + CFunctionEngineDirectInvoker invoker = + cif->signatureHash != 0 + ? ensureHermesCFunctionEngineDirectInvoker(function, cif) + : nullptr; + @try { + const napi_value* invocationArgs = getPreparedInvocationArgs(); + NativeCallRuntimeUnlockScope unlockRuntime(env); + if (invoker != nullptr) { + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + } else { + const napi_value* dynamicArgs = + rawArgs != nullptr ? rawArgs : invocationArgs; + const size_t dynamicArgc = rawArgs != nullptr ? actualArgc : cif->argc; + didInvoke = InvokeCFunctionEngineDirectDynamic( + env, function, cif, dynamicArgc, dynamicArgs, 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); +} + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled) { + return TryCallHermesCFunctionFastImpl(env, offset, actualArgc, rawArgs, + nullptr, handled); +} + +napi_value TryCallHermesCFunctionFastFromFrame( + napi_env env, MDSectionOffset offset, size_t actualArgc, + const uint64_t* argsBase, bool* handled) { + return TryCallHermesCFunctionFastImpl(env, offset, actualArgc, nullptr, + argsBase, handled); +} + +} // 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..0a344575 --- /dev/null +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -0,0 +1,1963 @@ +#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 "Interop.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, +}; + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + +class JSCFastRoundTripCacheFrameGuard { + public: + JSCFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState, + Cif* cif) + : env_(env), bridgeState_(bridgeState), + active_(needsRoundTripCacheFrame(cif) && bridgeState != nullptr) { + if (active_) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~JSCFastRoundTripCacheFrameGuard() { + if (active_) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; + bool active_ = false; +}; + +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 = + obj != nil ? member->bridgeState->findCachedObjectWrapper(env, obj) + : nullptr; + if (converted == nullptr) { + 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 (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + napi_value cached = member->bridgeState->findCachedObjectWrapper(env, obj); + if (cached != nullptr) { + *result = ToJSValue(cached); + 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(); + JSCFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, cif); + 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; + JSCFastRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); + @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) { + napi_env env = napi_env__::get(const_cast(ctx)); + if (env != nullptr) { + JSValueRef bindingValue = + JSObjectGetPropertyForKey(ctx, function, env->function_info_symbol, + nullptr); + if (bindingValue != nullptr && JSValueIsObject(ctx, bindingValue)) { + JSObjectRef bindingObject = JSValueToObject(ctx, bindingValue, nullptr); + if (bindingObject != nullptr) { + binding = static_cast( + JSObjectGetPrivate(bindingObject)); + } + } + } + } + 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 = "NativeScriptFastNativeBinding"; + definition.finalize = finalizeFastFunction; + return JSClassCreate(&definition); + }(); + return cls; +} + +JSObjectRef makeFastFunction(napi_env env, JSCFastNativeKind kind, void* data, + const char* name) { + auto* binding = new JSCFastNativeBinding{env, kind, data}; + ScopedJSString functionName(name != nullptr ? name : ""); + JSObjectRef function = JSObjectMakeFunctionWithCallback( + env->context, name != nullptr ? static_cast(functionName) + : nullptr, + callFastFunction); + if (function == nullptr) { + delete binding; + return nullptr; + } + + JSObjectRef bindingObject = + JSObjectMake(env->context, fastFunctionClass(), binding); + if (bindingObject == nullptr) { + delete binding; + return nullptr; + } + JSObjectSetPropertyForKey(env->context, function, env->function_info_symbol, + bindingObject, + kJSPropertyAttributeDontEnum | + kJSPropertyAttributeReadOnly | + kJSPropertyAttributeDontDelete, + nullptr); + 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; + } + if (Pointer::isInstance(env, value) || Reference::isInstance(env, value)) { + if (TryFastConvertNapiArgument(env, kind, value, result)) { + return true; + } + if (kind == mdTypeClass) { + void* data = nullptr; + if (Pointer::isInstance(env, value)) { + Pointer* pointer = Pointer::unwrap(env, value); + data = pointer != nullptr ? pointer->data : nullptr; + } else { + Reference* reference = Reference::unwrap(env, value); + data = reference != nullptr ? reference->data : nullptr; + } + id nativeObject = static_cast(data); + if (nativeObject != nil && object_isClass(nativeObject)) { + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + } + return false; + } + if (tryFastConvertJSCObjectArgument(env, kind, ToJSValue(value), result)) { + if (kind != mdTypeClass) { + napi_valuetype valueType = napi_undefined; + if (napi_typeof(env, value, &valueType) == napi_ok && + valueType == napi_object) { + id nativeObject = *reinterpret_cast(result); + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (nativeObject != nil && bridgeState != nullptr && + bridgeState->hasRoundTripCacheFrame()) { + bridgeState->cacheRoundTripObject(env, nativeObject, value); + } + } + } + return true; + } + return TryFastConvertNapiArgument(env, kind, 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, value, 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, + descriptor->utf8name); + 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, + descriptor->utf8name); + 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, + descriptor->utf8name); + JSObjectRef setter = nullptr; + if (descriptor->setter == ObjCClassMember::jsSetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCSetter, + descriptor->data, descriptor->utf8name); + } else if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCReadOnlySetter, + descriptor->data, descriptor->utf8name); + } 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..12941649 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,17 @@ struct JSObjectFinalizerContext { napi_ref ref; }; +struct HandleObjectRef { + napi_ref ref = nullptr; + bool ownsRef = false; +}; + +struct RecentObjectWrapperRef { + uintptr_t objectKey = 0; + uintptr_t objectClassKey = 0; + napi_ref ref = nullptr; +}; + void finalize_objc_object(napi_env /*env*/, void* data, void* hint); bool IsBridgeStateLive(const ObjCBridgeState* bridgeState, uint64_t token) noexcept; @@ -97,6 +109,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); @@ -124,6 +178,13 @@ class ObjCBridgeState { MDSectionOffset classOffset = 0, std::vector* protocolOffsets = nullptr); napi_value findCachedObjectWrapper(napi_env env, id object); + inline void deleteOwnedHandleObjectRef(napi_env env, HandleObjectRef& entry) { + if (entry.ownsRef && env != nullptr && entry.ref != nullptr) { + napi_delete_reference(env, entry.ref); + } + entry.ref = nullptr; + entry.ownsRef = false; + } inline void cacheHandleObject(napi_env env, void* handle, napi_value value) { if (handle == nullptr || value == nullptr) { return; @@ -132,7 +193,7 @@ class ObjCBridgeState { uintptr_t handleKey = NormalizeHandleKey(handle); auto it = handleObjectRefs.find(handleKey); if (it != handleObjectRefs.end()) { - auto existing = get_ref_value(env, it->second); + auto existing = get_ref_value(env, it->second.ref); if (existing != nullptr) { bool isSameValue = false; if (napi_strict_equals(env, existing, value, &isSameValue) == napi_ok && @@ -141,13 +202,35 @@ class ObjCBridgeState { } } - napi_delete_reference(env, it->second); + deleteOwnedHandleObjectRef(env, it->second); handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); } napi_ref ref = nullptr; napi_create_reference(env, value, 0, &ref); - handleObjectRefs[handleKey] = ref; + handleObjectRefs[handleKey] = HandleObjectRef{ref, true}; + bumpHandleObjectRefsGeneration(); + } + inline void cacheHandleObjectRef(void* handle, napi_ref ref) { + if (handle == nullptr || ref == nullptr) { + return; + } + + uintptr_t handleKey = NormalizeHandleKey(handle); + auto it = handleObjectRefs.find(handleKey); + if (it != handleObjectRefs.end()) { + if (it->second.ref == ref) { + return; + } + + deleteOwnedHandleObjectRef(env, it->second); + handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + } + + handleObjectRefs[handleKey] = HandleObjectRef{ref, false}; + bumpHandleObjectRefsGeneration(); } inline napi_value getCachedHandleObject(napi_env env, void* handle) { if (handle == nullptr) { @@ -155,19 +238,192 @@ class ObjCBridgeState { } uintptr_t handleKey = NormalizeHandleKey(handle); + const uint64_t generation = currentHandleObjectRefsGeneration(); + + struct LastHandleObjectRef { + const ObjCBridgeState* bridgeState = nullptr; + napi_env env = nullptr; + uintptr_t handleKey = 0; + napi_ref ref = nullptr; + uint64_t generation = 0; + }; + + static thread_local LastHandleObjectRef lastHandleObjectRef; + if (lastHandleObjectRef.bridgeState == this && + lastHandleObjectRef.env == env && + lastHandleObjectRef.handleKey == handleKey && + lastHandleObjectRef.ref != nullptr && + lastHandleObjectRef.generation == generation) { + napi_value value = get_ref_value(env, lastHandleObjectRef.ref); + if (value != nullptr) { + return value; + } + } + + static thread_local LastHandleObjectRef recentHandleObjectRefs[8]; + static thread_local unsigned int nextRecentHandleObjectRefSlot = 0; + for (const auto& entry : recentHandleObjectRefs) { + if (entry.bridgeState != this || + entry.env != env || + entry.handleKey != handleKey || + entry.ref == nullptr || + entry.generation != generation) { + continue; + } + + napi_value value = get_ref_value(env, entry.ref); + if (value != nullptr) { + lastHandleObjectRef = entry; + return value; + } + break; + } + auto it = handleObjectRefs.find(handleKey); if (it == handleObjectRefs.end()) { return nullptr; } - auto value = get_ref_value(env, it->second); + auto value = get_ref_value(env, it->second.ref); if (value == nullptr) { - napi_delete_reference(env, it->second); + deleteOwnedHandleObjectRef(env, it->second); handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + if (lastHandleObjectRef.bridgeState == this && + lastHandleObjectRef.handleKey == handleKey) { + lastHandleObjectRef = {}; + } + return nullptr; } + lastHandleObjectRef = LastHandleObjectRef{ + .bridgeState = this, + .env = env, + .handleKey = handleKey, + .ref = it->second.ref, + .generation = generation, + }; + recentHandleObjectRefs[nextRecentHandleObjectRefSlot++ & 7] = + lastHandleObjectRef; return value; } + inline bool ownsCachedHandleObjectRef(void* handle) const noexcept { + if (handle == nullptr) { + return false; + } + + auto it = handleObjectRefs.find(NormalizeHandleKey(handle)); + return it != handleObjectRefs.end() && it->second.ownsRef; + } + inline void removeCachedHandleObject(napi_env env, void* handle) noexcept { + if (handle == nullptr) { + return; + } + + uintptr_t handleKey = NormalizeHandleKey(handle); + auto it = handleObjectRefs.find(handleKey); + if (it == handleObjectRefs.end()) { + return; + } + + deleteOwnedHandleObjectRef(env, it->second); + handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + } + inline void deleteRecentObjectWrapperRef(napi_env env, + RecentObjectWrapperRef& entry) { + if (env != nullptr && entry.ref != nullptr) { + napi_delete_reference(env, entry.ref); + } + entry = {}; + } + inline void cacheRecentObjectWrapper(napi_env env, id object, + napi_value value) { + if (env == nullptr || object == nil || value == nullptr) { + return; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto& entry : recentObjectWrappers) { + if (entry.objectKey != objectKey || entry.objectClassKey != objectClassKey) { + continue; + } + + napi_value existing = get_ref_value(env, entry.ref); + if (existing != nullptr) { + bool isSameValue = false; + if (napi_strict_equals(env, existing, value, &isSameValue) == napi_ok && + isSameValue) { + return; + } + } + + deleteRecentObjectWrapperRef(env, entry); + napi_create_reference(env, value, 1, &entry.ref); + entry.objectKey = objectKey; + entry.objectClassKey = objectClassKey; + return; + } + + RecentObjectWrapperRef entry{ + .objectKey = objectKey, + .objectClassKey = objectClassKey, + .ref = nullptr, + }; + napi_create_reference(env, value, 1, &entry.ref); + + static constexpr size_t kRecentObjectWrapperLimit = 16; + if (recentObjectWrappers.size() < kRecentObjectWrapperLimit) { + recentObjectWrappers.push_back(entry); + return; + } + + RecentObjectWrapperRef& replaced = + recentObjectWrappers[nextRecentObjectWrapperSlot++ % kRecentObjectWrapperLimit]; + deleteRecentObjectWrapperRef(env, replaced); + replaced = entry; + } + inline napi_value getRecentObjectWrapper(napi_env env, id object) { + if (env == nullptr || object == nil) { + return nullptr; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto it = recentObjectWrappers.begin(); it != recentObjectWrappers.end();) { + if (it->objectKey != objectKey || it->objectClassKey != objectClassKey) { + ++it; + continue; + } + + napi_value value = get_ref_value(env, it->ref); + if (value != nullptr) { + return value; + } + + deleteRecentObjectWrapperRef(env, *it); + it = recentObjectWrappers.erase(it); + } + + return nullptr; + } + inline void removeRecentObjectWrapper(napi_env env, id object) noexcept { + if (env == nullptr || object == nil) { + return; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto it = recentObjectWrappers.begin(); it != recentObjectWrappers.end();) { + if (it->objectKey == objectKey && it->objectClassKey == objectClassKey) { + deleteRecentObjectWrapperRef(env, *it); + it = recentObjectWrappers.erase(it); + } else { + ++it; + } + } + } void unregisterObject(id object) noexcept; bool unregisterObjectIfRefMatches(id object, napi_ref ref) noexcept; @@ -182,10 +438,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; } } @@ -193,6 +449,14 @@ class ObjCBridgeState { return false; } + inline uint64_t currentObjectRefsGeneration() const noexcept { + return objectRefsGeneration.load(std::memory_order_relaxed); + } + + inline uint64_t currentHandleObjectRefsGeneration() const noexcept { + return handleObjectRefsGeneration.load(std::memory_order_relaxed); + } + inline void beginRoundTripCacheFrame(napi_env /*env*/) { roundTripCacheFrames.emplace_back(); } @@ -412,19 +676,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 +736,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; @@ -689,12 +940,15 @@ class ObjCBridgeState { std::thread::id jsThreadId = std::this_thread::get_id(); CFRunLoopRef jsRunLoop = CFRunLoopGetCurrent(); std::unordered_map objectRefs; - std::unordered_map handleObjectRefs; + std::unordered_map handleObjectRefs; + std::vector recentObjectWrappers; + size_t nextRecentObjectWrapperSlot = 0; napi_ref pointerClass = nullptr; napi_ref referenceClass = nullptr; napi_ref functionReferenceClass = nullptr; napi_ref createNativeProxy = nullptr; + napi_ref createNativeFastArrayIndexes = nullptr; napi_ref createFastEnumeratorIterator = nullptr; napi_ref transferOwnershipToNative = nullptr; @@ -703,6 +957,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; @@ -720,9 +975,18 @@ class ObjCBridgeState { MDMetadataReader* metadata; private: + inline void bumpObjectRefsGeneration() noexcept { + objectRefsGeneration.fetch_add(1, std::memory_order_relaxed); + } + + inline void bumpHandleObjectRefsGeneration() noexcept { + handleObjectRefsGeneration.fetch_add(1, std::memory_order_relaxed); + } + inline void storeObjectRef(id object, napi_ref ref) noexcept { std::lock_guard lock(objectRefsMutex); objectRefs[object] = ref; + bumpObjectRefsGeneration(); } inline napi_value getNormalizedObjectRef(napi_env env, id object) const { @@ -734,10 +998,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; } @@ -762,12 +1026,15 @@ class ObjCBridgeState { napi_ref ref = exact->second; objectRefs.erase(exact); + bumpObjectRefsGeneration(); return ref; } std::unordered_map structInfoCache; std::vector> roundTripCacheFrames; std::unordered_map recentRoundTripCache; + std::atomic objectRefsGeneration{1}; + std::atomic handleObjectRefsGeneration{1}; mutable std::mutex objectRefsMutex; void* trackedObjectLiveness = nullptr; void* objc_autoreleasePool; diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index bb304ea7..f504aab3 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -45,6 +45,9 @@ const unsigned char __attribute__((section("__objc_metadata,__objc_metadata"))) std::unordered_map gLiveBridgeStates; std::atomic gNextBridgeStateToken{1}; constexpr const char* kNativePointerProperty = "__ns_native_ptr"; +#ifdef TARGET_ENGINE_HERMES +constexpr NSUInteger kHermesFastArrayIndexPropertyLimit = 1024; +#endif inline void deleteReferenceNow(napi_env env, napi_ref ref, bool unrefFirst) { if (env == nullptr || ref == nullptr) { @@ -858,10 +861,17 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat recentRoundTripCache.clear(); for (auto& entry : handleObjectRefs) { - deleteRef(entry.second); + if (entry.second.ownsRef) { + deleteRef(entry.second.ref); + } } handleObjectRefs.clear(); + for (auto& entry : recentObjectWrappers) { + deleteRef(entry.ref); + } + recentObjectWrappers.clear(); + std::unordered_set classAndProtocolConstructorRefs; classAndProtocolConstructorRefs.reserve(classes.size() + protocols.size()); for (const auto& pair : classes) { @@ -887,6 +897,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat deleteRef(referenceClass); deleteRef(functionReferenceClass); deleteRef(createNativeProxy); + deleteRef(createNativeFastArrayIndexes); deleteRef(createFastEnumeratorIterator); deleteRef(transferOwnershipToNative); @@ -949,13 +960,56 @@ 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]; + bool shouldProxyArray = nativeIsArray; +#ifdef TARGET_ENGINE_HERMES + if (nativeIsArray) { + const char* nativeArrayClassName = object_getClassName(nativeObject); + const bool nativeIsPlaceholderArray = + nativeArrayClassName != nullptr && + std::strcmp(nativeArrayClassName, "__NSPlaceholderArray") == 0; + const bool nativeIsMutableArray = + !nativeIsPlaceholderArray && [nativeObject isKindOfClass:NSMutableArray.class]; + if (!nativeIsPlaceholderArray && createNativeFastArrayIndexes != nullptr) { + const bool shouldUseFastArray = + nativeIsMutableArray || + [(NSArray*)nativeObject count] <= kHermesFastArrayIndexPropertyLimit; + if (!shouldUseFastArray) { + shouldProxyArray = true; + } else { + shouldProxyArray = false; + napi_value factory = get_ref_value(env, createNativeFastArrayIndexes); + if (factory != nullptr) { + napi_value global; + napi_value isMutableArray; + napi_value maxIndexedProperties; + napi_value args[3] = {object, nullptr, nullptr}; + napi_get_global(env, &global); + napi_get_boolean(env, nativeIsMutableArray, &isMutableArray); + napi_create_uint32(env, static_cast(kHermesFastArrayIndexPropertyLimit), + &maxIndexedProperties); + args[1] = isMutableArray; + args[2] = maxIndexedProperties; + napi_call_function(env, global, factory, 3, args, &result); + } + } + } + } +#endif + if (shouldProxyArray) { + 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 +1032,8 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); + cacheHandleObjectRef(nativeObject, ref); + cacheRecentObjectWrapper(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..4c532721 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"; @@ -179,6 +190,41 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt const char* nativeObjectProxySource = R"( (function (object, isArray, transferOwnershipToNative) { let isTransfered = false; + const boundMethods = Object.create(null); + let objectAtIndexMethod; + let addObjectMethod; + let removeObjectAtIndexMethod; + let setObjectAtIndexedSubscriptMethod; + + function bindTargetMethod(target, name) { + const cached = boundMethods[name]; + if (cached !== undefined) { + return cached; + } + + const value = target[name]; + if (typeof value !== "function") { + return value; + } + + if (value.__ns_proxy_bound === true) { + boundMethods[name] = value; + return value; + } + + const wrapper = value.bind(target); + Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + boundMethods[name] = wrapper; + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; + } return new Proxy(object, { get (target, name, receiver) { @@ -186,47 +232,102 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return target.class().superclass(); } - if (name in target) { - const value = target[name]; + if (isArray && name === "count") { + return target.count; + } + + if (isArray) { + switch (name) { + case "objectAtIndex": + return objectAtIndexMethod !== undefined + ? objectAtIndexMethod + : (objectAtIndexMethod = bindTargetMethod(target, name)); + case "addObject": + return addObjectMethod !== undefined + ? addObjectMethod + : (addObjectMethod = bindTargetMethod(target, name)); + case "removeObjectAtIndex": + return removeObjectAtIndexMethod !== undefined + ? removeObjectAtIndexMethod + : (removeObjectAtIndexMethod = bindTargetMethod(target, name)); + case "setObjectAtIndexedSubscript": + return setObjectAtIndexedSubscriptMethod !== undefined + ? setObjectAtIndexedSubscriptMethod + : (setObjectAtIndexedSubscriptMethod = bindTargetMethod(target, name)); + } + } + + if (typeof name === "string") { + const boundMethod = boundMethods[name]; + if (boundMethod !== undefined) { + return boundMethod; + } + } + + 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; + const resolvedClassType = typeof resolvedClass; if (resolvedClass != null && - (typeof resolvedClass === "object" || typeof resolvedClass === "function")) { + (resolvedClassType === "object" || resolvedClassType === "function")) { try { const runtimeName = typeof NSStringFromClass === "function" ? NSStringFromClass(resolvedClass) : null; - if (typeof runtimeName === "string" && runtimeName.length > 0) { - const registry = globalThis.__nsConstructorsByObjCClassName; - if (registry && registry[runtimeName]) { - resolvedClass = registry[runtimeName]; - } else if (typeof globalThis[runtimeName] !== "undefined") { - resolvedClass = globalThis[runtimeName]; - } - } + if (typeof runtimeName === "string" && runtimeName.length > 0) { + const registry = globalThis.__nsConstructorsByObjCClassName; + if (registry && registry[runtimeName]) { + resolvedClass = registry[runtimeName]; + } else if (typeof globalThis[runtimeName] !== "undefined") { + resolvedClass = globalThis[runtimeName]; + } + } } catch (_) { } } - 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 }); + if (typeof name === "string") { + boundMethods[name] = wrapper; + } + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; } return value; } @@ -252,6 +353,27 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return true; } + if (typeof name === "string" && boundMethods[name] !== undefined) { + delete boundMethods[name]; + } + + if (isArray) { + switch (name) { + case "objectAtIndex": + objectAtIndexMethod = undefined; + break; + case "addObject": + addObjectMethod = undefined; + break; + case "removeObjectAtIndex": + removeObjectAtIndexMethod = undefined; + break; + case "setObjectAtIndexedSubscript": + setObjectAtIndexedSubscriptMethod = undefined; + break; + } + } + if (isArray) { const index = Number(name); if (!isNaN(index)) { @@ -273,12 +395,69 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt }) )"; +const char* nativeObjectFastArrayIndexesSource = R"( + (function (object, isMutableArray, maxIndexedProperties) { + const prototype = Object.getPrototypeOf(object); + const flag = isMutableArray + ? "__ns_mutable_array_index_accessors" + : "__ns_array_index_accessors"; + + function makeGetter(index) { + return function () { + return this.objectAtIndex(index); + }; + } + + function makeSetter(index) { + return function (value) { + this.setObjectAtIndexedSubscript(value, index); + }; + } + + if (prototype != null && + !Object.prototype.hasOwnProperty.call(prototype, flag)) { + for (let i = 0; i < maxIndexedProperties; i++) { + const descriptor = { + configurable: true, + enumerable: false, + get: makeGetter(i) + }; + if (isMutableArray) { + descriptor.set = makeSetter(i); + } + Object.defineProperty(prototype, i, descriptor); + } + + Object.defineProperty(prototype, flag, { + configurable: false, + enumerable: false, + value: true + }); + } + + if (!Object.prototype.hasOwnProperty.call(object, "superclass")) { + Object.defineProperty(object, "superclass", { + configurable: true, + get: function () { + return this.class().superclass(); + } + }); + } + + return object; + }) +)"; + void initProxyFactory(napi_env env, ObjCBridgeState* state) { napi_value script, result; napi_create_string_utf8(env, nativeObjectProxySource, NAPI_AUTO_LENGTH, &script); napi_run_script(env, script, &result); state->createNativeProxy = make_ref(env, result); + napi_create_string_utf8(env, nativeObjectFastArrayIndexesSource, NAPI_AUTO_LENGTH, &script); + napi_run_script(env, script, &result); + state->createNativeFastArrayIndexes = make_ref(env, result); + napi_value transferOwnershipToNative; napi_create_function(env, "transferOwnershipToNative", NAPI_AUTO_LENGTH, JS_transferOwnershipToNative, nullptr, &transferOwnershipToNative); @@ -371,10 +550,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; @@ -437,12 +624,39 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { return roundTrip; } + if (napi_value recentWrapper = getRecentObjectWrapper(env, obj); recentWrapper != nullptr) { + return recentWrapper; + } + if (napi_value handleCached = getCachedHandleObject(env, (void*)obj); handleCached != nullptr) { + bool isRawHandleRoundTrip = ownsCachedHandleObjectRef((void*)obj); void* wrapped = nullptr; if (napi_unwrap(env, handleCached, &wrapped) == napi_ok && 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; + } + } + } + + if (isRawHandleRoundTrip) { + return handleCached; + } + + removeCachedHandleObject(env, (void*)obj); } if (napi_value existing = getNormalizedObjectRef(env, obj); existing != nullptr) { @@ -640,6 +854,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, // #endif if (takeObjectRef(object) != nullptr) { + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); [object release]; } } @@ -649,6 +865,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, return false; } + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); [object release]; return true; } @@ -656,6 +874,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, void ObjCBridgeState::detachObject(id object) noexcept { takeObjectRef(object); removeRoundTripObject(object); + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); if (object == nil) { return; 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..2ebfc994 --- /dev/null +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -0,0 +1,1909 @@ +#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 "Interop.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 bool needsRoundTripCacheFrame(nativescript::Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + +class QuickJSFastRoundTripCacheFrameGuard { + public: + QuickJSFastRoundTripCacheFrameGuard( + napi_env env, nativescript::ObjCBridgeState* bridgeState, + nativescript::Cif* cif) + : env_(env), bridgeState_(bridgeState), + active_(needsRoundTripCacheFrame(cif) && bridgeState != nullptr) { + if (active_) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~QuickJSFastRoundTripCacheFrameGuard() { + if (active_) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + nativescript::ObjCBridgeState* bridgeState_ = nullptr; + bool active_ = false; +}; + +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 = + obj != nil ? member->bridgeState->findCachedObjectWrapper(env, obj) + : nullptr; + if (converted == nullptr) { + 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 (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + QuickJSFastStackHandleScope scope(env); + napi_value cached = member->bridgeState->findCachedObjectWrapper(env, obj); + if (cached != nullptr) { + bool ok = duplicateQuickJSNapiResult(context, cached, result); + scope.close(); + return ok; + } + scope.close(); + } + + 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(); + QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, cif); + 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; + QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, cif); + @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; + } + if (Pointer::isInstance(env, value) || Reference::isInstance(env, value)) { + if (TryFastConvertNapiArgument(env, kind, value, result)) { + return true; + } + if (kind == mdTypeClass) { + void* data = nullptr; + if (Pointer::isInstance(env, value)) { + Pointer* pointer = Pointer::unwrap(env, value); + data = pointer != nullptr ? pointer->data : nullptr; + } else { + Reference* reference = Reference::unwrap(env, value); + data = reference != nullptr ? reference->data : nullptr; + } + id nativeObject = static_cast(data); + if (nativeObject != nil && object_isClass(nativeObject)) { + *reinterpret_cast(result) = static_cast(nativeObject); + return true; + } + } + return false; + } + if (tryFastConvertQuickJSObjectArgument(env, kind, ToJSValue(value), + result)) { + if (kind != mdTypeClass) { + napi_valuetype valueType = napi_undefined; + if (napi_typeof(env, value, &valueType) == napi_ok && + valueType == napi_object) { + id nativeObject = *reinterpret_cast(result); + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (nativeObject != nil && bridgeState != nullptr && + bridgeState->hasRoundTripCacheFrame()) { + bridgeState->cacheRoundTripObject(env, nativeObject, value); + } + } + } + return true; + } + return TryFastConvertNapiArgument(env, kind, 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..0170d630 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -11,22 +11,487 @@ #include "Cif.h" #include "js_native_api.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif namespace nativescript { +#ifndef NS_LIKELY +#define NS_LIKELY(value) __builtin_expect(!!(value), 1) +#endif +#ifndef NS_UNLIKELY +#define NS_UNLIKELY(value) __builtin_expect(!!(value), 0) +#endif + 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 +#ifdef TARGET_ENGINE_HERMES +struct HermesObjCReturnContext { + void* bridgeState = nullptr; + napi_value jsThis = nullptr; + id self = nil; + Class declaredClass = nil; + bool returnOwned = false; + bool receiverIsClass = false; + bool classMethod = false; + bool propertyAccess = false; +}; + +using ObjCHermesDirectReturnInvoker = bool (*)(napi_env env, Cif* cif, + void* fnptr, id self, + SEL selector, + const HermesObjCReturnContext* returnContext, + const napi_value* argv, + napi_value* result); +using CFunctionHermesDirectReturnInvoker = + bool (*)(napi_env env, Cif* cif, void* fnptr, + const napi_value* argv, napi_value* result); +using ObjCHermesFrameDirectReturnInvoker = bool (*)( + napi_env env, Cif* cif, void* fnptr, id self, SEL selector, + const HermesObjCReturnContext* returnContext, const uint64_t* argsBase, + napi_value* result); +using CFunctionHermesFrameDirectReturnInvoker = bool (*)( + napi_env env, Cif* cif, void* fnptr, const uint64_t* argsBase, + napi_value* result); +using BlockHermesFrameDirectReturnInvoker = bool (*)( + napi_env env, Cif* cif, void* fnptr, void* block, + const uint64_t* argsBase, napi_value* result); + +bool TryFastSetHermesGeneratedObjCObjectReturnValue( + napi_env env, Cif* cif, const HermesObjCReturnContext* context, + SEL selector, MDTypeKind kind, id value, napi_value* result); + +constexpr uint64_t kHermesDispatchFirstTaggedValue = 0xfff9000000000000ULL; +constexpr uint64_t kHermesDispatchBoolETag = 0x1fff6ULL; +constexpr uint64_t kHermesDispatchBoolBit = 1ULL << 46; + +inline bool isHermesDispatchNumber(uint64_t raw) { + return raw < kHermesDispatchFirstTaggedValue; +} + +inline bool isHermesDispatchBool(uint64_t raw) { + return (raw >> 47) == kHermesDispatchBoolETag; +} + +inline uint64_t hermesDispatchRawValueBits(napi_value value) { + return value != nullptr ? *reinterpret_cast(value) : 0; +} + +inline napi_value hermesDispatchFrameArg(const uint64_t* argsBase, + size_t index) { + return argsBase != nullptr + ? reinterpret_cast( + const_cast(argsBase - (index + 1))) + : nullptr; +} + +inline uint64_t hermesDispatchFrameRawArg(const uint64_t* argsBase, + size_t index) { + return argsBase != nullptr ? *(argsBase - (index + 1)) : 0; +} + +inline double hermesDispatchRawToDouble(uint64_t raw) { + double value = 0.0; + std::memcpy(&value, &raw, sizeof(value)); + return value; +} + +inline bool hermesDispatchRawDoubleIsFinite(uint64_t raw) { + constexpr uint64_t kExponentMask = 0x7ff0000000000000ULL; + return (raw & kExponentMask) != kExponentMask; +} + +inline bool readHermesDispatchFiniteNumber(napi_value value, double* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = hermesDispatchRawValueBits(value); + if (!isHermesDispatchNumber(raw)) { + return false; + } + + *result = hermesDispatchRawDoubleIsFinite(raw) + ? hermesDispatchRawToDouble(raw) + : 0.0; + return true; +} + +inline bool readHermesDispatchFiniteNumberRaw(uint64_t raw, double* result) { + if (result == nullptr || !isHermesDispatchNumber(raw)) { + return false; + } + + *result = hermesDispatchRawDoubleIsFinite(raw) + ? hermesDispatchRawToDouble(raw) + : 0.0; + return true; +} + +inline napi_value makeHermesDispatchRawValue(Cif* cif, uint64_t raw) { + if (cif != nullptr) { + cif->hermesRawReturnSlot = raw; + return reinterpret_cast(&cif->hermesRawReturnSlot); + } + + static thread_local uint64_t slots[64] = {}; + static thread_local unsigned int nextSlot = 0; + uint64_t* slot = &slots[nextSlot++ & 63]; + *slot = raw; + return reinterpret_cast(slot); +} + +inline napi_value makeHermesDispatchRawNumberValue(Cif* cif, double value) { + uint64_t raw = 0; + std::memcpy(&raw, &value, sizeof(raw)); + return makeHermesDispatchRawValue(cif, raw); +} + +inline napi_value makeHermesDispatchRawBoolValue(Cif* cif, bool value) { + return makeHermesDispatchRawValue( + cif, + (kHermesDispatchBoolETag << 47) | + (value ? kHermesDispatchBoolBit : 0)); +} + +inline bool TryFastConvertHermesGeneratedBoolArgument( + napi_env env, napi_value value, uint8_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + const uint64_t raw = hermesDispatchRawValueBits(value); + if (!isHermesDispatchBool(raw)) { + return false; + } + *result = (raw & kHermesDispatchBoolBit) != 0 ? static_cast(1) + : static_cast(0); + return true; +} + +inline bool TryFastConvertHermesGeneratedBoolRawArgument(uint64_t raw, + uint8_t* result) { + if (result == nullptr || !isHermesDispatchBool(raw)) { + return false; + } + *result = (raw & kHermesDispatchBoolBit) != 0 ? static_cast(1) + : static_cast(0); + return true; +} + +inline bool TryFastConvertHermesGeneratedDoubleArgument( + napi_env env, napi_value value, double* result) { + return readHermesDispatchFiniteNumber(value, result); +} + +inline bool TryFastConvertHermesGeneratedDoubleRawArgument(uint64_t raw, + double* result) { + return readHermesDispatchFiniteNumberRaw(raw, result); +} + +inline bool TryFastConvertHermesGeneratedFloatArgument( + napi_env env, napi_value value, float* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedFloatRawArgument(uint64_t raw, + float* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt8Argument( + napi_env env, napi_value value, int8_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt8RawArgument(uint64_t raw, + int8_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt8Argument( + napi_env env, napi_value value, uint8_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt8RawArgument(uint64_t raw, + uint8_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt16Argument( + napi_env env, napi_value value, int16_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt16RawArgument(uint64_t raw, + int16_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt16Argument( + napi_env env, napi_value value, uint16_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt16RawArgument(uint64_t raw, + uint16_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt32Argument( + napi_env env, napi_value value, int32_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt32RawArgument(uint64_t raw, + int32_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt32Argument( + napi_env env, napi_value value, uint32_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt32RawArgument(uint64_t raw, + uint32_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt64Argument( + napi_env env, napi_value value, int64_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedInt64RawArgument(uint64_t raw, + int64_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt64Argument( + napi_env env, napi_value value, uint64_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumber(value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool TryFastConvertHermesGeneratedUInt64RawArgument(uint64_t raw, + uint64_t* result) { + double converted = 0.0; + if (!readHermesDispatchFiniteNumberRaw(raw, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +inline bool SetHermesGeneratedVoidReturn(napi_env env, napi_value* result) { + return napi_get_null(env, result) == napi_ok; +} + +inline bool SetHermesGeneratedBoolReturn(Cif* cif, napi_value* result, + bool value) { + *result = makeHermesDispatchRawBoolValue(cif, value); + return true; +} + +inline bool SetHermesGeneratedInt8Return(Cif* cif, napi_value* result, + int8_t value) { + if (value == 0 || value == 1) { + *result = makeHermesDispatchRawBoolValue(cif, value == 1); + } else { + *result = + makeHermesDispatchRawNumberValue(cif, static_cast(value)); + } + return true; +} + +inline bool SetHermesGeneratedUInt8Return(Cif* cif, napi_value* result, + uint8_t value) { + if (value == 0 || value == 1) { + *result = makeHermesDispatchRawBoolValue(cif, value == 1); + } else { + *result = + makeHermesDispatchRawNumberValue(cif, static_cast(value)); + } + return true; +} + +inline bool SetHermesGeneratedInt16Return(Cif* cif, napi_value* result, + int16_t value) { + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedUInt16Return(napi_env env, Cif* cif, + napi_value* result, + uint16_t value) { + if (value >= 32 && value <= 126) { + const char buffer[2] = {static_cast(value), '\0'}; + return napi_create_string_utf8(env, buffer, 1, result) == napi_ok; + } + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedInt32Return(Cif* cif, napi_value* result, + int32_t value) { + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedUInt32Return(Cif* cif, napi_value* result, + uint32_t value) { + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedInt64Return(napi_env env, Cif* cif, + napi_value* result, + int64_t value) { + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (NS_UNLIKELY(value > kMaxSafeInteger || value < -kMaxSafeInteger)) { + return napi_create_bigint_int64(env, value, result) == napi_ok; + } + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedUInt64Return(napi_env env, Cif* cif, + napi_value* result, + uint64_t value) { + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (NS_UNLIKELY(value > kMaxSafeInteger)) { + return napi_create_bigint_uint64(env, value, result) == napi_ok; + } + *result = makeHermesDispatchRawNumberValue(cif, static_cast(value)); + return true; +} + +inline bool SetHermesGeneratedDoubleReturn(Cif* cif, napi_value* result, + double value) { + *result = makeHermesDispatchRawNumberValue(cif, value); + return true; +} +#endif struct ObjCDispatchEntry { uint64_t dispatchId; @@ -38,6 +503,11 @@ struct CFunctionDispatchEntry { CFunctionPreparedInvoker invoker; }; +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + struct ObjCNapiDispatchEntry { uint64_t dispatchId; ObjCNapiInvoker invoker; @@ -48,6 +518,54 @@ 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 +#ifdef TARGET_ENGINE_HERMES +struct ObjCHermesDirectReturnDispatchEntry { + uint64_t dispatchId; + ObjCHermesDirectReturnInvoker invoker; +}; + +struct CFunctionHermesDirectReturnDispatchEntry { + uint64_t dispatchId; + CFunctionHermesDirectReturnInvoker invoker; +}; + +struct ObjCHermesFrameDirectReturnDispatchEntry { + uint64_t dispatchId; + ObjCHermesFrameDirectReturnInvoker invoker; +}; + +struct CFunctionHermesFrameDirectReturnDispatchEntry { + uint64_t dispatchId; + CFunctionHermesFrameDirectReturnInvoker invoker; +}; + +struct BlockHermesFrameDirectReturnDispatchEntry { + uint64_t dispatchId; + BlockHermesFrameDirectReturnInvoker invoker; +}; +#endif + inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; @@ -71,8 +589,114 @@ 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)); + } +} + +inline void setV8DispatchUInt16ReturnValue( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + uint16_t value) { + if (value >= 32 && value <= 126) { + const char buffer[2] = {static_cast(value), '\0'}; + info.GetReturnValue().Set( + v8::String::NewFromUtf8(isolate, buffer, v8::NewStringType::kNormal, + 1) + .ToLocalChecked()); + } 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 +705,26 @@ 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 + +#ifndef NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH 0 +#endif + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" @@ -93,6 +737,8 @@ inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { {0, nullptr}}; inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; } // namespace nativescript #endif @@ -105,6 +751,53 @@ 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 + +#if defined(TARGET_ENGINE_HERMES) && \ + !NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH +namespace nativescript { +inline constexpr ObjCHermesDirectReturnDispatchEntry + kGeneratedObjCHermesDirectReturnDispatchEntries[] = {{0, nullptr}}; +inline constexpr CFunctionHermesDirectReturnDispatchEntry + kGeneratedCFunctionHermesDirectReturnDispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + +#if defined(TARGET_ENGINE_HERMES) && \ + !NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH +namespace nativescript { +inline constexpr ObjCHermesFrameDirectReturnDispatchEntry + kGeneratedObjCHermesFrameDirectReturnDispatchEntries[] = {{0, nullptr}}; +inline constexpr CFunctionHermesFrameDirectReturnDispatchEntry + kGeneratedCFunctionHermesFrameDirectReturnDispatchEntries[] = { + {0, nullptr}}; +} // namespace nativescript +#endif + +#if defined(TARGET_ENGINE_HERMES) && \ + !NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH +namespace nativescript { +inline constexpr BlockHermesFrameDirectReturnDispatchEntry + kGeneratedBlockHermesFrameDirectReturnDispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + namespace nativescript { template @@ -156,10 +849,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 +874,101 @@ 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 + +#ifdef TARGET_ENGINE_HERMES +inline ObjCHermesDirectReturnInvoker lookupObjCHermesDirectReturnInvoker( + uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCHermesDirectReturnDispatchEntries, dispatchId); +} + +inline CFunctionHermesDirectReturnInvoker +lookupCFunctionHermesDirectReturnInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionHermesDirectReturnDispatchEntries, dispatchId); +} + +inline ObjCHermesFrameDirectReturnInvoker +lookupObjCHermesFrameDirectReturnInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCHermesFrameDirectReturnDispatchEntries, dispatchId); +} + +inline CFunctionHermesFrameDirectReturnInvoker +lookupCFunctionHermesFrameDirectReturnInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionHermesFrameDirectReturnDispatchEntries, dispatchId); +} + +inline BlockHermesFrameDirectReturnInvoker +lookupBlockHermesFrameDirectReturnInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedBlockHermesFrameDirectReturnDispatchEntries, dispatchId); +} +#endif + } // namespace nativescript #endif // NS_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index 4e6dfcee..82246f19 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -7,11 +7,16 @@ #include "ffi.h" #include "js_native_api.h" #include "objc/runtime.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif using namespace metagen; namespace nativescript { +class Cif; + typedef enum ConvertToJSFlags : uint32_t { kReturnOwned = 1 << 0, kBlockParam = 1 << 1, @@ -56,6 +61,154 @@ 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, Cif* cif, MDTypeKind kind, + const void* value, napi_value* 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..7e4c04b2 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/TypeConv.mm @@ -38,6 +38,41 @@ + (void)transferOwnership:(napi_env)env of:(napi_value)value toNative:(id)object namespace { +static napi_value findRegisteredClassConstructor(napi_env env, Class cls) { + if (env == nullptr || cls == nil) { + return nullptr; + } + + const char* runtimeName = class_getName(cls); + if (runtimeName == nullptr || runtimeName[0] == '\0') { + return nullptr; + } + + napi_value global = nullptr; + napi_value classRegistry = nullptr; + bool hasClassRegistry = false; + if (napi_get_global(env, &global) != napi_ok || global == nullptr || + napi_has_named_property(env, global, "__nsConstructorsByObjCClassName", + &hasClassRegistry) != napi_ok || + !hasClassRegistry || + napi_get_named_property(env, global, "__nsConstructorsByObjCClassName", + &classRegistry) != napi_ok || + classRegistry == nullptr) { + return nullptr; + } + + bool hasConstructor = false; + napi_value constructor = nullptr; + if (napi_has_named_property(env, classRegistry, runtimeName, &hasConstructor) == napi_ok && + hasConstructor && + napi_get_named_property(env, classRegistry, runtimeName, &constructor) == napi_ok && + constructor != nullptr) { + return constructor; + } + + return nullptr; +} + static size_t getBufferElementSize(napi_typedarray_type type) { switch (type) { case napi_int8_array: @@ -215,13 +250,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 +1267,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 +1317,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; } @@ -2116,12 +2176,19 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { return null; } - auto bridgeState = ObjCBridgeState::InstanceData(env); + auto bridgeState = ObjCBridgeState::InstanceData(env); - if (bridgeState != nullptr) { - auto normalizePtr = [](void* ptr) -> uintptr_t { - return normalizeRuntimePointer(reinterpret_cast(ptr)); - }; + if (bridgeState != nullptr) { + if (object_isClass(obj)) { + if (napi_value constructor = findRegisteredClassConstructor(env, (Class)obj); + constructor != nullptr) { + return constructor; + } + } + + auto normalizePtr = [](void* ptr) -> uintptr_t { + return normalizeRuntimePointer(reinterpret_cast(ptr)); + }; auto protocolIt = bridgeState->mdProtocolsByPointer.find((Protocol*)obj); if (protocolIt != bridgeState->mdProtocolsByPointer.end()) { @@ -2130,15 +2197,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); + } } } } @@ -2612,6 +2682,7 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, } *res = (id)wrapped; + cacheRoundTrip(*res); return; break; @@ -2752,19 +2823,24 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, kind = mdTypeClass; } - napi_value toJS(napi_env env, void* value, uint32_t flags) override { - Class cls = *((Class*)value); + napi_value toJS(napi_env env, void* value, uint32_t flags) override { + Class cls = *((Class*)value); - if (cls == nullptr) { - napi_value null; - napi_get_null(env, &null); - return null; - } + if (cls == nullptr) { + napi_value null; + napi_get_null(env, &null); + return null; + } - auto bridgeState = ObjCBridgeState::InstanceData(env); - return bridgeState != nullptr ? bridgeState->getObject(env, (id)cls, kUnownedObject, 0, nullptr) - : nullptr; - } + if (napi_value constructor = findRegisteredClassConstructor(env, cls); + constructor != nullptr) { + return constructor; + } + + auto bridgeState = ObjCBridgeState::InstanceData(env); + return bridgeState != nullptr ? bridgeState->getObject(env, (id)cls, kUnownedObject, 0, nullptr) + : nullptr; + } void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, bool* shouldFreeAny) override { @@ -3916,6 +3992,7 @@ bool tryFastConvertObjCObjectValue(napi_env env, napi_value value, napi_valuetyp } *out = (id)wrapped; + cacheRoundTrip(*out); return true; } 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..e7a2b851 --- /dev/null +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -0,0 +1,2299 @@ +#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; + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr && bridgeState->hasRoundTripCacheFrame()) { + bridgeState->cacheRoundTripObject(env, nativeObject, + v8impl::JsValueFromV8LocalValue(value)); + } + return true; + } + } + + if (TryFastUnwrapV8PointerLikeObjectArgument(env, value, result)) { + return true; + } + + if (hasV8NativePointerProperty(env, object)) { + id nativeObject = tryUnwrapV8NativeObject(env, value); + if (nativeObject != nil) { + *result = nativeObject; + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr && bridgeState->hasRoundTripCacheFrame()) { + bridgeState->cacheRoundTripObject(env, nativeObject, + v8impl::JsValueFromV8LocalValue(value)); + } + 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: { + uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + *result = + v8::String::NewFromUtf8(isolate, buffer, v8::NewStringType::kNormal, + 1) + .ToLocalChecked(); + } else { + *result = v8::Integer::NewFromUnsigned(isolate, raw); + } + 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; + } + + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + 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) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + 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) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + 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 = 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/ffi/jsi/NativeApiJsi.h b/NativeScript/ffi/jsi/NativeApiJsi.h new file mode 100644 index 00000000..da2495dc --- /dev/null +++ b/NativeScript/ffi/jsi/NativeApiJsi.h @@ -0,0 +1,39 @@ +#ifndef NATIVE_API_JSI_H +#define NATIVE_API_JSI_H + +#include +#include +#include + +#include + +namespace nativescript { + +class NativeApiJsiScheduler { + public: + virtual ~NativeApiJsiScheduler() = default; + virtual void invokeOnJS(std::function task) = 0; + virtual void invokeOnUI(std::function task) = 0; +}; + +struct NativeApiJsiConfig { + const char* metadataPath = nullptr; + const void* metadataPtr = nullptr; + const char* globalName = "__nativeScriptNativeApi"; + std::shared_ptr scheduler = nullptr; +}; + +facebook::jsi::Object CreateNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +void InstallNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSI( + facebook::jsi::Runtime* runtime, const char* metadataPath); + +#endif // NATIVE_API_JSI_H diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm new file mode 100644 index 00000000..a6f7f78c --- /dev/null +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -0,0 +1,4654 @@ +#include "NativeApiJsi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffi.h" +#include "Metadata.h" +#include "MetadataReader.h" + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace nativescript { +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::Value; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +thread_local bool gDispatchNativeCallsToUI = false; +thread_local bool gExecutingDispatchedUINativeCall = false; +std::atomic gSynchronousNativeInvocationDepth{0}; + +class ScopedNativeApiUINativeCallDispatch final { + public: + ScopedNativeApiUINativeCallDispatch() + : previous_(gDispatchNativeCallsToUI) { + gDispatchNativeCallsToUI = true; + } + + ~ScopedNativeApiUINativeCallDispatch() { + gDispatchNativeCallsToUI = previous_; + } + + private: + bool previous_ = false; +}; + +bool shouldDispatchNativeCallToUI() { + return gDispatchNativeCallsToUI && ![NSThread isMainThread]; +} + +class ScopedNativeApiSynchronousInvocation final { + public: + ScopedNativeApiSynchronousInvocation() { + gSynchronousNativeInvocationDepth.fetch_add(1, std::memory_order_acq_rel); + } + + ~ScopedNativeApiSynchronousInvocation() { + gSynchronousNativeInvocationDepth.fetch_sub(1, std::memory_order_acq_rel); + } +}; + +template +void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { + NSString* exceptionDescription = nil; + auto run = [&]() { + ScopedNativeApiSynchronousInvocation synchronousInvocation; + @try { + invocation(); + } @catch (NSException* exception) { + exceptionDescription = [exception.description copy]; + } + }; + + if (shouldDispatchNativeCallToUI()) { + dispatch_sync(dispatch_get_main_queue(), ^{ + bool previous = gExecutingDispatchedUINativeCall; + gExecutingDispatchedUINativeCall = true; + run(); + gExecutingDispatchedUINativeCall = previous; + }); + } else { + run(); + } + + if (exceptionDescription != nil) { + std::string message = exceptionDescription.UTF8String ?: ""; + [exceptionDescription release]; + throw facebook::jsi::JSError(runtime, message); + } +} + +enum class NativeApiSymbolKind { + Class, + Function, + Constant, + Protocol, + Enum, + Struct, + Union, +}; + +struct NativeApiSymbol { + NativeApiSymbolKind kind; + MDSectionOffset offset = 0; + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL; + std::string name; + std::string runtimeName; +}; + +struct NativeApiMember { + std::string name; + std::string selectorName; + std::string setterSelectorName; + MDSectionOffset signatureOffset = MD_SECTION_OFFSET_NULL; + MDSectionOffset setterSignatureOffset = MD_SECTION_OFFSET_NULL; + MDMemberFlag flags = metagen::mdMemberFlagNull; + bool property = false; + bool readonly = false; +}; + +struct NativeApiJsiAggregateInfo; + +struct NativeApiJsiFfiType { + ffi_type type = {}; + std::vector elements; + + NativeApiJsiFfiType() { + type.type = FFI_TYPE_STRUCT; + type.size = 0; + type.alignment = 0; + type.elements = nullptr; + } + + void finalize() { + elements.push_back(nullptr); + type.elements = elements.data(); + } +}; + +struct NativeApiJsiType { + MDTypeKind kind = metagen::mdTypeVoid; + ffi_type* ffiType = &ffi_type_void; + bool supported = true; + bool returnOwned = false; + MDSectionOffset signatureOffset = MD_SECTION_OFFSET_NULL; + MDSectionOffset aggregateOffset = MD_SECTION_OFFSET_NULL; + bool aggregateIsUnion = false; + uint16_t arraySize = 0; + std::shared_ptr elementType; + std::shared_ptr aggregateInfo; + std::shared_ptr ownedFfiType; +}; + +struct NativeApiJsiAggregateField { + std::string name; + uint16_t offset = 0; + NativeApiJsiType type; +}; + +struct NativeApiJsiAggregateInfo { + std::string name; + uint16_t size = 0; + bool isUnion = false; + MDSectionOffset offset = MD_SECTION_OFFSET_NULL; + std::vector fields; + std::shared_ptr ffi; +}; + +std::string jsifySelector(const char* selector) { + std::string jsifiedSelector; + bool nextUpper = false; + for (const char* c = selector; c != nullptr && *c != '\0'; c++) { + if (*c == ':') { + nextUpper = true; + } else if (nextUpper) { + jsifiedSelector += static_cast(toupper(*c)); + nextUpper = false; + } else { + jsifiedSelector += *c; + } + } + return jsifiedSelector; +} + +std::string setterSelectorForProperty(const std::string& property) { + if (property.empty()) { + return property; + } + + std::string selector = "set"; + selector += static_cast(toupper(property[0])); + selector += property.substr(1); + selector += ":"; + return selector; +} + +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); + +class NativeApiJsiBridge { + public: + explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) + : metadata_(loadMetadata(config)), scheduler_(config.scheduler) { + selfDl_ = dlopen(nullptr, RTLD_NOW); + buildSymbolIndexes(); + } + + ~NativeApiJsiBridge() { + if (selfDl_ != nullptr) { + dlclose(selfDl_); + } + } + + MDMetadataReader* metadata() const { return metadata_.get(); } + + void* selfDl() const { return selfDl_; } + + const NativeApiSymbol* find(const std::string& name) const { + auto it = symbolsByName_.find(name); + return it != symbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findClass(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + if (symbol != nullptr && symbol->kind == NativeApiSymbolKind::Class) { + return symbol; + } + auto it = classSymbolsByRuntimeName_.find(name); + return it != classSymbolsByRuntimeName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findFunction(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Function + ? symbol + : nullptr; + } + + const NativeApiSymbol* findConstant(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Constant + ? symbol + : nullptr; + } + + const NativeApiSymbol* findProtocol(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Protocol + ? symbol + : nullptr; + } + + const NativeApiSymbol* findEnum(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Enum + ? symbol + : nullptr; + } + + const NativeApiSymbol* findStruct(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Struct + ? symbol + : nullptr; + } + + const NativeApiSymbol* findUnion(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Union + ? symbol + : nullptr; + } + + const NativeApiSymbol* findAggregate(const std::string& name) const { + const NativeApiSymbol* symbol = findStruct(name); + if (symbol != nullptr) { + return symbol; + } + return findUnion(name); + } + + size_t classCount() const { return classNames_.size(); } + size_t functionCount() const { return functionNames_.size(); } + size_t constantCount() const { return constantNames_.size(); } + size_t protocolCount() const { return protocolNames_.size(); } + size_t enumCount() const { return enumNames_.size(); } + size_t structCount() const { return structNames_.size(); } + size_t unionCount() const { return unionNames_.size(); } + + const std::vector& classNames() const { return classNames_; } + const std::vector& functionNames() const { return functionNames_; } + const std::vector& constantNames() const { return constantNames_; } + const std::vector& protocolNames() const { return protocolNames_; } + const std::vector& enumNames() const { return enumNames_; } + const std::vector& structNames() const { return structNames_; } + const std::vector& unionNames() const { return unionNames_; } + std::shared_ptr scheduler() const { return scheduler_; } + std::thread::id jsThreadId() const { return jsThreadId_; } + + void retainJsiLifetime(std::shared_ptr lifetime) { + if (lifetime == nullptr) { + return; + } + std::lock_guard lock(retainedLifetimesMutex_); + retainedLifetimes_.push_back(std::move(lifetime)); + } + + const std::vector& membersForClass( + const NativeApiSymbol& symbol) const { + auto cached = membersByClassOffset_.find(symbol.offset); + if (cached != membersByClassOffset_.end()) { + return cached->second; + } + + auto inserted = membersByClassOffset_.emplace( + symbol.offset, readMembersForClassHierarchy(symbol)); + return inserted.first->second; + } + + std::shared_ptr aggregateInfoFor( + MDSectionOffset aggregateOffset, bool isUnion); + + std::shared_ptr aggregateInfoFor( + const NativeApiSymbol& symbol) { + return aggregateInfoFor(symbol.offset, + symbol.kind == NativeApiSymbolKind::Union); + } + + private: + static std::unique_ptr loadMetadataFromFile( + const char* metadataPath) { + const char* path = metadataPath != nullptr ? metadataPath : "metadata.nsmd"; + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + throw std::runtime_error(std::string("metadata.nsmd not found: ") + path); + } + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + throw std::runtime_error(std::string("metadata.nsmd is empty: ") + path); + } + + void* buffer = malloc(static_cast(size)); + if (buffer == nullptr) { + fclose(file); + throw std::bad_alloc(); + } + + size_t read = fread(buffer, 1, static_cast(size), file); + fclose(file); + if (read != static_cast(size)) { + free(buffer); + throw std::runtime_error(std::string("failed to read metadata: ") + path); + } + + return std::make_unique(buffer, true); + } + + static std::unique_ptr loadMetadata( + const NativeApiJsiConfig& config) { + if (config.metadataPtr != nullptr && + *static_cast(config.metadataPtr) != '\0') { +#ifdef EMBED_METADATA_SIZE + return std::make_unique((void*)embedded_metadata); +#else + return std::make_unique( + const_cast(config.metadataPtr)); +#endif + } + +#ifdef EMBED_METADATA_SIZE + if (config.metadataPath == nullptr) { + return std::make_unique((void*)embedded_metadata); + } +#endif + + unsigned long segmentSize = 0; + auto segmentData = getsegmentdata( + reinterpret_cast(_dyld_get_image_header(0)), + "__objc_metadata", &segmentSize); + if (segmentData != nullptr && segmentSize > 0) { + return std::make_unique(segmentData); + } + + return loadMetadataFromFile(config.metadataPath); + } + + void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, + const char* name, const char* runtimeName = nullptr, + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL) { + if (name == nullptr || name[0] == '\0') { + return; + } + + NativeApiSymbol symbol{ + .kind = kind, + .offset = offset, + .superclassOffset = superclassOffset, + .name = name, + .runtimeName = runtimeName != nullptr ? runtimeName : name, + }; + + switch (kind) { + case NativeApiSymbolKind::Class: + classNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Function: + functionNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Constant: + constantNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Protocol: + protocolNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Enum: + enumNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Struct: + structNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Union: + unionNames_.push_back(symbol.name); + break; + } + + symbolsByName_[symbol.name] = symbol; + if (kind == NativeApiSymbolKind::Class) { + classSymbolsByOffset_[symbol.offset] = symbol; + classSymbolsByRuntimeName_[symbol.runtimeName] = std::move(symbol); + } else if (kind == NativeApiSymbolKind::Struct) { + structSymbolsByOffset_[symbol.offset] = symbol; + } else if (kind == NativeApiSymbolKind::Union) { + unionSymbolsByOffset_[symbol.offset] = symbol; + } + } + + void addAggregateAliases(NativeApiSymbolKind kind, MDSectionOffset offset, + const std::string& name) { + if (name.empty()) { + return; + } + + if (!name.empty() && name[0] == '_') { + std::string alias = name.substr(1); + if (!alias.empty() && symbolsByName_.find(alias) == symbolsByName_.end()) { + addSymbol(kind, offset, alias.c_str(), name.c_str()); + } + } + + constexpr const char* suffix = "Struct"; + if (name.size() < std::strlen(suffix) || + name.compare(name.size() - std::strlen(suffix), std::strlen(suffix), + suffix) != 0) { + std::string alias = name + suffix; + if (symbolsByName_.find(alias) == symbolsByName_.end()) { + addSymbol(kind, offset, alias.c_str(), name.c_str()); + } + } + } + + void buildSymbolIndexes() { + if (metadata_ == nullptr) { + return; + } + + indexConstants(); + indexEnums(); + indexFunctions(); + indexProtocols(); + indexClasses(); + indexStructs(); + indexUnions(); + } + + static void skipConstantValue(MDMetadataReader* metadata, + MDSectionOffset& offset, + metagen::MDVariableEvalKind evalKind) { + switch (evalKind) { + case metagen::mdEvalNone: + skipMetadataJsiType(metadata, &offset); + break; + case metagen::mdEvalInt64: + offset += sizeof(int64_t); + break; + case metagen::mdEvalDouble: + offset += sizeof(double); + break; + case metagen::mdEvalString: + offset += sizeof(MDSectionOffset); + break; + } + } + + void indexConstants() { + MDSectionOffset offset = metadata_->constantsOffset; + while (offset < metadata_->enumsOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Constant, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + auto evalKind = metadata_->getVariableEvalKind(offset); + offset += sizeof(metagen::MDVariableEvalKind); + skipConstantValue(metadata_.get(), offset, evalKind); + } + } + + void indexEnums() { + MDSectionOffset offset = metadata_->enumsOffset; + while (offset < metadata_->signaturesOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Enum, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + + bool next = true; + while (next) { + auto nameOffset = metadata_->getOffset(offset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + offset += sizeof(MDSectionOffset); + offset += sizeof(int64_t); + } + } + } + + void indexFunctions() { + MDSectionOffset offset = metadata_->functionsOffset; + while (offset < metadata_->protocolsOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Function, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + offset += sizeof(metagen::MDFunctionFlag); + } + } + + void indexProtocols() { + MDSectionOffset offset = metadata_->protocolsOffset; + while (offset < metadata_->classesOffset) { + MDSectionOffset originalOffset = offset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + addSymbol(NativeApiSymbolKind::Protocol, originalOffset, + metadata_->resolveString(nameOffset)); + + while (next) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + next = true; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + if (flags == metagen::mdMemberFlagNull) { + break; + } + + skipMember(flags, offset); + } + } + } + + void indexClasses() { + MDSectionOffset offset = metadata_->classesOffset; + while (offset < metadata_->structsOffset) { + MDSectionOffset originalOffset = offset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + auto runtimeNameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + + auto name = metadata_->resolveString(nameOffset); + const char* runtimeName = name; + if (runtimeNameOffset != MD_SECTION_OFFSET_NULL) { + runtimeName = metadata_->resolveString(runtimeNameOffset); + } + + while (hasProtocols) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + MDSectionOffset superclassOffset = + superclass & ~metagen::mdSectionOffsetNext; + if (superclassOffset != MD_SECTION_OFFSET_NULL) { + superclassOffset += metadata_->classesOffset; + } + + addSymbol(NativeApiSymbolKind::Class, originalOffset, name, runtimeName, + superclassOffset); + + bool next = (superclass & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + skipMember(flags, offset); + } + } + } + + void skipAggregateFields(MDSectionOffset& offset, bool isUnion) const { + bool next = true; + while (next) { + MDSectionOffset nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + if (nameOffset == MD_SECTION_OFFSET_NULL) { + break; + } + if (!isUnion) { + offset += sizeof(uint16_t); + } + skipMetadataJsiType(metadata_.get(), &offset); + } + } + + void indexStructs() { + MDSectionOffset offset = metadata_->structsOffset; + while (offset < metadata_->unionsOffset) { + if (metadata_->getOffset(offset) == 0) { + break; + } + MDSectionOffset originalOffset = offset; + const char* name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(uint16_t); + addSymbol(NativeApiSymbolKind::Struct, originalOffset, name); + addAggregateAliases(NativeApiSymbolKind::Struct, originalOffset, + name != nullptr ? name : ""); + skipAggregateFields(offset, false); + } + } + + void indexUnions() { + MDSectionOffset offset = metadata_->unionsOffset; + while (metadata_->getOffset(offset) != 0) { + MDSectionOffset originalOffset = offset; + const char* name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(uint16_t); + addSymbol(NativeApiSymbolKind::Union, originalOffset, name); + addAggregateAliases(NativeApiSymbolKind::Union, originalOffset, + name != nullptr ? name : ""); + skipAggregateFields(offset, true); + } + } + + void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { + if ((flags & metagen::mdMemberProperty) != 0) { + bool readonly = (flags & metagen::mdMemberReadonly) != 0; + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + if (!readonly) { + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + } + return; + } + + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + } + + std::vector readMembersForClass( + MDSectionOffset classOffset) const { + std::vector members; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset offset = classOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + + bool next = (superclass & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + if (flags == metagen::mdMemberFlagNull) { + break; + } + + NativeApiMember member; + member.flags = flags; + if ((flags & metagen::mdMemberProperty) != 0) { + member.property = true; + member.readonly = (flags & metagen::mdMemberReadonly) != 0; + member.name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + + if (!member.readonly) { + member.setterSelectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.setterSignatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + } + } else { + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + member.name = jsifySelector(member.selectorName.c_str()); + } + members.push_back(std::move(member)); + } + + return members; + } + + std::vector readMembersForClassHierarchy( + const NativeApiSymbol& symbol) const { + std::vector members = readMembersForClass(symbol.offset); + if (symbol.superclassOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + auto superclass = classSymbolsByOffset_.find(symbol.superclassOffset); + if (superclass != classSymbolsByOffset_.end()) { + const auto& inheritedMembers = membersForClass(superclass->second); + members.insert(members.end(), inheritedMembers.begin(), + inheritedMembers.end()); + } + return members; + } + + std::unique_ptr metadata_; + void* selfDl_ = nullptr; + std::unordered_map symbolsByName_; + std::unordered_map classSymbolsByRuntimeName_; + std::unordered_map classSymbolsByOffset_; + std::vector classNames_; + std::vector functionNames_; + std::vector constantNames_; + std::vector protocolNames_; + std::vector enumNames_; + std::vector structNames_; + std::vector unionNames_; + std::shared_ptr scheduler_; + mutable std::unordered_map> + membersByClassOffset_; + std::unordered_map structSymbolsByOffset_; + std::unordered_map unionSymbolsByOffset_; + std::unordered_map> + aggregateInfoByOffset_; + std::unordered_set aggregateInfoInProgress_; + std::thread::id jsThreadId_ = std::this_thread::get_id(); + std::mutex retainedLifetimesMutex_; + std::vector> retainedLifetimes_; +}; + +Value makeString(Runtime& runtime, const std::string& value) { + return String::createFromUtf8(runtime, value); +} + +std::string readStringArg(Runtime& runtime, const Value* args, size_t count, + size_t index, const char* argumentName) { + if (index >= count || !args[index].isString()) { + throw facebook::jsi::JSError( + runtime, std::string(argumentName) + " must be a string."); + } + return args[index].asString(runtime).utf8(runtime); +} + +const char* kindName(NativeApiSymbolKind kind) { + switch (kind) { + case NativeApiSymbolKind::Class: + return "class"; + case NativeApiSymbolKind::Function: + return "function"; + case NativeApiSymbolKind::Constant: + return "constant"; + case NativeApiSymbolKind::Protocol: + return "protocol"; + case NativeApiSymbolKind::Enum: + return "enum"; + case NativeApiSymbolKind::Struct: + return "struct"; + case NativeApiSymbolKind::Union: + return "union"; + } + return "unknown"; +} + +Array namesToArray(Runtime& runtime, const std::vector& names) { + Array result(runtime, names.size()); + for (size_t i = 0; i < names.size(); i++) { + result.setValueAtIndex(runtime, i, makeString(runtime, names[i])); + } + return result; +} + +void addPropertyName(Runtime& runtime, std::vector& names, + const char* name) { + names.push_back(PropNameID::forAscii(runtime, name)); +} + +class NativeApiPointerHostObject; +class NativeApiObjectHostObject; +class NativeApiClassHostObject; +class NativeApiProtocolHostObject; + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol, const Value* args, + size_t count); + +Value callObjCSelector(Runtime& runtime, + const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const std::string& selectorName, + const NativeApiMember* member, + const Value* args, size_t count); + +Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { + Object result(runtime); + result.setProperty(runtime, "kind", makeString(runtime, kindName(symbol.kind))); + result.setProperty(runtime, "name", makeString(runtime, symbol.name)); + result.setProperty(runtime, "runtimeName", + makeString(runtime, symbol.runtimeName)); + result.setProperty(runtime, "metadataOffset", + static_cast(symbol.offset)); + + if (symbol.kind == NativeApiSymbolKind::Class) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + result.setProperty(runtime, "available", cls != nil); + if (cls != nil) { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", cls); + result.setProperty(runtime, "nativeAddress", makeString(runtime, address)); + } + } else if (symbol.kind == NativeApiSymbolKind::Struct || + symbol.kind == NativeApiSymbolKind::Union) { + result.setProperty(runtime, "available", true); + } + + return result; +} + +size_t nativeSizeForType(const NativeApiJsiType& type); + +class NativeApiPointerHostObject final : public HostObject { + public: + NativeApiPointerHostObject(void* pointer, std::string kind = "pointer", + bool adopted = false) + : pointer_(pointer), kind_(std::move(kind)), adopted_(adopted) {} + + ~NativeApiPointerHostObject() override { + if (adopted_ && pointer_ != nullptr) { + free(pointer_); + pointer_ = nullptr; + } + } + + void* pointer() const { return pointer_; } + bool adopted() const { return adopted_; } + void adopt() { adopted_ = true; } + void clearWithoutFree() { + pointer_ = nullptr; + adopted_ = false; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, kind_); + } + if (property == "address") { + return static_cast(reinterpret_cast(pointer_)); + } + if (property == "adopted") { + return adopted_; + } + if (property == "add" || property == "subtract") { + void* pointer = pointer_; + bool add = property == "add"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [pointer, add](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isNumber()) { + throw facebook::jsi::JSError(runtime, "Pointer offset must be a number."); + } + intptr_t offset = static_cast(args[0].getNumber()); + intptr_t base = reinterpret_cast(pointer); + void* result = reinterpret_cast(add ? base + offset : base - offset); + return Object::createFromHostObject( + runtime, std::make_shared(result)); + }); + } + if (property == "toNumber") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toNumber"), 0, + [pointer](Runtime&, const Value&, const Value*, size_t) -> Value { + return static_cast(reinterpret_cast(pointer)); + }); + } + if (property == "toBigInt") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toBigInt"), 0, + [pointer](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + auto bigIntCtor = + runtime.global().getPropertyAsFunction(runtime, "BigInt"); + char decimal[32] = {}; + snprintf(decimal, sizeof(decimal), "%llu", + static_cast( + reinterpret_cast(pointer))); + return bigIntCtor.call(runtime, String::createFromUtf8(runtime, decimal)); + }); + } + if (property == "toHexString" || property == "toDecimalString") { + void* pointer = pointer_; + bool hex = property == "toHexString"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [pointer, hex](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; + if (hex) { + snprintf(text, sizeof(text), "0x%llx", + static_cast( + reinterpret_cast(pointer))); + } else { + snprintf(text, sizeof(text), "%lld", + static_cast(reinterpret_cast(pointer))); + } + return makeString(runtime, text); + }); + } + if (property == "toString") { + void* pointer = pointer_; + std::string kind = kind_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [pointer, kind](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", pointer); + return makeString(runtime, "[NativeApiJsi " + kind + " " + + std::string(address) + "]"); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(3); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "adopted"); + addPropertyName(runtime, names, "add"); + addPropertyName(runtime, names, "subtract"); + addPropertyName(runtime, names, "toNumber"); + addPropertyName(runtime, names, "toBigInt"); + addPropertyName(runtime, names, "toHexString"); + addPropertyName(runtime, names, "toDecimalString"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + void* pointer_ = nullptr; + std::string kind_; + bool adopted_ = false; +}; + +class NativeApiReferenceHostObject final : public HostObject { + public: + NativeApiReferenceHostObject(std::shared_ptr bridge, + NativeApiJsiType type, void* data, bool ownsData) + : bridge_(std::move(bridge)), + type_(std::move(type)), + data_(data), + ownsData_(ownsData) {} + + ~NativeApiReferenceHostObject() override { + if (ownsData_ && data_ != nullptr) { + free(data_); + data_ = nullptr; + } + } + + void* data() const { return data_; } + const NativeApiJsiType& type() const { return type_; } + void ensureStorage(NativeApiJsiType type) { + if (data_ == nullptr) { + type_ = std::move(type); + data_ = calloc(1, std::max(nativeSizeForType(type_), sizeof(void*))); + ownsData_ = true; + } + } + + Value get(Runtime& runtime, const PropNameID& name) override; + void set(Runtime& runtime, const PropNameID& name, const Value& value) override; + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "value"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiJsiType type_; + void* data_ = nullptr; + bool ownsData_ = false; +}; + +class NativeApiStructObjectHostObject final : public HostObject { + public: + NativeApiStructObjectHostObject( + std::shared_ptr bridge, + std::shared_ptr info, + const void* data = nullptr, bool ownsData = true) + : bridge_(std::move(bridge)), info_(std::move(info)), ownsData_(ownsData) { + size_t size = info_ != nullptr ? info_->size : 0; + if (ownsData_) { + ownedData_.assign(size, 0); + if (data != nullptr && size > 0) { + std::memcpy(ownedData_.data(), data, size); + } + data_ = ownedData_.empty() ? nullptr : ownedData_.data(); + } else { + data_ = const_cast(data); + } + } + + void* data() const { return data_; } + std::shared_ptr info() const { return info_; } + + Value get(Runtime& runtime, const PropNameID& name) override; + void set(Runtime& runtime, const PropNameID& name, const Value& value) override; + std::vector getPropertyNames(Runtime& runtime) override; + + private: + std::shared_ptr bridge_; + std::shared_ptr info_; + std::vector ownedData_; + void* data_ = nullptr; + bool ownsData_ = true; +}; + +class NativeApiObjectHostObject final : public HostObject { + public: + NativeApiObjectHostObject(std::shared_ptr bridge, + id object, bool ownsObject) + : bridge_(std::move(bridge)), object_(object), ownsObject_(ownsObject) { + if (object_ != nil && !ownsObject_) { + [object_ retain]; + ownsObject_ = true; + } + } + + ~NativeApiObjectHostObject() override { + if (ownsObject_ && object_ != nil) { + [object_ release]; + object_ = nil; + } + } + + id object() const { return object_; } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "object"); + } + if (property == "className") { + return makeString(runtime, object_ != nil ? object_getClassName(object_) : ""); + } + if (property == "nativeAddress") { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", object_); + return makeString(runtime, address); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, object](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + return callObjCSelector(runtime, bridge, object, false, selectorName, + nullptr, args + 1, count - 1); + }); + } + if (property == "toString") { + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [object](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + NSString* description = + object != nil ? [object description] : @""; + return makeString(runtime, description.UTF8String ?: ""); + }); + } + + if (object_ != nil) { + if (const NativeApiSymbol* symbol = + bridge_->findClass(object_getClassName(object_))) { + for (const auto& member : bridge_->membersForClass(*symbol)) { + if ((member.flags & metagen::mdMemberStatic) != 0) { + continue; + } + if (member.name != property) { + continue; + } + + if (member.property) { + return callObjCSelector(runtime, bridge_, object_, false, + member.selectorName, &member, nullptr, 0); + } + + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), + member.property ? 0 : 0, + [bridge, object, member](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, object, false, + member.selectorName, &member, args, + count); + }); + } + } + + SEL selector = sel_getUid(property.c_str()); + Method method = class_getInstanceMethod(object_getClass(object_), selector); + if (method != nullptr && method_getNumberOfArguments(method) == 2) { + return callObjCSelector(runtime, bridge_, object_, false, property, + nullptr, nullptr, 0); + } + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (object_ == nil) { + throw facebook::jsi::JSError(runtime, "Cannot set property on nil object."); + } + + if (const NativeApiSymbol* symbol = bridge_->findClass(object_getClassName(object_))) { + for (const auto& member : bridge_->membersForClass(*symbol)) { + if (!member.property || member.readonly || + (member.flags & metagen::mdMemberStatic) != 0 || + member.name != property) { + continue; + } + NativeApiMember setterMember = member; + setterMember.selectorName = member.setterSelectorName; + setterMember.signatureOffset = member.setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, + setterMember.selectorName, &setterMember, args, 1); + return; + } + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getInstanceMethod(object_getClass(object_), selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, setterSelectorName, + nullptr, args, 1); + return; + } + + throw facebook::jsi::JSError(runtime, + "No writable native property: " + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(6); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "className"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + addPropertyName(runtime, names, "toString"); + if (object_ != nil) { + if (const NativeApiSymbol* symbol = + bridge_->findClass(object_getClassName(object_))) { + for (const auto& member : bridge_->membersForClass(*symbol)) { + if ((member.flags & metagen::mdMemberStatic) == 0) { + addPropertyName(runtime, names, member.name.c_str()); + } + } + } + } + return names; + } + + private: + std::shared_ptr bridge_; + id object_ = nil; + bool ownsObject_ = false; +}; + +class NativeApiClassHostObject final : public HostObject { + public: + NativeApiClassHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Class nativeClass() const { + return objc_lookUpClass(symbol_.runtimeName.c_str()); + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "class"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return objc_lookUpClass(symbol_.runtimeName.c_str()) != nil; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol = symbol_](Runtime& runtime, const Value&, + const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiJsiClass " + symbol.name + "]"); + }); + } + if (property == "construct" || property == "alloc" || property == "new") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property), 0, + [bridge, symbol, property](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + + id result = nil; + if (property == "new") { + if (count != 0) { + throw facebook::jsi::JSError( + runtime, "new does not take arguments; use invoke for an " + "explicit Objective-C selector."); + } + result = [cls new]; + } else { + if (count != 0) { + throw facebook::jsi::JSError( + runtime, "alloc does not take arguments; call invoke on the " + "allocated object for an explicit init selector."); + } + result = [cls alloc]; + } + + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, result, true)); + }); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, symbol](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + selectorName, nullptr, args + 1, + count - 1); + }); + } + + for (const auto& member : bridge_->membersForClass(symbol_)) { + if ((member.flags & metagen::mdMemberStatic) == 0 || + member.name != property) { + continue; + } + + auto bridge = bridge_; + auto symbol = symbol_; + if (member.property) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + member.selectorName, &member, nullptr, 0); + } + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, symbol, member](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + member.selectorName, &member, args, count); + }); + } + + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls != nil) { + SEL selector = sel_getUid(property.c_str()); + Method method = class_getClassMethod(cls, selector); + if (method != nullptr && method_getNumberOfArguments(method) == 2) { + return callObjCSelector(runtime, bridge_, static_cast(cls), true, + property, nullptr, nullptr, 0); + } + } + + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(8); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "toString"); + addPropertyName(runtime, names, "construct"); + addPropertyName(runtime, names, "alloc"); + addPropertyName(runtime, names, "new"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + for (const auto& member : bridge_->membersForClass(symbol_)) { + if ((member.flags & metagen::mdMemberStatic) != 0) { + addPropertyName(runtime, names, member.name.c_str()); + } + } + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +class NativeApiProtocolHostObject final : public HostObject { + public: + explicit NativeApiProtocolHostObject(NativeApiSymbol symbol) + : symbol_(std::move(symbol)) {} + + Protocol* nativeProtocol() const { + Protocol* protocol = objc_getProtocol(symbol_.runtimeName.c_str()); + if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { + protocol = objc_getProtocol(symbol_.name.c_str()); + } + return protocol; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "protocol"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return nativeProtocol() != nullptr; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "nativeAddress") { + return static_cast( + reinterpret_cast(nativeProtocol())); + } + if (property == "toString") { + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiJsiProtocol " + symbol.name + "]"); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + NativeApiSymbol symbol_; +}; + +bool isObjectiveCObjectType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +struct NativeApiJsiSignature { + ffi_cif cif = {}; + NativeApiJsiType returnType; + std::vector argumentTypes; + std::vector ffiTypes; + bool variadic = false; + bool prepared = false; + unsigned int implicitArgumentCount = 0; +}; + +class NativeApiJsiArgumentFrame { + public: + explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(count) {} + + ~NativeApiJsiArgumentFrame() { + for (char* string : ownedCStrings_) { + free(string); + } + for (id object : ownedObjects_) { + [object release]; + } + } + + void* storageAt(size_t index, size_t size) { + storage_[index].assign(std::max(size, sizeof(void*)), 0); + values_[index] = storage_[index].data(); + return values_[index]; + } + + void addCString(char* value) { ownedCStrings_.push_back(value); } + void addObject(id value) { ownedObjects_.push_back(value); } + void** values() { return values_.empty() ? nullptr : values_.data(); } + + private: + std::vector> storage_; + std::vector values_; + std::vector ownedCStrings_; + std::vector ownedObjects_; +}; + +class NativeApiMutableBuffer final : public MutableBuffer { + public: + explicit NativeApiMutableBuffer(size_t size) : data_(size) {} + NativeApiMutableBuffer(const void* data, size_t size) : data_(size) { + if (data != nullptr && size > 0) { + std::memcpy(data_.data(), data, size); + } + } + + size_t size() const override { return data_.size(); } + uint8_t* data() override { return data_.empty() ? nullptr : data_.data(); } + + private: + std::vector data_; +}; + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame); + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); + +Value wrapNativeFunctionPointer(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, + bool block); + +bool isObjectiveCObjectType(const NativeApiJsiType& type); + +struct NativeApiJsiBlockDescriptor { + unsigned long reserved = 0; + unsigned long size = 0; + void (*copyHelper)(void*, void*) = nullptr; + void (*disposeHelper)(void*) = nullptr; + const char* signature = nullptr; +}; + +struct NativeApiJsiBlockLiteral { + void* isa = nullptr; + int flags = 0; + int reserved = 0; + void* invoke = nullptr; + NativeApiJsiBlockDescriptor* descriptor = nullptr; + void* callback = nullptr; +}; + +constexpr int kNativeApiJsiBlockNeedsFree = (1 << 24); +constexpr int kNativeApiJsiBlockHasCopyDispose = (1 << 25); +constexpr int kNativeApiJsiBlockRefCountOne = (1 << 1); +constexpr int kNativeApiJsiBlockHasSignature = (1 << 30); + +void* nativeApiJsiMallocBlockIsa() { + static void* isa = dlsym(RTLD_DEFAULT, "_NSConcreteMallocBlock"); + if (isa == nullptr) { + isa = dlsym(RTLD_DEFAULT, "_NSConcreteStackBlock"); + } + return isa; +} + +void nativeApiJsiBlockCopy(void*, void*) {} + +void nativeApiJsiBlockDispose(void*) {} + +std::string objcEncodingForJsiType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeVoid: + return "v"; + case metagen::mdTypeBool: + return "B"; + case metagen::mdTypeChar: + return "c"; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return "C"; + case metagen::mdTypeSShort: + return "s"; + case metagen::mdTypeUShort: + return "S"; + case metagen::mdTypeSInt: + return "i"; + case metagen::mdTypeUInt: + return "I"; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return "q"; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return "Q"; + case metagen::mdTypeFloat: + return "f"; + case metagen::mdTypeDouble: + return "d"; + case metagen::mdTypeString: + return "*"; + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + return "@"; + case metagen::mdTypeClassObject: + case metagen::mdTypeClass: + return "#"; + case metagen::mdTypeSelector: + return ":"; + case metagen::mdTypeBlock: + return "@?"; + case metagen::mdTypeFunctionPointer: + return "^?"; + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + if (type.elementType != nullptr && + type.elementType->kind != metagen::mdTypeVoid) { + return "^" + objcEncodingForJsiType(*type.elementType); + } + return "^v"; + case metagen::mdTypeStruct: + return "{" + + (type.aggregateInfo != nullptr ? type.aggregateInfo->name + : std::string("?")) + + "=}"; + case metagen::mdTypeArray: + return "[" + std::to_string(type.arraySize) + + (type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + : std::string("?")) + + "]"; + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + return type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + : "?"; + default: + return "?"; + } +} + +std::string objcBlockSignatureForJsiSignature( + const NativeApiJsiSignature& signature) { + std::string encoding = objcEncodingForJsiType(signature.returnType); + encoding += "@?"; + for (const auto& argType : signature.argumentTypes) { + encoding += objcEncodingForJsiType(argType); + } + return encoding; +} + +[[noreturn]] void throwNativeApiJsiCallbackException( + const std::string& message) { + NSString* reason = [NSString stringWithUTF8String:message.c_str()]; + @throw [NSException exceptionWithName:@"NativeScriptJSICallbackException" + reason:reason + userInfo:nil]; +} + +class NativeApiJsiCallback; + +void nativeApiJsiCallbackTrampoline(ffi_cif* cif, void* ret, void* args[], + void* data); + +class NativeApiJsiCallback final { + public: + NativeApiJsiCallback(Runtime& runtime, + std::shared_ptr bridge, + std::shared_ptr signature, + Function function, bool block) + : runtime_(&runtime), + bridge_(std::move(bridge)), + signature_(std::move(signature)), + function_(std::make_shared(std::move(function))), + block_(block) { + closure_ = static_cast( + ffi_closure_alloc(sizeof(ffi_closure), &executable_)); + if (closure_ == nullptr || executable_ == nullptr || + signature_ == nullptr || !signature_->prepared) { + throw facebook::jsi::JSError(runtime, + "Unable to allocate native JSI callback."); + } + + ffi_status status = ffi_prep_closure_loc( + closure_, &signature_->cif, nativeApiJsiCallbackTrampoline, this, + executable_); + if (status != FFI_OK) { + ffi_closure_free(closure_); + closure_ = nullptr; + executable_ = nullptr; + throw facebook::jsi::JSError(runtime, + "Unable to prepare native JSI callback."); + } + + if (block_) { + blockSignature_ = objcBlockSignatureForJsiSignature(*signature_); + descriptor_ = std::make_unique(); + descriptor_->reserved = 0; + descriptor_->size = sizeof(NativeApiJsiBlockLiteral); + descriptor_->copyHelper = nativeApiJsiBlockCopy; + descriptor_->disposeHelper = nativeApiJsiBlockDispose; + descriptor_->signature = blockSignature_.c_str(); + + blockLiteral_ = std::make_unique(); + blockLiteral_->isa = nativeApiJsiMallocBlockIsa(); + blockLiteral_->flags = kNativeApiJsiBlockNeedsFree | + kNativeApiJsiBlockHasCopyDispose | + kNativeApiJsiBlockRefCountOne | + kNativeApiJsiBlockHasSignature; + blockLiteral_->invoke = executable_; + blockLiteral_->descriptor = descriptor_.get(); + blockLiteral_->callback = this; + } + } + + ~NativeApiJsiCallback() { + if (closure_ != nullptr) { + ffi_closure_free(closure_); + closure_ = nullptr; + executable_ = nullptr; + } + } + + void* functionPointer() const { + return block_ && blockLiteral_ != nullptr + ? static_cast(blockLiteral_.get()) + : executable_; + } + + void invoke(void* ret, void* args[]) { + if (runtime_ == nullptr || function_ == nullptr || signature_ == nullptr) { + throwNativeApiJsiCallbackException("Invalid JSI callback."); + } + + std::string error; + auto call = [&]() { invokeOnCurrentThread(ret, args, &error); }; + bool direct = std::this_thread::get_id() == bridge_->jsThreadId() || + gExecutingDispatchedUINativeCall || + gSynchronousNativeInvocationDepth.load( + std::memory_order_acquire) > 0; + if (direct) { + call(); + } else if (auto scheduler = bridge_->scheduler()) { + dispatch_semaphore_t done = dispatch_semaphore_create(0); + scheduler->invokeOnJS([call, done]() mutable { + call(); + dispatch_semaphore_signal(done); + }); + dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); + } else { + error = "Native callback was invoked off the JS thread without a JS scheduler."; + } + + if (!error.empty()) { + throwNativeApiJsiCallbackException(error); + } + } + + private: + void invokeOnCurrentThread(void* ret, void* args[], std::string* error) { + @autoreleasepool { + try { + size_t nativeArgOffset = block_ ? 1 : 0; + std::vector jsArgs; + jsArgs.reserve(signature_->argumentTypes.size()); + for (size_t i = 0; i < signature_->argumentTypes.size(); i++) { + jsArgs.emplace_back(convertNativeReturnValue( + *runtime_, bridge_, signature_->argumentTypes[i], + args[i + nativeArgOffset])); + } + + Value result = + jsArgs.empty() + ? function_->call(*runtime_) + : function_->call(*runtime_, + static_cast(jsArgs.data()), + static_cast(jsArgs.size())); + storeReturnValue(result, ret); + } catch (const std::exception& exception) { + if (error != nullptr) { + *error = exception.what(); + } + zeroReturnValue(ret); + } catch (...) { + if (error != nullptr) { + *error = "Unknown exception in native JSI callback."; + } + zeroReturnValue(ret); + } + } + } + + void zeroReturnValue(void* ret) { + if (ret == nullptr || signature_ == nullptr || + signature_->returnType.kind == metagen::mdTypeVoid) { + return; + } + size_t size = nativeSizeForType(signature_->returnType); + if (size > 0) { + std::memset(ret, 0, size); + } + } + + void storeReturnValue(const Value& result, void* ret) { + if (ret == nullptr || + signature_->returnType.kind == metagen::mdTypeVoid) { + return; + } + + zeroReturnValue(ret); + const auto& returnType = signature_->returnType; + if (returnType.kind == metagen::mdTypeString && result.isString()) { + std::string utf8 = result.asString(*runtime_).utf8(*runtime_); + *static_cast(ret) = strdup(utf8.c_str()); + return; + } + if ((returnType.kind == metagen::mdTypePointer || + returnType.kind == metagen::mdTypeOpaquePointer) && + result.isString()) { + std::string utf8 = result.asString(*runtime_).utf8(*runtime_); + *static_cast(ret) = strdup(utf8.c_str()); + return; + } + + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(*runtime_, bridge_, returnType, result, ret, frame); + if (isObjectiveCObjectType(returnType)) { + id object = *static_cast(ret); + if (object != nil) { + [object retain]; + [object autorelease]; + } + } + } + + Runtime* runtime_ = nullptr; + std::shared_ptr bridge_; + std::shared_ptr signature_; + std::shared_ptr function_; + bool block_ = false; + ffi_closure* closure_ = nullptr; + void* executable_ = nullptr; + std::string blockSignature_; + std::unique_ptr descriptor_; + std::unique_ptr blockLiteral_; +}; + +void nativeApiJsiCallbackTrampoline(ffi_cif*, void* ret, void* args[], + void* data) { + auto callback = static_cast(data); + if (callback == nullptr) { + return; + } + callback->invoke(ret, args); +} + +size_t nativeSizeForType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeStruct: + if (type.aggregateInfo != nullptr) { + return type.aggregateInfo->size; + } + break; + case metagen::mdTypeArray: + if (type.elementType != nullptr) { + return nativeSizeForType(*type.elementType) * + static_cast(type.arraySize); + } + break; + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + if (type.elementType != nullptr) { + size_t lanes = std::max(type.arraySize, 1); + size_t abiLanes = lanes == 3 ? 4 : lanes; + return nativeSizeForType(*type.elementType) * abiLanes; + } + break; + default: + break; + } + + if (type.ffiType != nullptr && type.ffiType->size > 0) { + return type.ffiType->size; + } + if (type.ffiType == &ffi_type_void) { + return 0; + } + return sizeof(void*); +} + +bool readJsiBuffer(Runtime& runtime, const Object& object, const uint8_t** data, + size_t* byteLength) { + if (data == nullptr || byteLength == nullptr) { + return false; + } + + if (object.isArrayBuffer(runtime)) { + ArrayBuffer buffer = object.getArrayBuffer(runtime); + *data = buffer.data(runtime); + *byteLength = buffer.size(runtime); + return true; + } + + Value bufferValue = object.getProperty(runtime, "buffer"); + if (!bufferValue.isObject()) { + return false; + } + Object bufferObject = bufferValue.asObject(runtime); + if (!bufferObject.isArrayBuffer(runtime)) { + return false; + } + + size_t byteOffset = 0; + size_t viewByteLength = 0; + Value offsetValue = object.getProperty(runtime, "byteOffset"); + if (offsetValue.isNumber()) { + byteOffset = static_cast(std::max(0, offsetValue.getNumber())); + } + Value lengthValue = object.getProperty(runtime, "byteLength"); + if (lengthValue.isNumber()) { + viewByteLength = static_cast(std::max(0, lengthValue.getNumber())); + } + + ArrayBuffer buffer = bufferObject.getArrayBuffer(runtime); + if (byteOffset > buffer.size(runtime)) { + return false; + } + if (viewByteLength == 0 || byteOffset + viewByteLength > buffer.size(runtime)) { + viewByteLength = buffer.size(runtime) - byteOffset; + } + *data = buffer.data(runtime) + byteOffset; + *byteLength = viewByteLength; + return true; +} + +uint32_t rawTypeKind(MDTypeKind kind) { + return static_cast(kind); +} + +MDTypeKind stripTypeFlags(MDTypeKind kind) { + uint32_t raw = rawTypeKind(kind); + raw &= ~static_cast(metagen::mdTypeFlagNext); + raw &= ~static_cast(metagen::mdTypeFlagVariadic); + return static_cast(raw); +} + +size_t alignUp(size_t value, size_t alignment) { + if (alignment == 0) { + return value; + } + return ((value + alignment - 1) / alignment) * alignment; +} + +ffi_type* ffiTypeForJsiKind(MDTypeKind kind) { + switch (kind) { + case metagen::mdTypeChar: + return &ffi_type_sint8; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + case metagen::mdTypeBool: + return &ffi_type_uint8; + case metagen::mdTypeSShort: + return &ffi_type_sint16; + case metagen::mdTypeUShort: + return &ffi_type_uint16; + case metagen::mdTypeSInt: + return &ffi_type_sint32; + case metagen::mdTypeUInt: + return &ffi_type_uint32; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return &ffi_type_sint64; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return &ffi_type_uint64; + case metagen::mdTypeFloat: + return &ffi_type_float; + case metagen::mdTypeDouble: + return &ffi_type_double; + case metagen::mdTypeVoid: + return &ffi_type_void; + case metagen::mdTypeString: + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + case metagen::mdTypeClass: + case metagen::mdTypeSelector: + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + return &ffi_type_pointer; + default: + return nullptr; + } +} + +bool isSupportedJsiKind(MDTypeKind kind) { + switch (kind) { + default: + return ffiTypeForJsiKind(kind) != nullptr; + } +} + +void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, + MDTypeKind kind); + +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset) { + MDTypeKind kind = stripTypeFlags(metadata->getTypeKind(*offset)); + *offset += sizeof(MDTypeKind); + skipMetadataJsiTypePayload(metadata, offset, kind); +} + +void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, + MDTypeKind kind) { + switch (kind) { + case metagen::mdTypeClassObject: { + auto classOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool next = (classOffset & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeProtocolObject: { + bool next = true; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + *offset += sizeof(uint16_t); + skipMetadataJsiType(metadata, offset); + break; + case metagen::mdTypeStruct: + *offset += sizeof(MDSectionOffset); + break; + case metagen::mdTypePointer: + skipMetadataJsiType(metadata, offset); + break; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + *offset += sizeof(MDSectionOffset); + break; + default: + break; + } +} + +NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, + MDSectionOffset* offset, + NativeApiJsiBridge* bridge) { + MDTypeKind rawKind = metadata->getTypeKind(*offset); + MDTypeKind kind = stripTypeFlags(rawKind); + *offset += sizeof(MDTypeKind); + + NativeApiJsiType type; + type.kind = kind; + + switch (kind) { + case metagen::mdTypeArray: { + type.arraySize = metadata->getArraySize(*offset); + *offset += sizeof(uint16_t); + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + auto ffiOwner = std::make_shared(); + ffiOwner->elements.reserve(static_cast(type.arraySize) + 1); + ffi_type* elementFfiType = type.elementType->ffiType != nullptr + ? type.elementType->ffiType + : &ffi_type_pointer; + for (uint16_t i = 0; i < type.arraySize; i++) { + ffiOwner->elements.push_back(elementFfiType); + } + ffiOwner->finalize(); + type.ownedFfiType = ffiOwner; + type.ffiType = &ffiOwner->type; + type.supported = type.elementType->supported; + return type; + } + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: { + type.arraySize = metadata->getArraySize(*offset); + *offset += sizeof(uint16_t); + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + auto ffiOwner = std::make_shared(); +#if defined(FFI_TYPE_EXT_VECTOR) + ffiOwner->type.type = + kind == metagen::mdTypeComplex ? FFI_TYPE_COMPLEX : FFI_TYPE_EXT_VECTOR; +#else + ffiOwner->type.type = + kind == metagen::mdTypeComplex ? FFI_TYPE_COMPLEX : FFI_TYPE_STRUCT; +#endif + ffi_type* elementFfiType = type.elementType->ffiType != nullptr + ? type.elementType->ffiType + : &ffi_type_float; + size_t lanes = std::max(type.arraySize, 1); + size_t abiLanes = lanes == 3 ? 4 : lanes; + size_t elementSize = std::max(elementFfiType->size, sizeof(float)); + size_t elementAlignment = + std::max(elementFfiType->alignment, static_cast(1)); + ffiOwner->elements.reserve(abiLanes + 1); + for (size_t i = 0; i < abiLanes; i++) { + ffiOwner->elements.push_back(elementFfiType); + } + ffiOwner->finalize(); + size_t vectorAlignment = elementAlignment; + if (kind != metagen::mdTypeComplex) { + size_t packedSize = abiLanes * elementSize; + size_t preferredAlignment = packedSize >= 16 ? 16 : packedSize; + vectorAlignment = std::max(vectorAlignment, preferredAlignment); + } + vectorAlignment = std::min(vectorAlignment, 16); + ffiOwner->type.alignment = static_cast(vectorAlignment); + ffiOwner->type.size = alignUp(abiLanes * elementSize, vectorAlignment); + type.ownedFfiType = ffiOwner; + type.ffiType = &ffiOwner->type; + type.supported = type.elementType->supported; + return type; + } + case metagen::mdTypeStruct: { + auto structOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool isUnion = (structOffset & metagen::mdSectionOffsetNext) != 0; + structOffset &= ~metagen::mdSectionOffsetNext; + if (structOffset == MD_SECTION_OFFSET_NULL || bridge == nullptr) { + type.kind = metagen::mdTypePointer; + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + } + + MDSectionOffset absoluteOffset = + structOffset + (isUnion ? metadata->unionsOffset : metadata->structsOffset); + type.aggregateOffset = absoluteOffset; + type.aggregateIsUnion = isUnion; + type.aggregateInfo = bridge->aggregateInfoFor(absoluteOffset, isUnion); + type.ffiType = type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr + ? &type.aggregateInfo->ffi->type + : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + case metagen::mdTypePointer: + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + type.signatureOffset = metadata->getOffset(*offset) + metadata->signaturesOffset; + *offset += sizeof(MDSectionOffset); + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + case metagen::mdTypeClassObject: { + auto classOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool next = (classOffset & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeProtocolObject: { + bool next = true; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + default: + break; + } + + type.ffiType = ffiTypeForJsiKind(kind); + type.supported = type.ffiType != nullptr && isSupportedJsiKind(kind); + return type; +} + +std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( + MDSectionOffset aggregateOffset, bool isUnion) { + if (metadata_ == nullptr || aggregateOffset == MD_SECTION_OFFSET_NULL) { + return nullptr; + } + + auto cached = aggregateInfoByOffset_.find(aggregateOffset); + if (cached != aggregateInfoByOffset_.end()) { + return cached->second; + } + + auto info = std::make_shared(); + info->offset = aggregateOffset; + info->isUnion = isUnion; + aggregateInfoByOffset_[aggregateOffset] = info; + + if (aggregateInfoInProgress_.find(aggregateOffset) != + aggregateInfoInProgress_.end()) { + auto ffiOwner = std::make_shared(); + ffiOwner->elements.push_back(&ffi_type_pointer); + ffiOwner->finalize(); + info->ffi = ffiOwner; + return info; + } + + aggregateInfoInProgress_.insert(aggregateOffset); + + MDSectionOffset offset = aggregateOffset; + const char* name = metadata_->getString(offset); + info->name = name != nullptr ? name : ""; + offset += sizeof(MDSectionOffset); + info->size = metadata_->getArraySize(offset); + offset += sizeof(uint16_t); + + bool next = true; + while (next) { + MDSectionOffset nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + if (nameOffset == MD_SECTION_OFFSET_NULL) { + break; + } + + NativeApiJsiAggregateField field; + const char* fieldName = metadata_->resolveString(nameOffset); + field.name = fieldName != nullptr ? fieldName : ""; + if (!isUnion) { + field.offset = metadata_->getArraySize(offset); + offset += sizeof(uint16_t); + } + field.type = parseMetadataJsiType(metadata_.get(), &offset, this); + info->fields.push_back(std::move(field)); + } + + auto ffiOwner = std::make_shared(); + if (isUnion) { + ffi_type* largest = &ffi_type_uint8; + size_t largestSize = 0; + for (const auto& field : info->fields) { + size_t fieldSize = nativeSizeForType(field.type); + if (field.type.ffiType != nullptr && fieldSize >= largestSize) { + largest = field.type.ffiType; + largestSize = fieldSize; + } + } + ffiOwner->elements.push_back(largest); + } else { + for (const auto& field : info->fields) { + ffiOwner->elements.push_back(field.type.ffiType != nullptr + ? field.type.ffiType + : &ffi_type_pointer); + } + if (ffiOwner->elements.empty()) { + ffiOwner->elements.push_back(&ffi_type_uint8); + } + } + ffiOwner->finalize(); + info->ffi = ffiOwner; + aggregateInfoInProgress_.erase(aggregateOffset); + return info; +} + +std::optional parseMetadataJsiSignature( + MDMetadataReader* metadata, MDSectionOffset signatureOffset, + unsigned int implicitArgumentCount, NativeApiJsiBridge* bridge, + bool returnOwned = false) { + if (metadata == nullptr || signatureOffset == MD_SECTION_OFFSET_NULL) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = implicitArgumentCount; + + MDSectionOffset offset = signatureOffset; + MDTypeKind returnKind = metadata->getTypeKind(offset); + uint32_t returnKindRaw = rawTypeKind(returnKind); + bool next = + (returnKindRaw & static_cast(metagen::mdTypeFlagNext)) != 0; + signature.variadic = + (returnKindRaw & static_cast(metagen::mdTypeFlagVariadic)) != 0; + signature.returnType = parseMetadataJsiType(metadata, &offset, bridge); + signature.returnType.returnOwned = returnOwned; + + while (next) { + MDTypeKind argKind = metadata->getTypeKind(offset); + next = (rawTypeKind(argKind) & + static_cast(metagen::mdTypeFlagNext)) != 0; + signature.argumentTypes.push_back(parseMetadataJsiType(metadata, &offset, bridge)); + } + + signature.ffiTypes.reserve(signature.argumentTypes.size() + + implicitArgumentCount); + for (unsigned int i = 0; i < implicitArgumentCount; i++) { + signature.ffiTypes.push_back(&ffi_type_pointer); + } + for (const auto& argType : signature.argumentTypes) { + signature.ffiTypes.push_back(argType.ffiType != nullptr ? argType.ffiType + : &ffi_type_pointer); + } + + ffi_status status = ffi_prep_cif( + &signature.cif, FFI_DEFAULT_ABI, + static_cast(signature.ffiTypes.size()), + signature.returnType.ffiType != nullptr ? signature.returnType.ffiType + : &ffi_type_void, + signature.ffiTypes.empty() ? nullptr : signature.ffiTypes.data()); + signature.prepared = status == FFI_OK; + return signature; +} + +const char* skipObjCTypeQualifiers(const char* encoding) { + while (encoding != nullptr && *encoding != '\0' && + std::strchr("rnNoORV", *encoding) != nullptr) { + encoding++; + } + return encoding; +} + +NativeApiJsiType parseObjCEncodedJsiType(const char* encoding) { + encoding = skipObjCTypeQualifiers(encoding); + NativeApiJsiType type; + + if (encoding == nullptr || *encoding == '\0') { + type.kind = metagen::mdTypePointer; + type.ffiType = &ffi_type_pointer; + return type; + } + + switch (*encoding) { + case 'c': + type.kind = metagen::mdTypeChar; + break; + case 'i': + type.kind = metagen::mdTypeSInt; + break; + case 's': + type.kind = metagen::mdTypeSShort; + break; + case 'l': + case 'q': + type.kind = metagen::mdTypeSInt64; + break; + case 'C': + type.kind = metagen::mdTypeUInt8; + break; + case 'I': + type.kind = metagen::mdTypeUInt; + break; + case 'S': + type.kind = metagen::mdTypeUShort; + break; + case 'L': + case 'Q': + type.kind = metagen::mdTypeUInt64; + break; + case 'f': + type.kind = metagen::mdTypeFloat; + break; + case 'd': + type.kind = metagen::mdTypeDouble; + break; + case 'B': + type.kind = metagen::mdTypeBool; + break; + case 'v': + type.kind = metagen::mdTypeVoid; + break; + case '*': + type.kind = metagen::mdTypeString; + break; + case '@': + if (std::strncmp(encoding, "@\"NSString\"", 11) == 0) { + type.kind = metagen::mdTypeNSStringObject; + } else if (std::strncmp(encoding, "@\"NSMutableString\"", 18) == 0) { + type.kind = metagen::mdTypeNSMutableStringObject; + } else { + type.kind = metagen::mdTypeAnyObject; + } + break; + case '#': + type.kind = metagen::mdTypeClass; + break; + case ':': + type.kind = metagen::mdTypeSelector; + break; + case '^': + type.kind = metagen::mdTypePointer; + break; + case '{': + case '[': + case '(': + type.kind = metagen::mdTypeStruct; + type.supported = false; + type.ffiType = nullptr; + return type; + default: + type.kind = metagen::mdTypePointer; + break; + } + + type.ffiType = ffiTypeForJsiKind(type.kind); + type.supported = type.ffiType != nullptr; + return type; +} + +std::optional parseObjCMethodJsiSignature(Method method) { + if (method == nullptr) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = 2; + + char* returnEncoding = method_copyReturnType(method); + signature.returnType = parseObjCEncodedJsiType(returnEncoding); + if (returnEncoding != nullptr) { + free(returnEncoding); + } + + unsigned int totalArgc = method_getNumberOfArguments(method); + for (unsigned int i = 2; i < totalArgc; i++) { + char* argEncoding = method_copyArgumentType(method, i); + signature.argumentTypes.push_back(parseObjCEncodedJsiType(argEncoding)); + if (argEncoding != nullptr) { + free(argEncoding); + } + } + + signature.ffiTypes.reserve(totalArgc); + signature.ffiTypes.push_back(&ffi_type_pointer); + signature.ffiTypes.push_back(&ffi_type_pointer); + for (const auto& argType : signature.argumentTypes) { + signature.ffiTypes.push_back(argType.ffiType != nullptr ? argType.ffiType + : &ffi_type_pointer); + } + + ffi_status status = ffi_prep_cif( + &signature.cif, FFI_DEFAULT_ABI, + static_cast(signature.ffiTypes.size()), + signature.returnType.ffiType != nullptr ? signature.returnType.ffiType + : &ffi_type_void, + signature.ffiTypes.data()); + signature.prepared = status == FFI_OK; + return signature; +} + +bool unsupportedJsiType(const NativeApiJsiType& type) { + return !type.supported || type.ffiType == nullptr; +} + +bool signatureSupportedForJsiCallback(const NativeApiJsiSignature& signature) { + if (!signature.prepared || signature.variadic || + unsupportedJsiType(signature.returnType)) { + return false; + } + for (const auto& argType : signature.argumentTypes) { + if (unsupportedJsiType(argType)) { + return false; + } + } + return true; +} + +std::shared_ptr createJsiCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiJsiType& type, Function function, bool block) { + if (bridge == nullptr || bridge->metadata() == nullptr || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native callback metadata is unavailable."); + } + + auto parsed = parseMetadataJsiSignature( + bridge->metadata(), type.signatureOffset, block ? 1 : 0, bridge.get()); + if (!parsed || !signatureSupportedForJsiCallback(*parsed)) { + throw facebook::jsi::JSError( + runtime, "Native callback signature is not supported by pure JSI."); + } + + auto signature = + std::make_shared(std::move(*parsed)); + auto callback = std::make_shared( + runtime, bridge, std::move(signature), std::move(function), block); + bridge->retainJsiLifetime(callback); + return callback; +} + +id objectFromJsiValue(Runtime& runtime, const Value& value, + NativeApiJsiArgumentFrame& frame, bool mutableString) { + if (value.isNull() || value.isUndefined()) { + return nil; + } + if (value.isString()) { + std::string utf8 = value.asString(runtime).utf8(runtime); + id string = mutableString + ? [[NSMutableString alloc] initWithUTF8String:utf8.c_str()] + : [[NSString alloc] initWithUTF8String:utf8.c_str()]; + frame.addObject(string); + return string; + } + if (value.isBool()) { + return [NSNumber numberWithBool:value.getBool()]; + } + if (value.isNumber()) { + return [NSNumber numberWithDouble:value.getNumber()]; + } + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->nativeClass()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime) + ->nativeProtocol()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->pointer()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->data()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->data()); + } + + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + return [NSData dataWithBytes:bytes length:byteLength]; + } + + if (object.isArray(runtime)) { + Array array = object.getArray(runtime); + NSMutableArray* nativeArray = + [NSMutableArray arrayWithCapacity:array.size(runtime)]; + for (size_t i = 0; i < array.size(runtime); i++) { + id element = objectFromJsiValue(runtime, array.getValueAtIndex(runtime, i), + frame, false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + return nativeArray; + } + + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + Array propertyNames = object.getPropertyNames(runtime); + for (size_t i = 0; i < propertyNames.size(runtime); i++) { + Value propertyNameValue = propertyNames.getValueAtIndex(runtime, i); + if (!propertyNameValue.isString()) { + continue; + } + std::string key = propertyNameValue.asString(runtime).utf8(runtime); + Value propertyValue = object.getProperty(runtime, key.c_str()); + if (propertyValue.isUndefined()) { + continue; + } + id nativeValue = objectFromJsiValue(runtime, propertyValue, frame, false); + NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; + if (nativeKey != nil) { + [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:nativeKey]; + } + } + return dictionary; + } + throw facebook::jsi::JSError(runtime, + "Value cannot be converted to Objective-C object."); +} + +bool readNativePointerProperty(Runtime& runtime, const Object& object, + void** pointer) { + if (pointer == nullptr) { + return false; + } + + Value nativePointerValue = + object.getProperty(runtime, "__nativeApiPointer"); + if (nativePointerValue.isNumber()) { + *pointer = reinterpret_cast( + static_cast(nativePointerValue.getNumber())); + return true; + } + + Value nativeAddressValue = object.getProperty(runtime, "nativeAddress"); + if (nativeAddressValue.isNumber()) { + *pointer = reinterpret_cast( + static_cast(nativeAddressValue.getNumber())); + return true; + } + + return false; +} + +void* pointerFromJsiValue(Runtime& runtime, const Value& value, + NativeApiJsiArgumentFrame& frame) { + if (value.isNull() || value.isUndefined()) { + return nullptr; + } + if (value.isNumber()) { + return reinterpret_cast(static_cast(value.getNumber())); + } + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->pointer(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime) + ->nativeProtocol(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->data(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->data(); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return nativePointer; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + return const_cast(bytes); + } + } + if (value.isString()) { + std::string utf8 = value.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + frame.addCString(string); + return string; + } + throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); +} + +template +void writeNumericArgument(Runtime& runtime, const Value& value, void* target, + const char* typeName) { + if (!value.isNumber() && !value.isBool()) { + throw facebook::jsi::JSError(runtime, + std::string("Expected numeric ") + typeName + + " argument."); + } + double number = value.isBool() ? (value.getBool() ? 1.0 : 0.0) + : value.getNumber(); + *static_cast(target) = static_cast(number); +} + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame); + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); + +void convertAggregateArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + size_t size = nativeSizeForType(type); + if (size == 0) { + return; + } + + std::memset(target, 0, size); + if (value.isNull() || value.isUndefined()) { + return; + } + + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + auto structObject = object.getHostObject(runtime); + if (structObject->data() != nullptr) { + std::memcpy(target, structObject->data(), + std::min(size, static_cast(structObject->info()->size))); + } + return; + } + if (object.isHostObject(runtime)) { + void* data = object.getHostObject(runtime)->data(); + if (data != nullptr) { + std::memcpy(target, data, size); + } + return; + } + if (object.isHostObject(runtime)) { + void* data = object.getHostObject(runtime)->pointer(); + if (data != nullptr) { + std::memcpy(target, data, size); + } + return; + } + + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (bytes != nullptr) { + std::memcpy(target, bytes, std::min(byteLength, size)); + } + return; + } + } + + if (type.aggregateInfo == nullptr) { + throw facebook::jsi::JSError(runtime, "Missing native struct metadata."); + } + if (!value.isObject()) { + throw facebook::jsi::JSError(runtime, "Expected struct descriptor object."); + } + + Object object = value.asObject(runtime); + for (const auto& field : type.aggregateInfo->fields) { + if (!object.hasProperty(runtime, field.name.c_str())) { + continue; + } + Value fieldValue = object.getProperty(runtime, field.name.c_str()); + void* fieldTarget = static_cast(target) + field.offset; + convertJsiArgument(runtime, bridge, field.type, fieldValue, fieldTarget, + frame); + } +} + +void convertIndexedAggregateArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + size_t size = nativeSizeForType(type); + std::memset(target, 0, size); + if (value.isNull() || value.isUndefined()) { + return; + } + if (value.isObject()) { + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, value.asObject(runtime), &bytes, &byteLength)) { + if (bytes != nullptr) { + std::memcpy(target, bytes, std::min(byteLength, size)); + } + return; + } + } + if (!value.isObject() || !value.asObject(runtime).isArray(runtime)) { + throw facebook::jsi::JSError(runtime, "Expected array, ArrayBuffer, or typed array."); + } + + Array array = value.asObject(runtime).getArray(runtime); + size_t elementSize = type.elementType != nullptr ? nativeSizeForType(*type.elementType) : 0; + if (elementSize == 0 || type.elementType == nullptr) { + throw facebook::jsi::JSError(runtime, "Invalid native array element type."); + } + size_t count = std::min(type.arraySize, array.size(runtime)); + for (size_t i = 0; i < count; i++) { + void* slot = static_cast(target) + (i * elementSize); + convertJsiArgument(runtime, bridge, *type.elementType, + array.getValueAtIndex(runtime, i), slot, frame); + } +} + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError(runtime, + "This native signature is not supported by " + "the pure JSI bridge yet."); + } + + switch (type.kind) { + case metagen::mdTypeBool: + if (!value.isNumber() && !value.isBool()) { + throw facebook::jsi::JSError(runtime, + "Expected boolean or numeric argument."); + } + *static_cast(target) = + value.isBool() ? static_cast(value.getBool()) + : static_cast(value.getNumber() != 0); + break; + case metagen::mdTypeChar: + writeNumericArgument(runtime, value, target, "int8"); + break; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + writeNumericArgument(runtime, value, target, "uint8"); + break; + case metagen::mdTypeSShort: + writeNumericArgument(runtime, value, target, "int16"); + break; + case metagen::mdTypeUShort: + if (value.isString()) { + std::string text = value.asString(runtime).utf8(runtime); + if (text.size() != 1) { + throw facebook::jsi::JSError( + runtime, "Expected a single-character string."); + } + *static_cast(target) = + static_cast(static_cast(text[0])); + } else { + writeNumericArgument(runtime, value, target, "uint16"); + } + break; + case metagen::mdTypeSInt: + writeNumericArgument(runtime, value, target, "int32"); + break; + case metagen::mdTypeUInt: + writeNumericArgument(runtime, value, target, "uint32"); + break; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + writeNumericArgument(runtime, value, target, "int64"); + break; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + writeNumericArgument(runtime, value, target, "uint64"); + break; + case metagen::mdTypeFloat: + writeNumericArgument(runtime, value, target, "float"); + break; + case metagen::mdTypeDouble: + writeNumericArgument(runtime, value, target, "double"); + break; + case metagen::mdTypeString: { + if (value.isNull() || value.isUndefined()) { + *static_cast(target) = nullptr; + break; + } + if (!value.isString()) { + throw facebook::jsi::JSError(runtime, "Expected string argument."); + } + std::string utf8 = value.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + frame.addCString(string); + *static_cast(target) = string; + break; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: { + id object = objectFromJsiValue( + runtime, value, frame, + type.kind == metagen::mdTypeNSMutableStringObject); + *static_cast(target) = object; + break; + } + case metagen::mdTypeClass: { + Class cls = nil; + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + cls = objc_lookUpClass(name.c_str()); + } else if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + cls = object.getHostObject(runtime) + ->nativeClass(); + } else if (object.isHostObject(runtime)) { + id nativeObject = + object.getHostObject(runtime)->object(); + cls = nativeObject != nil ? object_getClass(nativeObject) : nil; + } + } + *static_cast(target) = cls; + break; + } + case metagen::mdTypeSelector: { + if (value.isNull() || value.isUndefined()) { + *static_cast(target) = nullptr; + break; + } + if (!value.isString()) { + throw facebook::jsi::JSError(runtime, "Expected selector string."); + } + std::string selectorName = value.asString(runtime).utf8(runtime); + *static_cast(target) = sel_registerName(selectorName.c_str()); + break; + } + case metagen::mdTypePointer: + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + auto reference = object.getHostObject(runtime); + if (type.elementType != nullptr) { + reference->ensureStorage(*type.elementType); + } + *static_cast(target) = reference->data(); + break; + } + } + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + case metagen::mdTypeOpaquePointer: + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + if (value.isObject()) { + Object object = value.asObject(runtime); + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + *static_cast(target) = nativePointer; + break; + } + if (object.isFunction(runtime)) { + auto callback = createJsiCallback( + runtime, bridge, type, object.asFunction(runtime), + type.kind == metagen::mdTypeBlock); + void* pointer = callback->functionPointer(); + try { + object.setProperty( + runtime, "__nativeApiPointer", + static_cast(reinterpret_cast(pointer))); + } catch (const std::exception&) { + } + *static_cast(target) = pointer; + break; + } + } + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + } + case metagen::mdTypeStruct: + convertAggregateArgument(runtime, bridge, type, value, target, frame); + break; + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + convertIndexedAggregateArgument(runtime, bridge, type, value, target, + frame); + break; + default: + throw facebook::jsi::JSError(runtime, "Unsupported JSI argument type."); + } +} + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value) { + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError(runtime, + "This native return type is not supported by " + "the pure JSI bridge yet."); + } + + switch (type.kind) { + case metagen::mdTypeVoid: + return Value::undefined(); + case metagen::mdTypeBool: + return *static_cast(value) != 0; + case metagen::mdTypeChar: + return static_cast(*static_cast(value)); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return static_cast(*static_cast(value)); + case metagen::mdTypeSShort: + return static_cast(*static_cast(value)); + case metagen::mdTypeUShort: { + uint16_t raw = *static_cast(value); + if (raw >= 32 && raw <= 126) { + char buffer[2] = {static_cast(raw), '\0'}; + return String::createFromUtf8(runtime, buffer); + } + return static_cast(raw); + } + case metagen::mdTypeSInt: + return static_cast(*static_cast(value)); + case metagen::mdTypeUInt: + return static_cast(*static_cast(value)); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return static_cast(*static_cast(value)); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return static_cast(*static_cast(value)); + case metagen::mdTypeFloat: + return static_cast(*static_cast(value)); + case metagen::mdTypeDouble: + return *static_cast(value); + case metagen::mdTypeString: { + const char* string = *static_cast(value); + return string != nullptr ? makeString(runtime, string) : Value::null(); + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: { + id object = *static_cast(value); + if (object == nil) { + return Value::null(); + } + if ([object isKindOfClass:[NSString class]]) { + std::string utf8 = [static_cast(object) UTF8String] ?: ""; + if (type.returnOwned) { + [object release]; + } + return makeString(runtime, utf8); + } + if ([object isKindOfClass:[NSNumber class]]) { + NSNumber* number = static_cast(object); + const char* objCType = [number objCType]; + bool isBool = + objCType != nullptr && std::strcmp(objCType, @encode(BOOL)) == 0; + Value result = isBool ? Value(static_cast([number boolValue])) + : Value([number doubleValue]); + if (type.returnOwned) { + [object release]; + } + return result; + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, object, type.returnOwned)); + } + case metagen::mdTypeClass: { + Class cls = *static_cast(value); + if (cls == nil) { + return Value::null(); + } + const char* name = class_getName(cls); + NativeApiSymbol symbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; + if (const NativeApiSymbol* found = bridge->findClass(symbol.name)) { + symbol = *found; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); + } + case metagen::mdTypeSelector: { + SEL selector = *static_cast(value); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + return selectorName != nullptr ? makeString(runtime, selectorName) + : Value::null(); + } + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + return Object::createFromHostObject( + runtime, std::make_shared(pointer)); + } + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + return wrapNativeFunctionPointer(runtime, bridge, type, pointer, + type.kind == metagen::mdTypeBlock); + } + case metagen::mdTypeStruct: + if (type.aggregateInfo == nullptr) { + return ArrayBuffer( + runtime, std::make_shared( + value, nativeSizeForType(type))); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, type.aggregateInfo, value, true)); + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: { + Array result(runtime, type.arraySize); + if (type.elementType == nullptr) { + return result; + } + size_t elementSize = nativeSizeForType(*type.elementType); + auto base = static_cast(value); + for (uint16_t i = 0; i < type.arraySize; i++) { + result.setValueAtIndex( + runtime, i, + convertNativeReturnValue(runtime, bridge, *type.elementType, + base + (static_cast(i) * elementSize))); + } + return result; + } + default: + throw facebook::jsi::JSError(runtime, "Unsupported JSI return type."); + } +} + +Value NativeApiReferenceHostObject::get(Runtime& runtime, + const PropNameID& name) { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "reference"); + } + if (property == "address") { + return static_cast(reinterpret_cast(data_)); + } + if (property == "value") { + if (data_ == nullptr) { + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge_, type_, data_); + } + if (property == "toString") { + void* data = data_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [data](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", data); + return makeString(runtime, "[NativeApiJsi Reference " + + std::string(address) + "]"); + }); + } + return Value::undefined(); +} + +void NativeApiReferenceHostObject::set(Runtime& runtime, + const PropNameID& name, + const Value& value) { + std::string property = name.utf8(runtime); + if (property != "value") { + return; + } + if (data_ == nullptr) { + size_t size = nativeSizeForType(type_); + data_ = calloc(1, std::max(size, sizeof(void*))); + ownsData_ = true; + } + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge_, type_, value, data_, frame); +} + +Value NativeApiStructObjectHostObject::get(Runtime& runtime, + const PropNameID& name) { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, info_ != nullptr && info_->isUnion ? "union" : "struct"); + } + if (property == "name") { + return makeString(runtime, info_ != nullptr ? info_->name : ""); + } + if (property == "sizeof") { + return static_cast(info_ != nullptr ? info_->size : 0); + } + if (property == "address") { + return static_cast(reinterpret_cast(data_)); + } + if (property == "toString") { + auto info = info_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [info](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, + std::string("[NativeApiJsi ") + + (info != nullptr && info->isUnion ? "Union " : "Struct ") + + (info != nullptr ? info->name : "") + "]"); + }); + } + + if (info_ != nullptr && data_ != nullptr) { + for (const auto& field : info_->fields) { + if (field.name != property) { + continue; + } + void* fieldData = static_cast(data_) + field.offset; + return convertNativeReturnValue(runtime, bridge_, field.type, fieldData); + } + } + return Value::undefined(); +} + +void NativeApiStructObjectHostObject::set(Runtime& runtime, + const PropNameID& name, + const Value& value) { + std::string property = name.utf8(runtime); + if (info_ == nullptr || data_ == nullptr) { + throw facebook::jsi::JSError(runtime, "Struct is not initialized."); + } + for (const auto& field : info_->fields) { + if (field.name != property) { + continue; + } + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge_, field.type, value, + static_cast(data_) + field.offset, frame); + return; + } + throw facebook::jsi::JSError(runtime, "No native struct field: " + property); +} + +std::vector NativeApiStructObjectHostObject::getPropertyNames( + Runtime& runtime) { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "sizeof"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "toString"); + if (info_ != nullptr) { + for (const auto& field : info_->fields) { + addPropertyName(runtime, names, field.name.c_str()); + } + } + return names; +} + +NativeApiJsiType primitiveInteropType(MDTypeKind kind) { + NativeApiJsiType type; + type.kind = kind; + type.ffiType = ffiTypeForJsiKind(kind); + type.supported = type.ffiType != nullptr; + return type; +} + +std::optional interopTypeFromValue( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value) { + if (value.isNumber()) { + MDTypeKind kind = stripTypeFlags(static_cast( + static_cast(value.getNumber()))); + switch (kind) { + case metagen::mdTypeVoid: + case metagen::mdTypeBool: + case metagen::mdTypeChar: + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + case metagen::mdTypeSShort: + case metagen::mdTypeUShort: + case metagen::mdTypeSInt: + case metagen::mdTypeUInt: + case metagen::mdTypeSLong: + case metagen::mdTypeULong: + case metagen::mdTypeSInt64: + case metagen::mdTypeUInt64: + case metagen::mdTypeFloat: + case metagen::mdTypeDouble: + case metagen::mdTypeString: + case metagen::mdTypeAnyObject: + case metagen::mdTypeClass: + case metagen::mdTypeSelector: + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + return primitiveInteropType(kind); + default: + return std::nullopt; + } + } + + if (!value.isObject()) { + return std::nullopt; + } + + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + auto structObject = object.getHostObject(runtime); + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = structObject->info(); + type.aggregateOffset = type.aggregateInfo != nullptr + ? type.aggregateInfo->offset + : MD_SECTION_OFFSET_NULL; + type.aggregateIsUnion = type.aggregateInfo != nullptr && + type.aggregateInfo->isUnion; + type.ffiType = type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr + ? &type.aggregateInfo->ffi->type + : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + + Value kindValue = object.getProperty(runtime, "kind"); + Value offsetValue = object.getProperty(runtime, "metadataOffset"); + if (kindValue.isString() && offsetValue.isNumber()) { + std::string kindName = kindValue.asString(runtime).utf8(runtime); + if (kindName == "struct" || kindName == "union") { + bool isUnion = kindName == "union"; + auto info = bridge->aggregateInfoFor( + static_cast(offsetValue.getNumber()), isUnion); + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = info; + type.aggregateOffset = info != nullptr ? info->offset : MD_SECTION_OFFSET_NULL; + type.aggregateIsUnion = isUnion; + type.ffiType = info != nullptr && info->ffi != nullptr ? &info->ffi->type : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + } + + return std::nullopt; +} + +Value makeAggregateConstructor(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol) { + auto info = bridge->aggregateInfoFor(symbol); + auto constructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, symbol.name.c_str()), 1, + [bridge, symbol, info](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (info == nullptr) { + throw facebook::jsi::JSError(runtime, + "Native aggregate metadata is unavailable: " + + symbol.name); + } + + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = info; + type.aggregateOffset = info->offset; + type.aggregateIsUnion = info->isUnion; + type.ffiType = info->ffi != nullptr ? &info->ffi->type : nullptr; + type.supported = type.ffiType != nullptr; + + std::vector storage(info->size, 0); + if (count > 0) { + NativeApiJsiArgumentFrame frame(1); + convertAggregateArgument(runtime, bridge, type, args[0], + storage.data(), frame); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, info, storage.data(), true)); + }); + + constructor.setProperty(runtime, "kind", + makeString(runtime, symbol.kind == NativeApiSymbolKind::Union + ? "union" + : "struct")); + constructor.setProperty(runtime, "name", makeString(runtime, symbol.name)); + constructor.setProperty(runtime, "runtimeName", makeString(runtime, symbol.runtimeName)); + constructor.setProperty(runtime, "metadataOffset", static_cast(symbol.offset)); + constructor.setProperty(runtime, "sizeof", + static_cast(info != nullptr ? info->size : 0)); + Array fields(runtime, info != nullptr ? info->fields.size() : 0); + if (info != nullptr) { + for (size_t i = 0; i < info->fields.size(); i++) { + fields.setValueAtIndex(runtime, i, makeString(runtime, info->fields[i].name)); + } + } + constructor.setProperty(runtime, "fields", fields); + return constructor; +} + +size_t sizeofInteropType(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value) { + if (auto type = interopTypeFromValue(runtime, bridge, value)) { + return nativeSizeForType(*type); + } + + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime) || + object.isHostObject(runtime) || + object.isHostObject(runtime) || + object.isHostObject(runtime)) { + return sizeof(void*); + } + Value sizeValue = object.getProperty(runtime, "sizeof"); + if (sizeValue.isNumber()) { + return static_cast(sizeValue.getNumber()); + } + } + + throw facebook::jsi::JSError(runtime, "Invalid type for interop.sizeof."); +} + +Object createPointer(Runtime& runtime, void* pointer, bool adopted = false) { + return Object::createFromHostObject( + runtime, std::make_shared(pointer, "pointer", + adopted)); +} + +Class classFromJsiValue(Runtime& runtime, const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + return objc_lookUpClass(name.c_str()); + } + if (!value.isObject()) { + return Nil; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + if (object.isHostObject(runtime)) { + id nativeObject = object.getHostObject(runtime)->object(); + return nativeObject != nil ? object_getClass(nativeObject) : Nil; + } + return Nil; +} + +Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol == nullptr) { + constexpr const char* suffix = "Protocol"; + if (name.size() > std::strlen(suffix) && + name.compare(name.size() - std::strlen(suffix), std::strlen(suffix), + suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - std::strlen(suffix)).c_str()); + } + } + return protocol; + } + if (!value.isObject()) { + return nullptr; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime) + ->nativeProtocol(); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->pointer()); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return static_cast(nativePointer); + } + Value nameValue = object.getProperty(runtime, "name"); + if (nameValue.isString()) { + return protocolFromJsiValue(runtime, nameValue); + } + return nullptr; +} + +Object createInteropObject(Runtime& runtime, + const std::shared_ptr& bridge) { + Object interop(runtime); + Object types(runtime); + auto setType = [&](const char* name, MDTypeKind kind) { + types.setProperty(runtime, name, static_cast(kind)); + }; + setType("void", metagen::mdTypeVoid); + setType("bool", metagen::mdTypeBool); + setType("int8", metagen::mdTypeChar); + setType("uint8", metagen::mdTypeUInt8); + setType("int16", metagen::mdTypeSShort); + setType("uint16", metagen::mdTypeUShort); + setType("int32", metagen::mdTypeSInt); + setType("uint32", metagen::mdTypeUInt); + setType("int64", metagen::mdTypeSInt64); + setType("uint64", metagen::mdTypeUInt64); + setType("float", metagen::mdTypeFloat); + setType("double", metagen::mdTypeDouble); + setType("UTF8CString", metagen::mdTypeString); + setType("unichar", metagen::mdTypeUShort); + setType("id", metagen::mdTypeAnyObject); + setType("class", metagen::mdTypeClass); + setType("SEL", metagen::mdTypeSelector); + setType("selector", metagen::mdTypeSelector); + setType("pointer", metagen::mdTypePointer); + setType("block", metagen::mdTypeBlock); + setType("functionPointer", metagen::mdTypeFunctionPointer); + interop.setProperty(runtime, "types", types); + + interop.setProperty( + runtime, "Pointer", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Pointer"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count > 0 && args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + return Value(runtime, object); + } + } + void* pointer = nullptr; + if (count > 0 && !args[0].isNull() && !args[0].isUndefined()) { + if (!args[0].isNumber()) { + throw facebook::jsi::JSError(runtime, + "Pointer expects a numeric address."); + } + pointer = reinterpret_cast( + static_cast(args[0].getNumber())); + } + return createPointer(runtime, pointer); + })); + + interop.setProperty( + runtime, "Reference", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Reference"), 2, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + NativeApiJsiType type = primitiveInteropType(metagen::mdTypePointer); + bool hasType = count > 0 && + interopTypeFromValue(runtime, bridge, args[0]).has_value(); + if (hasType) { + type = *interopTypeFromValue(runtime, bridge, args[0]); + } + + size_t size = hasType ? nativeSizeForType(type) : sizeof(void*); + void* data = nullptr; + bool ownsData = false; + if (hasType) { + data = calloc(1, std::max(size, sizeof(void*))); + ownsData = true; + if (count > 1) { + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, args[1], data, frame); + } + } + + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, type, data, ownsData)); + })); + + interop.setProperty( + runtime, "sizeof", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "sizeof"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1) { + throw facebook::jsi::JSError(runtime, "sizeof expects a type."); + } + return static_cast(sizeofInteropType(runtime, bridge, args[0])); + })); + + interop.setProperty( + runtime, "alloc", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "alloc"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isNumber()) { + throw facebook::jsi::JSError(runtime, "alloc expects a byte size."); + } + size_t size = static_cast(std::max(0, args[0].getNumber())); + return createPointer(runtime, calloc(1, size), false); + })); + + interop.setProperty( + runtime, "free", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "free"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + return Value::undefined(); + } + Object object = args[0].asObject(runtime); + if (!object.isHostObject(runtime)) { + return Value::undefined(); + } + auto pointer = object.getHostObject(runtime); + void* raw = pointer->pointer(); + if (raw != nullptr) { + free(raw); + pointer->clearWithoutFree(); + } + return Value::undefined(); + })); + + interop.setProperty( + runtime, "adopt", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "adopt"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError(runtime, "adopt expects a Pointer."); + } + Object object = args[0].asObject(runtime); + if (!object.isHostObject(runtime)) { + throw facebook::jsi::JSError(runtime, "adopt expects a Pointer."); + } + object.getHostObject(runtime)->adopt(); + return Value(runtime, object); + })); + + interop.setProperty( + runtime, "handleof", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "handleof"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || args[0].isNull() || args[0].isUndefined()) { + return Value::null(); + } + if (args[0].isString()) { + std::string utf8 = args[0].asString(runtime).utf8(runtime); + char* data = strdup(utf8.c_str()); + return createPointer(runtime, data); + } + if (!args[0].isObject()) { + return Value::null(); + } + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + return Value(runtime, object); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, + object.getHostObject(runtime)->data()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, + object.getHostObject(runtime)->data()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, + object.getHostObject(runtime)->object()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, + object.getHostObject(runtime)->nativeClass()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, + object.getHostObject(runtime) + ->nativeProtocol()); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return createPointer(runtime, nativePointer); + } + Value nativeName = object.getProperty(runtime, "nativeName"); + if (nativeName.isString()) { + std::string name = nativeName.asString(runtime).utf8(runtime); + void* symbol = dlsym(bridge->selfDl(), name.c_str()); + if (symbol != nullptr) { + return createPointer(runtime, symbol); + } + } + return Value::null(); + })); + + interop.setProperty( + runtime, "stringFromCString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "stringFromCString"), 2, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || args[0].isNull() || args[0].isUndefined()) { + return Value::null(); + } + NativeApiJsiArgumentFrame frame(1); + const char* data = + static_cast(pointerFromJsiValue(runtime, args[0], frame)); + if (data == nullptr) { + return Value::null(); + } + if (count > 1 && args[1].isNumber()) { + size_t length = static_cast(std::max(0, args[1].getNumber())); + return String::createFromUtf8(runtime, + reinterpret_cast(data), + length); + } + return makeString(runtime, data); + })); + + interop.setProperty( + runtime, "bufferFromData", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "bufferFromData"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError(runtime, "Invalid data."); + } + Object object = args[0].asObject(runtime); + if (object.isArrayBuffer(runtime)) { + return Value(runtime, object); + } + id native = nil; + if (object.isHostObject(runtime)) { + native = object.getHostObject(runtime)->object(); + } else if (object.isHostObject(runtime)) { + native = static_cast( + object.getHostObject(runtime)->pointer()); + } + if (native == nil || ![native isKindOfClass:[NSData class]]) { + throw facebook::jsi::JSError(runtime, "Invalid data."); + } + NSData* data = static_cast(native); + return ArrayBuffer( + runtime, std::make_shared( + data.bytes, static_cast(data.length))); + })); + + interop.setProperty( + runtime, "addMethod", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "addMethod"), 2, + [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + throw facebook::jsi::JSError( + runtime, + "interop.addMethod requires the JSI class builder layer."); + })); + interop.setProperty( + runtime, "addProtocol", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "addProtocol"), 2, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 2) { + throw facebook::jsi::JSError( + runtime, "interop.addProtocol expects class and protocol."); + } + Class cls = classFromJsiValue(runtime, args[0]); + Protocol* protocol = protocolFromJsiValue(runtime, args[1]); + if (cls == Nil || protocol == nullptr) { + return false; + } + return class_addProtocol(cls, protocol); + })); + + return interop; +} + +bool isValidMetadataStringOffset(MDMetadataReader* metadata, + MDSectionOffset offset) { + if (metadata == nullptr || metadata->constantsOffset < metadata->stringsOffset) { + return false; + } + return offset < metadata->constantsOffset - metadata->stringsOffset; +} + +Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, + const NativeApiSymbol& symbol) { + Object result(runtime); + if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { + return result; + } + + MDSectionOffset offset = symbol.offset + sizeof(MDSectionOffset); + bool next = true; + while (next) { + auto nameOffset = metadata->getOffset(offset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + offset += sizeof(MDSectionOffset); + + const char* memberName = metadata->resolveString(nameOffset); + int64_t value = metadata->getEnumValue(offset); + offset += sizeof(int64_t); + result.setProperty(runtime, memberName, static_cast(value)); + } + return result; +} + +Value constantToValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol) { + MDMetadataReader* metadata = bridge->metadata(); + if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { + return Value::undefined(); + } + + MDSectionOffset offset = symbol.offset + sizeof(MDSectionOffset); + auto evalKind = metadata->getVariableEvalKind(offset); + offset += sizeof(metagen::MDVariableEvalKind); + + switch (evalKind) { + case metagen::mdEvalInt64: + return static_cast(metadata->getInt64(offset)); + case metagen::mdEvalDouble: + return metadata->getDouble(offset); + case metagen::mdEvalString: { + auto stringOffset = metadata->getOffset(offset); + if (isValidMetadataStringOffset(metadata, stringOffset)) { + return makeString(runtime, metadata->resolveString(stringOffset)); + } + + void* symbolPtr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (symbolPtr == nullptr) { + return Value::undefined(); + } + + NativeApiJsiType stringObjectType; + stringObjectType.kind = metagen::mdTypeNSStringObject; + stringObjectType.ffiType = &ffi_type_pointer; + stringObjectType.supported = true; + return convertNativeReturnValue(runtime, bridge, stringObjectType, + symbolPtr); + } + case metagen::mdEvalNone: + break; + } + + MDSectionOffset typeOffset = offset; + NativeApiJsiType type = parseMetadataJsiType(metadata, &typeOffset, bridge.get()); + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError( + runtime, "Native constant type is not supported by pure JSI: " + + symbol.name); + } + + void* symbolPtr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (symbolPtr == nullptr) { + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge, type, symbolPtr); +} + +void prepareJsiArguments(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiSignature& signature, + const Value* args, size_t count, + NativeApiJsiArgumentFrame& frame) { + if (count != signature.argumentTypes.size()) { + throw facebook::jsi::JSError( + runtime, "Actual arguments count: \"" + std::to_string(count) + + "\". Expected: \"" + + std::to_string(signature.argumentTypes.size()) + "\"."); + } + + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + const auto& type = signature.argumentTypes[i]; + size_t size = type.ffiType != nullptr && type.ffiType->size > 0 + ? type.ffiType->size + : nativeSizeForType(type); + void* target = frame.storageAt(i, size); + convertJsiArgument(runtime, bridge, type, args[i], target, frame); + } +} + +Value callNativeFunctionPointer( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, bool block, const Value* args, + size_t count) { + if (pointer == nullptr) { + throw facebook::jsi::JSError(runtime, "Native function pointer is null."); + } + if (bridge == nullptr || bridge->metadata() == nullptr || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native function pointer metadata is unavailable."); + } + + auto signature = parseMetadataJsiSignature( + bridge->metadata(), type.signatureOffset, block ? 1 : 0, bridge.get()); + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + throw facebook::jsi::JSError( + runtime, + "Native function pointer signature is not supported by pure JSI."); + } + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + + std::vector values; + if (block) { + values.reserve(signature->argumentTypes.size() + 1); + values.push_back(&pointer); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.push_back(frame.values()[i]); + } + } + + void* callable = pointer; + if (block) { + auto literal = static_cast(pointer); + if (literal == nullptr || literal->invoke == nullptr) { + throw facebook::jsi::JSError(runtime, "Native block invoke pointer is null."); + } + callable = literal->invoke; + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + performNativeInvocation(runtime, [&]() { + ffi_call(&signature->cif, FFI_FN(callable), returnStorage.data(), + block ? values.data() : frame.values()); + }); + + return convertNativeReturnValue(runtime, bridge, signature->returnType, + returnStorage.data()); +} + +Value wrapNativeFunctionPointer(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, + bool block) { + const char* functionName = block ? "NativeApiJsiBlock" : "NativeApiJsiFunctionPointer"; + auto function = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, functionName), 0, + [bridge, type, pointer, block](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + return callNativeFunctionPointer(runtime, bridge, type, pointer, block, + args, count); + }); + function.setProperty(runtime, "kind", + makeString(runtime, block ? "block" : "functionPointer")); + function.setProperty( + runtime, "__nativeApiPointer", + static_cast(reinterpret_cast(pointer))); + function.setProperty( + runtime, "nativeAddress", + static_cast(reinterpret_cast(pointer))); + function.setProperty( + runtime, "toString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [pointer, block](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", pointer); + return makeString(runtime, + std::string("[NativeApiJsi ") + + (block ? "Block " : "FunctionPointer ") + + address + "]"); + })); + return function; +} + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol, const Value* args, + size_t count) { + MDMetadataReader* metadata = bridge->metadata(); + if (metadata == nullptr) { + throw facebook::jsi::JSError(runtime, "Native metadata is not loaded."); + } + + void* fnptr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (fnptr == nullptr) { + throw facebook::jsi::JSError(runtime, + "Native function is not available: " + + symbol.name); + } + + MDSectionOffset signatureOffset = + metadata->signaturesOffset + + metadata->getOffset(symbol.offset + sizeof(MDSectionOffset)); + auto signature = parseMetadataJsiSignature( + metadata, signatureOffset, 0, bridge.get(), + (metadata->getFunctionFlag(symbol.offset + sizeof(MDSectionOffset) * 2) & + metagen::mdFunctionReturnOwned) != 0); + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + throw facebook::jsi::JSError( + runtime, "Native function signature is not supported by pure JSI: " + + symbol.name); + } + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, [&]() { + ffi_call(&signature->cif, FFI_FN(fnptr), returnStorage.data(), + frame.values()); + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiJsiType returnType = signature->returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} + +Value callObjCSelector(Runtime& runtime, + const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const std::string& selectorName, + const NativeApiMember* member, + const Value* args, size_t count) { + if (receiver == nil) { + throw facebook::jsi::JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + SEL selector = sel_registerName(selectorName.c_str()); + Class receiverClass = + receiverIsClass ? static_cast(receiver) : object_getClass(receiver); + Method method = receiverIsClass ? class_getClassMethod(receiverClass, selector) + : class_getInstanceMethod(receiverClass, selector); + if (method == nullptr) { + throw facebook::jsi::JSError(runtime, + "Objective-C selector is not available: " + + selectorName); + } + + std::optional signature; + if (member != nullptr && + member->signatureOffset != MD_SECTION_OFFSET_NULL && + member->signatureOffset != 0) { + signature = parseMetadataJsiSignature( + bridge->metadata(), member->signatureOffset, 2, bridge.get(), + (member->flags & metagen::mdMemberReturnOwned) != 0); + } + if (!signature) { + signature = parseObjCMethodJsiSignature(method); + } + + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + throw facebook::jsi::JSError( + runtime, "Objective-C signature is not supported by pure JSI: " + + selectorName); + } + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + + std::vector values; + values.reserve(signature->argumentTypes.size() + 2); + values.push_back(&receiver); + values.push_back(&selector); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.push_back(frame.values()[i]); + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, [&]() { +#if defined(__x86_64__) + bool isStret = signature->returnType.ffiType->size > 16 && + signature->returnType.ffiType->type == FFI_TYPE_STRUCT; + ffi_call(&signature->cif, + isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#else + ffi_call(&signature->cif, FFI_FN(objc_msgSend), returnStorage.data(), + values.data()); +#endif + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiJsiType returnType = signature->returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} + +class NativeApiHostObject final : public HostObject { + public: + explicit NativeApiHostObject(std::shared_ptr bridge) + : bridge_(std::move(bridge)) {} + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "runtime") { + return makeString(runtime, "jsi"); + } + if (property == "backend") { + return makeString(runtime, "hermes"); + } + if (property == "metadata") { + return metadataObject(runtime); + } + if (property == "hasScheduler") { + return bridge_->scheduler() != nullptr; + } + if (property == "interop") { + return createInteropObject(runtime, bridge_); + } + if (property == "runOnUI") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUI"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + auto scheduler = bridge->scheduler(); + if (scheduler == nullptr) { + throw facebook::jsi::JSError( + runtime, + "NativeApiJsi was installed without a UI scheduler."); + } + + std::shared_ptr callback; + if (count > 0 && !args[0].isNull() && !args[0].isUndefined()) { + if (!args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "runOnUI expects a function callback."); + } + + Object callbackObject = args[0].asObject(runtime); + if (!callbackObject.isFunction(runtime)) { + throw facebook::jsi::JSError( + runtime, "runOnUI expects a function callback."); + } + callback = std::make_shared( + callbackObject.asFunction(runtime)); + } + + Runtime* runtimePtr = &runtime; + auto promiseCtor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + return promiseCtor.callAsConstructor( + runtime, + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUIPromise"), + 2, + [scheduler, runtimePtr, callback]( + Runtime& promiseRuntime, const Value&, + const Value* promiseArgs, + size_t promiseArgc) -> Value { + if (promiseArgc < 2 || !promiseArgs[0].isObject() || + !promiseArgs[1].isObject()) { + return Value::undefined(); + } + + auto resolve = std::make_shared( + promiseArgs[0].asObject(promiseRuntime) + .asFunction(promiseRuntime)); + auto reject = std::make_shared( + promiseArgs[1].asObject(promiseRuntime) + .asFunction(promiseRuntime)); + if (callback == nullptr) { + scheduler->invokeOnUI([scheduler, runtimePtr, resolve]() { + scheduler->invokeOnJS([runtimePtr, resolve]() { + resolve->call(*runtimePtr); + }); + }); + return Value::undefined(); + } + + scheduler->invokeOnJS([runtimePtr, callback, resolve, reject]() { + try { + { + ScopedNativeApiUINativeCallDispatch uiDispatch; + callback->call(*runtimePtr); + } + resolve->call(*runtimePtr); + } catch (const std::exception& error) { + reject->call( + *runtimePtr, + String::createFromUtf8(*runtimePtr, error.what())); + } + }); + + return Value::undefined(); + })); + }); + } + if (property == "import") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "import"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string path = readStringArg(runtime, args, count, 0, "path"); + std::string frameworkPath = path; + if (!frameworkPath.empty() && frameworkPath[0] != '/') { + frameworkPath = "/System/Library/Frameworks/" + frameworkPath + + ".framework"; + } + + NSBundle* bundle = [NSBundle + bundleWithPath:[NSString stringWithUTF8String:frameworkPath.c_str()]]; + if (bundle == nil || ![bundle load]) { + throw facebook::jsi::JSError( + runtime, "Could not load bundle: " + frameworkPath); + } + return true; + }); + } + if (property == "lookup") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "lookup"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string symbolName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->find(symbolName); + if (symbol == nullptr) { + return Value::null(); + } + return symbolToObject(runtime, *symbol); + }); + } + if (property == "getClass") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getClass"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string className = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findClass(className); + if (symbol == nullptr) { + Class cls = objc_lookUpClass(className.c_str()); + if (cls == nil) { + return Value::null(); + } + NativeApiSymbol runtimeSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = className, + .runtimeName = className, + }; + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, std::move(runtimeSymbol))); + } + + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, *symbol)); + }); + } + if (property == "getFunction") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getFunction"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string functionName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findFunction(functionName); + if (symbol == nullptr) { + return Value::null(); + } + auto function = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, symbol->name), 0, + [bridge, symbol = *symbol](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, symbol, args, count); + }); + function.setProperty(runtime, "kind", makeString(runtime, "function")); + function.setProperty(runtime, "nativeName", + makeString(runtime, symbol->name)); + function.setProperty(runtime, "metadataOffset", + static_cast(symbol->offset)); + return function; + }); + } + if (property == "getConstant") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getConstant"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string constantName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findConstant(constantName); + if (symbol == nullptr) { + return Value::undefined(); + } + return constantToValue(runtime, bridge, *symbol); + }); + } + if (property == "getEnum") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getEnum"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string enumName = readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findEnum(enumName); + if (symbol == nullptr) { + return Value::undefined(); + } + return enumToObject(runtime, bridge->metadata(), *symbol); + }); + } + if (property == "getProtocol") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getProtocol"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string protocolName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findProtocol(protocolName); + if (symbol == nullptr) { + Protocol* protocol = objc_getProtocol(protocolName.c_str()); + if (protocol == nullptr) { + return Value::null(); + } + NativeApiSymbol runtimeSymbol{ + .kind = NativeApiSymbolKind::Protocol, + .offset = MD_SECTION_OFFSET_NULL, + .name = protocolName, + .runtimeName = protocolName, + }; + return Object::createFromHostObject( + runtime, + std::make_shared( + std::move(runtimeSymbol))); + } + return Object::createFromHostObject( + runtime, + std::make_shared(*symbol)); + }); + } + if (property == "getStruct" || property == "getUnion") { + auto bridge = bridge_; + bool isUnion = property == "getUnion"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, isUnion](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string aggregateName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = + isUnion ? bridge->findUnion(aggregateName) + : bridge->findStruct(aggregateName); + if (symbol == nullptr) { + return Value::undefined(); + } + return makeAggregateConstructor(runtime, bridge, *symbol); + }); + } + + if (const NativeApiSymbol* classSymbol = bridge_->findClass(property)) { + return Object::createFromHostObject( + runtime, + std::make_shared(bridge_, *classSymbol)); + } + + if (const NativeApiSymbol* functionSymbol = bridge_->findFunction(property)) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, symbol = *functionSymbol](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, symbol, args, count); + }); + } + + if (const NativeApiSymbol* constantSymbol = bridge_->findConstant(property)) { + return constantToValue(runtime, bridge_, *constantSymbol); + } + + if (const NativeApiSymbol* enumSymbol = bridge_->findEnum(property)) { + return enumToObject(runtime, bridge_->metadata(), *enumSymbol); + } + + if (const NativeApiSymbol* protocolSymbol = + bridge_->findProtocol(property)) { + return Object::createFromHostObject( + runtime, + std::make_shared(*protocolSymbol)); + } + + if (const NativeApiSymbol* aggregateSymbol = + bridge_->findAggregate(property)) { + return makeAggregateConstructor(runtime, bridge_, *aggregateSymbol); + } + + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(11); + addPropertyName(runtime, names, "runtime"); + addPropertyName(runtime, names, "backend"); + addPropertyName(runtime, names, "metadata"); + addPropertyName(runtime, names, "hasScheduler"); + addPropertyName(runtime, names, "interop"); + addPropertyName(runtime, names, "runOnUI"); + addPropertyName(runtime, names, "import"); + addPropertyName(runtime, names, "lookup"); + addPropertyName(runtime, names, "getClass"); + addPropertyName(runtime, names, "getFunction"); + addPropertyName(runtime, names, "getConstant"); + addPropertyName(runtime, names, "getEnum"); + addPropertyName(runtime, names, "getProtocol"); + addPropertyName(runtime, names, "getStruct"); + addPropertyName(runtime, names, "getUnion"); + return names; + } + + private: + Object metadataObject(Runtime& runtime) const { + Object metadata(runtime); + metadata.setProperty(runtime, "classes", + static_cast(bridge_->classCount())); + metadata.setProperty(runtime, "functions", + static_cast(bridge_->functionCount())); + metadata.setProperty(runtime, "constants", + static_cast(bridge_->constantCount())); + metadata.setProperty(runtime, "protocols", + static_cast(bridge_->protocolCount())); + metadata.setProperty(runtime, "enums", + static_cast(bridge_->enumCount())); + metadata.setProperty(runtime, "structs", + static_cast(bridge_->structCount())); + metadata.setProperty(runtime, "unions", + static_cast(bridge_->unionCount())); + + metadata.setProperty( + runtime, "classNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "classNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->classNames()); + })); + metadata.setProperty( + runtime, "functionNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "functionNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->functionNames()); + })); + metadata.setProperty( + runtime, "constantNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "constantNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->constantNames()); + })); + metadata.setProperty( + runtime, "protocolNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "protocolNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->protocolNames()); + })); + metadata.setProperty( + runtime, "enumNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "enumNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->enumNames()); + })); + metadata.setProperty( + runtime, "structNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "structNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->structNames()); + })); + metadata.setProperty( + runtime, "unionNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "unionNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->unionNames()); + })); + return metadata; + } + + std::shared_ptr bridge_; +}; + +} // namespace + +Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + auto bridge = std::make_shared(config); + return Object::createFromHostObject( + runtime, std::make_shared(std::move(bridge))); +} + +void InstallAggregateGlobals(Runtime& runtime, Object& api, const char* namesFunction) { + Value metadataValue = api.getProperty(runtime, "metadata"); + if (!metadataValue.isObject()) { + return; + } + Object metadata = metadataValue.asObject(runtime); + Value namesValue = metadata.getProperty(runtime, namesFunction); + if (!namesValue.isObject()) { + return; + } + Object namesObject = namesValue.asObject(runtime); + if (!namesObject.isFunction(runtime)) { + return; + } + Value namesResult = namesObject.asFunction(runtime).call(runtime); + if (!namesResult.isObject() || !namesResult.asObject(runtime).isArray(runtime)) { + return; + } + Array names = namesResult.asObject(runtime).getArray(runtime); + Object global = runtime.global(); + for (size_t i = 0; i < names.size(runtime); i++) { + Value nameValue = names.getValueAtIndex(runtime, i); + if (!nameValue.isString()) { + continue; + } + std::string name = nameValue.asString(runtime).utf8(runtime); + if (name.empty() || global.hasProperty(runtime, name.c_str())) { + continue; + } + try { + Value aggregate = api.getProperty(runtime, name.c_str()); + if (!aggregate.isUndefined()) { + global.setProperty(runtime, name.c_str(), aggregate); + } + } catch (const std::exception&) { + // Some React Native globals are read-only even when hasProperty misses + // them. Keep NativeScript initialization resilient and skip collisions. + } + } +} + +void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + const char* globalName = config.globalName != nullptr && config.globalName[0] != '\0' + ? config.globalName + : "__nativeScriptNativeApi"; + Object api = CreateNativeApiJSI(runtime, config); + runtime.global().setProperty(runtime, globalName, api); + runtime.global().setProperty(runtime, "interop", + api.getProperty(runtime, "interop")); + InstallAggregateGlobals(runtime, api, "protocolNames"); + InstallAggregateGlobals(runtime, api, "structNames"); + InstallAggregateGlobals(runtime, api, "unionNames"); +} + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSI( + facebook::jsi::Runtime* runtime, const char* metadataPath) { + if (runtime == nullptr) { + return; + } + nativescript::NativeApiJsiConfig config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiJSI(*runtime, config); +} + +#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h new file mode 100644 index 00000000..4e3f1de6 --- /dev/null +++ b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h @@ -0,0 +1,85 @@ +#ifndef NATIVE_API_JSI_REACT_NATIVE_H +#define NATIVE_API_JSI_REACT_NATIVE_H + +#include "NativeApiJsi.h" + +#if __has_include() +#include +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 1 +#elif __has_include() +#include +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 1 +#else +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 0 +#endif + +#if NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER + +#include + +namespace nativescript { + +class ReactNativeCallInvokerScheduler final : public NativeApiJsiScheduler { + public: + ReactNativeCallInvokerScheduler( + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker) + : jsInvoker_(std::move(jsInvoker)), uiInvoker_(std::move(uiInvoker)) {} + + void invokeOnJS(std::function task) override { + if (jsInvoker_) { + jsInvoker_->invokeAsync(std::move(task)); + return; + } + task(); + } + + void invokeOnUI(std::function task) override { + if (uiInvoker_) { + uiInvoker_->invokeAsync(std::move(task)); + return; + } + auto heapTask = std::make_shared>(std::move(task)); + dispatch_async(dispatch_get_main_queue(), ^{ + (*heapTask)(); + }); + } + + private: + std::shared_ptr jsInvoker_; + std::shared_ptr uiInvoker_; +}; + +inline NativeApiJsiConfig MakeReactNativeNativeApiJsiConfig( + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker, + const char* metadataPath = nullptr, + const void* metadataPtr = nullptr, + const char* globalName = "__nativeScriptNativeApi") { + NativeApiJsiConfig config; + config.metadataPath = metadataPath; + config.metadataPtr = metadataPtr; + config.globalName = globalName; + config.scheduler = std::make_shared( + std::move(jsInvoker), std::move(uiInvoker)); + return config; +} + +inline void InstallReactNativeNativeApiJSI( + facebook::jsi::Runtime& runtime, + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker, + const char* metadataPath = nullptr, + const void* metadataPtr = nullptr, + const char* globalName = "__nativeScriptNativeApi") { + InstallNativeApiJSI( + runtime, + MakeReactNativeNativeApiJsiConfig(std::move(jsInvoker), std::move(uiInvoker), + metadataPath, metadataPtr, globalName)); +} + +} // namespace nativescript + +#endif // NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER + +#endif // NATIVE_API_JSI_REACT_NATIVE_H diff --git a/NativeScript/ffi/jsi/README.md b/NativeScript/ffi/jsi/README.md new file mode 100644 index 00000000..3fbad151 --- /dev/null +++ b/NativeScript/ffi/jsi/README.md @@ -0,0 +1,47 @@ +# Native API JSI bridge + +This directory contains the Hermes-first JSI entrypoint for NativeScript Native +API access. + +The core installer is engine-host agnostic: + +```cpp +nativescript::NativeApiJsiConfig config; +config.metadataPath = metadataPath; +config.metadataPtr = metadataPtr; +nativescript::InstallNativeApiJSI(runtime, config); +``` + +NativeScript's Hermes runtime installs this automatically as +`globalThis.__nativeScriptNativeApi`. + +React Native integrations should include `NativeApiJsiReactNative.h` from a +TurboModule implementation and pass the module's JS/UI `CallInvoker`s: + +```cpp +nativescript::InstallReactNativeNativeApiJSI( + runtime, jsInvoker, uiInvoker, metadataPath, metadataPtr); +``` + +The React Native adapter is intentionally only a scheduler/config shim. The +native API host object, metadata loading, primitive C function dispatch, +Objective-C class/object handles, and selector invocation live in the shared +JSI implementation so they can be used by both NativeScript Hermes and a React +Native TurboModule without going through Node-API. + +The direct JSI backend is still moving toward full NativeScript bridge parity. +It covers the metadata-backed Objective-C class/function/constant/enum paths +needed by the React Native TurboModule, plus metadata-backed structs/unions, +primitive array/vector value marshalling, JS blocks, C function pointer +callbacks, protocol wrappers, pointer/reference helpers, and the core `interop` +helpers (`Pointer`, `Reference`, `sizeof`, `alloc`, `free`, `adopt`, +`handleof`, `stringFromCString`, `bufferFromData`, and `addProtocol`). Struct +and union constructors, plus protocol symbols, are installed on `globalThis` +along with `interop` so common NativeScript-style calls such as +`CGRect({ origin, size })`, `interop.sizeof(CGRect)`, and +`interop.handleof(value)` work through JSI. + +The remaining parity work is class-builder heavy: `interop.addMethod` and +JavaScript-defined Objective-C subclasses still need the full method IMP +installation layer before pure JSI can be considered fully compatible with the +Node-API bridge. diff --git a/NativeScript/napi/hermes/jsr.cpp b/NativeScript/napi/hermes/jsr.cpp index 9bd91383..ad23e9b0 100644 --- a/NativeScript/napi/hermes/jsr.cpp +++ b/NativeScript/napi/hermes/jsr.cpp @@ -19,6 +19,14 @@ class RuntimeLockGuard { }; } // namespace +int js_current_env_lock_depth(napi_env env) { + auto itFound = JSR::env_to_jsr_cache.find(env); + if (itFound == JSR::env_to_jsr_cache.end() || itFound->second == nullptr) { + return 0; + } + return itFound->second->currentLockDepth(); +} + JSR::JSR() { hermes::vm::RuntimeConfig config = hermes::vm::RuntimeConfig::Builder() .withMicrotaskQueue(true) @@ -64,6 +72,14 @@ napi_status js_create_napi_env(napi_env* env, napi_runtime runtime) { return napi_ok; } +facebook::jsi::Runtime* js_get_jsi_runtime(napi_env env) { + auto itFound = JSR::env_to_jsr_cache.find(env); + if (itFound == JSR::env_to_jsr_cache.end()) { + return nullptr; + } + return itFound->second->rt; +} + napi_status js_set_runtime_flags(const char* flags) { return napi_ok; } napi_status js_free_napi_env(napi_env env) { @@ -138,6 +154,7 @@ extern "C" napi_status jsr_drain_microtasks(napi_env env, return napi_invalid_arg; } - *result = itFound->second->runtime->drainMicrotasks(max_count_hint); + NapiScope scope(env, false); + *result = itFound->second->rt->drainMicrotasks(max_count_hint); return napi_ok; } diff --git a/NativeScript/napi/hermes/jsr.h b/NativeScript/napi/hermes/jsr.h index 18154170..cb6221ab 100644 --- a/NativeScript/napi/hermes/jsr.h +++ b/NativeScript/napi/hermes/jsr.h @@ -9,24 +9,45 @@ #include "jsi/threadsafe.h" #include "jsr_common.h" +#include + class JSR { public: JSR(); std::unique_ptr runtime; facebook::jsi::Runtime* rt; std::recursive_mutex js_mutex; + static inline thread_local std::unordered_map lock_depth; void lock() { runtime->lock(); js_mutex.lock(); + lock_depth[this] += 1; } void unlock() { - runtime->unlock(); + auto depth = lock_depth.find(this); + if (depth != lock_depth.end()) { + depth->second -= 1; + if (depth->second <= 0) { + lock_depth.erase(depth); + } + } js_mutex.unlock(); + runtime->unlock(); + } + int currentLockDepth() const { + auto depth = lock_depth.find(const_cast(this)); + if (depth == lock_depth.end()) { + return 0; + } + return depth->second; } static std::unordered_map env_to_jsr_cache; }; +int js_current_env_lock_depth(napi_env env); +facebook::jsi::Runtime* js_get_jsi_runtime(napi_env env); + typedef struct napi_runtime__ { JSR* hermes; } napi_runtime__; diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 50fcbf14..7b591ef8 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; @@ -596,8 +598,37 @@ class FunctionInfo : public NativeInfo { if (info == nullptr) { return napi_set_last_error(env, napi_generic_failure); } - JSObjectRef function = - JSObjectMake(env->context, xyz::functionInfoClass, info); + + JSString name(utf8name != nullptr ? utf8name : "", + utf8name != nullptr ? length : 0); + JSObjectRef function = JSObjectMakeFunctionWithCallback( + env->context, utf8name != nullptr ? static_cast(name) + : nullptr, + FunctionInfo::CallAsFunction); + if (function == nullptr) { + delete info; + return napi_set_last_error(env, napi_generic_failure); + } + + NativeInfo::SetNativeInfoKey(env->context, function, xyz::functionInfoClass, + env->function_info_symbol, info); + + JSValueRef exception{}; + JSObjectSetProperty(env->context, function, JSString("length"), + JSValueMakeNumber(env->context, 0), + kJSPropertyAttributeDontEnum | + kJSPropertyAttributeReadOnly | + kJSPropertyAttributeDontDelete, + &exception); + if (utf8name != nullptr) { + JSObjectSetProperty(env->context, function, JSString("name"), + JSValueMakeString(env->context, name), + kJSPropertyAttributeDontEnum | + kJSPropertyAttributeReadOnly | + kJSPropertyAttributeDontDelete, + &exception); + } + *result = ToNapi(function); return napi_ok; } @@ -638,6 +669,16 @@ class FunctionInfo : public NativeInfo { JSValueRef* exception) { FunctionInfo* info = reinterpret_cast(JSObjectGetPrivate(function)); + if (info == nullptr) { + napi_env env = napi_env__::get(const_cast(ctx)); + if (env != nullptr) { + info = NativeInfo::GetNativeInfoKey( + ctx, function, env->function_info_symbol); + } + } + if (info == nullptr) { + return JSValueMakeUndefined(ctx); + } // Make sure any errors encountered last time we were in N-API are gone. napi_clear_last_error(info->_env); @@ -791,13 +832,16 @@ class WrapperInfo : public BaseInfoT { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); WrapperInfo* info{}; - bool hasOwnProperty = NativeInfo::GetNativeInfoKey( - env->context, ToJSObject(env, object), - env->wrapper_info_symbol) != nullptr; + info = GetCached(env, object); + if (info != nullptr) { + *result = info; + return napi_ok; + } - if (hasOwnProperty) { - CHECK_NAPI(Unwrap(env, object, &info)); - RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + info = NativeInfo::GetNativeInfoKey( + env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (info != nullptr) { + env->wrapper_info_cache[object] = info; *result = info; return napi_ok; } @@ -811,6 +855,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,12 +871,37 @@ 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 = GetCached(env, object); + if (cachedInfo != nullptr) { + *result = cachedInfo; + return napi_ok; + } + *result = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (*result != nullptr) { + env->wrapper_info_cache[object] = *result; + } return napi_ok; } private: + static WrapperInfo* GetCached(napi_env env, napi_value object) { + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo == env->wrapper_info_cache.end()) { + return nullptr; + } + + auto info = NativeInfo::GetNativeInfoKey( + env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (info == nullptr || info != static_cast(cachedInfo->second)) { + env->wrapper_info_cache.erase(cachedInfo); + return nullptr; + } + + return info; + } + WrapperInfo(napi_env env) : BaseInfoT{env, "Native (Wrapper)"} {} }; @@ -1282,6 +1358,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)); @@ -1624,18 +1704,20 @@ napi_status napi_typeof(napi_env env, napi_value value, case kJSTypeString: *result = napi_string; break; - case kJSTypeSymbol: - *result = napi_symbol; - break; - default: - JSObjectRef object{ToJSObject(env, value)}; - if (JSObjectIsFunction(env->context, object)) { - *result = napi_function; - } else { - NativeInfo* info = NativeInfo::Get(object); - if (info != nullptr && info->Type() == NativeType::External) { - *result = napi_external; - } else { + case kJSTypeSymbol: + *result = napi_symbol; + break; + default: + JSObjectRef object{ToJSObject(env, value)}; + NativeInfo* info = NativeInfo::Get(object); + if (JSObjectIsFunction(env->context, object) || + (info != nullptr && (info->Type() == NativeType::Function || + info->Type() == NativeType::Constructor))) { + *result = napi_function; + } else { + if (info != nullptr && info->Type() == NativeType::External) { + *result = napi_external; + } else { *result = napi_object; } } @@ -2104,6 +2186,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/NativeScript/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index 7ba14da0..4f3841fc 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -12,6 +12,9 @@ #ifdef TARGET_ENGINE_V8 #include "v8-api.h" #endif // TARGET_ENGINE_V8 +#ifdef TARGET_ENGINE_HERMES +#include "ffi/jsi/NativeApiJsi.h" +#endif // TARGET_ENGINE_HERMES #include #include "NativeScript.h" @@ -361,6 +364,15 @@ void Runtime::Init(bool isWorker) { const char* metadata_path = std::getenv("NS_METADATA_PATH"); nativescript_init(env_, metadata_path, RuntimeConfig.MetadataPtr); +#ifdef TARGET_ENGINE_HERMES + if (auto* jsiRuntime = js_get_jsi_runtime(env_)) { + NativeApiJsiConfig nativeApiJsiConfig; + nativeApiJsiConfig.metadataPath = metadata_path; + nativeApiJsiConfig.metadataPtr = RuntimeConfig.MetadataPtr; + InstallNativeApiJSI(*jsiRuntime, nativeApiJsiConfig); + } +#endif // TARGET_ENGINE_HERMES + napi_close_handle_scope(env_, scope); } diff --git a/NativeScript/runtime/modules/node/Process.cpp b/NativeScript/runtime/modules/node/Process.cpp index 5a0b23fc..ef77ba05 100644 --- a/NativeScript/runtime/modules/node/Process.cpp +++ b/NativeScript/runtime/modules/node/Process.cpp @@ -186,6 +186,9 @@ napi_value CreateVersionsObject(napi_env env) { #elif defined(TARGET_ENGINE_QUICKJS) napi_set_named_property(env, versions, "engine", napi_util::to_js_string(env, "quickjs")); +#elif defined(TARGET_ENGINE_JSC) + napi_set_named_property(env, versions, "engine", + napi_util::to_js_string(env, "jsc")); #endif napi_set_named_property(env, versions, "nativescript", diff --git a/NativeScript/runtime/modules/timers/Timers.mm b/NativeScript/runtime/modules/timers/Timers.mm index c5800a5f..e15168e3 100644 --- a/NativeScript/runtime/modules/timers/Timers.mm +++ b/NativeScript/runtime/modules/timers/Timers.mm @@ -192,6 +192,26 @@ void MarkTimerActive(NSTimerHandle* handle) { } } +bool MarkTimerInactive(NSTimerHandle* handle) { + if (handle == nil) { + return false; + } + + bool didDeactivate = false; + @synchronized(handle) { + if (handle->activeCounted) { + handle->activeCounted = false; + didDeactivate = true; + } + } + + if (didDeactivate) { + gActiveTimers.fetch_sub(1, std::memory_order_relaxed); + } + + return didDeactivate; +} + void AddTimerToMainRunLoop(NSTimer* timer) { if (timer == nil) { return; @@ -239,19 +259,12 @@ void DisposeTimerHandle(napi_env callEnv, NSTimerHandle* handle, bool invalidate } napi_ref callback = nullptr; - bool shouldDecrementActiveCount = false; @synchronized(handle) { - if (handle->activeCounted) { - handle->activeCounted = false; - shouldDecrementActiveCount = true; - } callback = handle->callback; handle->callback = nullptr; } - if (shouldDecrementActiveCount) { - gActiveTimers.fetch_sub(1, std::memory_order_relaxed); - } + MarkTimerInactive(handle); napi_env cleanupEnv = callEnv != nullptr ? callEnv : handle->env; #ifdef TARGET_ENGINE_HERMES @@ -370,6 +383,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { } NapiScope scope(callbackEnv); + MarkTimerInactive(handle); #ifdef TARGET_ENGINE_HERMES DispatchHermesTimerCallback(callbackEnv, "__nsDispatchTimeout", timerId); #else 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..6f1a2ed8 --- /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", "--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/examples/react-native-demo/App.tsx b/examples/react-native-demo/App.tsx new file mode 100644 index 00000000..fd0d29e3 --- /dev/null +++ b/examples/react-native-demo/App.tsx @@ -0,0 +1,201 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import { + Platform, + Pressable, + SafeAreaView, + StyleSheet, + Text, + View, +} from 'react-native'; +import NativeScript from '@nativescript/react-native'; + +type NativeApiHost = { + backend?: string; + metadata?: { + classes?: number; + functions?: number; + constants?: number; + enums?: number; + }; +}; + +function installNativeScriptGlobals(): NativeApiHost { + NativeScript.init(); + const api = (globalThis as any).__nativeScriptNativeApi as + | NativeApiHost + | undefined; + if (!api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + return api; +} + +async function applyUIKitTweaks() { + if (Platform.OS !== 'ios') { + throw new Error('This demo uses UIKit and must run on iOS'); + } + + const api = installNativeScriptGlobals(); + + let nativeCallsRanOnMainThread = false; + await NativeScript.runOnUI(() => { + nativeCallsRanOnMainThread = NSThread.isMainThread === true; + if (!nativeCallsRanOnMainThread) { + throw new Error('runOnUI did not dispatch native calls to the main thread'); + } + + const app = UIApplication.sharedApplication; + const window = app.keyWindow; + if (!window) { + throw new Error('No key UIWindow is available yet'); + } + + const nativeAccent = UIColor.systemPinkColor ?? UIColor.magentaColor; + const nativeBackdrop = UIColor.colorWithRedGreenBlueAlpha( + 0.04, + 0.08, + 0.12, + 1, + ); + + window.tintColor = nativeAccent; + window.backgroundColor = nativeBackdrop; + window.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark; + + const rootView = window.rootViewController?.view; + if (rootView) { + rootView.tintColor = nativeAccent; + rootView.backgroundColor = nativeBackdrop; + } + }); + + return { + backend: api.backend, + turboBackend: NativeScript.getRuntimeBackend(), + classes: api.metadata?.classes ?? 0, + functions: api.metadata?.functions ?? 0, + constants: api.metadata?.constants ?? 0, + enums: api.metadata?.enums ?? 0, + timeoutConstant: NSURLErrorTimedOut, + darkStyle: UIUserInterfaceStyle.Dark, + nativeCallsRanOnMainThread, + }; +} + +export default function App(): React.JSX.Element { + const [status, setStatus] = useState('Ready'); + const [details, setDetails] = useState(''); + const [busy, setBusy] = useState(false); + + const runDemo = useCallback(async () => { + setBusy(true); + setStatus('Applying UIKit tweaks'); + try { + const result = await applyUIKitTweaks(); + setStatus('UIKit updated from JavaScript'); + setDetails(JSON.stringify(result, null, 2)); + } catch (error) { + setStatus('Demo failed'); + setDetails(error instanceof Error ? error.message : String(error)); + } finally { + setBusy(false); + } + }, []); + + useEffect(() => { + runDemo(); + }, [runDemo]); + + const buttonLabel = useMemo( + () => (busy ? 'Working...' : 'Run UIKit Tweak Again'), + [busy], + ); + + return ( + + + NativeScript for React Native + Hermes JSI UIKit Demo + {status} + + {details} + + [ + styles.button, + busy && styles.buttonDisabled, + pressed && !busy && styles.buttonPressed, + ]}> + {buttonLabel} + + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: 'transparent', + justifyContent: 'center', + padding: 24, + }, + panel: { + gap: 14, + borderRadius: 8, + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'rgba(255,255,255,0.26)', + backgroundColor: 'rgba(8,14,22,0.82)', + padding: 20, + }, + kicker: { + color: '#9bd4ff', + fontSize: 13, + fontWeight: '700', + letterSpacing: 0, + textTransform: 'uppercase', + }, + title: { + color: '#ffffff', + fontSize: 28, + fontWeight: '800', + letterSpacing: 0, + }, + status: { + color: '#f7d276', + fontSize: 17, + fontWeight: '700', + letterSpacing: 0, + }, + details: { + minHeight: 112, + color: '#d8e7f4', + fontFamily: Platform.select({ios: 'Menlo', default: 'monospace'}), + fontSize: 13, + lineHeight: 18, + letterSpacing: 0, + }, + button: { + minHeight: 46, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8, + backgroundColor: '#ff4fa3', + paddingHorizontal: 16, + }, + buttonPressed: { + backgroundColor: '#e23f90', + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: '#ffffff', + fontSize: 16, + fontWeight: '800', + letterSpacing: 0, + }, +}); diff --git a/examples/react-native-demo/README.md b/examples/react-native-demo/README.md new file mode 100644 index 00000000..651f18a0 --- /dev/null +++ b/examples/react-native-demo/README.md @@ -0,0 +1,19 @@ +# React Native NativeScript Demo + +This demo is generated into `build/react-native-demo` so the +repository does not need to commit React Native boilerplate. + +Run it from the repository root: + +```sh +npm run demo-rn-turbomodule +``` + +The generated app installs the local +`@nativescript/react-native` tarball, enables Hermes and the New +Architecture, then launches an iOS simulator app. The app installs the +NativeScript Native API JSI host object with `NativeScript.init()`, installs +NativeScript-style globals such as `UIApplication` and `UIColor`, and uses +`runOnUI` to execute a small UIKit tweak from JavaScript while dispatching the +native UIKit calls to the main thread. The script waits for a simulator marker +after the tweak succeeds. diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 94069b62..c8a2c855 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,110 @@ 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 makeHermesDirectReturnWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dh"; + 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 makeHermesFrameDirectReturnWrapperName(DispatchKind kind, + size_t index) { + std::ostringstream stream; + stream << "hf"; + 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 +530,204 @@ 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 canSetHermesReturnDirectly(MDTypeKind kind) { + return canSetV8ReturnDirectly(kind); +} + +bool canSetHermesObjCReturnDirectly(MDTypeKind kind) { + if (canSetHermesReturnDirectly(kind)) { + return true; + } + + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + 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 mdTypeUInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUShort: + out << " setV8DispatchUInt16ReturnValue(info.GetIsolate(), info, " + << "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; + } +} + +void writeHermesDirectReturnValue(std::ostringstream& out, DispatchKind dispatchKind, + MDTypeKind kind, + const std::string& valueExpr) { + switch (kind) { + case mdTypeVoid: + out << " if (!SetHermesGeneratedVoidReturn(env, result)) {\n"; + break; + case mdTypeBool: + out << " if (!SetHermesGeneratedBoolReturn(cif, result, " << valueExpr + << " != 0)) {\n"; + break; + case mdTypeChar: + out << " if (!SetHermesGeneratedInt8Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + out << " if (!SetHermesGeneratedUInt8Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSShort: + out << " if (!SetHermesGeneratedInt16Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUShort: + out << " if (!SetHermesGeneratedUInt16Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSInt: + out << " if (!SetHermesGeneratedInt32Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUInt: + out << " if (!SetHermesGeneratedUInt32Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " if (!SetHermesGeneratedInt64Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " if (!SetHermesGeneratedUInt64Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " if (!SetHermesGeneratedDoubleReturn(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (dispatchKind == DispatchKind::ObjCMethod) { + out << " if (!TryFastSetHermesGeneratedObjCObjectReturnValue(" + "env, cif, returnContext, selector, cif->returnType->kind, " + << valueExpr << ", result)) {\n"; + } else { + out << " if (!TryFastConvertHermesReturnValue(env, cif, " + "cif->returnType->kind, &" + << valueExpr << ", result)) {\n"; + } + break; + default: + out << " if (!TryFastConvertHermesReturnValue(env, cif, static_cast(" + << static_cast(kind) << "), &" << valueExpr << ", result)) {\n"; + break; + } +} + bool argKindMayNeedCleanup(MDTypeKind kind) { switch (kind) { case mdTypeAnyObject: @@ -630,16 +931,951 @@ void writeFastNapiArgConversion(std::ostringstream& out, const MDTypeInfo* type, << " ? 1 : 0);\n"; break; } - default: - out << failCleanup; - out << " return false;\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"); + } +} + +const char* hermesFrameRawConverterForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "TryFastConvertHermesGeneratedBoolRawArgument"; + case mdTypeChar: + return "TryFastConvertHermesGeneratedInt8RawArgument"; + case mdTypeUChar: + case mdTypeUInt8: + return "TryFastConvertHermesGeneratedUInt8RawArgument"; + case mdTypeSShort: + return "TryFastConvertHermesGeneratedInt16RawArgument"; + case mdTypeUShort: + return "TryFastConvertHermesGeneratedUInt16RawArgument"; + case mdTypeSInt: + return "TryFastConvertHermesGeneratedInt32RawArgument"; + case mdTypeUInt: + return "TryFastConvertHermesGeneratedUInt32RawArgument"; + case mdTypeSLong: + case mdTypeSInt64: + return "TryFastConvertHermesGeneratedInt64RawArgument"; + case mdTypeULong: + case mdTypeUInt64: + return "TryFastConvertHermesGeneratedUInt64RawArgument"; + case mdTypeFloat: + return "TryFastConvertHermesGeneratedFloatRawArgument"; + case mdTypeDouble: + return "TryFastConvertHermesGeneratedDoubleRawArgument"; + default: + return nullptr; + } +} + +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index, + const std::string& valueExpr = "") { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + const std::string argValue = + valueExpr.empty() ? "argv[" + std::to_string(index) + "]" : valueExpr; + const char* converter = engineDirectConverterMacroForKind(type->kind); + out << " if (!" << converter << "(env, "; + if (engineDirectConverterTakesKind(type->kind)) { + out << "static_cast(" << static_cast(type->kind) + << "), "; + } + out << argValue << ", &arg" << index << ")) {\n"; + if (argKindMayNeedCleanup(type->kind)) { + out << " cif->argTypes[" << index << "]->toNative(env, " << argValue + << ", &arg" << index << ", &shouldFree" << index + << ", &shouldFreeAny);\n"; + } else { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, " << argValue + << ", &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; +} + +void writeHermesFrameArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index) { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + if (const char* rawConverter = hermesFrameRawConverterForKind(type->kind)) { + out << " if (!" << rawConverter << "(argRaw" << index << ", &arg" + << index << ")) {\n"; + out << " napi_value argValue" << index + << " = hermesDispatchFrameArg(argsBase, " << index << ");\n"; + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, argValue" + << index << ", &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + out << " }\n"; + return; + } + + writeEngineDirectArgConversion( + out, type, index, "argValue" + std::to_string(index)); +} + +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 << " }\n"; + } + 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 << ", "; + } + 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 writeHermesDirectReturnWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind == DispatchKind::BlockInvoke || signature == nullptr || + signature->returnType == nullptr) { + return; + } + + const bool canSetReturnDirectly = + kind == DispatchKind::ObjCMethod + ? canSetHermesObjCReturnDirectly(signature->returnType->kind) + : canSetHermesReturnDirectly(signature->returnType->kind); + if (!canSetReturnDirectly) { + 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, " + "const HermesObjCReturnContext* returnContext, "; + } + out << "const napi_value* argv, napi_value* result) {\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 << " }\n"; + } + 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 << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, ""); + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, + "nativeResult"); + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return true;\n"; + out << "}\n\n"; +} + +void writeHermesFrameDirectReturnWrapper(std::ostringstream& out, + DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (signature == nullptr || signature->returnType == nullptr) { + return; + } + + const bool canSetReturnDirectly = + kind == DispatchKind::ObjCMethod + ? canSetHermesObjCReturnDirectly(signature->returnType->kind) + : canSetHermesReturnDirectly(signature->returnType->kind); + if (!canSetReturnDirectly) { + 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, " + "const HermesObjCReturnContext* returnContext, "; + } else if (kind == DispatchKind::BlockInvoke) { + out << "void* block, "; + } + out << "const uint64_t* argsBase, napi_value* result) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } else if (kind == DispatchKind::BlockInvoke) { + out << "void*"; + 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++) { + if (hermesFrameRawConverterForKind(argTypeInfos[i]->kind) != nullptr) { + out << " uint64_t argRaw" << i + << " = hermesDispatchFrameRawArg(argsBase, " << i << ");\n"; + } else { + out << " napi_value argValue" << i + << " = hermesDispatchFrameArg(argsBase, " << i << ");\n"; + } + 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::ObjCMethod && 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::ObjCMethod && 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 << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + writeHermesFrameArgConversion(out, argTypeInfos[i], i); + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } else if (kind == DispatchKind::BlockInvoke) { + callExpr << "block"; + 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"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, ""); + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, + "nativeResult"); + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\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 +1897,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 +1933,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 +1948,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 +1989,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 +2005,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 +2059,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 +2085,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 +2275,29 @@ 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 objcHermesDirectReturnEntries; + std::unordered_map cFunctionHermesDirectReturnEntries; + std::unordered_map objcHermesFrameDirectReturnEntries; + std::unordered_map cFunctionHermesFrameDirectReturnEntries; + std::unordered_map blockHermesFrameDirectReturnEntries; + std::unordered_map blockPreparedEntries; std::unordered_map dispatchEncoding; std::unordered_set collidedDispatchIds; @@ -912,6 +2327,16 @@ 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); + objcHermesDirectReturnEntries.erase(dispatchId); + cFunctionHermesDirectReturnEntries.erase(dispatchId); + objcHermesFrameDirectReturnEntries.erase(dispatchId); + cFunctionHermesFrameDirectReturnEntries.erase(dispatchId); + blockHermesFrameDirectReturnEntries.erase(dispatchId); + blockPreparedEntries.erase(dispatchId); dispatchEncoding.erase(dispatchId); continue; } @@ -924,12 +2349,35 @@ 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); + if (signature->returnType != nullptr && + canSetHermesObjCReturnDirectly(signature->returnType->kind)) { + objcHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + objcHermesFrameDirectReturnEntries.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); + if (signature->returnType != nullptr && + canSetHermesReturnDirectly(signature->returnType->kind)) { + cFunctionHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + cFunctionHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + } else if (use.kind == DispatchKind::BlockInvoke) { + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + blockPreparedEntries.emplace(dispatchId, wrapperKey); + if (signature->returnType != nullptr && + canSetHermesReturnDirectly(signature->returnType->kind)) { + blockHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + } } } @@ -940,6 +2388,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 +2405,65 @@ 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 hermesDirectReturnWrapperNameByKey; + hermesDirectReturnWrapperNameByKey.reserve(wrappers.size()); + size_t hermesDirectReturnWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + hermesDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesDirectReturnWrapperName(wrapper.second.first, + hermesDirectReturnWrapperIndex++)); + } + + std::unordered_map + hermesFrameDirectReturnWrapperNameByKey; + hermesFrameDirectReturnWrapperNameByKey.reserve(wrappers.size()); + size_t hermesFrameDirectReturnWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + hermesFrameDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesFrameDirectReturnWrapperName( + wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); + } + + std::unordered_map + hermesBlockFrameDirectReturnWrapperNameByKey; + hermesBlockFrameDirectReturnWrapperNameByKey.reserve(preparedWrappers.size()); + for (const auto& wrapper : preparedWrappers) { + hermesBlockFrameDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesFrameDirectReturnWrapperName( + wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); + } + + 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 +2476,418 @@ 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> + sortedObjCHermesDirectReturnEntries( + objcHermesDirectReturnEntries.begin(), + objcHermesDirectReturnEntries.end()); + std::sort(sortedObjCHermesDirectReturnEntries.begin(), + sortedObjCHermesDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionHermesDirectReturnEntries( + cFunctionHermesDirectReturnEntries.begin(), + cFunctionHermesDirectReturnEntries.end()); + std::sort(sortedCFunctionHermesDirectReturnEntries.begin(), + sortedCFunctionHermesDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedObjCHermesFrameDirectReturnEntries( + objcHermesFrameDirectReturnEntries.begin(), + objcHermesFrameDirectReturnEntries.end()); + std::sort(sortedObjCHermesFrameDirectReturnEntries.begin(), + sortedObjCHermesFrameDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionHermesFrameDirectReturnEntries( + cFunctionHermesFrameDirectReturnEntries.begin(), + cFunctionHermesFrameDirectReturnEntries.end()); + std::sort(sortedCFunctionHermesFrameDirectReturnEntries.begin(), + sortedCFunctionHermesFrameDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedBlockHermesFrameDirectReturnEntries( + blockHermesFrameDirectReturnEntries.begin(), + blockHermesFrameDirectReturnEntries.end()); + std::sort(sortedBlockHermesFrameDirectReturnEntries.begin(), + sortedBlockHermesFrameDirectReturnEntries.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 << "#if NS_GSD_BACKEND_HERMES\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH 1\n"; + generated << "#undef " + "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH\n"; + generated << "#define " + "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH 1\n"; + generated << "#undef " + "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH\n"; + generated << "#define " + "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_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 " + "TryFastConvertHermesGeneratedBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesGeneratedInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesGeneratedUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesGeneratedInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesGeneratedUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesGeneratedInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesGeneratedUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesGeneratedInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesGeneratedUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesGeneratedFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesGeneratedDoubleArgument\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_HERMES\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertHermesArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertHermesGeneratedBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesGeneratedInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesGeneratedUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesGeneratedInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesGeneratedUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesGeneratedInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesGeneratedUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesGeneratedInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesGeneratedUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesGeneratedFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesGeneratedDoubleArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertHermesSelectorArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertHermesObjectArgument\n"; + for (const auto& wrapper : wrappers) { + writeHermesDirectReturnWrapper( + generated, wrapper.second.first, + hermesDirectReturnWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + for (const auto& wrapper : wrappers) { + writeHermesFrameDirectReturnWrapper( + generated, wrapper.second.first, + hermesFrameDirectReturnWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + for (const auto& wrapper : preparedWrappers) { + writeHermesFrameDirectReturnWrapper( + generated, wrapper.second.first, + hermesBlockFrameDirectReturnWrapperNameByKey.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_HERMES\n"; + generated << "inline constexpr ObjCHermesDirectReturnDispatchEntry " + "kGeneratedObjCHermesDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCHermesDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionHermesDirectReturnDispatchEntry " + "kGeneratedCFunctionHermesDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionHermesDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n"; + + generated << "inline constexpr ObjCHermesFrameDirectReturnDispatchEntry " + "kGeneratedObjCHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesFrameDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionHermesFrameDirectReturnDispatchEntry " + "kGeneratedCFunctionHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesFrameDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n"; + + generated << "inline constexpr BlockHermesFrameDirectReturnDispatchEntry " + "kGeneratedBlockHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedBlockHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesBlockFrameDirectReturnWrapperNameByKey.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 +2905,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..03639055 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "build-vision": "./scripts/build_all_vision.sh", "build-visionos": "./scripts/build_all_vision.sh", "build-node-api": "./scripts/build_all_react_native.sh", + "build-rn-turbomodule": "./scripts/build_react_native_turbomodule.sh", + "test-rn-turbomodule": "./scripts/test_react_native_turbomodule.sh", + "demo-rn-turbomodule": "./scripts/create_react_native_demo.sh", "pack:ios": "./scripts/build_npm_ios.sh", "pack:macos": "./scripts/build_npm_macos.sh", "pack:visionos": "./scripts/build_npm_vision.sh", @@ -48,7 +51,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/packages/ios/package.json b/packages/ios/package.json index 767acf5e..f96f7163 100644 --- a/packages/ios/package.json +++ b/packages/ios/package.json @@ -1,6 +1,6 @@ { "name": "@nativescript/ios", - "version": "9.0.0-napi-v8.2", + "version": "0.0.1", "description": "NativeScript Runtime for iOS", "keywords": [ "NativeScript", diff --git a/packages/react-native/LICENSE b/packages/react-native/LICENSE new file mode 100644 index 00000000..6f231e7c --- /dev/null +++ b/packages/react-native/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Yagiz Nizipli and Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec new file mode 100644 index 00000000..8aaa5f14 --- /dev/null +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -0,0 +1,52 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +folly_compiler_flags = "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1 -Wno-comma -Wno-shorten-64-to-32" + +Pod::Spec.new do |s| + s.name = "NativeScriptNativeApi" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://github.com/NativeScript/napi-ios" + s.license = "Apache-2.0" + s.author = package["author"] + s.platforms = { :ios => "13.0" } + s.source = { :git => "https://github.com/NativeScript/napi-ios.git", :tag => "react-native-v#{s.version}" } + s.requires_arc = false + + s.source_files = [ + "ios/**/*.{h,mm}", + "native-api-jsi/**/*.{h,mm}" + ] + s.public_header_files = "ios/**/*.h" + s.resource_bundles = { + "NativeScriptNativeApi" => ["metadata/*.nsmd"] + } + s.vendored_frameworks = "ios/vendor/Libffi.xcframework" + + s.compiler_flags = folly_compiler_flags + s.pod_target_xcconfig = { + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "CLANG_CXX_LIBRARY" => "libc++", + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) TARGET_ENGINE_HERMES=1", + "HEADER_SEARCH_PATHS" => [ + "\"$(PODS_TARGET_SRCROOT)/ios\"", + "\"$(PODS_TARGET_SRCROOT)/native-api-jsi\"", + "\"$(PODS_TARGET_SRCROOT)/native-api-jsi/metadata/include\"", + "\"$(PODS_TARGET_SRCROOT)/ios/vendor/libffi/include\"", + "\"$(PODS_ROOT)/Headers/Public/React-Codegen\"", + "\"$(PODS_ROOT)/Headers/Private/React-Codegen\"", + "\"$(PODS_ROOT)/Headers/Public/ReactCommon\"", + "\"$(PODS_ROOT)/Headers/Private/ReactCommon\"" + ].join(" ") + } + + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency "React-Core" + s.dependency "React-jsi" + s.dependency "ReactCommon/turbomodule/core" + end +end diff --git a/packages/react-native/README.md b/packages/react-native/README.md new file mode 100644 index 00000000..b7571da7 --- /dev/null +++ b/packages/react-native/README.md @@ -0,0 +1,69 @@ +# @nativescript/react-native + +React Native TurboModule wrapper for the NativeScript Native API JSI bridge on +Hermes. + +The module exposes one small TurboModule whose `init()` method attaches the +NativeScript Native API host object to `globalThis.__nativeScriptNativeApi` and +installs lazy NativeScript-style globals for classes and C functions. The host +object itself is pure JSI and is shared with the NativeScript Hermes runtime. + +```ts +import NativeScript from "@nativescript/react-native"; + +NativeScript.init(); + +const object = NSObject.new(); +``` + +For UIKit work that must happen on the main thread, pass a callback to the JSI +host object's `runOnUI()` helper. The callback itself stays on React Native's JS +thread; NativeScript native calls made inside the callback are synchronously +performed on UIKit's main thread. + +```ts +await NativeScript.runOnUI(() => { + UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; +}); +``` + +The published package includes generated NativeScript metadata, the libffi +xcframework, and generated iOS SDK TypeScript declarations. Build it from the +repository root with: + +```sh +npm run build-rn-turbomodule +``` + +The tarball is written to `packages/react-native/dist/` and copied to +`build/npm-tarballs/`. + +To verify it inside a generated React Native iOS app: + +```sh +npm run test-rn-turbomodule +``` + +## Using the package in a React Native app + +1. Build or download the package tarball. +2. Install it in an RN app that has Hermes and the New Architecture enabled: + + ```sh + npm install /path/to/nativescript-react-native-0.0.1.tgz + cd ios + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + ``` + +3. Initialize it before using native APIs: + + ```ts + import NativeScript from "@nativescript/react-native"; + + NativeScript.init(); + + await NativeScript.runOnUI(() => { + UIApplication.sharedApplication.keyWindow.tintColor = + UIColor.systemPinkColor; + }); + ``` diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.h b/packages/react-native/ios/NativeScriptNativeApiModule.h new file mode 100644 index 00000000..77a68967 --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModule.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace facebook::react { + +class NativeScriptNativeApiModule + : public NativeScriptNativeApiCxxSpec { + public: + explicit NativeScriptNativeApiModule(std::shared_ptr jsInvoker); + + bool install(jsi::Runtime& runtime, std::string metadataPath); + bool isInstalled(jsi::Runtime& runtime); + std::string defaultMetadataPath(jsi::Runtime& runtime); + std::string getRuntimeBackend(jsi::Runtime& runtime); + + private: + std::shared_ptr jsInvoker_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm new file mode 100644 index 00000000..74b045b6 --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModule.mm @@ -0,0 +1,99 @@ +#include "NativeScriptNativeApiModule.h" + +#import +#import + +#include + +#include "NativeApiJsiReactNative.h" + +namespace { + +std::string pathForResource(NSBundle* bundle, NSString* name, NSString* type) { + if (bundle == nil) { + return ""; + } + NSString* path = [bundle pathForResource:name ofType:type]; + return path != nil ? path.UTF8String : ""; +} + +std::string bundledMetadataPath() { +#if TARGET_OS_SIMULATOR +#if defined(__x86_64__) + NSString* metadataName = @"metadata.ios-sim.x86_64"; +#else + NSString* metadataName = @"metadata.ios-sim.arm64"; +#endif +#else + NSString* metadataName = @"metadata.ios.arm64"; +#endif + + std::string path = pathForResource([NSBundle mainBundle], metadataName, @"nsmd"); + if (!path.empty()) { + return path; + } + + Class providerClass = NSClassFromString(@"NativeScriptNativeApiModuleProvider"); + NSBundle* providerBundle = + providerClass != Nil ? [NSBundle bundleForClass:providerClass] : nil; + NSString* resourceBundlePath = + [providerBundle pathForResource:@"NativeScriptNativeApi" ofType:@"bundle"]; + NSBundle* resourceBundle = + resourceBundlePath != nil ? [NSBundle bundleWithPath:resourceBundlePath] : nil; + + path = pathForResource(resourceBundle, metadataName, @"nsmd"); + if (!path.empty()) { + return path; + } + + return pathForResource(resourceBundle, @"metadata", @"nsmd"); +} + +void writeSmokeMarkerIfRequested(const char* stage) { + const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER"); + if (enabled == nullptr || enabled[0] == '\0') { + return; + } + + NSString* path = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; + NSString* content = + [NSString stringWithFormat:@"stage=%s\n", stage != nullptr ? stage : ""]; + [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + +} // namespace + +namespace facebook::react { + +NativeScriptNativeApiModule::NativeScriptNativeApiModule( + std::shared_ptr jsInvoker) + : NativeScriptNativeApiCxxSpec(jsInvoker), jsInvoker_(std::move(jsInvoker)) {} + +bool NativeScriptNativeApiModule::install(jsi::Runtime& runtime, + std::string metadataPath) { + std::string resolvedMetadataPath = + metadataPath.empty() ? bundledMetadataPath() : metadataPath; + const char* metadataPathArg = + resolvedMetadataPath.empty() ? nullptr : resolvedMetadataPath.c_str(); + + auto config = nativescript::MakeReactNativeNativeApiJsiConfig( + jsInvoker_, nullptr, metadataPathArg); + nativescript::InstallNativeApiJSI(runtime, config); + return isInstalled(runtime); +} + +bool NativeScriptNativeApiModule::isInstalled(jsi::Runtime& runtime) { + return runtime.global().hasProperty(runtime, "__nativeScriptNativeApi"); +} + +std::string NativeScriptNativeApiModule::defaultMetadataPath(jsi::Runtime&) { + return bundledMetadataPath(); +} + +std::string NativeScriptNativeApiModule::getRuntimeBackend(jsi::Runtime&) { + writeSmokeMarkerIfRequested("getRuntimeBackend"); + return "hermes-jsi"; +} + +} // namespace facebook::react diff --git a/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h new file mode 100644 index 00000000..ea58c873 --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h @@ -0,0 +1,9 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NativeScriptNativeApiModuleProvider : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm new file mode 100644 index 00000000..997cc91f --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm @@ -0,0 +1,16 @@ +#import "NativeScriptNativeApiModuleProvider.h" + +#import +#import + +#include "NativeScriptNativeApiModule.h" + +@implementation NativeScriptNativeApiModuleProvider + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams&)params { + return std::make_shared( + params.jsInvoker); +} + +@end diff --git a/packages/react-native/package.json b/packages/react-native/package.json new file mode 100644 index 00000000..0108c004 --- /dev/null +++ b/packages/react-native/package.json @@ -0,0 +1,52 @@ +{ + "name": "@nativescript/react-native", + "version": "0.0.1", + "description": "React Native TurboModule for NativeScript Native API access", + "keywords": [ + "NativeScript", + "React Native", + "TurboModule", + "JSI", + "iOS" + ], + "repository": { + "type": "git", + "url": "https://github.com/NativeScript/napi-ios", + "directory": "packages/react-native" + }, + "author": { + "name": "NativeScript Team", + "email": "oss@nativescript.org" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "react-native": "src/index.ts", + "types": "src/index.d.ts", + "files": [ + "src", + "types", + "ios", + "metadata", + "native-api-jsi", + "NativeScriptNativeApi.podspec", + "README.md", + "LICENSE" + ], + "peerDependencies": { + "react": "*", + "react-native": ">=0.79" + }, + "codegenConfig": { + "name": "NativeScriptNativeApiSpec", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "org.nativescript.nativeapi" + }, + "ios": { + "modulesProvider": { + "NativeScriptNativeApi": "NativeScriptNativeApiModuleProvider" + } + } + } +} diff --git a/packages/react-native/react-native.config.js b/packages/react-native/react-native.config.js new file mode 100644 index 00000000..22b2e13d --- /dev/null +++ b/packages/react-native/react-native.config.js @@ -0,0 +1,7 @@ +module.exports = { + dependency: { + platforms: { + android: null, + }, + }, +}; diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts new file mode 100644 index 00000000..6b15741b --- /dev/null +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -0,0 +1,11 @@ +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +export interface Spec extends TurboModule { + readonly install: (metadataPath: string) => boolean; + readonly isInstalled: () => boolean; + readonly defaultMetadataPath: () => string; + readonly getRuntimeBackend: () => string; +} + +export default TurboModuleRegistry.getEnforcing('NativeScriptNativeApi'); diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts new file mode 100644 index 00000000..70110704 --- /dev/null +++ b/packages/react-native/src/index.d.ts @@ -0,0 +1,43 @@ +/// + +export type NativeApiHost = { + runtime?: string; + backend?: string; + metadata?: { + classes?: number; + functions?: number; + constants?: number; + protocols?: number; + enums?: number; + classNames?: () => string[]; + functionNames?: () => string[]; + constantNames?: () => string[]; + enumNames?: () => string[]; + }; + runOnUI?: (callback?: () => void) => Promise; + [name: string]: unknown; +}; + +export type InstallOptions = { + globals?: boolean; +}; + +export function init(metadataPath?: string, options?: InstallOptions): boolean; +export const install: typeof init; +export function installGlobals(): boolean; +export function isInstalled(): boolean; +export function defaultMetadataPath(): string; +export function getRuntimeBackend(): string; +export function runOnUI(callback?: () => void): Promise; + +declare const NativeScript: { + init: typeof init; + install: typeof install; + installGlobals: typeof installGlobals; + isInstalled: typeof isInstalled; + defaultMetadataPath: typeof defaultMetadataPath; + getRuntimeBackend: typeof getRuntimeBackend; + runOnUI: typeof runOnUI; +}; + +export default NativeScript; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts new file mode 100644 index 00000000..d6d89aef --- /dev/null +++ b/packages/react-native/src/index.ts @@ -0,0 +1,132 @@ +import NativeScriptNativeApi from './NativeScriptNativeApi'; + +type NativeApiHost = { + metadata?: { + classNames?: () => string[]; + functionNames?: () => string[]; + constantNames?: () => string[]; + enumNames?: () => string[]; + }; + runOnUI?: (callback?: () => void) => Promise; + [name: string]: unknown; +}; + +export type InstallOptions = { + globals?: boolean; +}; + +const nativeApiGlobalName = '__nativeScriptNativeApi'; + +function nativeApiHost(): NativeApiHost | undefined { + return (globalThis as Record)[nativeApiGlobalName] as + | NativeApiHost + | undefined; +} + +function requireNativeApiHost(): NativeApiHost { + const api = nativeApiHost(); + if (!api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + return api; +} + +function defineLazyNativeGlobal( + name: string, + resolve: (name: string) => unknown, +) { + if (!name || name in globalThis) { + return; + } + + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + get() { + const value = resolve(name); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value, + }); + return value; + }, + }); +} + +export function installGlobals(): boolean { + const api = nativeApiHost(); + if (!api) { + return false; + } + + const classNames = api.metadata?.classNames?.() ?? []; + for (const name of classNames) { + defineLazyNativeGlobal(name, (className) => api[className]); + } + + const functionNames = api.metadata?.functionNames?.() ?? []; + for (const name of functionNames) { + defineLazyNativeGlobal(name, (functionName) => api[functionName]); + } + + const constantNames = api.metadata?.constantNames?.() ?? []; + for (const name of constantNames) { + defineLazyNativeGlobal(name, (constantName) => api[constantName]); + } + + const enumNames = api.metadata?.enumNames?.() ?? []; + for (const name of enumNames) { + defineLazyNativeGlobal(name, (enumName) => api[enumName]); + } + + return true; +} + +export function init( + metadataPath = '', + options: InstallOptions = {}, +): boolean { + const installed = NativeScriptNativeApi.install(metadataPath); + if (installed && options.globals !== false) { + installGlobals(); + } + return installed; +} + +export const install = init; + +export function isInstalled(): boolean { + return NativeScriptNativeApi.isInstalled(); +} + +export function defaultMetadataPath(): string { + return NativeScriptNativeApi.defaultMetadataPath(); +} + +export function getRuntimeBackend(): string { + return NativeScriptNativeApi.getRuntimeBackend(); +} + +export function runOnUI(callback?: () => void): Promise { + const run = requireNativeApiHost().runOnUI; + if (typeof run !== 'function') { + throw new Error( + 'NativeScript Native API JSI host was installed without runOnUI', + ); + } + return run(callback); +} + +const NativeScript = { + init, + install, + installGlobals, + isInstalled, + defaultMetadataPath, + getRuntimeBackend, + runOnUI, +}; + +export default NativeScript; diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index 47edbfa5..578bfc3c 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 @@ -53,7 +53,7 @@ if $EMBED_METADATA; then checkpoint "... All metadata generated!" 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..3ec7ccec 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -14,7 +14,10 @@ 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}} +GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" for arg in $@; do case $arg in @@ -39,6 +42,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 +71,83 @@ if ! $VERBOSE; then QUIET=-quiet fi +function assemble_node_api_xcframework () { + local output_dir="$1" + shift + + if command -v deno >/dev/null 2>&1; then + deno run -A ./scripts/build_xcframework.mts --output "$output_dir" "$@" + return + fi + + if [ ! -d "$SCRIPT_DIR/node_modules/react-native-node-api" ] || [ ! -d "$SCRIPT_DIR/node_modules/yargs-parser" ]; then + npm --prefix "$SCRIPT_DIR" install --no-audit --no-fund + fi + + node ./scripts/build_xcframework.mts --output "$output_dir" "$@" +} + +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_stamp () { + local platform="$1" + local backend + backend=$(effective_gsd_backend) + printf "platform=%s\nbackend=%s\ntarget_engine=%s\nmetadata_size=%s\n" \ + "$platform" "$backend" "$TARGET_ENGINE" "$METADATA_SIZE" +} + +function ensure_signature_dispatch_bindings () { + local platform="$1" + local backend + backend=$(effective_gsd_backend) + if [ "$TARGET_ENGINE" == "none" ] || [ "$backend" == "none" ]; then + return + fi + + if [ -z "$platform" ]; then + return + fi + + local expected_stamp + expected_stamp=$(signature_dispatch_stamp "$platform") + if [ -f "$GENERATED_SIGNATURE_DISPATCH" ] && \ + [ -f "$GENERATED_SIGNATURE_DISPATCH_STAMP" ] && \ + [ "$(cat "$GENERATED_SIGNATURE_DISPATCH_STAMP")" == "$expected_stamp" ]; 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 ($backend)..." + NS_SIGNATURE_BINDINGS_CPP_PATH="$GENERATED_SIGNATURE_DISPATCH" npm run metagen "$platform" + mkdir -p "$(dirname "$GENERATED_SIGNATURE_DISPATCH_STAMP")" + printf "%s" "$expected_stamp" > "$GENERATED_SIGNATURE_DISPATCH_STAMP" +} + DEV_TEAM=${DEVELOPMENT_TEAM:-} DIST=$(PWD)/dist mkdir -p $DIST @@ -84,6 +171,8 @@ function cmake_build () { is_macos_napi=true fi + ensure_signature_dispatch_bindings "$platform" + local libffi_build_dir= case "$platform" in ios) libffi_build_dir="iphoneos-arm64" ;; @@ -102,10 +191,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 +221,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 \ @@ -223,7 +322,7 @@ if [[ -n "${XCFRAMEWORKS[@]}" ]]; then # https://github.com/callstackincubator/react-native-node-api/blob/9b231c14459b62d7df33360f930a00343d8c46e6/docs/PREBUILDS.md OUTPUT_DIR="packages/ios-node-api/build/$CONFIG_BUILD/NativeScript.apple.node" rm -rf $OUTPUT_DIR - deno run -A ./scripts/build_xcframework.mts --output "$OUTPUT_DIR" ${XCFRAMEWORKS[@]} + assemble_node_api_xcframework "$OUTPUT_DIR" "${XCFRAMEWORKS[@]}" else checkpoint "Creating NativeScript.xcframework" @@ -246,7 +345,7 @@ if $BUILD_MACOS; then # https://github.com/callstackincubator/react-native-node-api/blob/9b231c14459b62d7df33360f930a00343d8c46e6/docs/PREBUILDS.md OUTPUT_DIR="packages/macos-node-api/build/$CONFIG_BUILD/NativeScript.apple.node" rm -rf $OUTPUT_DIR - deno run -A ./scripts/build_xcframework.mts --output "$OUTPUT_DIR" ${XCFRAMEWORKS[@]} + assemble_node_api_xcframework "$OUTPUT_DIR" "${XCFRAMEWORKS[@]}" fi fi diff --git a/scripts/build_npm_ios.sh b/scripts/build_npm_ios.sh index d4970c93..aec9a08e 100755 --- a/scripts/build_npm_ios.sh +++ b/scripts/build_npm_ios.sh @@ -6,13 +6,27 @@ checkpoint "Preparing npm package for iOS..." PACKAGE_DIR="packages/ios" OUTPUT_DIR="$PACKAGE_DIR/dist" STAGING_DIR="$OUTPUT_DIR/package" +PACKAGE_NAME_OVERRIDE=${NPM_PACKAGE_NAME:-} +PACKAGE_VERSION_OVERRIDE=${NPM_PACKAGE_VERSION:-} +PACK_DESTINATION=${NPM_PACK_DESTINATION:-..} rm -rf "$OUTPUT_DIR" mkdir -p "$STAGING_DIR/framework/internal" +mkdir -p "$PACK_DESTINATION" cp "$PACKAGE_DIR/package.json" "$STAGING_DIR" cp "$PACKAGE_DIR/README.md" "$STAGING_DIR" cp "$PACKAGE_DIR/LICENSE" "$STAGING_DIR" +if [ -n "$PACKAGE_NAME_OVERRIDE" ] || [ -n "$PACKAGE_VERSION_OVERRIDE" ]; then + TMP_FILE=$(mktemp) + jq \ + --arg name "$PACKAGE_NAME_OVERRIDE" \ + --arg version "$PACKAGE_VERSION_OVERRIDE" \ + 'if $name != "" then .name = $name else . end | if $version != "" then .version = $version else . end' \ + "$STAGING_DIR/package.json" > "$TMP_FILE" + mv "$TMP_FILE" "$STAGING_DIR/package.json" +fi + cp -R "./templates/ios/." "$STAGING_DIR/framework" cp -R "dist/NativeScript.xcframework" "$STAGING_DIR/framework/internal" @@ -33,7 +47,7 @@ cp -R "metadata-generator/dist/arm64/." "$STAGING_DIR/framework/internal/metadat ) pushd "$STAGING_DIR" -npm pack --pack-destination .. +npm pack --pack-destination "$PACK_DESTINATION" popd checkpoint "npm package created." diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh new file mode 100755 index 00000000..a655f4e1 --- /dev/null +++ b/scripts/build_react_native_turbomodule.sh @@ -0,0 +1,184 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +PACKAGE_DIR="packages/react-native" +OUTPUT_DIR="$PACKAGE_DIR/dist" +PACK_DESTINATION=${NPM_PACK_DESTINATION:-"$REPO_ROOT/build/npm-tarballs"} +VERSION_OVERRIDE=${NPM_PACKAGE_VERSION:-} +SKIP_PACK=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-pack) + SKIP_PACK=true + shift + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +checkpoint "Preparing @nativescript/react-native TurboModule package..." + +rm -rf \ + "$PACKAGE_DIR/native-api-jsi" \ + "$PACKAGE_DIR/metadata" \ + "$PACKAGE_DIR/ios/vendor" \ + "$PACKAGE_DIR/types" +mkdir -p \ + "$PACKAGE_DIR/native-api-jsi/metadata/include" \ + "$PACKAGE_DIR/metadata" \ + "$PACKAGE_DIR/ios/vendor/libffi/include" \ + "$PACKAGE_DIR/types/ios" \ + "$PACKAGE_DIR/types/objc-node-api" \ + "$PACK_DESTINATION" + +cp NativeScript/ffi/jsi/NativeApiJsi.h "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/jsi/NativeApiJsi.mm "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/jsi/NativeApiJsiReactNative.h "$PACKAGE_DIR/native-api-jsi/" +cp metadata-generator/include/Metadata.h "$PACKAGE_DIR/native-api-jsi/metadata/include/" +cp metadata-generator/include/MetadataReader.h "$PACKAGE_DIR/native-api-jsi/metadata/include/" +cp NativeScript/libffi/iphonesimulator-universal/include/ffi.h "$PACKAGE_DIR/ios/vendor/libffi/include/" +cp NativeScript/libffi/iphonesimulator-universal/include/ffitarget.h "$PACKAGE_DIR/ios/vendor/libffi/include/" + +cp metadata-generator/metadata/metadata.ios-sim.arm64.nsmd "$PACKAGE_DIR/metadata/" +cp metadata-generator/metadata/metadata.ios-sim.x86_64.nsmd "$PACKAGE_DIR/metadata/" +cp metadata-generator/metadata/metadata.ios.arm64.nsmd "$PACKAGE_DIR/metadata/" + +checkpoint "Staging iOS SDK TypeScript declarations..." +cp packages/objc-node-api/index.d.ts "$PACKAGE_DIR/types/objc-node-api/" +cp packages/objc-node-api/inline_functions.d.ts "$PACKAGE_DIR/types/objc-node-api/" +cp packages/ios/types/*.d.ts "$PACKAGE_DIR/types/ios/" +perl -0pi -e 's#/// #/// #g' \ + "$PACKAGE_DIR"/types/ios/*.d.ts +node - "$PACKAGE_DIR/types/ios" <<'NODE' +const fs = require('fs'); +const path = require('path'); + +const typesDir = process.argv[2]; +const reservedParameterNames = [ + 'abstract', + 'as', + 'asserts', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'declare', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'from', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'infer', + 'instanceof', + 'interface', + 'is', + 'keyof', + 'let', + 'module', + 'namespace', + 'never', + 'new', + 'null', + 'of', + 'package', + 'private', + 'protected', + 'public', + 'readonly', + 'require', + 'return', + 'satisfies', + 'static', + 'super', + 'switch', + 'throw', + 'true', + 'try', + 'type', + 'typeof', + 'undefined', + 'unique', + 'unknown', + 'var', + 'void', + 'while', + 'with', + 'yield', +]; +const reservedPattern = new RegExp( + `([,(]\\s*)(${reservedParameterNames.join('|')})(\\??\\s*:)`, + 'g', +); + +for (const entry of fs.readdirSync(typesDir)) { + if (!entry.endsWith('.d.ts')) { + continue; + } + + const file = path.join(typesDir, entry); + let source = fs.readFileSync(file, 'utf8'); + source = source.replace(reservedPattern, '$1_$2$3'); + source = source.replace(/^[ \t]*:[^;\n]+;[ \t]*\n/gm, ''); + fs.writeFileSync(file, source); +} +NODE +{ + echo '/// ' + while IFS= read -r declaration; do + echo "/// " + done < <(find "$PACKAGE_DIR/types/ios" -maxdepth 1 -name '*.d.ts' ! -name 'index.d.ts' -exec basename {} \; | sort) +} > "$PACKAGE_DIR/types/ios/index.d.ts" + +checkpoint "Creating Libffi.xcframework for the TurboModule pod..." +xcodebuild -create-xcframework \ + -library NativeScript/libffi/iphoneos-arm64/libffi.a \ + -headers NativeScript/libffi/iphoneos-arm64/include \ + -library NativeScript/libffi/iphonesimulator-universal/libffi.a \ + -headers NativeScript/libffi/iphonesimulator-universal/include \ + -output "$PACKAGE_DIR/ios/vendor/Libffi.xcframework" + +if [[ -n "$VERSION_OVERRIDE" ]]; then + TMP_FILE=$(mktemp) + jq --arg version "$VERSION_OVERRIDE" '.version = $version' \ + "$PACKAGE_DIR/package.json" > "$TMP_FILE" + mv "$TMP_FILE" "$PACKAGE_DIR/package.json" +fi + +if [[ "$SKIP_PACK" == "true" ]]; then + checkpoint "@nativescript/react-native package staged." + exit 0 +fi + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +checkpoint "Packing @nativescript/react-native..." +( + cd "$PACKAGE_DIR" + npm pack --pack-destination "$REPO_ROOT/$OUTPUT_DIR" +) + +cp "$OUTPUT_DIR"/*.tgz "$PACK_DESTINATION/" + +checkpoint "@nativescript/react-native npm package created." diff --git a/scripts/create_react_native_demo.sh b/scripts/create_react_native_demo.sh new file mode 100755 index 00000000..ffcefee7 --- /dev/null +++ b/scripts/create_react_native_demo.sh @@ -0,0 +1,159 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +RN_VERSION=${RN_DEMO_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_DEMO_CLI_VERSION:-20.1.3} +APP_NAME=${RN_DEMO_APP_NAME:-NativeScriptNativeApiDemo} +APP_ROOT=${RN_DEMO_APP_ROOT:-"$REPO_ROOT/build/react-native-demo"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_DEMO_FORCE_RECREATE:-0} +RUN_BUILD=${RN_DEMO_BUILD:-1} +RUN_LAUNCH=${RN_DEMO_LAUNCH:-1} +BUILD_TIMEOUT_SECONDS=${RN_DEMO_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_DEMO_LAUNCH_TIMEOUT_SECONDS:-90} +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER="NATIVESCRIPT_RN_TURBO_DEMO_PASS" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" +DEMO_APP_TSX="$REPO_ROOT/examples/react-native-demo/App.tsx" + +checkpoint "Building @nativescript/react-native TurboModule tarball..." +"$SCRIPT_DIR/build_react_native_turbomodule.sh" +TARBALL=$(ls -t "$REPO_ROOT/packages/react-native/dist"/*.tgz | head -n 1) + +if [[ "$FORCE_RECREATE" == "1" ]]; then + rm -rf "$APP_DIR" +fi + +if [[ ! -d "$APP_DIR" ]]; then + checkpoint "Creating React Native demo app ($RN_VERSION)..." + mkdir -p "$APP_ROOT" + npx --yes "@react-native-community/cli@$RN_CLI_VERSION" init "$APP_NAME" \ + --version "$RN_VERSION" \ + --directory "$APP_DIR" \ + --skip-git-init \ + --install-pods false \ + --pm npm +fi + +checkpoint "Installing local TurboModule tarball into demo app..." +( + cd "$APP_DIR" + npm install "$TARBALL" +) + +checkpoint "Installing demo entrypoint..." +cp "$DEMO_APP_TSX" "$APP_DIR/App.tsx" + +checkpoint "Installing CocoaPods for demo app..." +( + cd "$APP_DIR/ios" + if [[ -f Gemfile ]]; then + bundle install + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 bundle exec pod install + else + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + fi +) + +if [[ "$RUN_BUILD" != "1" ]]; then + checkpoint "React Native demo app is ready at $APP_DIR" + exit 0 +fi + +UDID=$(node <<'NODE' +const cp = require('child_process'); +const devices = JSON.parse(cp.execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], {encoding: 'utf8'})); +const runtimes = Object.keys(devices.devices).filter((runtime) => runtime.includes('iOS')).sort().reverse(); +for (const runtime of runtimes) { + const booted = devices.devices[runtime].find((device) => device.state === 'Booted' && device.name.includes('iPhone')); + if (booted) { + console.log(booted.udid); + process.exit(0); + } +} +for (const runtime of runtimes) { + const candidate = devices.devices[runtime].find((device) => device.name.includes('iPhone')); + if (candidate) { + console.log(candidate.udid); + process.exit(0); + } +} +process.exit(1); +NODE +) + +if [[ -z "$UDID" ]]; then + echo "No available iOS simulator found." >&2 + exit 1 +fi + +checkpoint "Building demo app for simulator..." +xcrun simctl boot "$UDID" >/dev/null 2>&1 || true +xcrun simctl bootstatus "$UDID" -b + +xcodebuild \ + -workspace "$APP_DIR/ios/$APP_NAME.xcworkspace" \ + -scheme "$APP_NAME" \ + -configuration "$CONFIGURATION" \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,id=$UDID" \ + -derivedDataPath "$APP_DIR/ios/build/DerivedData" \ + build | tee "$APP_ROOT/xcodebuild.log" & +BUILD_PID=$! + +SECONDS_WAITED=0 +while kill -0 "$BUILD_PID" >/dev/null 2>&1; do + if [[ "$SECONDS_WAITED" -ge "$BUILD_TIMEOUT_SECONDS" ]]; then + kill "$BUILD_PID" >/dev/null 2>&1 || true + echo "Demo app build timed out after ${BUILD_TIMEOUT_SECONDS}s." >&2 + exit 1 + fi + sleep 5 + SECONDS_WAITED=$((SECONDS_WAITED + 5)) +done +wait "$BUILD_PID" + +APP_BUNDLE=$(find "$APP_DIR/ios/build/DerivedData/Build/Products/$CONFIGURATION-iphonesimulator" -maxdepth 1 -name "$APP_NAME.app" -print -quit) +if [[ -z "$APP_BUNDLE" ]]; then + echo "Built app bundle not found." >&2 + exit 1 +fi + +if [[ "$RUN_LAUNCH" == "1" ]]; then + checkpoint "Launching demo app..." + xcrun simctl install "$UDID" "$APP_BUNDLE" + DATA_CONTAINER=$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data) + MARKER_FILE="$DATA_CONTAINER/tmp/$MARKER_FILE_NAME" + rm -f "$MARKER_FILE" + + SIMCTL_CHILD_NATIVESCRIPT_RN_TURBO_SMOKE_MARKER=1 \ + xcrun simctl launch --terminate-running-process "$UDID" "$BUNDLE_ID" + + node - "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" <<'NODE' +const fs = require('fs'); +const [markerFile, marker, timeoutSecondsText] = process.argv.slice(2); +const timeoutMs = Number(timeoutSecondsText) * 1000; +const startedAt = Date.now(); + +function poll() { + if (fs.existsSync(markerFile)) { + const content = fs.readFileSync(markerFile, 'utf8'); + console.log(`${marker} ${JSON.stringify({markerFile, content: content.trim()})}`); + process.exit(0); + } + + if (Date.now() - startedAt > timeoutMs) { + console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + process.exit(1); + } + + setTimeout(poll, 2000); +} + +poll(); +NODE +fi + +checkpoint "React Native NativeScript demo app is ready at $APP_DIR" 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, diff --git a/scripts/test_react_native_turbomodule.sh b/scripts/test_react_native_turbomodule.sh new file mode 100755 index 00000000..1bb7c173 --- /dev/null +++ b/scripts/test_react_native_turbomodule.sh @@ -0,0 +1,229 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +RN_VERSION=${RN_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_CLI_VERSION:-20.1.3} +APP_NAME=${RN_SMOKE_APP_NAME:-NativeScriptNativeApiSmoke} +APP_ROOT=${RN_SMOKE_APP_ROOT:-"$REPO_ROOT/build/react-native-smoke"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_SMOKE_FORCE_RECREATE:-1} +BUILD_TIMEOUT_SECONDS=${RN_SMOKE_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_SMOKE_LAUNCH_TIMEOUT_SECONDS:-90} +MARKER="NATIVESCRIPT_RN_TURBO_SMOKE_PASS" +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" + +checkpoint "Building @nativescript/react-native TurboModule tarball..." +"$SCRIPT_DIR/build_react_native_turbomodule.sh" +TARBALL=$(ls -t "$REPO_ROOT/packages/react-native/dist"/*.tgz | head -n 1) + +if [[ "$FORCE_RECREATE" == "1" ]]; then + rm -rf "$APP_DIR" +fi + +if [[ ! -d "$APP_DIR" ]]; then + checkpoint "Creating React Native smoke app ($RN_VERSION)..." + mkdir -p "$APP_ROOT" + npx --yes "@react-native-community/cli@$RN_CLI_VERSION" init "$APP_NAME" \ + --version "$RN_VERSION" \ + --directory "$APP_DIR" \ + --skip-git-init \ + --install-pods false \ + --pm npm +fi + +checkpoint "Installing local TurboModule tarball into smoke app..." +( + cd "$APP_DIR" + npm install "$TARBALL" +) + +checkpoint "Writing smoke app entrypoint..." +node - "$APP_DIR/App.tsx" <<'NODE' +const fs = require('fs'); +const target = process.argv[2]; + +fs.writeFileSync(target, `import React from 'react'; +import {useEffect, useState} from 'react'; +import {SafeAreaView, Text} from 'react-native'; +import NativeScript from '@nativescript/react-native'; + +const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; + +async function runSmoke(): Promise { + try { + const installed = NativeScript.init(); + const api = (globalThis as any).__nativeScriptNativeApi; + if (!installed || !api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + + const nsObject = NSObject; + if (!nsObject || typeof nsObject.alloc !== 'function') { + throw new Error('NSObject global install failed'); + } + + if (NSURLErrorTimedOut !== -1001) { + throw new Error('constant global install failed'); + } + + if (NSComparisonResult.Same !== 0 || UIUserInterfaceStyle.Dark !== 2) { + throw new Error('enum global install failed'); + } + + let nativeCallsRanOnMainThread = false; + await NativeScript.runOnUI(() => { + nativeCallsRanOnMainThread = NSThread?.isMainThread === true; + if (!nativeCallsRanOnMainThread) { + throw new Error('runOnUI did not dispatch native calls to the main thread'); + } + }); + + const summary = { + installed, + nativeCallsRanOnMainThread, + runtime: api.runtime, + backend: api.backend, + classes: api.metadata?.classes ?? 0, + constants: api.metadata?.constants ?? 0, + enums: api.metadata?.enums ?? 0, + constant: NSURLErrorTimedOut, + enumValue: UIUserInterfaceStyle.Dark, + metadataPath: NativeScript.defaultMetadataPath(), + turboBackend: NativeScript.getRuntimeBackend(), + }; + + console.log(marker + ' ' + JSON.stringify(summary)); + return JSON.stringify(summary, null, 2); + } catch (error) { + console.error('NATIVESCRIPT_RN_TURBO_SMOKE_FAIL', error); + throw error; + } +} + +export default function App(): React.JSX.Element { + const [result, setResult] = useState('Running NativeScript TurboModule smoke test...'); + + useEffect(() => { + runSmoke() + .then(setResult) + .catch((error) => { + setResult(error instanceof Error ? error.message : String(error)); + }); + }, []); + + return ( + + {result} + + ); +} +`); +NODE + +checkpoint "Installing CocoaPods for smoke app..." +( + cd "$APP_DIR/ios" + if [[ -f Gemfile ]]; then + bundle install + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 bundle exec pod install + else + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + fi +) + +UDID=$(node <<'NODE' +const cp = require('child_process'); +const devices = JSON.parse(cp.execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], {encoding: 'utf8'})); +const runtimes = Object.keys(devices.devices).filter((runtime) => runtime.includes('iOS')).sort().reverse(); +for (const runtime of runtimes) { + const booted = devices.devices[runtime].find((device) => device.state === 'Booted' && device.name.includes('iPhone')); + if (booted) { + console.log(booted.udid); + process.exit(0); + } +} +for (const runtime of runtimes) { + const candidate = devices.devices[runtime].find((device) => device.name.includes('iPhone')); + if (candidate) { + console.log(candidate.udid); + process.exit(0); + } +} +process.exit(1); +NODE +) + +if [[ -z "$UDID" ]]; then + echo "No available iOS simulator found." >&2 + exit 1 +fi + +checkpoint "Building smoke app for simulator..." +xcrun simctl boot "$UDID" >/dev/null 2>&1 || true +xcrun simctl bootstatus "$UDID" -b + +xcodebuild \ + -workspace "$APP_DIR/ios/$APP_NAME.xcworkspace" \ + -scheme "$APP_NAME" \ + -configuration "$CONFIGURATION" \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,id=$UDID" \ + -derivedDataPath "$APP_DIR/ios/build/DerivedData" \ + build | tee "$APP_ROOT/xcodebuild.log" & +BUILD_PID=$! + +SECONDS_WAITED=0 +while kill -0 "$BUILD_PID" >/dev/null 2>&1; do + if [[ "$SECONDS_WAITED" -ge "$BUILD_TIMEOUT_SECONDS" ]]; then + kill "$BUILD_PID" >/dev/null 2>&1 || true + echo "Smoke app build timed out after ${BUILD_TIMEOUT_SECONDS}s." >&2 + exit 1 + fi + sleep 5 + SECONDS_WAITED=$((SECONDS_WAITED + 5)) +done +wait "$BUILD_PID" + +APP_BUNDLE=$(find "$APP_DIR/ios/build/DerivedData/Build/Products/$CONFIGURATION-iphonesimulator" -maxdepth 1 -name "$APP_NAME.app" -print -quit) +if [[ -z "$APP_BUNDLE" ]]; then + echo "Built app bundle not found." >&2 + exit 1 +fi + +checkpoint "Launching smoke app and waiting for TurboModule marker..." +xcrun simctl install "$UDID" "$APP_BUNDLE" +DATA_CONTAINER=$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data) +MARKER_FILE="$DATA_CONTAINER/tmp/$MARKER_FILE_NAME" +rm -f "$MARKER_FILE" + +SIMCTL_CHILD_NATIVESCRIPT_RN_TURBO_SMOKE_MARKER=1 \ + xcrun simctl launch --terminate-running-process "$UDID" "$BUNDLE_ID" + +node - "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" <<'NODE' +const fs = require('fs'); +const [markerFile, marker, timeoutSecondsText] = process.argv.slice(2); +const timeoutMs = Number(timeoutSecondsText) * 1000; +const startedAt = Date.now(); + +function poll() { + if (fs.existsSync(markerFile)) { + const content = fs.readFileSync(markerFile, 'utf8'); + console.log(`${marker} ${JSON.stringify({markerFile, content: content.trim()})}`); + process.exit(0); + } + + if (Date.now() - startedAt > timeoutMs) { + console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + process.exit(1); + } + + setTimeout(poll, 2000); +} + +poll(); +NODE + +checkpoint "React Native NativeScript TurboModule smoke test passed." diff --git a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h index 708ee3cd..2e770aeb 100644 --- a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h +++ b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h @@ -3,5 +3,6 @@ long long (*functionWhichReturnsSimpleFunctionPointer())(long long); void functionWithSimpleFunctionPointer(int (*f)(int)); +int functionWithSimpleFunctionPointerOnBackground(int (*f)(int)); void functionWithComplexFunctionPointer(TNSNestedStruct (*f)(char p1, short p2, int p3, long p4, long long p5, unsigned char p6, unsigned short p7, unsigned int p8, unsigned long p9, unsigned long long p10, float p11, double p12, SEL p13, Class p14, Protocol* p15, NSObject* p16, TNSNestedStruct p17)); void* functionReturningFunctionPtrAsVoidPtr(); diff --git a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m index 9b4149b2..41cea1f4 100644 --- a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m +++ b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m @@ -13,6 +13,17 @@ void functionWithSimpleFunctionPointer(int (*f)(int)) { TNSLog([NSString stringWithFormat:@"%d", result]); } +int functionWithSimpleFunctionPointerOnBackground(int (*f)(int)) { + __block int result = -1; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + result = f([NSThread isMainThread] ? 1 : 0); + dispatch_semaphore_signal(semaphore); + }); + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + return result; +} + void functionWithComplexFunctionPointer(TNSNestedStruct (*f)(char p1, short p2, int p3, long p4, long long p5, unsigned char p6, unsigned short p7, unsigned int p8, unsigned long p9, unsigned long long p10, float p11, double p12, SEL p13, Class p14, Protocol* p15, NSObject* p16, TNSNestedStruct p17)) { static NSObject* object = nil; diff --git a/test/runtime/fixtures/exported-symbols.txt b/test/runtime/fixtures/exported-symbols.txt index c506cba1..83e6c908 100644 --- a/test/runtime/fixtures/exported-symbols.txt +++ b/test/runtime/fixtures/exported-symbols.txt @@ -43,6 +43,7 @@ _functionWithSelector _functionWithShort _functionWithShortPtr _functionWithSimpleFunctionPointer +_functionWithSimpleFunctionPointerOnBackground _functionWithStructPtr _functionWithUChar _functionWithUCharPtr diff --git a/test/runtime/runner/app/tests/ApiTests.js b/test/runtime/runner/app/tests/ApiTests.js index f5ca4fe1..a0c289c3 100644 --- a/test/runtime/runner/app/tests/ApiTests.js +++ b/test/runtime/runner/app/tests/ApiTests.js @@ -883,13 +883,20 @@ describe(module.id, function () { // expect(stack).toBe(expectedStack); // }); - // it("should allow calling callbacks from another thread", function () { - // var result = TNSTestNativeCallbacks.callOnThread(function() { - // return 'method called'; - // }); + it("should invoke block callbacks on the native caller thread", function () { + if (!global.process || + !global.process.versions || + global.process.versions.engine !== "hermes") { + pending("Same-thread callback dispatch currently requires the Hermes thread-safe runtime."); + } - // expect(result).toBe('method called'); - // }); + var jsThreadHash = String(NSThread.currentThread.hash); + var callbackThreadHash = TNSTestNativeCallbacks.callOnThread(function() { + return String(NSThread.currentThread.hash); + }); + + expect(callbackThreadHash).not.toBe(jsThreadHash); + }); it("Unimplemented properties from UIBarItem class should be provided by the inheritors", function () { if (hasGlobalSymbol("UIBarButtonItem") && hasGlobalSymbol("UITabBarItem")) { diff --git a/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js b/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js index 392f72e8..691a2cf4 100644 --- a/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js +++ b/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js @@ -20,6 +20,21 @@ describe(module.id, function () { expect(TNSGetOutput()).toBe('4'); }); + it("SimpleFunctionPointerCallbackThread", function () { + if (!global.process || + !global.process.versions || + global.process.versions.engine !== "hermes") { + pending("Same-thread callback dispatch currently requires the Hermes thread-safe runtime."); + } + + var result = functionWithSimpleFunctionPointerOnBackground(function (nativeCallerWasMainThread) { + expect(nativeCallerWasMainThread).toBe(0); + return NSThread.isMainThread ? 1 : 0; + }); + + expect(result).toBe(0); + }); + it("SimpleFunctionPointerParameter", function () { var func = functionReturningFunctionPtrAsVoidPtr(); functionWithSimpleFunctionPointer(func); diff --git a/test/runtime/runner/app/tests/NativeApiJsiTests.js b/test/runtime/runner/app/tests/NativeApiJsiTests.js new file mode 100644 index 00000000..1f17f704 --- /dev/null +++ b/test/runtime/runner/app/tests/NativeApiJsiTests.js @@ -0,0 +1,40 @@ +describe("Native API JSI bridge", function () { + function apiOrPending() { + var api = global.__nativeScriptNativeApi; + if (!api) { + pending("Native API JSI bridge is only installed for Hermes."); + } + return api; + } + + afterEach(function () { + TNSClearOutput(); + }); + + it("exposes the Hermes JSI host object", function () { + var api = apiOrPending(); + + expect(api.runtime).toBe("jsi"); + expect(api.backend).toBe("hermes"); + expect(api.metadata.classes).toBeGreaterThan(0); + expect(api.metadata.functions).toBeGreaterThan(0); + expect(api.getClass("NSObject").available).toBe(true); + }); + + it("calls metadata-backed C functions through pure JSI", function () { + var api = apiOrPending(); + var fn = api.getFunction("functionWithInt"); + + expect(typeof fn).toBe("function"); + expect(fn(42)).toBe(42); + expect(TNSGetOutput()).toBe("42"); + }); + + it("sends Objective-C selectors through pure JSI", function () { + var api = apiOrPending(); + var primitives = api.getClass("TNSPrimitives").alloc().invoke("init"); + + expect(primitives.methodWithInt(24)).toBe(24); + expect(TNSGetOutput()).toBe("24"); + }); +}); diff --git a/test/runtime/runner/app/tests/index.js b/test/runtime/runner/app/tests/index.js index be0e4671..c9ecc04e 100644 --- a/test/runtime/runner/app/tests/index.js +++ b/test/runtime/runner/app/tests/index.js @@ -241,6 +241,7 @@ loadTest("./Promises"); loadTest("./Modules"); // loadTest("./RuntimeImplementedAPIs"); +loadTest("./NativeApiJsiTests"); loadTest("./WebNodeBuiltins"); loadTest("./VMTests");