From 73e85c0d44c67e62c29a743694b8c2e35d1223eb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 20 May 2026 23:27:32 -0400 Subject: [PATCH 01/31] perf(ios): add v8 native api dispatch fast path --- NativeScript/CMakeLists.txt | 49 + NativeScript/ffi/Block.h | 5 + NativeScript/ffi/Block.mm | 62 +- NativeScript/ffi/CFunction.h | 5 + NativeScript/ffi/CFunction.mm | 5 + NativeScript/ffi/ClassMember.h | 6 +- NativeScript/ffi/ClassMember.mm | 20 +- NativeScript/ffi/Interop.mm | 18 +- NativeScript/ffi/ObjCBridge.h | 8 +- NativeScript/ffi/ObjCBridge.mm | 5 + NativeScript/ffi/Object.h | 1 + NativeScript/ffi/Object.mm | 91 +- NativeScript/ffi/SignatureDispatch.h | 109 +- NativeScript/ffi/TypeConv.h | 16 + NativeScript/ffi/TypeConv.mm | 53 +- NativeScript/ffi/V8FastNativeApi.h | 22 + NativeScript/ffi/V8FastNativeApi.mm | 2522 +++++++++++++++++ NativeScript/napi/v8/v8-api.cpp | 6 + benchmarks/objc-dispatch/README.md | 42 + .../objc-dispatch/objc-dispatch-benchmarks.js | 215 ++ benchmarks/objc-dispatch/run.js | 917 ++++++ .../src/SignatureDispatchEmitter.cpp | 666 ++++- package.json | 3 +- scripts/build_all_ios.sh | 2 +- scripts/build_nativescript.sh | 19 +- 25 files changed, 4800 insertions(+), 67 deletions(-) create mode 100644 NativeScript/ffi/V8FastNativeApi.h create mode 100644 NativeScript/ffi/V8FastNativeApi.mm create mode 100644 benchmarks/objc-dispatch/README.md create mode 100644 benchmarks/objc-dispatch/objc-dispatch-benchmarks.js create mode 100644 benchmarks/objc-dispatch/run.js diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 418e6cab..788aa085 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, 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 napi none) if (BUILD_MACOS_NODE_API) set(BUILD_FRAMEWORK OFF) @@ -132,6 +134,26 @@ 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") + else() + set(NS_EFFECTIVE_GSD_BACKEND "napi") + endif() +elseif(NS_GSD_BACKEND STREQUAL "v8" 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() + +message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND})") + # Set up sources include_directories( ./ @@ -207,6 +229,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) @@ -345,6 +368,32 @@ 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_NAPI=0) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi") + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_NAPI=1) +else() + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=0 NS_GSD_BACKEND_NAPI=0) +endif() + set(FRAMEWORK_VERSION_VALUE "${VERSION}") if(TARGET_PLATFORM_MACOS) # macOS framework consumers (including Xcode's copy/sign phases) expect diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/Block.h index 97097831..4cd81962 100644 --- a/NativeScript/ffi/Block.h +++ b/NativeScript/ffi/Block.h @@ -1,6 +1,7 @@ #ifndef BLOCK_H #define BLOCK_H +#include #include #include "Cif.h" @@ -15,6 +16,10 @@ class FunctionPointer { metagen::MDSectionOffset offset; Cif* cif; bool ownsCif = false; + bool dispatchLookupCached = false; + uint64_t dispatchLookupSignatureHash = 0; + uint64_t dispatchId = 0; + void* preparedInvoker = nullptr; static napi_value wrap(napi_env env, void* function, metagen::MDSectionOffset offset, bool isBlock); diff --git a/NativeScript/ffi/Block.mm b/NativeScript/ffi/Block.mm index 6d60da6e..612a7e50 100644 --- a/NativeScript/ffi/Block.mm +++ b/NativeScript/ffi/Block.mm @@ -7,6 +7,7 @@ #include #include "Interop.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -65,8 +66,32 @@ inline bool removeCachedBlockJsFunctionEntry(void* blockPtr, BlockJsFunctionEntr } inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) { - nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, - entry.bridgeStateToken, entry.ref); + nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, entry.bridgeStateToken, + entry.ref); +} + +inline nativescript::BlockPreparedInvoker ensureFunctionPointerPreparedInvoker( + nativescript::FunctionPointer* ref, nativescript::SignatureCallKind kind) { + if (ref == nullptr || ref->cif == nullptr || ref->cif->signatureHash == 0) { + return nullptr; + } + + if (!ref->dispatchLookupCached || + ref->dispatchLookupSignatureHash != ref->cif->signatureHash) { + ref->dispatchLookupSignatureHash = ref->cif->signatureHash; + ref->dispatchId = + nativescript::composeSignatureDispatchId(ref->cif->signatureHash, kind, 0); + if (kind == nativescript::SignatureCallKind::BlockInvoke) { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupBlockPreparedInvoker(ref->dispatchId)); + } else { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupCFunctionPreparedInvoker(ref->dispatchId)); + } + ref->dispatchLookupCached = true; + } + + return reinterpret_cast(ref->preparedInvoker); } void block_copy(void* dest, void* src) { @@ -144,8 +169,7 @@ inline void cacheBlockJsFunction(napi_env env, void* blockPtr, napi_value jsFunc entry.ref = nativescript::make_ref(env, jsFunction, 0); entry.env = env; entry.bridgeState = nativescript::ObjCBridgeState::InstanceData(env); - entry.bridgeStateToken = - entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; + entry.bridgeStateToken = entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; entry.jsThreadId = closure != nullptr ? closure->jsThreadId : std::this_thread::get_id(); entry.jsRunLoop = closure != nullptr ? closure->jsRunLoop : CFRunLoopGetCurrent(); g_blockToJsFunction[blockPtr] = entry; @@ -174,7 +198,7 @@ void block_finalize_now(napi_env env, void* data, void* hint) { free(block); } - + void finalizeFunctionPointerNow(napi_env env, void* finalize_data, void* finalize_hint) { auto ref = static_cast(finalize_data); if (ref == nullptr) { @@ -259,14 +283,23 @@ bool isObjCBlockObject(id obj) { return false; } + static thread_local std::unordered_map blockClassCache; + auto cached = blockClassCache.find(cls); + if (cached != blockClassCache.end()) { + return cached->second; + } + const char* className = class_getName(cls); if (className == nullptr) { + blockClassCache.emplace(cls, false); return false; } // Runtime block classes are typically internal names like // __NSGlobalBlock__, __NSMallocBlock__, __NSStackBlock__. - return className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + bool isBlock = className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + blockClassCache.emplace(cls, isBlock); + return isBlock; } const char* getObjCBlockSignature(void* blockPtr) { @@ -444,7 +477,13 @@ bool isObjCBlockObject(id obj) { } } - ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + auto preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::CFunction); + if (preparedInvoker != nullptr) { + preparedInvoker(ref->function, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + } if (shouldFreeAny) { for (unsigned int i = 0; i < cif->argc; i++) { @@ -484,7 +523,14 @@ bool isObjCBlockObject(id obj) { } } - ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + BlockPreparedInvoker preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::BlockInvoke); + + if (preparedInvoker != nullptr) { + preparedInvoker(block->invoke, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + } if (shouldFreeAny) { for (unsigned int i = 0; i < cif->argc; i++) { diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/CFunction.h index d0003f12..beaab398 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -2,10 +2,13 @@ #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); @@ -14,6 +17,7 @@ class CFunction { ~CFunction(); void* fnptr; + ObjCBridgeState* bridgeState = nullptr; Cif* cif = nullptr; uint8_t dispatchFlags = 0; bool dispatchLookupCached = false; @@ -21,6 +25,7 @@ class CFunction { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* v8Invoker = nullptr; }; } // namespace nativescript diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 3f3a21f7..e91c312d 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -189,6 +189,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->dispatchId = 0; function->preparedInvoker = nullptr; function->napiInvoker = nullptr; + function->v8Invoker = nullptr; } return; } @@ -204,6 +205,9 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->preparedInvoker = reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); function->napiInvoker = reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); +#ifdef TARGET_ENGINE_V8 + function->v8Invoker = reinterpret_cast(lookupCFunctionV8Invoker(function->dispatchId)); +#endif function->dispatchLookupCached = true; } @@ -241,6 +245,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { MDFunctionFlag functionFlags = metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); auto cFunction = new CFunction(dlsym(self_dl, metadata->getString(offset))); + cFunction->bridgeState = this; cFunction->cif = getCFunctionCif(env, sigOffset); cFunction->dispatchFlags = (functionFlags & mdFunctionReturnOwned) != 0 ? 1 : 0; cFunctionCache[offset] = cFunction; diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/ClassMember.h index aea44b72..fb507de3 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -33,6 +33,9 @@ class MethodDescriptor { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* v8Invoker = nullptr; + bool nserrorOutSignatureCached = false; + bool nserrorOutSignature = false; MethodDescriptor() {} @@ -62,7 +65,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; } diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index 59f9f6a2..b88bc5cd 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -47,7 +47,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; @@ -342,6 +343,9 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); descriptor->napiInvoker = reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif descriptor->dispatchLookupCached = true; } } @@ -414,6 +418,10 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); descriptor->napiInvoker = reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = + reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif descriptor->dispatchLookupCached = true; } @@ -598,7 +606,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 +969,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; } @@ -1654,7 +1661,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 +1697,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; } diff --git a/NativeScript/ffi/Interop.mm b/NativeScript/ffi/Interop.mm index 6e5ca5ef..c558a85d 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; + } } } } diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index d2e81b2a..70c02b17 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -182,10 +182,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; } } @@ -734,10 +734,10 @@ class ObjCBridgeState { } uintptr_t objectKey = NormalizeHandleKey((void*)object); - Class objectClass = object_getClass(object); + uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (const auto& entry : objectRefs) { if (NormalizeHandleKey((void*)entry.first) != objectKey || - object_getClass(entry.first) != objectClass) { + NormalizeHandleKey((void*)object_getClass(entry.first)) != objectClassKey) { continue; } diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index bb304ea7..f1a21d5c 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -949,6 +949,9 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_value ObjCBridgeState::proxyNativeObject(napi_env env, napi_value object, id nativeObject) { NAPI_PREAMBLE +#ifdef TARGET_ENGINE_V8 + napi_value result = object; +#else napi_value factory = get_ref_value(env, createNativeProxy); napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); napi_value result, global; @@ -956,6 +959,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_get_boolean(env, [nativeObject isKindOfClass:NSArray.class], &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 +982,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); + cacheHandleObject(env, nativeObject, result); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/Object.h b/NativeScript/ffi/Object.h index 2290468f..3174bb91 100644 --- a/NativeScript/ffi/Object.h +++ b/NativeScript/ffi/Object.h @@ -7,6 +7,7 @@ namespace nativescript { void initProxyFactory(napi_env env, ObjCBridgeState* bridgeState); void attachObjectLifecycleAssociation(napi_env env, id object); +void transferOwnershipToNative(napi_env env, napi_value value, id object); } // namespace nativescript diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/Object.mm index 10f043c9..79cbc585 100644 --- a/NativeScript/ffi/Object.mm +++ b/NativeScript/ffi/Object.mm @@ -3,6 +3,9 @@ #include "Interop.h" #include "JSObject.h" #include "ObjCBridge.h" +#ifdef TARGET_ENGINE_V8 +#include "V8FastNativeApi.h" +#endif #include "js_native_api.h" #include "node_api_util.h" @@ -128,6 +131,14 @@ napi_value JS_transferOwnershipToNative(napi_env env, napi_callback_info cbinfo) napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, id object, Class cls = nil); +void transferOwnershipToNative(napi_env env, napi_value value, id object) { + if (env == nullptr || value == nullptr || object == nil) { + return; + } + + [JSWrapperObjectAssociation transferOwnership:env of:value toNative:object]; +} + namespace { constexpr const char* kNativePointerProperty = "__ns_native_ptr"; @@ -186,14 +197,18 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return target.class().superclass(); } - if (name in target) { - const value = target[name]; + const value = target[name]; + if (value !== undefined || name in target) { if (typeof value === "function" && name !== "constructor") { + if (value.__ns_proxy_bound === true) { + return value; + } + + let wrapper; if ((name === "isKindOfClass" || name === "isMemberOfClass")) { - return function (cls, ...args) { + wrapper = function (cls, a1, a2, a3) { let resolvedClass = cls; - if (resolvedClass != null && - (typeof resolvedClass === "object" || typeof resolvedClass === "function")) { + if (resolvedClass != null && typeof resolvedClass === "object") { try { const runtimeName = typeof NSStringFromClass === "function" ? NSStringFromClass(resolvedClass) @@ -210,23 +225,37 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt } } - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, [resolvedClass, ...args]); - } finally { - value.__ns_bound_receiver = undefined; + switch (arguments.length) { + case 0: + case 1: + return value.call(target, resolvedClass); + case 2: + return value.call(target, resolvedClass, a1); + case 3: + return value.call(target, resolvedClass, a1, a2); + case 4: + return value.call(target, resolvedClass, a1, a2, a3); + default: { + const args = Array.prototype.slice.call(arguments); + args[0] = resolvedClass; + return Reflect.apply(value, target, args); + } } }; + } else { + wrapper = value.bind(target); } - return function (...args) { - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, args); - } finally { - value.__ns_bound_receiver = undefined; - } - }; + Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; } return value; } @@ -371,10 +400,18 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { return nullptr; } +#ifdef TARGET_ENGINE_V8 + result = CreateV8NativeWrapperObject(env); + if (result == nullptr) { + napi_throw_error(env, "NativeScriptException", "Unable to create V8 native wrapper object."); + return nullptr; + } +#else NAPI_GUARD(napi_create_object(env, &result)) { NAPI_THROW_LAST_ERROR return nullptr; } +#endif napi_value global; napi_value objectCtor; @@ -443,6 +480,24 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { NormalizeHandleKey(wrapped) == NormalizeHandleKey((void*)obj)) { return handleCached; } + + bool hasNativePointer = false; + if (napi_has_named_property(env, handleCached, kNativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, handleCached, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && + NormalizeHandleKey(pointer->data) == NormalizeHandleKey((void*)obj)) { + return handleCached; + } + } + } + + return handleCached; } if (napi_value existing = getNormalizedObjectRef(env, obj); existing != nullptr) { diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 14367c8b..82596c6c 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -11,23 +11,39 @@ #include "Cif.h" #include "js_native_api.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif namespace nativescript { enum class SignatureCallKind : uint8_t { ObjCMethod = 1, CFunction = 2, + BlockInvoke = 3, }; using ObjCPreparedInvoker = void (*)(void* fnptr, void** avalues, void* rvalue); using CFunctionPreparedInvoker = void (*)(void* fnptr, void** avalues, void* rvalue); +using BlockPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); using ObjCNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, SEL selector, const napi_value* argv, void* rvalue); using CFunctionNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, const napi_value* argv, void* rvalue); +#ifdef TARGET_ENGINE_V8 +using ObjCV8Invoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, + SEL selector, + const v8::FunctionCallbackInfo& info, + void* rvalue); +using CFunctionV8Invoker = + bool (*)(napi_env env, Cif* cif, void* fnptr, + const v8::FunctionCallbackInfo& info, void* rvalue); +#endif + struct ObjCDispatchEntry { uint64_t dispatchId; ObjCPreparedInvoker invoker; @@ -38,6 +54,11 @@ struct CFunctionDispatchEntry { CFunctionPreparedInvoker invoker; }; +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + struct ObjCNapiDispatchEntry { uint64_t dispatchId; ObjCNapiInvoker invoker; @@ -48,6 +69,18 @@ struct CFunctionNapiDispatchEntry { CFunctionNapiInvoker invoker; }; +#ifdef TARGET_ENGINE_V8 +struct ObjCV8DispatchEntry { + uint64_t dispatchId; + ObjCV8Invoker invoker; +}; + +struct CFunctionV8DispatchEntry { + uint64_t dispatchId; + CFunctionV8Invoker invoker; +}; +#endif + inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; @@ -71,8 +104,37 @@ 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); +} +#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_NAPI +#if NS_GSD_BACKEND_V8 +#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 + #ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH #define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 #endif @@ -81,6 +143,10 @@ 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 + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" @@ -93,6 +159,8 @@ inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { {0, nullptr}}; inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; } // namespace nativescript #endif @@ -105,6 +173,15 @@ inline constexpr CFunctionNapiDispatchEntry } // namespace nativescript #endif +#if defined(TARGET_ENGINE_V8) && !NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH +namespace nativescript { +inline constexpr ObjCV8DispatchEntry kGeneratedObjCV8DispatchEntries[] = { + {0, nullptr}}; +inline constexpr CFunctionV8DispatchEntry + kGeneratedCFunctionV8DispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + namespace nativescript { template @@ -156,10 +233,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 +258,29 @@ inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { if (!isGeneratedDispatchEnabled()) { return nullptr; } - return lookupDispatchInvoker( + return lookupDispatchInvoker( kGeneratedCFunctionNapiDispatchEntries, dispatchId); } +#ifdef TARGET_ENGINE_V8 +inline ObjCV8Invoker lookupObjCV8Invoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCV8DispatchEntries, dispatchId); +} + +inline CFunctionV8Invoker lookupCFunctionV8Invoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionV8DispatchEntries, dispatchId); +} +#endif + } // namespace nativescript #endif // NS_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index 4e6dfcee..dee44ae3 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -7,6 +7,9 @@ #include "ffi.h" #include "js_native_api.h" #include "objc/runtime.h" +#ifdef TARGET_ENGINE_V8 +#include +#endif using namespace metagen; @@ -56,6 +59,19 @@ 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 + // 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..cc50987a 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/TypeConv.mm @@ -215,13 +215,22 @@ static id resolveCachedHandleObject(napi_env env, void* handle) { if (napi_has_named_property(env, cachedValue, "__ns_native_ptr", &hasNativePointer) == napi_ok && hasNativePointer) { napi_value nativePointerValue = nullptr; - void* nativePointer = nullptr; if (napi_get_named_property(env, cachedValue, "__ns_native_ptr", &nativePointerValue) == - napi_ok && - napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && - nativePointer != nullptr) { - bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); - return static_cast(nativePointer); + napi_ok) { + if (nativescript::Pointer::isInstance(env, nativePointerValue)) { + nativescript::Pointer* pointer = nativescript::Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(pointer->data), cachedValue); + return static_cast(pointer->data); + } + } else { + void* nativePointer = nullptr; + if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && + nativePointer != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); + return static_cast(nativePointer); + } + } } } @@ -1272,6 +1281,21 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, void* wrapped = nullptr; napi_status unwrapStatus = napi_unwrap(env, input, &wrapped); if (unwrapStatus != napi_ok) { + bool hasNativePointer = false; + if (napi_has_named_property(env, input, "__ns_native_ptr", &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, input, "__ns_native_ptr", &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + *out = pointer->data; + return true; + } + } + } return false; } @@ -2130,15 +2154,18 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { return get_ref_value(env, proto->constructor); } } else { + const uintptr_t objPtr = reinterpret_cast((void*)obj); const uintptr_t objNormalized = normalizePtr((void*)obj); - for (const auto& entry : bridgeState->mdProtocolsByPointer) { - if (normalizePtr((void*)entry.first) != objNormalized) { - continue; - } + if (objNormalized != objPtr) { + for (const auto& entry : bridgeState->mdProtocolsByPointer) { + if (normalizePtr((void*)entry.first) != objNormalized) { + continue; + } - auto proto = bridgeState->getProtocol(env, entry.second); - if (proto != nullptr) { - return get_ref_value(env, proto->constructor); + auto proto = bridgeState->getProtocol(env, entry.second); + if (proto != nullptr) { + return get_ref_value(env, proto->constructor); + } } } } diff --git a/NativeScript/ffi/V8FastNativeApi.h b/NativeScript/ffi/V8FastNativeApi.h new file mode 100644 index 00000000..8175f24a --- /dev/null +++ b/NativeScript/ffi/V8FastNativeApi.h @@ -0,0 +1,22 @@ +#ifndef V8_FAST_NATIVE_API_H +#define V8_FAST_NATIVE_API_H + +#ifdef TARGET_ENGINE_V8 + +#include + +#include "js_native_api.h" + +namespace nativescript { + +napi_value CreateV8NativeWrapperObject(napi_env env); + +bool V8TryDefineFastNativeProperty(napi_env env, v8::Local object, + v8::Local propertyName, + const napi_property_descriptor* descriptor); + +} // namespace nativescript + +#endif // TARGET_ENGINE_V8 + +#endif // V8_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm new file mode 100644 index 00000000..9caf5aa1 --- /dev/null +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -0,0 +1,2522 @@ +#include "V8FastNativeApi.h" + +#ifdef TARGET_ENGINE_V8 + +#import +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "Object.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" +#include "ffi/NativeScriptException.h" +#include "v8-api.h" + +namespace nativescript { +namespace { + +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; +constexpr int kNativeWrapperReferenceField = 0; +constexpr int kNativeWrapperMarkerField = 1; +constexpr int kNativeWrapperFieldCount = 2; + +#if V8_MAJOR_VERSION >= 14 +#define NS_V8_INTERCEPTED v8::Intercepted +#define NS_V8_RETURN_YES return v8::Intercepted::kYes +#define NS_V8_RETURN_NO return v8::Intercepted::kNo +#define NS_V8_SETTER_INFO v8::PropertyCallbackInfo +#else +#define NS_V8_INTERCEPTED void +#define NS_V8_RETURN_YES return +#define NS_V8_RETURN_NO return +#define NS_V8_SETTER_INFO v8::PropertyCallbackInfo +#endif + +struct V8CFunctionBinding { + ObjCBridgeState* bridgeState = nullptr; + MDSectionOffset offset = 0; + CFunction* function = nullptr; +}; + +id tryReadWrappedReference(napi_env env, v8::Local object); +bool TryFastConvertV8FoundationObject(napi_env env, id value, v8::Local* result); +bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, + v8::Local* result); + +napi_env envFromHandlerData(v8::Local data) { + if (data.IsEmpty() || !data->IsExternal()) { + return nullptr; + } + + return static_cast(data.As()->Value()); +} + +void* nativeWrapperMarker() { + static uintptr_t marker; + return ▮ +} + +bool isV8NativeWrapperObject(v8::Local object) { + return !object.IsEmpty() && object->InternalFieldCount() > kNativeWrapperMarkerField && + object->GetAlignedPointerFromInternalField(kNativeWrapperMarkerField) == + nativeWrapperMarker(); +} + +void throwV8Error(v8::Isolate* isolate, const char* message) { + if (isolate == nullptr) { + return; + } + + v8::Local errorMessage; + if (!v8::String::NewFromUtf8(isolate, message != nullptr ? message : "", + v8::NewStringType::kNormal) + .ToLocal(&errorMessage)) { + return; + } + + isolate->ThrowException(v8::Exception::Error(errorMessage)); +} + +void throwNativeScriptExceptionToV8(napi_env env, v8::Isolate* isolate, + NativeScriptException& exception) { + if (env == nullptr || isolate == nullptr) { + return; + } + + napi_value error = nullptr; + exception.ReThrowToJS(env, &error); + if (error != nullptr) { + isolate->ThrowException(v8impl::V8LocalValueFromJsValue(error)); + return; + } + + throwV8Error(isolate, exception.Description().c_str()); +} + +thread_local bool isDefiningNativeWrapperProperty = false; + +class NativeWrapperPropertyDefinitionGuard { + public: + NativeWrapperPropertyDefinitionGuard() : previous_(isDefiningNativeWrapperProperty) { + isDefiningNativeWrapperProperty = true; + } + + ~NativeWrapperPropertyDefinitionGuard() { + isDefiningNativeWrapperProperty = previous_; + } + + private: + bool previous_; +}; + +bool definePlainValueProperty(v8::Local context, v8::Local object, + v8::Local property, v8::Local value) { + NativeWrapperPropertyDefinitionGuard guard; + return object->CreateDataProperty(context, property, value).FromMaybe(false); +} + +bool isInternalNativeProperty(v8::Isolate* isolate, v8::Local property) { + if (property.IsEmpty() || !property->IsString()) { + return true; + } + + v8::String::Utf8Value name(isolate, property); + if (*name == nullptr) { + return true; + } + + return strcmp(*name, "napi_external") == 0 || strcmp(*name, "napi_typetag") == 0 || + strcmp(*name, kNativePointerProperty) == 0; +} + +NS_V8_INTERCEPTED nativeWrapperNamedSetter(v8::Local property, + v8::Local value, + const NS_V8_SETTER_INFO& info) { + if (isDefiningNativeWrapperProperty) { + NS_V8_RETURN_NO; + } + + napi_env env = envFromHandlerData(info.Data()); + v8::Local holder = info.Holder(); + + if (env != nullptr && !isInternalNativeProperty(info.GetIsolate(), property)) { + id nativeObject = tryReadWrappedReference(env, holder); + if (nativeObject != nil) { + transferOwnershipToNative(env, v8impl::JsValueFromV8LocalValue(holder), nativeObject); + } + } + + definePlainValueProperty(info.GetIsolate()->GetCurrentContext(), holder, property, value); + NS_V8_RETURN_YES; +} + +NS_V8_INTERCEPTED nativeWrapperIndexedGetter( + uint32_t index, const v8::PropertyCallbackInfo& info) { + napi_env env = envFromHandlerData(info.Data()); + if (env == nullptr) { + NS_V8_RETURN_NO; + } + + id nativeObject = tryReadWrappedReference(env, info.Holder()); + if (nativeObject == nil || ![nativeObject isKindOfClass:[NSArray class]]) { + NS_V8_RETURN_NO; + } + + @try { + id value = reinterpret_cast(objc_msgSend)( + nativeObject, @selector(objectAtIndex:), static_cast(index)); + if (value == nil) { + info.GetReturnValue().Set(v8::Null(info.GetIsolate())); + NS_V8_RETURN_YES; + } + + v8::Local fastValue; + if (TryFastConvertV8FoundationObject(env, value, &fastValue)) { + info.GetReturnValue().Set(fastValue); + NS_V8_RETURN_YES; + } + + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + napi_value result = nullptr; + if (state != nullptr) { + result = state->findCachedObjectWrapper(env, value); + if (result == nullptr) { + result = state->getObject(env, value, kUnownedObject, 0, nullptr); + } + } + if (result != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + NS_V8_RETURN_YES; + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + NS_V8_RETURN_YES; + } + + NS_V8_RETURN_NO; +} + +NS_V8_INTERCEPTED nativeWrapperIndexedSetter(uint32_t index, v8::Local value, + const NS_V8_SETTER_INFO& info) { + napi_env env = envFromHandlerData(info.Data()); + if (env == nullptr) { + NS_V8_RETURN_NO; + } + + id nativeObject = tryReadWrappedReference(env, info.Holder()); + if (nativeObject == nil || + ![nativeObject respondsToSelector:@selector(setObject:atIndexedSubscript:)]) { + NS_V8_RETURN_NO; + } + + id nativeValue = nil; + if (!TryFastConvertV8Argument(env, mdTypeAnyObject, value, &nativeValue)) { + NS_V8_RETURN_NO; + } + + @try { + reinterpret_cast(objc_msgSend)( + nativeObject, @selector(setObject:atIndexedSubscript:), nativeValue, + static_cast(index)); + NS_V8_RETURN_YES; + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + NS_V8_RETURN_YES; + } +} + +v8::Local nativeWrapperObjectTemplate(napi_env env) { + v8::Isolate* isolate = env->isolate; + static thread_local v8::Persistent objectTemplate; + static thread_local v8::Isolate* templateIsolate = nullptr; + static thread_local napi_env templateEnv = nullptr; + + if (objectTemplate.IsEmpty() || templateIsolate != isolate || templateEnv != env) { + objectTemplate.Reset(); + templateIsolate = isolate; + templateEnv = env; + v8::Local created = v8::ObjectTemplate::New(isolate); + created->SetInternalFieldCount(kNativeWrapperFieldCount); + v8::Local envData = v8::External::New(isolate, env); + v8::PropertyHandlerFlags namedFlags = static_cast( + static_cast(v8::PropertyHandlerFlags::kNonMasking) | + static_cast(v8::PropertyHandlerFlags::kOnlyInterceptStrings)); + created->SetHandler(v8::NamedPropertyHandlerConfiguration( + nullptr, nativeWrapperNamedSetter, nullptr, nullptr, nullptr, envData, namedFlags)); + created->SetHandler(v8::IndexedPropertyHandlerConfiguration( + nativeWrapperIndexedGetter, nativeWrapperIndexedSetter, nullptr, nullptr, nullptr, + envData)); + objectTemplate.Reset(isolate, created); + } + + return v8::Local::New(isolate, objectTemplate); +} + +} // namespace + +napi_value CreateV8NativeWrapperObject(napi_env env) { + if (env == nullptr) { + return nullptr; + } + + v8::EscapableHandleScope scope(env->isolate); + v8::Local object; + if (!nativeWrapperObjectTemplate(env)->NewInstance(env->context()).ToLocal(&object)) { + return nullptr; + } + object->SetAlignedPointerInInternalField(kNativeWrapperMarkerField, nativeWrapperMarker()); + + return v8impl::JsValueFromV8LocalValue(scope.Escape(object)); +} + +namespace { + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +bool TryFastConvertV8SelectorArgument(napi_env env, v8::Local value, SEL* selector) { + if (env == nullptr || selector == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *selector = nullptr; + return true; + } + + if (!value->IsString()) { + return false; + } + + v8::Local string = value.As(); + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + char* buffer = stackBuffer; + size_t length = 0; + size_t capacity = 0; + + if (string->IsOneByte() || string->ContainsOnlyOneByte()) { + length = static_cast(string->Length()); + capacity = length + 1; + if (capacity > kStackCapacity) { + buffer = static_cast(malloc(capacity)); + if (buffer == nullptr) { + return false; + } + } + string->WriteOneByteV2(env->isolate, 0, static_cast(length), + reinterpret_cast(buffer), + v8::String::WriteFlags::kNullTerminate); + } else { + length = string->Utf8LengthV2(env->isolate); + capacity = length + 1; + if (capacity > kStackCapacity) { + buffer = static_cast(malloc(capacity)); + if (buffer == nullptr) { + return false; + } + } + + size_t written = + string->WriteUtf8V2(env->isolate, buffer, capacity, v8::String::WriteFlags::kNullTerminate); + if (written == 0) { + if (buffer != stackBuffer) { + free(buffer); + } + return false; + } + length = buffer[written - 1] == '\0' ? written - 1 : written; + } + + buffer[length] = '\0'; + *selector = cachedSelectorForName(buffer, length); + if (buffer != stackBuffer) { + free(buffer); + } + return true; +} + +id tryUnwrapV8NativeObject(napi_env env, v8::Local value); + +v8::Local nativePointerPropertyName(v8::Isolate* isolate) { + static thread_local v8::Persistent name; + static thread_local v8::Isolate* nameIsolate = nullptr; + + if (name.IsEmpty() || nameIsolate != isolate) { + name.Reset(); + nameIsolate = isolate; + name.Reset(isolate, v8::String::NewFromUtf8(isolate, kNativePointerProperty, + v8::NewStringType::kInternalized) + .ToLocalChecked()); + } + + return v8::Local::New(isolate, name); +} + +bool hasV8NativePointerProperty(napi_env env, v8::Local object) { + if (env == nullptr || object.IsEmpty()) { + return false; + } + + return object->HasOwnProperty(env->context(), nativePointerPropertyName(env->isolate)) + .FromMaybe(false); +} + +id resolveCachedHandleObject(napi_env env, void* handle) { + if (env == nullptr || handle == nullptr) { + return nil; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr) { + return nil; + } + + napi_value cachedValue = bridgeState->getCachedHandleObject(env, handle); + if (cachedValue == nullptr) { + return nil; + } + + void* wrapped = nullptr; + if (napi_unwrap(env, cachedValue, &wrapped) == napi_ok && wrapped != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(wrapped), cachedValue); + return static_cast(wrapped); + } + + bool hasNativePointer = false; + if (napi_has_named_property(env, cachedValue, kNativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, cachedValue, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(pointer->data), cachedValue); + return static_cast(pointer->data); + } + } + } + + return nil; +} + +bool TryFastUnwrapV8PointerLikeObjectArgument(napi_env env, v8::Local value, + id* result) { + if (env == nullptr || result == nullptr || value.IsEmpty() || !value->IsObject()) { + return false; + } + + napi_value jsValue = v8impl::JsValueFromV8LocalValue(value); + void* data = nullptr; + if (Pointer::isInstance(env, jsValue)) { + Pointer* pointer = Pointer::unwrap(env, jsValue); + data = pointer != nullptr ? pointer->data : nullptr; + } else if (Reference::isInstance(env, jsValue)) { + Reference* reference = Reference::unwrap(env, jsValue); + data = reference != nullptr ? reference->data : nullptr; + } else { + return false; + } + + if (id cachedObject = resolveCachedHandleObject(env, data); cachedObject != nil) { + *result = cachedObject; + } else { + *result = static_cast(data); + } + return true; +} + +bool TryFastUnwrapV8ObjectArgument(napi_env env, v8::Local value, id* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *result = nil; + return true; + } + + if (!value->IsObject()) { + return false; + } + + v8::Local object = value.As(); + if (isV8NativeWrapperObject(object)) { + id nativeObject = tryReadWrappedReference(env, object); + if (nativeObject != nil) { + *result = nativeObject; + return true; + } + } + + if (TryFastUnwrapV8PointerLikeObjectArgument(env, value, result)) { + return true; + } + + if (hasV8NativePointerProperty(env, object)) { + id nativeObject = tryUnwrapV8NativeObject(env, value); + if (nativeObject != nil) { + *result = nativeObject; + return true; + } + } + + return false; +} + +bool TryFastUnwrapV8ClassArgument(napi_env env, v8::Local value, Class* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsNullOrUndefined()) { + *result = Nil; + return true; + } + + if (!value->IsObject()) { + return false; + } + + id nativeObject = tryUnwrapV8NativeObject(env, value); + if (nativeObject == nil || !object_isClass(nativeObject)) { + return false; + } + + *result = (Class)nativeObject; + return true; +} + +} // namespace + +bool TryFastConvertV8UInt16Argument(napi_env env, v8::Local value, uint16_t* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsString()) { + v8::String::Value chars(env->isolate, value); + if (chars.length() != 1) { + throwV8Error(env->isolate, "Expected a single-character string."); + *result = 0; + return false; + } + + *result = static_cast((*chars)[0]); + return true; + } + + uint32_t converted = 0; + if (!value->Uint32Value(env->context()).To(&converted)) { + return false; + } + + *result = static_cast(converted); + return true; +} + +bool TryFastConvertV8Argument(napi_env env, MDTypeKind kind, v8::Local value, + void* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + switch (kind) { + case mdTypeChar: { + int32_t converted = 0; + if (!value->Int32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + uint32_t converted = 0; + if (!value->Uint32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeSShort: { + int32_t converted = 0; + if (!value->Int32Value(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeUShort: + return TryFastConvertV8UInt16Argument(env, value, reinterpret_cast(result)); + + case mdTypeSInt: + return value->Int32Value(env->context()).To(reinterpret_cast(result)); + + case mdTypeUInt: + return value->Uint32Value(env->context()).To(reinterpret_cast(result)); + + case mdTypeSLong: + case mdTypeSInt64: + if (value->IsBigInt()) { + bool lossless = false; + *reinterpret_cast(result) = value.As()->Int64Value(&lossless); + return true; + } + return value->IntegerValue(env->context()).To(reinterpret_cast(result)); + + case mdTypeULong: + case mdTypeUInt64: + if (value->IsBigInt()) { + bool lossless = false; + *reinterpret_cast(result) = + value.As()->Uint64Value(&lossless); + return true; + } else { + int64_t converted = 0; + if (!value->IntegerValue(env->context()).To(&converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeFloat: { + double converted = 0.0; + if (!value->NumberValue(env->context()).To(&converted)) { + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeDouble: { + double converted = 0.0; + if (!value->NumberValue(env->context()).To(&converted)) { + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = converted; + return true; + } + + case mdTypeBool: + if (!value->IsBoolean()) { + return false; + } + *reinterpret_cast(result) = + value->BooleanValue(env->isolate) ? static_cast(1) : static_cast(0); + return true; + + case mdTypeSelector: { + return TryFastConvertV8SelectorArgument(env, value, reinterpret_cast(result)); + } + + case mdTypeClass: + if (TryFastUnwrapV8ClassArgument(env, value, reinterpret_cast(result))) { + return true; + } + return TryFastConvertNapiArgument(env, kind, v8impl::JsValueFromV8LocalValue(value), result); + + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (TryFastUnwrapV8ObjectArgument(env, value, reinterpret_cast(result))) { + return true; + } + return TryFastConvertNapiArgument(env, kind, v8impl::JsValueFromV8LocalValue(value), result); + + default: + return false; + } +} + +bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, const void* value, + v8::Local* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + v8::Isolate* isolate = env->isolate; + switch (kind) { + case mdTypeVoid: + *result = v8::Undefined(isolate); + return true; + + case mdTypeBool: + *result = v8::Boolean::New(isolate, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUChar: + case mdTypeUInt8: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSShort: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSInt: + *result = v8::Integer::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + int64_t nativeValue = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (nativeValue > kMaxSafeInteger || nativeValue < -kMaxSafeInteger) { + *result = v8::BigInt::New(isolate, nativeValue); + } else { + *result = v8::Number::New(isolate, static_cast(nativeValue)); + } + return true; + } + + case mdTypeULong: + case mdTypeUInt64: { + uint64_t nativeValue = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (nativeValue > kMaxSafeInteger) { + *result = v8::BigInt::NewFromUnsigned(isolate, nativeValue); + } else { + *result = v8::Number::New(isolate, static_cast(nativeValue)); + } + return true; + } + + case mdTypeFloat: + *result = v8::Number::New(isolate, *reinterpret_cast(value)); + return true; + + case mdTypeDouble: + *result = v8::Number::New(isolate, *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +namespace { + +bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, + v8::Local* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + NSString* str = *reinterpret_cast(value); + v8::Isolate* isolate = env->isolate; + if (str == nil) { + *result = v8::Null(isolate); + return true; + } + + const NSUInteger length = [str length]; + if (length == 0) { + *result = v8::String::Empty(isolate); + return true; + } + + if (length > static_cast(std::numeric_limits::max())) { + return false; + } + + const UniChar* directChars = CFStringGetCharactersPtr((CFStringRef)str); + if (directChars != nullptr) { + v8::Local stringValue; + if (!v8::String::NewFromTwoByte( + isolate, reinterpret_cast(directChars), v8::NewStringType::kNormal, + static_cast(length)) + .ToLocal(&stringValue)) { + return false; + } + *result = stringValue; + return true; + } + + constexpr NSUInteger kStackCapacity = 256; + UniChar stackBuffer[kStackCapacity]; + UniChar* buffer = length <= kStackCapacity + ? stackBuffer + : static_cast(malloc(length * sizeof(UniChar))); + if (buffer == nullptr) { + return false; + } + + [str getCharacters:buffer range:NSMakeRange(0, length)]; + + v8::Local stringValue; + bool converted = v8::String::NewFromTwoByte( + isolate, reinterpret_cast(buffer), + v8::NewStringType::kNormal, static_cast(length)) + .ToLocal(&stringValue); + if (buffer != stackBuffer) { + free(buffer); + } + + if (!converted) { + return false; + } + + *result = stringValue; + return true; +} + +bool TryFastConvertV8FoundationObject(napi_env env, id value, v8::Local* result) { + if (env == nullptr || value == nil || result == nullptr) { + return false; + } + + v8::Isolate* isolate = env->isolate; + if ([value isKindOfClass:[NSNull class]]) { + *result = v8::Null(isolate); + return true; + } + + if ([value isKindOfClass:[NSNumber class]] && ![value isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)value) == CFBooleanGetTypeID()) { + *result = v8::Boolean::New(isolate, [value boolValue] == YES); + return true; + } + + *result = v8::Number::New(isolate, [value doubleValue]); + return true; + } + + if ([value isKindOfClass:[NSString class]]) { + NSString* str = (NSString*)value; + return TryFastConvertV8NSStringReturnValue(env, &str, result); + } + + return false; +} + +bool TryFastSetV8ObjectReturnValue(napi_env env, + const v8::FunctionCallbackInfo& info, + ObjCBridgeState* bridgeState, id value, + ObjectOwnership ownership) { + if (env == nullptr || bridgeState == nullptr) { + return false; + } + + v8::Isolate* isolate = info.GetIsolate(); + if (value == nil) { + info.GetReturnValue().Set(v8::Null(isolate)); + return true; + } + + v8::Local fastValue; + if (TryFastConvertV8FoundationObject(env, value, &fastValue)) { + info.GetReturnValue().Set(fastValue); + return true; + } + + napi_value cached = bridgeState->getCachedHandleObject(env, (void*)value); + if (cached == nullptr) { + cached = bridgeState->findCachedObjectWrapper(env, value); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return true; + } + + napi_value result = bridgeState->getObject(env, value, ownership, 0, nullptr); + if (result == nullptr) { + return false; + } + + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return true; +} + +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 typeKindMayUseRoundTripCache(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +inline bool cifMayUseRoundTripCache(Cif* cif) { + if (cif == nullptr) { + return false; + } + + return cif->returnType != nullptr && typeKindMayUseRoundTripCache(cif->returnType->kind); +} + +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; +} + +inline bool isV8DirectObjectKind(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +inline bool isV8DirectIntegerKind(MDTypeKind kind) { + switch (kind) { + case mdTypeChar: + case mdTypeSInt: + case mdTypeSShort: + case mdTypeSLong: + case mdTypeSInt64: + case mdTypeUChar: + case mdTypeUInt: + case mdTypeUShort: + case mdTypeULong: + case mdTypeUInt64: + case mdTypeUInt8: + return true; + default: + return false; + } +} + +bool tryConvertV8NSUIntegerArgument(napi_env env, v8::Local value, + NSUInteger* result) { + if (env == nullptr || result == nullptr || value.IsEmpty()) { + return false; + } + + if (value->IsBigInt()) { + bool lossless = false; + *result = static_cast(value.As()->Uint64Value(&lossless)); + return true; + } + + int64_t converted = 0; + if (!value->IntegerValue(env->context()).To(&converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool tryInvokeObjCV8DirectFastPath(napi_env env, + const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, + Cif* cif, id self) { + if (env == nullptr || descriptor == nullptr || cif == nullptr || self == nil || + cif->returnType == nullptr) { + return false; + } + + v8::Isolate* isolate = info.GetIsolate(); + const MDTypeKind returnKind = cif->returnType->kind; + + @try { + if (cif->argc == 0) { + switch (returnKind) { + case mdTypeBool: { + BOOL value = reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); + return true; + } + case mdTypeChar: { + int8_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUChar: + case mdTypeUInt8: { + uint8_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSShort: { + int16_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUShort: { + uint16_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSInt: { + int32_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUInt: { + uint32_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSLong: + case mdTypeSInt64: { + int64_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (value > kMaxSafeInteger || value < -kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::New(isolate, value)); + } else { + info.GetReturnValue().Set(v8::Number::New(isolate, static_cast(value))); + } + return true; + } + case mdTypeULong: + case mdTypeUInt64: { + uint64_t value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (value > kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, value)); + } else { + info.GetReturnValue().Set(v8::Number::New(isolate, static_cast(value))); + } + return true; + } + case mdTypeFloat: { + float value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + case mdTypeDouble: { + double value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + case mdTypeNSStringObject: { + NSString* value = + reinterpret_cast(objc_msgSend)(self, descriptor->selector); + v8::Local result; + if (!TryFastConvertV8NSStringReturnValue(env, &value, &result)) { + return false; + } + info.GetReturnValue().Set(result); + return true; + } + default: + break; + } + } + + if (returnKind == mdTypeBool && cif->argc == 1 && + static_cast(info.Length()) >= 1) { + const MDTypeKind argKind = cif->argTypes[0]->kind; + if (argKind == mdTypeSelector) { + SEL selector = nullptr; + if (!TryFastConvertV8SelectorArgument(env, info[0], &selector)) { + return false; + } + BOOL value = reinterpret_cast(objc_msgSend)( + self, descriptor->selector, selector); + info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); + return true; + } + + if (argKind == mdTypeClass || argKind == mdTypeClassObject) { + Class cls = Nil; + if (!TryFastUnwrapV8ClassArgument(env, info[0], &cls)) { + return false; + } + BOOL value = reinterpret_cast(objc_msgSend)( + self, descriptor->selector, cls); + info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); + return true; + } + } + + if ((returnKind == mdTypeAnyObject || returnKind == mdTypeProtocolObject || + returnKind == mdTypeClassObject) && + cif->argc == 1 && static_cast(info.Length()) >= 1) { + const MDTypeKind argKind = cif->argTypes[0]->kind; + if (isV8DirectIntegerKind(argKind)) { + NSUInteger arg0 = 0; + if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { + return false; + } + id value = reinterpret_cast(objc_msgSend)( + self, descriptor->selector, arg0); + return TryFastSetV8ObjectReturnValue( + env, info, method != nullptr ? method->bridgeState : ObjCBridgeState::InstanceData(env), + value, method != nullptr && method->returnOwned ? kOwnedObject : kUnownedObject); + } + } + + if (returnKind == mdTypeVoid) { + if (cif->argc == 1 && static_cast(info.Length()) >= 1) { + const MDTypeKind argKind = cif->argTypes[0]->kind; + if (isV8DirectObjectKind(argKind)) { + id arg0 = nil; + if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0)) { + return false; + } + reinterpret_cast(objc_msgSend)(self, descriptor->selector, arg0); + info.GetReturnValue().Set(v8::Undefined(isolate)); + return true; + } + + if (isV8DirectIntegerKind(argKind)) { + NSUInteger arg0 = 0; + if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { + return false; + } + reinterpret_cast(objc_msgSend)( + self, descriptor->selector, arg0); + info.GetReturnValue().Set(v8::Undefined(isolate)); + return true; + } + } + + if (cif->argc == 2 && static_cast(info.Length()) >= 2) { + const MDTypeKind arg0Kind = cif->argTypes[0]->kind; + const MDTypeKind arg1Kind = cif->argTypes[1]->kind; + if (isV8DirectObjectKind(arg0Kind) && isV8DirectObjectKind(arg1Kind)) { + id arg0 = nil; + id arg1 = nil; + if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0) || + !TryFastUnwrapV8ObjectArgument(env, info[1], &arg1)) { + return false; + } + reinterpret_cast(objc_msgSend)( + self, descriptor->selector, arg0, arg1); + info.GetReturnValue().Set(v8::Undefined(isolate)); + return true; + } + + if (isV8DirectObjectKind(arg0Kind) && isV8DirectIntegerKind(arg1Kind)) { + id arg0 = nil; + NSUInteger arg1 = 0; + if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0) || + !tryConvertV8NSUIntegerArgument(env, info[1], &arg1)) { + return false; + } + reinterpret_cast(objc_msgSend)( + self, descriptor->selector, arg0, arg1); + info.GetReturnValue().Set(v8::Undefined(isolate)); + return true; + } + } + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return true; + } + + return false; +} + +bool invokeObjCPreparedOrFfi(napi_env env, Cif* cif, id self, bool classMethod, + MethodDescriptor* descriptor, uint8_t dispatchFlags, void** avalues, + void* rvalue) { + if (cif == nullptr || descriptor == nullptr) { + return false; + } + + Class receiverClass = classMethod ? (Class)self : object_getClass(self); + const bool supercall = receiverClassRequiresSuperCall(receiverClass); + if (supercall && classMethod) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + ClassBuilder* builder = + state != nullptr ? static_cast(state->classesByPointer[self]) : nullptr; + if (builder != nullptr && !builder->isFinal) { + builder->build(); + } + } + +#if defined(__x86_64__) + bool isStret = cif->returnType->type->size > 16 && cif->returnType->type->type == FFI_TYPE_STRUCT; +#endif + + @try { + if (!supercall) { + auto invoker = ensureObjCV8Invoker(cif, descriptor, dispatchFlags); + auto preparedInvoker = + descriptor != nullptr ? reinterpret_cast(descriptor->preparedInvoker) + : nullptr; + if (preparedInvoker != nullptr) { + preparedInvoker((void*)objc_msgSend, avalues, rvalue); + return true; + } + +#if defined(__x86_64__) + ffi_call(&cif->cif, isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend), rvalue, + avalues); +#else + ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); +#endif + (void)invoker; + } else { + Class superClass = classMethod ? class_getSuperclass(object_getClass((id)receiverClass)) + : class_getSuperclass(receiverClass); + struct objc_super superobj = {self, superClass}; + auto superobjPtr = &superobj; + avalues[0] = (void*)&superobjPtr; +#if defined(__x86_64__) + ffi_call(&cif->cif, isStret ? FFI_FN(objc_msgSendSuper_stret) : FFI_FN(objc_msgSendSuper), + rvalue, avalues); +#else + ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); +#endif + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + throwNativeScriptExceptionToV8(env, env != nullptr ? env->isolate : nullptr, + nativeScriptException); + return false; + } + + return true; +} + +void setObjCReturnValue(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, void* rvalue, bool propertyAccess) { + if (cif == nullptr || method == nullptr || descriptor == nullptr) { + return; + } + + if (cif->returnType->kind == mdTypeVoid) { + info.GetReturnValue().Set(v8::Undefined(info.GetIsolate())); + return; + } + + v8::Local fastResult; + if (TryFastConvertV8ReturnValue(env, cif->returnType->kind, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + if (cif->returnType->kind == mdTypeNSStringObject && + TryFastConvertV8NSStringReturnValue(env, rvalue, &fastResult)) { + info.GetReturnValue().Set(fastResult); + return; + } + + napi_value jsThis = v8impl::JsValueFromV8LocalValue(info.This()); + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(constructor)); + return; + } + + id classObject = receiverIsClass ? self : (id)object_getClass(self); + napi_value result = + method->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *((id*)rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, (void*)obj); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return; + } + } + } + napi_value result = method->bridgeState->getObject( + env, obj, constructor, method->returnOwned ? kOwnedObject : kUnownedObject); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + + if (cif->returnType->kind == mdTypeAnyObject && receiverIsClass) { + id obj = *((id*)rvalue); + Class receiverClass = (Class)self; + if (obj != nil && + (receiverClass == [NSString class] || receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (strcmp(selectorName, "string") == 0 || strcmp(selectorName, "stringWithString:") == 0 || + strcmp(selectorName, "stringWithCapacity:") == 0)) { + napi_value result = method->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); + return; + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeInstanceObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *((id*)rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, (void*)obj); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); + return; + } + } + } + } + + napi_value result = cif->returnType->toJS(env, rvalue, method->returnOwned ? kReturnOwned : 0); + info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(result)); +} + +bool invokeObjCSlow(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, bool propertyAccess) { + V8CifReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + throwV8Error(info.GetIsolate(), + "Unable to allocate return value storage for Objective-C call."); + return false; + } + + V8CifArgumentStorage argStorage(cif, 2); + if (!argStorage.valid()) { + throwV8Error(info.GetIsolate(), + "Unable to allocate argument storage for Objective-C call."); + return false; + } + + void* avalues[cif->cif.nargs]; + avalues[0] = (void*)&self; + avalues[1] = (void*)&descriptor->selector; + + const size_t actualArgc = static_cast(info.Length()); + const bool hasImplicitNSErrorOutArg = + !cif->isVariadic && isNSErrorOutMethodSignature(descriptor, cif) && + actualArgc + 1 == cif->argc; + NSError* implicitNSError = nil; + + bool shouldFreeAny = false; + bool shouldFree[cif->argc]; + v8::Local undefinedValue = v8::Undefined(info.GetIsolate()); + + for (unsigned int i = 0; i < cif->argc; i++) { + shouldFree[i] = false; + avalues[i + 2] = argStorage.at(i); + if (hasImplicitNSErrorOutArg && i == cif->argc - 1) { + NSError** implicitNSErrorOutArg = &implicitNSError; + *reinterpret_cast(avalues[i + 2]) = implicitNSErrorOutArg; + continue; + } + + v8::Local argValue = i < actualArgc ? info[i] : undefinedValue; + if (!TryFastConvertV8Argument(env, cif->argTypes[i]->kind, argValue, avalues[i + 2])) { + cif->argTypes[i]->toNative(env, v8impl::JsValueFromV8LocalValue(argValue), avalues[i + 2], + &shouldFree[i], &shouldFreeAny); + } + } + + void* rvalue = rvalueStorage.get(); + const bool didInvoke = invokeObjCPreparedOrFfi(env, cif, self, receiverIsClass, descriptor, + descriptor->dispatchFlags, avalues, rvalue); + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i + 2])); + } + } + } + + if (!didInvoke) { + return false; + } + + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessage + : "Unknown NSError"); + throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); + return false; + } + + setObjCReturnValue(env, info, method, descriptor, cif, self, receiverIsClass, rvalue, + propertyAccess); + return true; +} + +bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& info, + ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, + bool propertyAccess) { + if (env == nullptr || method == nullptr || descriptor == nullptr || cif == nullptr) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? (Class)self : object_getClass(self); + const bool requiresSuperCall = receiverClassRequiresSuperCall(receiverClass); + const size_t actualArgc = static_cast(info.Length()); + const bool isNSErrorOutMethod = isNSErrorOutMethodSignature(descriptor, cif); + if (!cif->isVariadic && isNSErrorOutMethod) { + if (actualArgc > cif->argc || actualArgc + 1 < cif->argc) { + throwArgumentsCountError(info.GetIsolate(), actualArgc, cif->argc); + return false; + } + } + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && !cif->isVariadic && actualArgc + 1 == cif->argc; + if (!isNSErrorOutMethod && !requiresSuperCall && + tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { + return true; + } + + const bool needsRoundTripCache = cifMayUseRoundTripCache(cif); + std::optional roundTripCacheFrame; + if (needsRoundTripCache) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + + std::optional rvalueStorage; + void* rvalue = nullptr; + if (cif->returnType == nullptr || cif->returnType->kind != mdTypeVoid) { + 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; + ObjCV8Invoker invoker = + requiresSuperCall ? nullptr : ensureObjCV8Invoker(cif, descriptor, descriptor->dispatchFlags); + if (!isNSErrorOutMethod && invoker != nullptr) { + @try { + didInvoke = invoker(env, cif, (void*)objc_msgSend, self, descriptor->selector, info, rvalue); + } @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); + } + + 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; + if (invoker != nullptr) { + @try { + didInvoke = invoker(env, cif, function->fnptr, info, cif->rvalue); + } @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; + } + + setCFunctionReturnValue(env, info, function, cif, cif->rvalue); +} + +bool isCompatLibdispatchFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return false; + } + + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +bool defineV8FunctionProperty(napi_env env, v8::Local object, + v8::Local propertyName, v8::Local function, + napi_property_attributes attributes) { + v8::PropertyDescriptor descriptor(function, (attributes & napi_writable) != 0); + descriptor.set_enumerable((attributes & napi_enumerable) != 0); + descriptor.set_configurable((attributes & napi_configurable) != 0); + + return object->DefineProperty(env->context(), propertyName, descriptor).FromMaybe(false); +} + +bool defineV8AccessorProperty(napi_env env, v8::Local object, + v8::Local propertyName, v8::Local getter, + v8::Local setter, napi_property_attributes attributes) { + v8::PropertyDescriptor descriptor(getter, setter); + descriptor.set_enumerable((attributes & napi_enumerable) != 0); + descriptor.set_configurable((attributes & napi_configurable) != 0); + + return object->DefineProperty(env->context(), propertyName, descriptor).FromMaybe(false); +} + +} // namespace + +bool V8TryDefineFastNativeProperty(napi_env env, v8::Local object, + v8::Local propertyName, + const napi_property_descriptor* descriptor) { +#if !NS_GSD_BACKEND_V8 + return false; +#else + if (env == nullptr || descriptor == nullptr) { + return false; + } + + v8::Local context = env->context(); + + if (descriptor->method == ObjCClassMember::jsCall) { + auto* method = static_cast(descriptor->data); + if (method == nullptr || !method->overloads.empty()) { + return false; + } + + Cif* cif = method->cif; + if (cif == nullptr) { + cif = method->cif = + method->bridgeState->getMethodCif(env, method->methodOrGetter.signatureOffset); + } + + v8::Local function; + if (!v8::Function::New(context, v8ObjCMethodCallback, v8::External::New(env->isolate, method)) + .ToLocal(&function)) { + return false; + } + + return defineV8FunctionProperty(env, object, propertyName, function, descriptor->attributes); + } + + if (descriptor->method == CFunction::jsCall) { + auto offset = static_cast(reinterpret_cast(descriptor->data)); + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (isCompatLibdispatchFunction(bridgeState, offset)) { + return false; + } + + if (bridgeState == nullptr) { + return false; + } + + auto* binding = new V8CFunctionBinding{bridgeState, offset, nullptr}; + v8::Local functionValue; + if (!v8::Function::New(context, v8CFunctionCallback, v8::External::New(env->isolate, binding)) + .ToLocal(&functionValue)) { + delete binding; + return false; + } + + return defineV8FunctionProperty(env, object, propertyName, functionValue, + descriptor->attributes); + } + + if (descriptor->getter == ObjCClassMember::jsGetter && descriptor->data != nullptr) { + auto* method = static_cast(descriptor->data); + Cif* getterCif = method->cif; + if (getterCif == nullptr) { + getterCif = method->cif = + method->bridgeState->getMethodCif(env, method->methodOrGetter.signatureOffset); + } + + v8::Local getter; + if (!v8::Function::New(context, v8ObjCGetterCallback, v8::External::New(env->isolate, method)) + .ToLocal(&getter)) { + return false; + } + + v8::Local setter; + if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { + if (!v8::Function::New(context, v8ReadOnlySetterCallback, + v8::External::New(env->isolate, method)) + .ToLocal(&setter)) { + return false; + } + } else if (descriptor->setter == ObjCClassMember::jsSetter) { + Cif* setterCif = method->setterCif; + if (setterCif == nullptr) { + setterCif = method->setterCif = + method->bridgeState->getMethodCif(env, method->setter.signatureOffset); + } + if (!v8::Function::New(context, v8ObjCSetterCallback, v8::External::New(env->isolate, method)) + .ToLocal(&setter)) { + return false; + } + } else if (descriptor->setter != nullptr) { + return false; + } + + return defineV8AccessorProperty(env, object, propertyName, getter, setter, + descriptor->attributes); + } + + return false; +#endif +} + +} // namespace nativescript + +#undef NS_V8_INTERCEPTED +#undef NS_V8_RETURN_YES +#undef NS_V8_RETURN_NO +#undef NS_V8_SETTER_INFO + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/napi/v8/v8-api.cpp b/NativeScript/napi/v8/v8-api.cpp index dc6bfeda..eec2772d 100644 --- a/NativeScript/napi/v8/v8-api.cpp +++ b/NativeScript/napi/v8/v8-api.cpp @@ -9,6 +9,7 @@ #define NAPI_EXPERIMENTAL +#include "ffi/V8FastNativeApi.h" #include "js_native_api.h" #include "v8-api.h" #include "v8-module-loader.h" @@ -1378,6 +1379,11 @@ napi_define_properties(napi_env env, napi_value object, size_t property_count, v8::Local property_name; STATUS_CALL(v8impl::V8NameFromPropertyDescriptor(env, p, &property_name)); + if (nativescript::V8TryDefineFastNativeProperty(env, obj, property_name, + p)) { + continue; + } + if (p->getter != nullptr || p->setter != nullptr) { v8::Local local_getter; v8::Local local_setter; diff --git a/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md new file mode 100644 index 00000000..ddaa79d1 --- /dev/null +++ b/benchmarks/objc-dispatch/README.md @@ -0,0 +1,42 @@ +# 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. + +The default benchmark cases include calls covered by the hand-written V8 direct +path and calls that rely on generated signature dispatch, so `gsd-on` versus +`gsd-off` shows both the shared callback-path baseline and the GSD-specific +delta. + +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 +--napi-v8-napi-backend-package-tgz /path/to/nativescript-ios-v8-napi-backend.tgz +--iterations 250000 +--include-napi-gsd-off +--include-napi-v8-napi-backend +``` 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..c2edbacb --- /dev/null +++ b/benchmarks/objc-dispatch/run.js @@ -0,0 +1,917 @@ +#!/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, + includeNapiV8NapiBackend: 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: "", + napiV8NapiBackendPackageTgz: "", + 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-v8-napi-backend-package-tgz") args.napiV8NapiBackendPackageTgz = path.resolve(next()); + else if (arg.startsWith("--napi-v8-napi-backend-package-tgz=")) args.napiV8NapiBackendPackageTgz = path.resolve(arg.slice("--napi-v8-napi-backend-package-tgz=".length)); + else if (arg === "--include-napi-gsd-off") args.includeNapiGsdOff = true; + else if (arg === "--include-napi-v8-napi-backend") args.includeNapiV8NapiBackend = 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-v8-napi-backend-package-tgz PATH + @nativescript/ios tgz built with TARGET_ENGINE=v8 and NS_GSD_BACKEND=napi + --include-napi-gsd-off Also run N-API with generated signature dispatch disabled + --include-napi-v8-napi-backend + Also run V8 runtime compiled to use the N-API GSD/callback path + --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 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 napiGsdOn = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-on"); + const napiGsdOff = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-off"); + const napiV8NapiBackend = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-v8-napi-backend"); + if (napiGsdOn && napiGsdOff) { + printPairComparison(napiGsdOn, napiGsdOff); + } + if (napiGsdOn && napiV8NapiBackend) { + printPairComparison(napiGsdOn, napiV8NapiBackend); + } + + const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); + if (napiGsdOn && legacyAotOn) { + printPairComparison(napiGsdOn, legacyAotOn); + } + + const legacyAotOff = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-off"); + if (napiGsdOn && legacyAotOff) { + printPairComparison(napiGsdOn, legacyAotOff); + } +} + +function printSavedResultsComparison(resultsPath) { + const parsed = JSON.parse(fs.readFileSync(resultsPath, "utf8")); + const reports = parsed.reports || []; + for (const report of reports) { + printReport(report); + } + printComparisons(reports); +} + +function runNapiNode(options, variant) { + if (!fs.existsSync(options.metadataPath)) { + throw new Error(`Metadata file not found: ${options.metadataPath}`); + } + + const runnerDir = path.join(options.workRoot, "node"); + ensureDir(runnerDir); + fs.copyFileSync(benchmarkFile, path.join(runnerDir, path.basename(benchmarkFile))); + + const runnerPath = path.join(runnerDir, `run-${variant}.cjs`); + const benchmarkOptions = { + iterations: options.iterations, + warmupIterations: options.warmupIterations + }; + fs.writeFileSync( + runnerPath, + [ + `global.__NS_BENCHMARK_RUNTIME = "napi-node";`, + `global.__NS_BENCHMARK_VARIANT = ${JSON.stringify(variant)};`, + `global.__NS_BENCHMARK_OPTIONS__ = ${JSON.stringify(benchmarkOptions)};`, + `import(${JSON.stringify(pathToFileUrl(path.join(repoRoot, "packages/macos-node-api/index.mjs")))}).then(() => {`, + ` require(${JSON.stringify(path.join(runnerDir, path.basename(benchmarkFile)))});`, + `}).catch((error) => { console.error(error && error.stack || error); process.exit(1); });`, + "" + ].join("\n") + ); + + const env = { ...process.env, METADATA_PATH: options.metadataPath }; + if (variant === "gsd-off") { + // Current runtime disables generated signature dispatch when this value is exactly "0". + env.NS_DISABLE_GSD = "0"; + } else { + delete env.NS_DISABLE_GSD; + } + + const result = run(process.execPath, [runnerPath], { cwd: repoRoot, env, timeout: options.timeoutMs }); + return parseBenchmarkOutput(result.stdout + result.stderr); +} + +function pathToFileUrl(filePath) { + return new URL(`file://${filePath}`).href; +} + +function destinationToUdid(destination) { + if (!destination) { + return ""; + } + const idMatch = destination.match(/id=([0-9A-Fa-f-]{36})/); + if (idMatch) { + return idMatch[1]; + } + if (/^[0-9A-Fa-f-]{36}$/.test(destination)) { + return destination; + } + return ""; +} + +function pickSimulator(destination) { + const explicit = destinationToUdid(destination); + if (explicit) { + return explicit; + } + + const result = run("xcrun", ["simctl", "list", "devices", "available", "--json"]); + const parsed = JSON.parse(result.stdout); + const devices = []; + for (const runtimeName of Object.keys(parsed.devices || {})) { + for (const device of parsed.devices[runtimeName]) { + if (device.isAvailable && /iPhone/.test(device.name)) { + devices.push(device); + } + } + } + + const booted = devices.find((device) => device.state === "Booted"); + const preferred = booted || devices.find((device) => /Pro/.test(device.name)) || devices[0]; + if (!preferred) { + throw new Error("No available iPhone simulator found"); + } + return preferred.udid; +} + +function bootSimulator(udid) { + const boot = childProcess.spawnSync("xcrun", ["simctl", "boot", udid], { encoding: "utf8" }); + if (boot.status !== 0 && !/Unable to boot device in current state: Booted/.test(boot.stderr || "")) { + throw new Error(`Unable to boot simulator ${udid}:\n${boot.stderr || boot.stdout}`); + } + runInherited("xcrun", ["simctl", "bootstatus", udid, "-b"], { timeout: 180000 }); +} + +function findBuiltApp(derivedDataPath, appName) { + const productsRoot = path.join(derivedDataPath, "Build/Products"); + const queue = [productsRoot]; + while (queue.length > 0) { + const current = queue.pop(); + if (!fs.existsSync(current)) { + continue; + } + const stats = fs.statSync(current); + if (stats.isDirectory() && path.basename(current) === `${appName}.app`) { + return current; + } + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + throw new Error(`Built app not found under ${productsRoot}`); +} + +function launchAndCollect(udid, bundleId, options, env = {}) { + return new Promise((resolve, reject) => { + let output = ""; + let settled = false; + const children = []; + const launchEnv = { ...process.env }; + for (const [key, value] of Object.entries(env)) { + launchEnv[`SIMCTL_CHILD_${key}`] = value; + } + + const logChild = childProcess.spawn( + "xcrun", + [ + "simctl", "spawn", udid, + "log", "stream", + "--style", "compact", + "--level", "debug", + "--predicate", `eventMessage CONTAINS "${marker}"` + ], + { env: process.env } + ); + children.push(logChild); + + const child = childProcess.spawn( + "xcrun", + ["simctl", "launch", "--console", "--terminate-running-process", udid, bundleId], + { env: launchEnv } + ); + children.push(child); + + const timeout = setTimeout(() => { + if (settled) { + return; + } + settled = true; + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + reject(new Error(`Timed out waiting for benchmark marker from ${bundleId}`)); + }, options.timeoutMs); + + function settleWithReport(report) { + settled = true; + clearTimeout(timeout); + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + resolve(report); + } + + function onData(data) { + const text = data.toString(); + output += text; + process.stdout.write(text); + if (!settled && output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + } catch (_) { + // log stream prints the predicate itself before app logs; wait for the + // actual console message containing marker JSON. + } + } + } + + logChild.stdout.on("data", onData); + logChild.stderr.on("data", onData); + child.stdout.on("data", onData); + child.stderr.on("data", onData); + for (const activeChild of children) { + activeChild.on("error", (error) => { + if (!settled) { + settled = true; + clearTimeout(timeout); + reject(error); + } + }); + } + child.on("exit", (code) => { + if (!settled) { + if (output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + } catch (_) { + // The unified log stream may still deliver the actual message after + // simctl launch exits. + } + } + } + }); + }); +} + +function xcodebuild(args, cwd, timeoutMs) { + runInherited("xcodebuild", args, { + cwd, + timeout: timeoutMs, + env: { + ...process.env, + PATH: ["/opt/homebrew/bin", "/usr/local/bin", process.env.PATH || ""].join(":"), + NSUnbufferedIO: "YES" + } + }); +} + +function installApp(udid, appPath, bundleId) { + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + childProcess.spawnSync("xcrun", ["simctl", "uninstall", udid, bundleId], { stdio: "ignore" }); + runInherited("xcrun", ["simctl", "install", udid, appPath], { timeout: 120000 }); +} + +function readAppBundleId(appPath, fallback) { + const plistPath = path.join(appPath, "Info.plist"); + if (!fs.existsSync(plistPath)) { + return fallback; + } + const result = childProcess.spawnSync( + "/usr/libexec/PlistBuddy", + ["-c", "Print :CFBundleIdentifier", plistPath], + { encoding: "utf8" } + ); + if (result.status === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + return fallback; +} + +async function runLegacyIOS(options, variant = "aot-on") { + const appName = "TestRunner"; + let bundleId = "com.descendra.TestRunner"; + const appDir = path.join(options.legacyRepo, "TestRunner/app"); + const indexPath = path.join(appDir, "index.js"); + const copiedBenchmarkPath = path.join(appDir, path.basename(benchmarkFile)); + const originalIndex = fs.readFileSync(indexPath, "utf8"); + const derivedDataPath = path.join(options.workRoot, "derived-data/legacy-ios"); + const udid = pickSimulator(options.destination); + + ensureDir(path.dirname(copiedBenchmarkPath)); + fs.copyFileSync(benchmarkFile, copiedBenchmarkPath); + writeJsonRunner(indexPath, "legacy-ios", variant, options); + + try { + bootSimulator(udid); + if (!options.skipBuild) { + xcodebuild([ + "-project", "v8ios.xcodeproj", + "-scheme", "TestRunner", + "-configuration", "Release", + "-destination", `platform=iOS Simulator,id=${udid}`, + "-derivedDataPath", derivedDataPath, + "CODE_SIGNING_ALLOWED=NO", + "CODE_SIGNING_REQUIRED=NO", + "CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION=NO", + "CLANG_WARN_NULLABILITY_COMPLETENESS=NO", + "OTHER_CPLUSPLUSFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "OTHER_CFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "build", + "-quiet" + ], options.legacyRepo, options.buildTimeoutMs); + } + let appPath; + try { + appPath = findBuiltApp(derivedDataPath, appName); + } catch (_) { + appPath = path.join(options.legacyRepo, "build/Release-iphonesimulator", `${appName}.app`); + if (!fs.existsSync(appPath)) { + throw _; + } + } + if (options.skipBuild) { + copyDirectoryContents(appDir, path.join(appPath, "app")); + } + bundleId = readAppBundleId(appPath, bundleId); + installApp(udid, appPath, bundleId); + const launchEnv = variant === "aot-off" ? { NS_DISABLE_AOT: "1" } : {}; + return await launchAndCollect(udid, bundleId, options, launchEnv); + } finally { + fs.writeFileSync(indexPath, originalIndex); + rmrf(copiedBenchmarkPath); + } +} + +function findDefaultNapiPackage() { + const distDir = path.join(repoRoot, "packages/ios/dist"); + const names = fs.readdirSync(distDir) + .filter((name) => /^nativescript-ios-.*\.tgz$/.test(name)) + .sort(); + if (names.length === 0) { + throw new Error(`No @nativescript/ios package tgz found in ${distDir}`); + } + return path.join(distDir, names[names.length - 1]); +} + +function replaceInTextFiles(root, search, replacement) { + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + const stats = fs.lstatSync(current); + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + continue; + } + if (!stats.isFile()) { + continue; + } + const buffer = fs.readFileSync(current); + if (buffer.includes(0)) { + continue; + } + const text = buffer.toString("utf8"); + if (text.includes(search)) { + fs.writeFileSync(current, text.split(search).join(replacement)); + } + } +} + +function renamePlaceholderPaths(root, search, replacement) { + const entries = []; + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + entries.push(current); + if (fs.lstatSync(current).isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + + entries.sort((a, b) => b.length - a.length); + for (const current of entries) { + const base = path.basename(current); + if (!base.includes(search) || !fs.existsSync(current)) { + continue; + } + fs.renameSync(current, path.join(path.dirname(current), base.split(search).join(replacement))); + } +} + +function scaffoldNapiIOSApp(options, variant, packageTgz) { + 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", variant, 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) { + const app = scaffoldNapiIOSApp(options, variant, packageTgz); + 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")); + if (options.includeNapiV8NapiBackend) { + if (!options.napiV8NapiBackendPackageTgz) { + throw new Error("--include-napi-v8-napi-backend requires --napi-v8-napi-backend-package-tgz"); + } + reports.push(await runNapiIOS(options, "gsd-v8-napi-backend", options.napiV8NapiBackendPackageTgz)); + } + if (options.includeNapiGsdOff) { + reports.push(await runNapiIOS(options, "gsd-off")); + } + } else if (runtime === "legacy-ios") { + reports.push(await runLegacyIOS(options, "aot-on")); + if (options.includeLegacyAotOff) { + reports.push(await runLegacyIOS({ ...options, skipBuild: true }, "aot-off")); + } + } else { + throw new Error(`Unknown runtime: ${runtime}`); + } + } + + for (const report of reports) { + printReport(report); + } + printComparisons(reports); + + const outPath = path.join(options.workRoot, `results-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); + fs.writeFileSync(outPath, JSON.stringify({ createdAt: new Date().toISOString(), reports }, null, 2) + "\n"); + console.log(`\nWrote ${outPath}`); +} + +main().catch((error) => { + console.error(error && error.stack ? error.stack : error); + process.exit(1); +}); diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 94069b62..7534e921 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,55 @@ 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 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(); } @@ -637,6 +685,166 @@ void writeFastNapiArgConversion(std::ostringstream& out, const MDTypeInfo* type, } } +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 (!argv[" << 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 (!argv[" << 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 (!argv[" << 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, argv[" << index + << "], &arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSInt: { + out << " if (!argv[" << index << "]->Int32Value(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeUInt: { + out << " if (!argv[" << 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 (argv[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = argv[" << index + << "].As()->Int64Value(&lossless" << index << ");\n"; + out << " } else if (!argv[" << 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 (argv[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = argv[" << index + << "].As()->Uint64Value(&lossless" << index << ");\n"; + out << " } else {\n"; + out << " int64_t signedValue" << index << " = 0;\n"; + out << " if (!argv[" << 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 (!argv[" << 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 (!argv[" << 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 (!argv[" << index << "]->IsBoolean()) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(argv[" << index + << "]->BooleanValue(info.GetIsolate()) ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} + void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, const std::string& wrapperName, const MDSignature* signature) { @@ -810,6 +1018,318 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, out << "}\n\n"; } +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; + } + + 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 v8::FunctionCallbackInfo& info, void* rvalue) {\n"; + out << " if (info.Length() < " << argTypes.size() << ") {\n"; + out << " return false;\n"; + out << " }\n"; + out << " v8::Local context = info.GetIsolate()->GetCurrentContext();\n"; + if (!argTypes.empty()) { + out << " v8::Local argv[" << argTypes.size() << "];\n"; + for (size_t i = 0; i < argTypes.size(); i++) { + out << " argv[" << i << "] = info[" << i << "];\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 << " } 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)) { + writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + out << " if (!TryFastConvertV8Argument(env, static_cast(" + << static_cast(argTypeInfos[i]->kind) << "), argv[" << i + << "], &arg" << i << ")) {\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(argv[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(argv[" << i + << "]), &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; + } else { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(argv[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(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 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 +1399,22 @@ 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 objcV8Entries; + std::unordered_map cFunctionV8Entries; + std::unordered_map blockPreparedEntries; std::unordered_map dispatchEncoding; std::unordered_set collidedDispatchIds; @@ -912,6 +1444,9 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collidedDispatchIds.insert(dispatchId); objcNapiEntries.erase(dispatchId); cFunctionNapiEntries.erase(dispatchId); + objcV8Entries.erase(dispatchId); + cFunctionV8Entries.erase(dispatchId); + blockPreparedEntries.erase(dispatchId); dispatchEncoding.erase(dispatchId); continue; } @@ -924,12 +1459,19 @@ 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 { + objcV8Entries.emplace(dispatchId, wrapperKey); + } else if (use.kind == DispatchKind::CFunction) { + wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); cFunctionNapiEntries.emplace(dispatchId, wrapperKey); + cFunctionV8Entries.emplace(dispatchId, wrapperKey); + } else if (use.kind == DispatchKind::BlockInvoke) { + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + blockPreparedEntries.emplace(dispatchId, wrapperKey); } } @@ -940,6 +1482,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 +1499,24 @@ 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 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 +1529,85 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, sortedCFunctionNapiEntries.begin(), sortedCFunctionNapiEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector> sortedObjCV8Entries( + objcV8Entries.begin(), objcV8Entries.end()); + std::sort( + sortedObjCV8Entries.begin(), sortedObjCV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> sortedCFunctionV8Entries( + cFunctionV8Entries.begin(), cFunctionV8Entries.end()); + std::sort( + sortedCFunctionV8Entries.begin(), sortedCFunctionV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> sortedBlockPreparedEntries( + blockPreparedEntries.begin(), blockPreparedEntries.end()); + std::sort( + sortedBlockPreparedEntries.begin(), sortedBlockPreparedEntries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::ostringstream generated; generated << "#ifndef NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; generated << "#define NS_GENERATED_SIGNATURE_DISPATCH_INC\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI\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\n"; generated << "namespace nativescript {\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI\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_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\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_NAPI\n"; generated << "inline constexpr ObjCNapiDispatchEntry " "kGeneratedObjCNapiDispatchEntries[] = {\n"; generated << " {0, nullptr},\n"; @@ -992,6 +1625,27 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, << wrapperNameByKey.at(entry.second) << "},\n"; } generated << "};\n\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8\n"; + generated << "inline constexpr ObjCV8DispatchEntry " + "kGeneratedObjCV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionV8DispatchEntry " + "kGeneratedCFunctionV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; generated << "} // namespace nativescript\n\n"; generated << "#endif // NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; diff --git a/package.json b/package.json index f0d5032b..a67d5d18 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "build:macos-napi": "./scripts/build_nativescript.sh --no-iphone --no-simulator --macos-napi", "nsr": "./dist/nsr", "test:macos": "node ./scripts/run-tests-macos.js ./build/test-results/macos-junit.xml", - "test:ios": "node ./scripts/run-tests-ios.js" + "test:ios": "node ./scripts/run-tests-ios.js", + "benchmark:objc-dispatch": "node ./benchmarks/objc-dispatch/run.js" }, "license": "Apache-2.0", "devDependencies": { diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index 062bac94..ba13723a 100755 --- a/scripts/build_all_ios.sh +++ b/scripts/build_all_ios.sh @@ -58,7 +58,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_nativescript.sh b/scripts/build_nativescript.sh index 7de343ac..cd53db20 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -14,6 +14,7 @@ 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} for arg in $@; do @@ -39,6 +40,10 @@ 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-napi) NS_GSD_BACKEND=napi ;; + --gsd-none) NS_GSD_BACKEND=none ;; + --gsd-backend=*) NS_GSD_BACKEND="${arg#--gsd-backend=}" ;; *) ;; esac done @@ -102,10 +107,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 +137,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 \ From 49c9a76a886f303cf677f6ecb4fe8c4a86ce3c71 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 20 May 2026 23:50:48 -0400 Subject: [PATCH 02/31] perf(ios): broaden v8 direct dispatch fast path --- NativeScript/ffi/V8FastNativeApi.mm | 196 ++++++++++++++++++++++++++++ scripts/build_all_ios.sh | 16 ++- scripts/build_all_macos.sh | 1 + scripts/build_all_node_api.sh | 2 +- scripts/build_nativescript.sh | 59 +++++++++ scripts/metagen.js | 7 + 6 files changed, 279 insertions(+), 2 deletions(-) diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm index 9caf5aa1..64272206 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -1640,6 +1640,179 @@ bool tryConvertV8NSUIntegerArgument(napi_env env, v8::Local value, return true; } +inline void setV8Int64ReturnValue(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(v8::Number::New(isolate, static_cast(value))); + } +} + +inline void setV8UInt64ReturnValue(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(v8::Number::New(isolate, static_cast(value))); + } +} + +bool tryInvokeObjCV8PrimitiveReturnWithObjectArg( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + MDTypeKind returnKind, id self, SEL selector, id arg0) { + switch (returnKind) { + case mdTypeBool: { + BOOL value = reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); + return true; + } + case mdTypeChar: { + int8_t value = reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUChar: + case mdTypeUInt8: { + uint8_t value = reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSShort: { + int16_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUShort: { + uint16_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSInt: { + int32_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUInt: { + uint32_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSLong: + case mdTypeSInt64: { + int64_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + setV8Int64ReturnValue(isolate, info, value); + return true; + } + case mdTypeULong: + case mdTypeUInt64: { + uint64_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + setV8UInt64ReturnValue(isolate, info, value); + return true; + } + case mdTypeFloat: { + float value = reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + case mdTypeDouble: { + double value = reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + default: + return false; + } +} + +bool tryInvokeObjCV8PrimitiveReturnWithNSUIntegerArg( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + MDTypeKind returnKind, id self, SEL selector, NSUInteger arg0) { + switch (returnKind) { + case mdTypeBool: { + BOOL value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); + return true; + } + case mdTypeChar: { + int8_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUChar: + case mdTypeUInt8: { + uint8_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSShort: { + int16_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUShort: { + uint16_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSInt: { + int32_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::New(isolate, value)); + return true; + } + case mdTypeUInt: { + uint32_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); + return true; + } + case mdTypeSLong: + case mdTypeSInt64: { + int64_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + setV8Int64ReturnValue(isolate, info, value); + return true; + } + case mdTypeULong: + case mdTypeUInt64: { + uint64_t value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + setV8UInt64ReturnValue(isolate, info, value); + return true; + } + case mdTypeFloat: { + float value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + case mdTypeDouble: { + double value = + reinterpret_cast(objc_msgSend)(self, selector, arg0); + info.GetReturnValue().Set(v8::Number::New(isolate, value)); + return true; + } + default: + return false; + } +} + bool tryInvokeObjCV8DirectFastPath(napi_env env, const v8::FunctionCallbackInfo& info, ObjCClassMember* method, MethodDescriptor* descriptor, @@ -1791,6 +1964,29 @@ bool tryInvokeObjCV8DirectFastPath(napi_env env, } } + if (cif->argc == 1 && static_cast(info.Length()) >= 1) { + const MDTypeKind argKind = cif->argTypes[0]->kind; + if (isV8DirectObjectKind(argKind)) { + id arg0 = nil; + if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0)) { + return false; + } + if (tryInvokeObjCV8PrimitiveReturnWithObjectArg(isolate, info, returnKind, self, + descriptor->selector, arg0)) { + return true; + } + } else if (isV8DirectIntegerKind(argKind)) { + NSUInteger arg0 = 0; + if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { + return false; + } + if (tryInvokeObjCV8PrimitiveReturnWithNSUIntegerArg(isolate, info, returnKind, self, + descriptor->selector, arg0)) { + return true; + } + } + } + if (returnKind == mdTypeVoid) { if (cif->argc == 1 && static_cast(info.Length()) >= 1) { const MDTypeKind argKind = cif->argTypes[0]->kind; diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index ba13723a..08a57490 100755 --- a/scripts/build_all_ios.sh +++ b/scripts/build_all_ios.sh @@ -24,7 +24,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 @@ -56,6 +56,20 @@ if $EMBED_METADATA; then fi checkpoint "... All metadata generated!" +elif [[ "$TARGET_ENGINE" != "none" ]]; then + GSD_PLATFORM= + if $BUILD_SIMULATOR; then + GSD_PLATFORM=ios-sim + elif $BUILD_IPHONE; then + GSD_PLATFORM=ios + elif $BUILD_MACOS; then + GSD_PLATFORM=macos + fi + + if [ -n "$GSD_PLATFORM" ]; then + checkpoint "Generating signature dispatch bindings for $GSD_PLATFORM..." + npm run metagen "$GSD_PLATFORM" + fi fi "$SCRIPT_DIR/build_nativescript.sh" --no-vision "$@" 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 cd53db20..e7b8d5d7 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -16,6 +16,7 @@ CONFIG_BUILD=RelWithDebInfo TARGET_ENGINE=${TARGET_ENGINE:=v8} # default to v8 for compat NS_GSD_BACKEND=${NS_GSD_BACKEND:=auto} METADATA_SIZE=${METADATA_SIZE:=0} +GENERATED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/GeneratedSignatureDispatch.inc}} for arg in $@; do case $arg in @@ -66,6 +67,64 @@ if ! $VERBOSE; then QUIET=-quiet fi +function effective_gsd_backend () { + case "$NS_GSD_BACKEND" in + auto) + if [ "$TARGET_ENGINE" == "none" ]; then + echo none + elif [ "$TARGET_ENGINE" == "v8" ]; then + echo v8 + else + echo napi + fi + ;; + *) + echo "$NS_GSD_BACKEND" + ;; + esac +} + +function signature_dispatch_platform () { + if $BUILD_SIMULATOR; then + echo ios-sim + elif $BUILD_IPHONE; then + echo ios + elif $BUILD_MACOS || $BUILD_MACOS_CLI || $BUILD_MACOS_NODE_API; then + echo macos + elif $BUILD_VISION; then + echo visionos-sim + elif $BUILD_CATALYST; then + echo catalyst + fi +} + +function ensure_signature_dispatch_bindings () { + local backend + backend=$(effective_gsd_backend) + if [ "$TARGET_ENGINE" == "none" ] || [ "$backend" == "none" ]; then + return + fi + + if [ -f "$GENERATED_SIGNATURE_DISPATCH" ]; then + return + fi + + local platform + platform=$(signature_dispatch_platform) + if [ -z "$platform" ]; then + return + fi + + if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi + + checkpoint "Generating signature dispatch bindings for $platform..." + npm run metagen "$platform" +} + +ensure_signature_dispatch_bindings + DEV_TEAM=${DEVELOPMENT_TEAM:-} DIST=$(PWD)/dist mkdir -p $DIST 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, From e4c133e0b7bb8e08a47d6388b48833c6a5be3e4d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 00:44:45 -0400 Subject: [PATCH 03/31] perf(ios): tighten generated v8 dispatch --- NativeScript/ffi/Cif.h | 3 + NativeScript/ffi/Cif.mm | 52 +++++ NativeScript/ffi/SignatureDispatch.h | 35 +++- NativeScript/ffi/V8FastNativeApi.mm | 131 +++++++++---- .../src/SignatureDispatchEmitter.cpp | 181 +++++++++++++++--- 5 files changed, 327 insertions(+), 75 deletions(-) diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/Cif.h index ee483acf..3614d6ab 100644 --- a/NativeScript/ffi/Cif.h +++ b/NativeScript/ffi/Cif.h @@ -21,6 +21,9 @@ class Cif { bool isVariadic = false; uint64_t signatureHash = 0; bool skipGeneratedNapiDispatch = false; + bool generatedDispatchHasRoundTripCacheArgument = false; + bool generatedDispatchUsesObjectReturnStorage = false; + bool generatedDispatchSetsV8ReturnDirectly = false; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/Cif.mm b/NativeScript/ffi/Cif.mm index 6f98b49f..9d8ba085 100644 --- a/NativeScript/ffi/Cif.mm +++ b/NativeScript/ffi/Cif.mm @@ -75,17 +75,69 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptrskipGeneratedNapiDispatch = false; + cif->generatedDispatchHasRoundTripCacheArgument = false; + cif->generatedDispatchUsesObjectReturnStorage = false; + cif->generatedDispatchSetsV8ReturnDirectly = false; + + if (cif->returnType != nullptr) { + cif->generatedDispatchUsesObjectReturnStorage = + typeKindMayUseRoundTripCache(cif->returnType->kind); + cif->generatedDispatchSetsV8ReturnDirectly = + typeKindCanSetV8ReturnDirectly(cif->returnType->kind); + } + cif->skipGeneratedNapiDispatch = typeRequiresSlowGeneratedNapiDispatch(cif->returnType); if (cif->skipGeneratedNapiDispatch) { return; } for (const auto& argType : cif->argTypes) { + if (argType != nullptr && typeKindMayUseRoundTripCache(argType->kind)) { + cif->generatedDispatchHasRoundTripCacheArgument = true; + } if (typeRequiresSlowGeneratedNapiDispatch(argType)) { cif->skipGeneratedNapiDispatch = true; return; diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 82596c6c..babb9a4c 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -36,12 +36,14 @@ using CFunctionNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, #ifdef TARGET_ENGINE_V8 using ObjCV8Invoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, - SEL selector, + SEL selector, void* bridgeState, bool returnOwned, + bool receiverIsClass, bool propertyAccess, const v8::FunctionCallbackInfo& info, - void* rvalue); + void* rvalue, bool* didSetReturnValue); using CFunctionV8Invoker = bool (*)(napi_env env, Cif* cif, void* fnptr, - const v8::FunctionCallbackInfo& info, void* rvalue); + const v8::FunctionCallbackInfo& info, void* rvalue, + bool* didSetReturnValue); #endif struct ObjCDispatchEntry { @@ -111,6 +113,33 @@ static_assert(sizeof(v8::Local) == sizeof(napi_value), inline napi_value v8LocalValueToNapiValue(v8::Local local) { return reinterpret_cast(*local); } + +inline void setV8DispatchInt64ReturnValue( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + int64_t value) { + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (value > kMaxSafeInteger || value < -kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::New(isolate, value)); + } else { + info.GetReturnValue().Set(static_cast(value)); + } +} + +inline void setV8DispatchUInt64ReturnValue( + v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, + uint64_t value) { + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (value > kMaxSafeInteger) { + info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, value)); + } else { + info.GetReturnValue().Set(static_cast(value)); + } +} + +bool TryFastSetV8GeneratedObjCObjectReturnValue( + napi_env env, const v8::FunctionCallbackInfo& info, + Cif* cif, void* bridgeState, id self, SEL selector, id value, + bool returnOwned, bool receiverIsClass, bool propertyAccess); #endif } // namespace nativescript diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm index 64272206..08f8c95c 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -906,6 +906,48 @@ bool TryFastSetV8ObjectReturnValue(napi_env env, 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; @@ -1287,28 +1329,6 @@ CFunctionV8Invoker ensureCFunctionV8Invoker(CFunction* function, Cif* cif) { return reinterpret_cast(function->v8Invoker); } -inline bool typeKindMayUseRoundTripCache(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return true; - default: - return false; - } -} - -inline bool cifMayUseRoundTripCache(Cif* cif) { - if (cif == nullptr) { - return false; - } - - return cif->returnType != nullptr && typeKindMayUseRoundTripCache(cif->returnType->kind); -} - inline bool selectorEndsWith(SEL selector, const char* suffix) { if (selector == nullptr || suffix == nullptr) { return false; @@ -2317,20 +2337,38 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf } const bool hasImplicitNSErrorOutArg = isNSErrorOutMethod && !cif->isVariadic && actualArgc + 1 == cif->argc; - if (!isNSErrorOutMethod && !requiresSuperCall && - tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { - return true; + const bool canUseGeneratedDispatch = !isNSErrorOutMethod && !requiresSuperCall; + ObjCV8Invoker invoker = + canUseGeneratedDispatch ? ensureObjCV8Invoker(cif, descriptor, descriptor->dispatchFlags) + : nullptr; + if (invoker == nullptr) { + if (canUseGeneratedDispatch && + tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { + return true; + } + + return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, + propertyAccess); } - const bool needsRoundTripCache = cifMayUseRoundTripCache(cif); + const bool generatedDispatchSetsReturnDirectly = + cif->generatedDispatchSetsV8ReturnDirectly; + const bool generatedDispatchUsesObjectReturnStorage = + !generatedDispatchSetsReturnDirectly && cif->generatedDispatchUsesObjectReturnStorage; + const bool needsRoundTripCache = + generatedDispatchUsesObjectReturnStorage && + cif->generatedDispatchHasRoundTripCacheArgument; std::optional roundTripCacheFrame; if (needsRoundTripCache) { roundTripCacheFrame.emplace(env, method->bridgeState); } std::optional rvalueStorage; + id objectRvalue = nil; void* rvalue = nullptr; - if (cif->returnType == nullptr || cif->returnType->kind != mdTypeVoid) { + if (generatedDispatchUsesObjectReturnStorage) { + rvalue = &objectRvalue; + } else if (!generatedDispatchSetsReturnDirectly) { rvalueStorage.emplace(cif); if (!rvalueStorage->valid()) { throwV8Error(info.GetIsolate(), @@ -2341,26 +2379,32 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf } bool didInvoke = false; - ObjCV8Invoker invoker = - requiresSuperCall ? nullptr : ensureObjCV8Invoker(cif, descriptor, descriptor->dispatchFlags); - if (!isNSErrorOutMethod && invoker != nullptr) { - @try { - didInvoke = invoker(env, cif, (void*)objc_msgSend, self, descriptor->selector, info, rvalue); - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); - return 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) { + if (canUseGeneratedDispatch && + tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { + return true; + } + return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, propertyAccess); } - setObjCReturnValue(env, info, method, descriptor, cif, self, receiverIsClass, rvalue, - propertyAccess); + if (!didSetReturnValue) { + setObjCReturnValue(env, info, method, descriptor, cif, self, receiverIsClass, rvalue, + propertyAccess); + } return true; } @@ -2557,9 +2601,10 @@ void v8CFunctionCallback(const v8::FunctionCallbackInfo& info) { CFunctionV8Invoker invoker = ensureCFunctionV8Invoker(function, cif); bool didInvoke = false; + bool didSetReturnValue = false; if (invoker != nullptr) { @try { - didInvoke = invoker(env, cif, function->fnptr, info, cif->rvalue); + didInvoke = invoker(env, cif, function->fnptr, info, cif->rvalue, &didSetReturnValue); } @catch (NSException* exception) { std::string message = exception.description.UTF8String; NativeScriptException nativeScriptException(message); @@ -2573,7 +2618,9 @@ void v8CFunctionCallback(const v8::FunctionCallbackInfo& info) { return; } - setCFunctionReturnValue(env, info, function, cif, cif->rvalue); + if (!didSetReturnValue) { + setCFunctionReturnValue(env, info, function, cif, cif->rvalue); + } } bool isCompatLibdispatchFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 7534e921..d10018fc 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -475,6 +475,102 @@ bool isFastManagedNapiKind(MDTypeKind kind) { } } +bool fastV8ArgConversionNeedsContext(MDTypeKind kind) { + switch (kind) { + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canSetV8ReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canTrySetV8ObjectReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, + const std::string& valueExpr) { + switch (kind) { + case mdTypeBool: + out << " info.GetReturnValue().Set(" << valueExpr << " != 0);\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeUShort: + case mdTypeUInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " setV8DispatchInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " setV8DispatchUInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + default: + break; + } +} + bool argKindMayNeedCleanup(MDTypeKind kind) { switch (kind) { case mdTypeAnyObject: @@ -697,7 +793,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, switch (type->kind) { case mdTypeChar: { out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (!argv[" << index << "]->Int32Value(context).To(&tmpArg" + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -711,7 +807,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, case mdTypeUChar: case mdTypeUInt8: { out << " uint32_t tmpArg" << index << " = 0;\n"; - out << " if (!argv[" << index << "]->Uint32Value(context).To(&tmpArg" + out << " if (!info[" << index << "]->Uint32Value(context).To(&tmpArg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -724,7 +820,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, } case mdTypeSShort: { out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (!argv[" << index << "]->Int32Value(context).To(&tmpArg" + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -736,7 +832,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, break; } case mdTypeUShort: { - out << " if (!TryFastConvertV8UInt16Argument(env, argv[" << index + out << " if (!TryFastConvertV8UInt16Argument(env, info[" << index << "], &arg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -746,7 +842,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, break; } case mdTypeSInt: { - out << " if (!argv[" << index << "]->Int32Value(context).To(&arg" + out << " if (!info[" << index << "]->Int32Value(context).To(&arg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -756,7 +852,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, break; } case mdTypeUInt: { - out << " if (!argv[" << index << "]->Uint32Value(context).To(&arg" + out << " if (!info[" << index << "]->Uint32Value(context).To(&arg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -767,11 +863,11 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, } case mdTypeSLong: case mdTypeSInt64: { - out << " if (argv[" << index << "]->IsBigInt()) {\n"; + out << " if (info[" << index << "]->IsBigInt()) {\n"; out << " bool lossless" << index << " = false;\n"; - out << " arg" << index << " = argv[" << index + out << " arg" << index << " = info[" << index << "].As()->Int64Value(&lossless" << index << ");\n"; - out << " } else if (!argv[" << index + out << " } else if (!info[" << index << "]->IntegerValue(context).To(&arg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -782,13 +878,13 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, } case mdTypeULong: case mdTypeUInt64: { - out << " if (argv[" << index << "]->IsBigInt()) {\n"; + out << " if (info[" << index << "]->IsBigInt()) {\n"; out << " bool lossless" << index << " = false;\n"; - out << " arg" << index << " = argv[" << index + out << " arg" << index << " = info[" << index << "].As()->Uint64Value(&lossless" << index << ");\n"; out << " } else {\n"; out << " int64_t signedValue" << index << " = 0;\n"; - out << " if (!argv[" << index + out << " if (!info[" << index << "]->IntegerValue(context).To(&signedValue" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -802,7 +898,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, } case mdTypeFloat: { out << " double tmpArg" << index << " = 0.0;\n"; - out << " if (!argv[" << index << "]->NumberValue(context).To(&tmpArg" + out << " if (!info[" << index << "]->NumberValue(context).To(&tmpArg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -814,7 +910,7 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, break; } case mdTypeDouble: { - out << " if (!argv[" << index << "]->NumberValue(context).To(&arg" + out << " if (!info[" << index << "]->NumberValue(context).To(&arg" << index << ")) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; @@ -828,13 +924,13 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, break; } case mdTypeBool: { - out << " if (!argv[" << index << "]->IsBoolean()) {\n"; + out << " if (!info[" << index << "]->IsBoolean()) {\n"; if (hasCleanupArgs) { out << " cleanupManagedArgs();\n"; } out << " return false;\n"; out << " }\n"; - out << " arg" << index << " = static_cast(argv[" << index + out << " arg" << index << " = static_cast(info[" << index << "]->BooleanValue(info.GetIsolate()) ? 1 : 0);\n"; break; } @@ -1046,19 +1142,26 @@ void writeV8Wrapper(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) {\n"; - out << " if (info.Length() < " << argTypes.size() << ") {\n"; - out << " return false;\n"; - out << " }\n"; - out << " v8::Local context = info.GetIsolate()->GetCurrentContext();\n"; + out << "const v8::FunctionCallbackInfo& info, void* rvalue, " + "bool* didSetReturnValue) {\n"; if (!argTypes.empty()) { - out << " v8::Local argv[" << argTypes.size() << "];\n"; - for (size_t i = 0; i < argTypes.size(); i++) { - out << " argv[" << i << "] = info[" << i << "];\n"; + 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 << " using Fn = " << returnType << " (*)("; bool first = true; @@ -1090,6 +1193,11 @@ void writeV8Wrapper(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"; } @@ -1145,18 +1253,18 @@ void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { out << " if (!TryFastConvertV8Argument(env, static_cast(" - << static_cast(argTypeInfos[i]->kind) << "), argv[" << i + << static_cast(argTypeInfos[i]->kind) << "), info[" << i << "], &arg" << i << ")) {\n"; if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { out << " cif->argTypes[" << i - << "]->toNative(env, v8LocalValueToNapiValue(argv[" << 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, v8LocalValueToNapiValue(argv[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i << "]), &arg" << i << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; } @@ -1164,14 +1272,14 @@ void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, } else { if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { out << " cif->argTypes[" << i - << "]->toNative(env, v8LocalValueToNapiValue(argv[" << 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, v8LocalValueToNapiValue(argv[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i << "]), &arg" << i << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; } @@ -1196,6 +1304,19 @@ void writeV8Wrapper(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 From db605a7cbd9c53026a6db8e68f41fe249ee0a695 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 12:53:43 -0400 Subject: [PATCH 04/31] perf(ios): restore generated dispatch baseline --- NativeScript/ffi/V8FastNativeApi.mm | 475 ---------------------------- benchmarks/objc-dispatch/README.md | 9 +- 2 files changed, 5 insertions(+), 479 deletions(-) diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm index 08f8c95c..feb5fa9a 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -1607,471 +1607,6 @@ bool receiverClassRequiresSuperCall(Class receiverClass) { return requiresSuperCall; } -inline bool isV8DirectObjectKind(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return true; - default: - return false; - } -} - -inline bool isV8DirectIntegerKind(MDTypeKind kind) { - switch (kind) { - case mdTypeChar: - case mdTypeSInt: - case mdTypeSShort: - case mdTypeSLong: - case mdTypeSInt64: - case mdTypeUChar: - case mdTypeUInt: - case mdTypeUShort: - case mdTypeULong: - case mdTypeUInt64: - case mdTypeUInt8: - return true; - default: - return false; - } -} - -bool tryConvertV8NSUIntegerArgument(napi_env env, v8::Local value, - NSUInteger* result) { - if (env == nullptr || result == nullptr || value.IsEmpty()) { - return false; - } - - if (value->IsBigInt()) { - bool lossless = false; - *result = static_cast(value.As()->Uint64Value(&lossless)); - return true; - } - - int64_t converted = 0; - if (!value->IntegerValue(env->context()).To(&converted)) { - return false; - } - *result = static_cast(converted); - return true; -} - -inline void setV8Int64ReturnValue(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(v8::Number::New(isolate, static_cast(value))); - } -} - -inline void setV8UInt64ReturnValue(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(v8::Number::New(isolate, static_cast(value))); - } -} - -bool tryInvokeObjCV8PrimitiveReturnWithObjectArg( - v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, - MDTypeKind returnKind, id self, SEL selector, id arg0) { - switch (returnKind) { - case mdTypeBool: { - BOOL value = reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); - return true; - } - case mdTypeChar: { - int8_t value = reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUChar: - case mdTypeUInt8: { - uint8_t value = reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSShort: { - int16_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUShort: { - uint16_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSInt: { - int32_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUInt: { - uint32_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSLong: - case mdTypeSInt64: { - int64_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - setV8Int64ReturnValue(isolate, info, value); - return true; - } - case mdTypeULong: - case mdTypeUInt64: { - uint64_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - setV8UInt64ReturnValue(isolate, info, value); - return true; - } - case mdTypeFloat: { - float value = reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - case mdTypeDouble: { - double value = reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - default: - return false; - } -} - -bool tryInvokeObjCV8PrimitiveReturnWithNSUIntegerArg( - v8::Isolate* isolate, const v8::FunctionCallbackInfo& info, - MDTypeKind returnKind, id self, SEL selector, NSUInteger arg0) { - switch (returnKind) { - case mdTypeBool: { - BOOL value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); - return true; - } - case mdTypeChar: { - int8_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUChar: - case mdTypeUInt8: { - uint8_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSShort: { - int16_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUShort: { - uint16_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSInt: { - int32_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUInt: { - uint32_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSLong: - case mdTypeSInt64: { - int64_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - setV8Int64ReturnValue(isolate, info, value); - return true; - } - case mdTypeULong: - case mdTypeUInt64: { - uint64_t value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - setV8UInt64ReturnValue(isolate, info, value); - return true; - } - case mdTypeFloat: { - float value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - case mdTypeDouble: { - double value = - reinterpret_cast(objc_msgSend)(self, selector, arg0); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - default: - return false; - } -} - -bool tryInvokeObjCV8DirectFastPath(napi_env env, - const v8::FunctionCallbackInfo& info, - ObjCClassMember* method, MethodDescriptor* descriptor, - Cif* cif, id self) { - if (env == nullptr || descriptor == nullptr || cif == nullptr || self == nil || - cif->returnType == nullptr) { - return false; - } - - v8::Isolate* isolate = info.GetIsolate(); - const MDTypeKind returnKind = cif->returnType->kind; - - @try { - if (cif->argc == 0) { - switch (returnKind) { - case mdTypeBool: { - BOOL value = reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); - return true; - } - case mdTypeChar: { - int8_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUChar: - case mdTypeUInt8: { - uint8_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSShort: { - int16_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUShort: { - uint16_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSInt: { - int32_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::New(isolate, value)); - return true; - } - case mdTypeUInt: { - uint32_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, value)); - return true; - } - case mdTypeSLong: - case mdTypeSInt64: { - int64_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - constexpr int64_t kMaxSafeInteger = 9007199254740991LL; - if (value > kMaxSafeInteger || value < -kMaxSafeInteger) { - info.GetReturnValue().Set(v8::BigInt::New(isolate, value)); - } else { - info.GetReturnValue().Set(v8::Number::New(isolate, static_cast(value))); - } - return true; - } - case mdTypeULong: - case mdTypeUInt64: { - uint64_t value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; - if (value > kMaxSafeInteger) { - info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, value)); - } else { - info.GetReturnValue().Set(v8::Number::New(isolate, static_cast(value))); - } - return true; - } - case mdTypeFloat: { - float value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - case mdTypeDouble: { - double value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - info.GetReturnValue().Set(v8::Number::New(isolate, value)); - return true; - } - case mdTypeNSStringObject: { - NSString* value = - reinterpret_cast(objc_msgSend)(self, descriptor->selector); - v8::Local result; - if (!TryFastConvertV8NSStringReturnValue(env, &value, &result)) { - return false; - } - info.GetReturnValue().Set(result); - return true; - } - default: - break; - } - } - - if (returnKind == mdTypeBool && cif->argc == 1 && - static_cast(info.Length()) >= 1) { - const MDTypeKind argKind = cif->argTypes[0]->kind; - if (argKind == mdTypeSelector) { - SEL selector = nullptr; - if (!TryFastConvertV8SelectorArgument(env, info[0], &selector)) { - return false; - } - BOOL value = reinterpret_cast(objc_msgSend)( - self, descriptor->selector, selector); - info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); - return true; - } - - if (argKind == mdTypeClass || argKind == mdTypeClassObject) { - Class cls = Nil; - if (!TryFastUnwrapV8ClassArgument(env, info[0], &cls)) { - return false; - } - BOOL value = reinterpret_cast(objc_msgSend)( - self, descriptor->selector, cls); - info.GetReturnValue().Set(v8::Boolean::New(isolate, value != NO)); - return true; - } - } - - if ((returnKind == mdTypeAnyObject || returnKind == mdTypeProtocolObject || - returnKind == mdTypeClassObject) && - cif->argc == 1 && static_cast(info.Length()) >= 1) { - const MDTypeKind argKind = cif->argTypes[0]->kind; - if (isV8DirectIntegerKind(argKind)) { - NSUInteger arg0 = 0; - if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { - return false; - } - id value = reinterpret_cast(objc_msgSend)( - self, descriptor->selector, arg0); - return TryFastSetV8ObjectReturnValue( - env, info, method != nullptr ? method->bridgeState : ObjCBridgeState::InstanceData(env), - value, method != nullptr && method->returnOwned ? kOwnedObject : kUnownedObject); - } - } - - if (cif->argc == 1 && static_cast(info.Length()) >= 1) { - const MDTypeKind argKind = cif->argTypes[0]->kind; - if (isV8DirectObjectKind(argKind)) { - id arg0 = nil; - if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0)) { - return false; - } - if (tryInvokeObjCV8PrimitiveReturnWithObjectArg(isolate, info, returnKind, self, - descriptor->selector, arg0)) { - return true; - } - } else if (isV8DirectIntegerKind(argKind)) { - NSUInteger arg0 = 0; - if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { - return false; - } - if (tryInvokeObjCV8PrimitiveReturnWithNSUIntegerArg(isolate, info, returnKind, self, - descriptor->selector, arg0)) { - return true; - } - } - } - - if (returnKind == mdTypeVoid) { - if (cif->argc == 1 && static_cast(info.Length()) >= 1) { - const MDTypeKind argKind = cif->argTypes[0]->kind; - if (isV8DirectObjectKind(argKind)) { - id arg0 = nil; - if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0)) { - return false; - } - reinterpret_cast(objc_msgSend)(self, descriptor->selector, arg0); - info.GetReturnValue().Set(v8::Undefined(isolate)); - return true; - } - - if (isV8DirectIntegerKind(argKind)) { - NSUInteger arg0 = 0; - if (!tryConvertV8NSUIntegerArgument(env, info[0], &arg0)) { - return false; - } - reinterpret_cast(objc_msgSend)( - self, descriptor->selector, arg0); - info.GetReturnValue().Set(v8::Undefined(isolate)); - return true; - } - } - - if (cif->argc == 2 && static_cast(info.Length()) >= 2) { - const MDTypeKind arg0Kind = cif->argTypes[0]->kind; - const MDTypeKind arg1Kind = cif->argTypes[1]->kind; - if (isV8DirectObjectKind(arg0Kind) && isV8DirectObjectKind(arg1Kind)) { - id arg0 = nil; - id arg1 = nil; - if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0) || - !TryFastUnwrapV8ObjectArgument(env, info[1], &arg1)) { - return false; - } - reinterpret_cast(objc_msgSend)( - self, descriptor->selector, arg0, arg1); - info.GetReturnValue().Set(v8::Undefined(isolate)); - return true; - } - - if (isV8DirectObjectKind(arg0Kind) && isV8DirectIntegerKind(arg1Kind)) { - id arg0 = nil; - NSUInteger arg1 = 0; - if (!TryFastUnwrapV8ObjectArgument(env, info[0], &arg0) || - !tryConvertV8NSUIntegerArgument(env, info[1], &arg1)) { - return false; - } - reinterpret_cast(objc_msgSend)( - self, descriptor->selector, arg0, arg1); - info.GetReturnValue().Set(v8::Undefined(isolate)); - return true; - } - } - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - throwNativeScriptExceptionToV8(env, info.GetIsolate(), nativeScriptException); - return true; - } - - return false; -} - bool invokeObjCPreparedOrFfi(napi_env env, Cif* cif, id self, bool classMethod, MethodDescriptor* descriptor, uint8_t dispatchFlags, void** avalues, void* rvalue) { @@ -2342,11 +1877,6 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf canUseGeneratedDispatch ? ensureObjCV8Invoker(cif, descriptor, descriptor->dispatchFlags) : nullptr; if (invoker == nullptr) { - if (canUseGeneratedDispatch && - tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { - return true; - } - return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, propertyAccess); } @@ -2392,11 +1922,6 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf } if (!didInvoke) { - if (canUseGeneratedDispatch && - tryInvokeObjCV8DirectFastPath(env, info, method, descriptor, cif, self)) { - return true; - } - return invokeObjCSlow(env, info, method, descriptor, cif, self, receiverIsClass, propertyAccess); } diff --git a/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md index ddaa79d1..c146771e 100644 --- a/benchmarks/objc-dispatch/README.md +++ b/benchmarks/objc-dispatch/README.md @@ -16,10 +16,11 @@ The runner can execute it in three modes: `TestRunner` app, builds it, runs it in Simulator, then restores the app entry point. -The default benchmark cases include calls covered by the hand-written V8 direct -path and calls that rely on generated signature dispatch, so `gsd-on` versus -`gsd-off` shows both the shared callback-path baseline and the GSD-specific -delta. +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. Examples: From 1aa111d6df3406ab459fd3008d597ea389ba3a7f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 14:19:44 -0400 Subject: [PATCH 05/31] perf(ios): add engine native dispatch fast paths --- NativeScript/CMakeLists.txt | 2 + NativeScript/ffi/CFunction.h | 3 + NativeScript/ffi/CFunction.mm | 76 +++-- NativeScript/ffi/ClassMember.h | 8 + NativeScript/ffi/ClassMember.mm | 106 ++++++- NativeScript/ffi/HermesFastCallbackInfo.h | 54 ++++ NativeScript/ffi/JSCFastNativeApi.h | 13 + NativeScript/ffi/JSCFastNativeApi.mm | 351 +++++++++++++++++++++ NativeScript/ffi/QuickJSFastNativeApi.h | 20 ++ NativeScript/ffi/QuickJSFastNativeApi.mm | 356 ++++++++++++++++++++++ NativeScript/napi/jsc/jsc-api.cpp | 6 + NativeScript/napi/quickjs/quickjs-api.c | 6 + benchmarks/objc-dispatch/run.js | 74 ++++- 13 files changed, 1018 insertions(+), 57 deletions(-) create mode 100644 NativeScript/ffi/HermesFastCallbackInfo.h create mode 100644 NativeScript/ffi/JSCFastNativeApi.h create mode 100644 NativeScript/ffi/JSCFastNativeApi.mm create mode 100644 NativeScript/ffi/QuickJSFastNativeApi.h create mode 100644 NativeScript/ffi/QuickJSFastNativeApi.mm diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 788aa085..7fba4222 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -286,6 +286,7 @@ if(ENABLE_JS_RUNTIME) # napi napi/quickjs/quickjs-api.c napi/quickjs/jsr.cpp + ffi/QuickJSFastNativeApi.mm ) elseif(TARGET_ENGINE_JSC) @@ -298,6 +299,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/jsc/jsc-api.cpp napi/jsc/jsr.cpp + ffi/JSCFastNativeApi.mm ) endif() else() diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/CFunction.h index beaab398..f1947def 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -12,6 +12,9 @@ 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(); diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index e91c312d..8a8007c1 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -4,6 +4,7 @@ #include #include "Block.h" #include "ClassMember.h" +#include "HermesFastCallbackInfo.h" #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -81,13 +82,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 +140,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; @@ -254,18 +248,59 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { } napi_value CFunction::jsCall(napi_env env, napi_callback_info cbinfo) { - void* _offset; +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* args = stackArgs; + const size_t actualArgc = fastInfo->argc; + if (actualArgc > 16) { + heapArgs.resize(actualArgc); + args = heapArgs.data(); + } + for (size_t i = 0; i < actualArgc; i++) { + args[i] = HermesFastArg(fastInfo, i); + } + + return jsCallDirect( + env, static_cast(reinterpret_cast(fastInfo->data)), + actualArgc, args); + } +#endif + + void* _offset = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, &_offset); + 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); @@ -279,23 +314,8 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { 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; diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/ClassMember.h index fb507de3..1295615c 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -83,6 +83,14 @@ class ObjCClassMember { static napi_value jsGetter(napi_env env, napi_callback_info cbinfo); static napi_value jsReadOnlySetter(napi_env env, napi_callback_info cbinfo); static napi_value jsSetter(napi_env env, napi_callback_info cbinfo); + static napi_value jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* callArgs); + static napi_value jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis); + static napi_value jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value); + static napi_value jsReadOnlySetterDirect(napi_env env); void addOverload(SEL selector, MDSectionOffset offset, uint8_t dispatchFlags); ObjCClassMember(ObjCBridgeState* bridgeState, SEL selector, diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index b88bc5cd..330bcfaa 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -13,6 +13,7 @@ #include "ClassBuilder.h" #include "Closure.h" #include "Interop.h" +#include "HermesFastCallbackInfo.h" #include "MetadataReader.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -1419,13 +1420,51 @@ explicit CifReturnStorage(Cif* cif) { } napi_value ObjCClassMember::jsCall(napi_env env, napi_callback_info cbinfo) { +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* args = stackArgs; + const size_t actualArgc = fastInfo->argc; + if (actualArgc > 16) { + heapArgs.resize(actualArgc); + args = heapArgs.data(); + } + for (size_t i = 0; i < actualArgc; i++) { + args[i] = HermesFastArg(fastInfo, i); + } + + return jsCallDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), actualArgc, args); + } +#endif + napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; size_t actualArgc = 16; napi_value stackArgs[16]; napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, &jsThis, (void**)&method); + if (actualArgc > 16) { + std::vector dynamicArgs(actualArgc); + size_t argcRetry = actualArgc; + napi_get_cb_info(env, cbinfo, &argcRetry, dynamicArgs.data(), &jsThis, (void**)&method); + dynamicArgs.resize(argcRetry); + return jsCallDirect(env, method, jsThis, argcRetry, dynamicArgs.data()); + } + + return jsCallDirect(env, method, jsThis, actualArgc, stackArgs); +} + +napi_value ObjCClassMember::jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* rawCallArgs) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C method metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1461,17 +1500,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(); } @@ -1756,11 +1788,28 @@ 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)) { + return jsGetterDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo)); + } +#endif + napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, nullptr, nullptr, &jsThis, (void**)&method); + return jsGetterDirect(env, method, jsThis); +} + +napi_value ObjCClassMember::jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C getter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1822,17 +1871,44 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } napi_value ObjCClassMember::jsReadOnlySetter(napi_env env, napi_callback_info cbinfo) { + return jsReadOnlySetterDirect(env); +} + +napi_value ObjCClassMember::jsReadOnlySetterDirect(napi_env env) { napi_throw_error(env, nullptr, "Attempted to assign to readonly property."); return nullptr; } napi_value ObjCClassMember::jsSetter(napi_env env, napi_callback_info cbinfo) { +#ifdef TARGET_ENGINE_HERMES + if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + napi_value value = nullptr; + if (fastInfo->argc > 0) { + value = HermesFastArg(fastInfo, 0); + } else { + napi_get_undefined(env, &value); + } + return jsSetterDirect(env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), value); + } +#endif + napi_value jsThis, argv; size_t argc = 1; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, &argc, &argv, &jsThis, (void**)&method); + return jsSetterDirect(env, method, jsThis, argv); +} + +napi_value ObjCClassMember::jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C setter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1850,7 +1926,7 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } if (cif->argc > 0) { - cif->argv[0] = argv; + cif->argv[0] = value; } bool didDirectInvoke = false; @@ -1875,7 +1951,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/HermesFastCallbackInfo.h b/NativeScript/ffi/HermesFastCallbackInfo.h new file mode 100644 index 00000000..26718648 --- /dev/null +++ b/NativeScript/ffi/HermesFastCallbackInfo.h @@ -0,0 +1,54 @@ +#ifndef NS_HERMES_FAST_CALLBACK_INFO_H +#define NS_HERMES_FAST_CALLBACK_INFO_H + +#include "js_native_api.h" + +#ifdef TARGET_ENGINE_HERMES + +#include +#include + +namespace nativescript { + +struct HermesFastCallbackInfo { + napi_env env = nullptr; + const uint64_t* thisArg = nullptr; + const uint64_t* argsBase = nullptr; + unsigned int argc = 0; + void* data = nullptr; + const uint64_t* newTarget = nullptr; +}; + +inline const HermesFastCallbackInfo* TryGetHermesFastCallbackInfo( + napi_env env, napi_callback_info cbinfo) { + if (env == nullptr || cbinfo == nullptr) { + return nullptr; + } + + auto* info = reinterpret_cast(cbinfo); + if (info->env != env || info->thisArg == nullptr || info->argsBase == nullptr) { + return nullptr; + } + + return info; +} + +inline napi_value HermesFastThisArg(const HermesFastCallbackInfo* info) { + return reinterpret_cast(const_cast(info->thisArg)); +} + +inline napi_value HermesFastArg(const HermesFastCallbackInfo* info, + size_t index) { + if (index >= info->argc) { + return nullptr; + } + + return reinterpret_cast( + const_cast(info->argsBase - (index + 1))); +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES + +#endif // NS_HERMES_FAST_CALLBACK_INFO_H diff --git a/NativeScript/ffi/JSCFastNativeApi.h b/NativeScript/ffi/JSCFastNativeApi.h new file mode 100644 index 00000000..8644d5b8 --- /dev/null +++ b/NativeScript/ffi/JSCFastNativeApi.h @@ -0,0 +1,13 @@ +#ifndef NS_JSC_FAST_NATIVE_API_H +#define NS_JSC_FAST_NATIVE_API_H + +#include "js_native_api.h" + +namespace nativescript { + +bool JSCTryDefineFastNativeProperty(napi_env env, napi_value object, + const napi_property_descriptor* descriptor); + +} // 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..dae40432 --- /dev/null +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -0,0 +1,351 @@ +#include "JSCFastNativeApi.h" + +#ifdef TARGET_ENGINE_JSC + +#import + +#include + +#include "CFunction.h" +#include "ClassMember.h" +#include "MetadataReader.h" +#include "ObjCBridge.h" +#include "jsc-api.h" + +namespace nativescript { +namespace { + +enum class JSCFastNativeKind : uint8_t { + ObjCMethod = 1, + ObjCGetter = 2, + ObjCSetter = 3, + ObjCReadOnlySetter = 4, + CFunction = 5, +}; + +struct JSCFastNativeBinding { + napi_env env = nullptr; + JSCFastNativeKind kind = JSCFastNativeKind::ObjCMethod; + void* data = nullptr; +}; + +inline JSValueRef ToJSValue(napi_value value) { + return reinterpret_cast(value); +} + +inline napi_value ToNapi(JSValueRef value) { + return reinterpret_cast(const_cast(value)); +} + +class ScopedJSString { + public: + explicit ScopedJSString(const char* value) + : value_(JSStringCreateWithUTF8CString(value != nullptr ? value : "")) {} + + ~ScopedJSString() { + if (value_ != nullptr) { + JSStringRelease(value_); + } + } + + operator JSStringRef() const { return value_; } + + private: + JSStringRef value_ = nullptr; +}; + +bool isCompatCFunction(napi_env env, void* data) { + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || data == nullptr) { + return true; + } + + auto offset = static_cast(reinterpret_cast(data)); + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || + strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || + strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +void initializeFastFunction(JSContextRef ctx, JSObjectRef object) { + JSObjectRef global = JSContextGetGlobalObject(ctx); + JSValueRef functionCtorValue = + JSObjectGetProperty(ctx, global, ScopedJSString("Function"), nullptr); + JSObjectRef functionCtor = JSValueToObject(ctx, functionCtorValue, nullptr); + if (functionCtor == nullptr) { + return; + } + + JSValueRef functionPrototype = + JSObjectGetProperty(ctx, functionCtor, ScopedJSString("prototype"), nullptr); + JSObjectRef functionPrototypeObject = + JSValueToObject(ctx, functionPrototype, nullptr); + if (functionPrototypeObject != nullptr) { + JSObjectSetPrototype(ctx, object, functionPrototype); + for (const char* name : {"bind", "call", "apply"}) { + ScopedJSString propertyName(name); + JSValueRef property = + JSObjectGetProperty(ctx, functionPrototypeObject, propertyName, nullptr); + if (property != nullptr && !JSValueIsUndefined(ctx, property)) { + JSObjectSetProperty(ctx, object, propertyName, property, + kJSPropertyAttributeDontEnum, nullptr); + } + } + } +} + +JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, + JSObjectRef thisObject, size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + auto* binding = + static_cast(JSObjectGetPrivate(function)); + if (binding == nullptr || binding->env == nullptr) { + return JSValueMakeUndefined(ctx); + } + + napi_env env = binding->env; + env->last_error.error_code = napi_ok; + env->last_error.engine_error_code = 0; + env->last_error.engine_reserved = nullptr; + + JSValueRef effectiveThis = + thisObject != nullptr ? thisObject : JSContextGetGlobalObject(ctx); + napi_value stackArgs[16]; + std::vector heapArgs; + napi_value* argv = stackArgs; + if (argumentCount > 16) { + heapArgs.resize(argumentCount); + argv = heapArgs.data(); + } + for (size_t i = 0; i < argumentCount; i++) { + argv[i] = ToNapi(arguments[i]); + } + napi_value jsThis = ToNapi(effectiveThis); + napi_value result = nullptr; + + switch (binding->kind) { + case JSCFastNativeKind::ObjCMethod: + result = ObjCClassMember::jsCallDirect( + env, static_cast(binding->data), jsThis, + argumentCount, argv); + break; + + case JSCFastNativeKind::ObjCGetter: + result = ObjCClassMember::jsGetterDirect( + env, static_cast(binding->data), jsThis); + break; + + case JSCFastNativeKind::ObjCSetter: { + JSValueRef undefined = JSValueMakeUndefined(ctx); + napi_value value = + argumentCount > 0 ? ToNapi(arguments[0]) : ToNapi(undefined); + result = ObjCClassMember::jsSetterDirect( + env, static_cast(binding->data), jsThis, value); + break; + } + + case JSCFastNativeKind::ObjCReadOnlySetter: + result = ObjCClassMember::jsReadOnlySetterDirect(env); + break; + + case JSCFastNativeKind::CFunction: + result = CFunction::jsCallDirect( + env, static_cast( + reinterpret_cast(binding->data)), + argumentCount, argv); + break; + } + + if (env->last_exception != nullptr) { + if (exception != nullptr) { + *exception = env->last_exception; + } + env->last_exception = nullptr; + return JSValueMakeUndefined(ctx); + } + + return result != nullptr ? ToJSValue(result) : JSValueMakeUndefined(ctx); +} + +void finalizeFastFunction(JSObjectRef object) { + delete static_cast(JSObjectGetPrivate(object)); +} + +JSClassRef fastFunctionClass() { + static JSClassRef cls = [] { + JSClassDefinition definition = kJSClassDefinitionEmpty; + definition.className = "NativeScriptFastNativeFunction"; + definition.initialize = initializeFastFunction; + definition.callAsFunction = callFastFunction; + definition.finalize = finalizeFastFunction; + return JSClassCreate(&definition); + }(); + return cls; +} + +JSObjectRef makeFastFunction(napi_env env, JSCFastNativeKind kind, + void* data) { + auto* binding = new JSCFastNativeBinding{env, kind, data}; + JSObjectRef function = JSObjectMake(env->context, fastFunctionClass(), binding); + if (function == nullptr) { + delete binding; + } + return function; +} + +bool setDescriptorValue(JSContextRef ctx, JSObjectRef descriptor, + const char* name, JSValueRef value) { + JSValueRef exception = nullptr; + JSObjectSetProperty(ctx, descriptor, ScopedJSString(name), value, + kJSPropertyAttributeNone, &exception); + return exception == nullptr; +} + +bool defineProperty(napi_env env, napi_value object, + const napi_property_descriptor* descriptor, + JSValueRef propertyName, JSObjectRef value, + JSObjectRef getter, JSObjectRef setter) { + JSContextRef ctx = env->context; + JSValueRef objectValue = ToJSValue(object); + if (!JSValueIsObject(ctx, objectValue)) { + return false; + } + + JSObjectRef jsObject = JSValueToObject(ctx, objectValue, nullptr); + JSObjectRef propertyDescriptor = JSObjectMake(ctx, nullptr, nullptr); + if (propertyDescriptor == nullptr) { + return false; + } + + if (!setDescriptorValue(ctx, propertyDescriptor, "configurable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_configurable) != 0)) || + !setDescriptorValue(ctx, propertyDescriptor, "enumerable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_enumerable) != 0))) { + return false; + } + + if (getter != nullptr || setter != nullptr) { + if (getter != nullptr && + !setDescriptorValue(ctx, propertyDescriptor, "get", getter)) { + return false; + } + if (setter != nullptr && + !setDescriptorValue(ctx, propertyDescriptor, "set", setter)) { + return false; + } + } else if (value != nullptr) { + if (!setDescriptorValue(ctx, propertyDescriptor, "writable", + JSValueMakeBoolean( + ctx, (descriptor->attributes & + napi_writable) != 0)) || + !setDescriptorValue(ctx, propertyDescriptor, "value", value)) { + return false; + } + } else { + return false; + } + + JSObjectRef global = JSContextGetGlobalObject(ctx); + JSValueRef objectCtorValue = + JSObjectGetProperty(ctx, global, ScopedJSString("Object"), nullptr); + JSObjectRef objectCtor = JSValueToObject(ctx, objectCtorValue, nullptr); + if (objectCtor == nullptr) { + return false; + } + + JSValueRef definePropertyValue = + JSObjectGetProperty(ctx, objectCtor, ScopedJSString("defineProperty"), + nullptr); + JSObjectRef definePropertyFunction = + JSValueToObject(ctx, definePropertyValue, nullptr); + if (definePropertyFunction == nullptr) { + return false; + } + + JSValueRef args[] = {jsObject, propertyName, propertyDescriptor}; + JSValueRef exception = nullptr; + JSObjectCallAsFunction(ctx, definePropertyFunction, objectCtor, 3, args, + &exception); + return exception == nullptr; +} + +bool makePropertyName(napi_env env, const napi_property_descriptor* descriptor, + JSValueRef* propertyName) { + if (descriptor->utf8name != nullptr) { + *propertyName = + JSValueMakeString(env->context, ScopedJSString(descriptor->utf8name)); + return true; + } + if (descriptor->name != nullptr) { + *propertyName = ToJSValue(descriptor->name); + return true; + } + return false; +} + +} // namespace + +bool JSCTryDefineFastNativeProperty( + napi_env env, napi_value object, + const napi_property_descriptor* descriptor) { + if (env == nullptr || object == nullptr || descriptor == nullptr) { + return false; + } + + JSValueRef propertyName = nullptr; + if (!makePropertyName(env, descriptor, &propertyName)) { + return false; + } + + if (descriptor->method == ObjCClassMember::jsCall && + descriptor->data != nullptr) { + JSObjectRef function = makeFastFunction( + env, JSCFastNativeKind::ObjCMethod, descriptor->data); + return function != nullptr && + defineProperty(env, object, descriptor, propertyName, function, + nullptr, nullptr); + } + + if (descriptor->method == CFunction::jsCall && descriptor->data != nullptr && + !isCompatCFunction(env, descriptor->data)) { + JSObjectRef function = makeFastFunction( + env, JSCFastNativeKind::CFunction, descriptor->data); + return function != nullptr && + defineProperty(env, object, descriptor, propertyName, function, + nullptr, nullptr); + } + + if (descriptor->getter == ObjCClassMember::jsGetter && + descriptor->data != nullptr) { + JSObjectRef getter = makeFastFunction( + env, JSCFastNativeKind::ObjCGetter, descriptor->data); + JSObjectRef setter = nullptr; + if (descriptor->setter == ObjCClassMember::jsSetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCSetter, + descriptor->data); + } else if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { + setter = makeFastFunction(env, JSCFastNativeKind::ObjCReadOnlySetter, + descriptor->data); + } else if (descriptor->setter != nullptr) { + return false; + } + + return getter != nullptr && + (descriptor->setter == nullptr || setter != nullptr) && + defineProperty(env, object, descriptor, propertyName, nullptr, + getter, setter); + } + + return false; +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/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..c5467503 --- /dev/null +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -0,0 +1,356 @@ +#include "QuickJSFastNativeApi.h" + +#ifdef TARGET_ENGINE_QUICKJS + +#include +#include + +#include +#include + +#include "CFunction.h" +#include "ClassMember.h" +#include "MetadataReader.h" +#include "ObjCBridge.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 napi_env__ { + JSValue referenceSymbolValue; + napi_runtime runtime; + JSContext* context; + LIST_HEAD(, napi_handle_scope__) handleScopeList; +}; + +namespace { + +enum QuickJSFastNativeKind : int { + kQuickJSFastObjCMethod = 1, + kQuickJSFastObjCGetter = 2, + kQuickJSFastObjCSetter = 3, + kQuickJSFastObjCReadOnlySetter = 4, + kQuickJSFastCFunction = 5, +}; + +inline JSValue ToJSValue(napi_value value) { + return value != nullptr ? *reinterpret_cast(value) : JS_UNDEFINED; +} + +bool readPointerData(JSContext* context, JSValue value, void** result) { + if (result == nullptr) { + return false; + } + + uint64_t raw = 0; + if (JS_ToBigUint64(context, &raw, value) != 0) { + *result = nullptr; + return false; + } + + *result = reinterpret_cast(static_cast(raw)); + return true; +} + +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; +} + +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; +}; + +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; + } + + void* data = nullptr; + if (!readPointerData(context, funcData[0], &data)) { + 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]); + } + + QuickJSFastStackHandleScope scope(env); + + napi_value jsThis = reinterpret_cast(&effectiveThis); + 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(JSContext* context, int kind, void* data) { + JSValue dataValue = JS_NewBigUint64( + context, static_cast(reinterpret_cast(data))); + 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 + +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(context, 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(context, 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(context, kQuickJSFastObjCGetter, descriptor->data); + if (JS_IsException(getter)) { + return false; + } + + JSValue setter = JS_UNDEFINED; + if (descriptor->setter == nativescript::ObjCClassMember::jsSetter) { + setter = + makeFastFunction(context, kQuickJSFastObjCSetter, descriptor->data); + if (JS_IsException(setter)) { + return false; + } + } else if (descriptor->setter == + nativescript::ObjCClassMember::jsReadOnlySetter) { + setter = makeFastFunction(context, 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/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 50fcbf14..5be6187d 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; @@ -1282,6 +1284,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)); diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index 4213296a..a2c16177 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) { diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js index c2edbacb..29624763 100644 --- a/benchmarks/objc-dispatch/run.js +++ b/benchmarks/objc-dispatch/run.js @@ -32,6 +32,7 @@ function parseArgs(argv) { buildTimeoutMs: 15 * 60 * 1000, napiPackageTgz: "", napiV8NapiBackendPackageTgz: "", + napiVariantLabel: "", skipBuild: false, compareResults: "" }; @@ -62,6 +63,8 @@ function parseArgs(argv) { else if (arg.startsWith("--napi-package-tgz=")) args.napiPackageTgz = path.resolve(arg.slice("--napi-package-tgz=".length)); else if (arg === "--napi-v8-napi-backend-package-tgz") args.napiV8NapiBackendPackageTgz = path.resolve(next()); else if (arg.startsWith("--napi-v8-napi-backend-package-tgz=")) args.napiV8NapiBackendPackageTgz = path.resolve(arg.slice("--napi-v8-napi-backend-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-napi-v8-napi-backend") args.includeNapiV8NapiBackend = true; else if (arg === "--include-legacy-aot-off") args.includeLegacyAotOff = true; @@ -100,6 +103,7 @@ Options: --napi-package-tgz PATH @nativescript/ios package tgz for napi-ios --napi-v8-napi-backend-package-tgz PATH @nativescript/ios tgz built with TARGET_ENGINE=v8 and NS_GSD_BACKEND=napi + --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-napi-v8-napi-backend Also run V8 runtime compiled to use the N-API GSD/callback path @@ -292,6 +296,21 @@ 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|gsd-v8-napi-backend)$/); + if (!match) { + return null; + } + return { + label: match[1] || "", + kind: match[2] + }; +} + function resultMap(report) { return new Map(report.results.map((result) => [result.name, result])); } @@ -360,14 +379,36 @@ function printComparisons(reports) { const baseline = reports[0]; printTotalsComparison(reports, baseline); - const napiGsdOn = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-on"); - const napiGsdOff = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-off"); - const napiV8NapiBackend = reports.find((report) => report.runtime === "napi-ios" && report.variant === "gsd-v8-napi-backend"); - if (napiGsdOn && napiGsdOff) { - printPairComparison(napiGsdOn, napiGsdOff); + 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); } - if (napiGsdOn && napiV8NapiBackend) { - printPairComparison(napiGsdOn, napiV8NapiBackend); + + let napiGsdOn = null; + for (const group of napiGroups.values()) { + const gsdOn = group.get("gsd-on"); + const gsdOff = group.get("gsd-off"); + const v8NapiBackend = group.get("gsd-v8-napi-backend"); + if (gsdOn && !napiGsdOn) { + napiGsdOn = gsdOn; + } + if (gsdOn && gsdOff) { + printPairComparison(gsdOn, gsdOff); + } + if (gsdOn && v8NapiBackend) { + printPairComparison(gsdOn, v8NapiBackend); + } } const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); @@ -743,7 +784,7 @@ function renamePlaceholderPaths(root, search, replacement) { } } -function scaffoldNapiIOSApp(options, variant, packageTgz) { +function scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant = variant) { const appName = "NativeScriptDispatchBench"; const bundleId = "org.nativescript.bench.dispatch.napi"; const tgz = packageTgz || options.napiPackageTgz || findDefaultNapiPackage(); @@ -785,7 +826,7 @@ function scaffoldNapiIOSApp(options, variant, packageTgz) { 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", variant, options); + 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")]); @@ -827,8 +868,8 @@ function writeInfoPlist(plistPath) { `); } -async function runNapiIOS(options, variant, packageTgz) { - const app = scaffoldNapiIOSApp(options, variant, packageTgz); +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); @@ -881,15 +922,20 @@ async function main() { reports.push(runNapiNode(options, "gsd-off")); } } else if (runtime === "napi-ios") { - reports.push(await runNapiIOS(options, "gsd-on")); + reports.push(await runNapiIOS(options, "gsd-on", undefined, labeledNapiVariant(options, "gsd-on"))); if (options.includeNapiV8NapiBackend) { if (!options.napiV8NapiBackendPackageTgz) { throw new Error("--include-napi-v8-napi-backend requires --napi-v8-napi-backend-package-tgz"); } - reports.push(await runNapiIOS(options, "gsd-v8-napi-backend", options.napiV8NapiBackendPackageTgz)); + reports.push(await runNapiIOS( + options, + "gsd-v8-napi-backend", + options.napiV8NapiBackendPackageTgz, + labeledNapiVariant(options, "gsd-v8-napi-backend") + )); } if (options.includeNapiGsdOff) { - reports.push(await runNapiIOS(options, "gsd-off")); + reports.push(await runNapiIOS(options, "gsd-off", undefined, labeledNapiVariant(options, "gsd-off"))); } } else if (runtime === "legacy-ios") { reports.push(await runLegacyIOS(options, "aot-on")); From a173af5b034945432a11ee9705b1bbe1ea7be233 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 15:33:04 -0400 Subject: [PATCH 06/31] perf(ios): add direct gsd backends for engines --- NativeScript/CMakeLists.txt | 35 +- NativeScript/ffi/CFunction.h | 1 + NativeScript/ffi/CFunction.mm | 24 +- NativeScript/ffi/ClassMember.h | 1 + NativeScript/ffi/ClassMember.mm | 42 +- NativeScript/ffi/HermesFastNativeApi.mm | 385 ++++++++++++ NativeScript/ffi/JSCFastNativeApi.mm | 523 ++++++++++++++++ NativeScript/ffi/QuickJSFastNativeApi.mm | 560 ++++++++++++++++++ NativeScript/ffi/SignatureDispatch.h | 89 ++- NativeScript/ffi/TypeConv.h | 42 ++ NativeScript/napi/jsc/jsc-api.cpp | 22 + NativeScript/napi/jsc/jsc-api.h | 4 + .../src/SignatureDispatchEmitter.cpp | 253 +++++++- scripts/build_nativescript.sh | 9 + 14 files changed, 1971 insertions(+), 19 deletions(-) create mode 100644 NativeScript/ffi/HermesFastNativeApi.mm diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 7fba4222..d715578c 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -20,11 +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, napi, or none") +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 napi none) +set_property(CACHE NS_GSD_BACKEND PROPERTY STRINGS auto v8 jsc quickjs hermes napi none) if (BUILD_MACOS_NODE_API) set(BUILD_FRAMEWORK OFF) @@ -137,10 +137,19 @@ 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}") @@ -151,6 +160,15 @@ 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})") @@ -254,6 +272,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp + ffi/HermesFastNativeApi.mm ) elseif(TARGET_ENGINE_QUICKJS) @@ -389,11 +408,17 @@ elseif(TARGET_ENGINE_JSC) endif() if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8") - target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=1 NS_GSD_BACKEND_NAPI=0) + 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_NAPI=1) + 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_NAPI=0) + 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}") diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/CFunction.h index f1947def..42ac39ef 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -28,6 +28,7 @@ class CFunction { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; void* v8Invoker = nullptr; }; diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 8a8007c1..864b1971 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -183,6 +183,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->dispatchId = 0; function->preparedInvoker = nullptr; function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; function->v8Invoker = nullptr; } return; @@ -199,6 +200,8 @@ 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 @@ -309,6 +312,8 @@ 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); @@ -337,9 +342,14 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; - if (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch && !isMainEntrypoint) { + if ((engineDirectInvoker != nullptr || + (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && + !isMainEntrypoint) { @try { - if (!napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue)) { + bool invoked = engineDirectInvoker != nullptr + ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue) + : napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue); + if (!invoked) { return nullptr; } } @catch (NSException* exception) { @@ -350,6 +360,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); } @@ -431,6 +446,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/ClassMember.h b/NativeScript/ffi/ClassMember.h index 1295615c..62ac9628 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -33,6 +33,7 @@ class MethodDescriptor { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + void* engineDirectInvoker = nullptr; void* v8Invoker = nullptr; bool nserrorOutSignatureCached = false; bool nserrorOutSignature = false; diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index 330bcfaa..df40875d 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -320,7 +320,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; } @@ -344,6 +344,8 @@ 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 @@ -351,16 +353,27 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho } } - auto invoker = descriptor != nullptr - ? reinterpret_cast(descriptor->napiInvoker) - : lookupObjCNapiInvoker(composeSignatureDispatchId( - cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); - if (invoker == nullptr) { + ObjCEngineDirectInvoker engineInvoker = + descriptor != nullptr + ? reinterpret_cast(descriptor->engineDirectInvoker) + : lookupObjCEngineDirectInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); + ObjCNapiInvoker invoker = + engineInvoker == nullptr && !cif->skipGeneratedNapiDispatch + ? (descriptor != nullptr + ? reinterpret_cast(descriptor->napiInvoker) + : lookupObjCNapiInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags))) + : nullptr; + if (engineInvoker == nullptr && invoker == nullptr) { return true; } @try { - if (!invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue)) { + bool invoked = engineInvoker != nullptr + ? engineInvoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue) + : invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue); + if (!invoked) { return false; } } @catch (NSException* exception) { @@ -419,6 +432,8 @@ 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)); @@ -1675,7 +1690,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; @@ -1867,6 +1888,11 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa method->returnOwned ? kOwnedObject : kUnownedObject); } + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } return cif->returnType->toJS(env, rvalue, 0); } diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm new file mode 100644 index 00000000..6e7c5a78 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -0,0 +1,385 @@ +#include "TypeConv.h" + +#ifdef TARGET_ENGINE_HERMES + +#include +#include +#include +#include +#include +#include + +namespace nativescript { +namespace { + +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 double hermesRawToDouble(uint64_t raw) { + double value = 0.0; + std::memcpy(&value, &raw, sizeof(value)); + return value; +} + +SEL cachedSelectorForName(const char* selectorName, size_t length) { + struct LastSelectorCacheEntry { + std::string name; + SEL selector = nullptr; + }; + + static thread_local LastSelectorCacheEntry lastSelector; + if (lastSelector.selector != nullptr && lastSelector.name.size() == length && + memcmp(lastSelector.name.data(), selectorName, length) == 0) { + return lastSelector.selector; + } + + static thread_local std::unordered_map selectorCache; + std::string key(selectorName, length); + auto cached = selectorCache.find(key); + if (cached != selectorCache.end()) { + lastSelector.name = cached->first; + lastSelector.selector = cached->second; + return cached->second; + } + + SEL selector = sel_registerName(key.c_str()); + if (selectorCache.size() < 4096) { + auto inserted = selectorCache.emplace(std::move(key), selector); + lastSelector.name = inserted.first->first; + } else { + lastSelector.name.assign(selectorName, length); + } + lastSelector.selector = selector; + return selector; +} + +bool tryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + size_t length = 0; + napi_status status = napi_get_value_string_utf8( + env, value, stackBuffer, kStackCapacity, &length); + if (status == napi_ok && length + 1 < kStackCapacity) { + *result = cachedSelectorForName(stackBuffer, length); + return true; + } + + if (status == napi_ok || status == napi_string_expected) { + if (status == napi_string_expected) { + napi_valuetype valueType = napi_undefined; + if (napi_typeof(env, value, &valueType) == napi_ok && + (valueType == napi_null || valueType == napi_undefined)) { + *result = nullptr; + return true; + } + return false; + } + + if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != + napi_ok) { + return false; + } + + std::vector heapBuffer(length + 1, '\0'); + if (napi_get_value_string_utf8(env, value, heapBuffer.data(), + heapBuffer.size(), &length) != napi_ok) { + return false; + } + *result = cachedSelectorForName(heapBuffer.data(), length); + return true; + } + + return false; +} + +bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + void* wrapped = nullptr; + if (napi_unwrap(env, value, &wrapped) != napi_ok || wrapped == nullptr) { + return false; + } + + if (kind == mdTypeClass) { + id nativeObject = static_cast(wrapped); + if (!object_isClass(nativeObject)) { + return false; + } + *reinterpret_cast(result) = static_cast(wrapped); + return true; + } + + *reinterpret_cast(result) = static_cast(wrapped); + return true; +} + +} // namespace + +bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = *reinterpret_cast(value); + switch (kind) { + case mdTypeBool: + if (!isHermesBool(raw)) { + return false; + } + *reinterpret_cast(result) = + (raw & kHermesBoolBit) != 0 ? 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 (!isHermesNumber(raw)) { + return false; + } + double converted = hermesRawToDouble(raw); + 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 (isHermesNumber(raw)) { + double converted = hermesRawToDouble(raw); + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + return TryFastConvertNapiUInt16Argument(env, value, + reinterpret_cast(result)); + + case mdTypeSLong: + case mdTypeSInt64: + if (isHermesNumber(raw)) { + double converted = hermesRawToDouble(raw); + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + { + bool lossless = false; + return napi_get_value_bigint_int64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + + case mdTypeULong: + case mdTypeUInt64: + if (isHermesNumber(raw)) { + double converted = hermesRawToDouble(raw); + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + { + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + + case mdTypeSelector: + return tryFastConvertHermesSelectorArgument( + env, value, reinterpret_cast(result)); + + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (tryFastUnwrapHermesObjectArgument(env, kind, value, result)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + default: + return false; + } +} + +bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeVoid: + return napi_get_null(env, result) == napi_ok; + + case mdTypeBool: + if (value == nullptr) { + return false; + } + return napi_get_boolean( + env, *reinterpret_cast(value) != 0, result) == + napi_ok; + + case mdTypeChar: { + if (value == nullptr) { + return false; + } + const int8_t raw = *reinterpret_cast(value); + if (raw == 0 || raw == 1) { + return napi_get_boolean(env, raw == 1, result) == napi_ok; + } + return napi_create_int32(env, raw, result) == napi_ok; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) { + return false; + } + const uint8_t raw = *reinterpret_cast(value); + if (raw == 0 || raw == 1) { + return napi_get_boolean(env, raw == 1, result) == napi_ok; + } + return napi_create_uint32(env, raw, result) == napi_ok; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + return napi_create_int32( + env, *reinterpret_cast(value), result) == + napi_ok; + + case mdTypeUShort: { + if (value == nullptr) { + return false; + } + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + return napi_create_string_utf8(env, buffer, NAPI_AUTO_LENGTH, + result) == napi_ok; + } + return napi_create_uint32(env, raw, result) == napi_ok; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + return napi_create_int32( + env, *reinterpret_cast(value), result) == + napi_ok; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + return napi_create_uint32( + env, *reinterpret_cast(value), result) == + napi_ok; + + 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; + } + return napi_create_int64(env, raw, result) == napi_ok; + } + + 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; + } + return napi_create_int64(env, static_cast(raw), result) == napi_ok; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + return napi_create_double( + env, *reinterpret_cast(value), result) == napi_ok; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + return napi_create_double( + env, *reinterpret_cast(value), result) == napi_ok; + + default: + return false; + } +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index dae40432..b0668991 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -4,12 +4,18 @@ #import +#include +#include +#include +#include +#include #include #include "CFunction.h" #include "ClassMember.h" #include "MetadataReader.h" #include "ObjCBridge.h" +#include "TypeConv.h" #include "jsc-api.h" namespace nativescript { @@ -290,8 +296,525 @@ bool makePropertyName(napi_env env, const napi_property_descriptor* descriptor, 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) { + 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)) { + return false; + } + *reinterpret_cast(result) = static_cast(wrapped); + return true; + } + + *reinterpret_cast(result) = + normalizeWrappedNativeObject(env, kind, wrapped); + return true; +} + } // namespace +bool TryFastConvertJSCArgument(napi_env env, MDTypeKind kind, napi_value value, + void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + switch (kind) { + case mdTypeBool: + if (!JSValueIsBoolean(ctx, jsValue)) { + return false; + } + *reinterpret_cast(result) = + JSValueToBoolean(ctx, jsValue) ? static_cast(1) : static_cast(0); + return true; + + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeFloat: + case mdTypeDouble: { + if (!JSValueIsNumber(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + switch (kind) { + case mdTypeChar: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeUChar: + case mdTypeUInt8: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeSShort: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeSInt: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeUInt: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeFloat: + *reinterpret_cast(result) = static_cast(converted); + break; + case mdTypeDouble: + *reinterpret_cast(result) = converted; + break; + default: + break; + } + return true; + } + + case mdTypeUShort: + if (JSValueIsString(ctx, jsValue)) { + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(ctx, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return false; + } + const size_t length = JSStringGetLength(str); + if (length != 1) { + JSStringRelease(str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *reinterpret_cast(result) = + static_cast(JSStringGetCharactersPtr(str)[0]); + JSStringRelease(str); + return true; + } + if (JSValueIsNumber(ctx, jsValue)) { + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + return false; + + case mdTypeSLong: + case mdTypeSInt64: + case mdTypeULong: + case mdTypeUInt64: + if (JSValueIsNumber(ctx, jsValue)) { + JSValueRef exception = nullptr; + double converted = JSValueToNumber(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + *reinterpret_cast(result) = static_cast(converted); + } else { + *reinterpret_cast(result) = static_cast(converted); + } + return true; + } + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + *reinterpret_cast(result) = + JSValueToInt64(ctx, jsValue, &exception); + } else { + *reinterpret_cast(result) = + JSValueToUInt64(ctx, jsValue, &exception); + } + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + if (kind == mdTypeSLong || kind == mdTypeSInt64) { + bool lossless = false; + return napi_get_value_bigint_int64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + { + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, reinterpret_cast(result), + &lossless) == napi_ok; + } + + case mdTypeSelector: { + SEL* selector = reinterpret_cast(result); + if (JSValueIsNull(ctx, jsValue) || JSValueIsUndefined(ctx, jsValue)) { + *selector = nullptr; + return true; + } + if (!JSValueIsString(ctx, jsValue)) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + std::vector heapBuffer; + const char* selectorName = nullptr; + size_t selectorLength = 0; + if (!readJSCStringUTF8(env, jsValue, &selectorName, &selectorLength, + stackBuffer, kStackCapacity, &heapBuffer)) { + return false; + } + *selector = cachedSelectorForName(selectorName, selectorLength); + return true; + } + + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (tryFastConvertJSCObjectArgument(env, kind, jsValue, result)) { + return true; + } + return TryFastConvertNapiArgument(env, kind, value, result); + + default: + return false; + } +} + +bool TryFastConvertJSCReturnValue(napi_env env, MDTypeKind kind, + const void* value, napi_value* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = nullptr; + switch (kind) { + case mdTypeVoid: + jsValue = JSValueMakeNull(ctx); + break; + + case mdTypeBool: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeBoolean( + ctx, *reinterpret_cast(value) != 0); + break; + + case mdTypeChar: { + if (value == nullptr) { + return false; + } + const int8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 + ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + break; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) { + return false; + } + const uint8_t raw = *reinterpret_cast(value); + jsValue = raw == 0 || raw == 1 + ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + break; + } + + case mdTypeSShort: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeUShort: { + if (value == nullptr) { + return false; + } + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + jsValue = JSValueMakeString(ctx, ScopedJSString(buffer)); + } else { + jsValue = JSValueMakeNumber(ctx, raw); + } + break; + } + + case mdTypeSInt: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeUInt: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) { + return false; + } + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_int64(env, raw, &bigint) == napi_ok) { + *result = bigint; + return true; + } + } + jsValue = JSValueMakeNumber(ctx, static_cast(raw)); + break; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) { + return false; + } + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (raw > kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_uint64(env, raw, &bigint) == napi_ok) { + *result = bigint; + return true; + } + } + jsValue = JSValueMakeNumber(ctx, static_cast(raw)); + break; + } + + case mdTypeFloat: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + case mdTypeDouble: + if (value == nullptr) { + return false; + } + jsValue = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + break; + + default: + return false; + } + + *result = ToNapi(jsValue); + return true; +} + bool JSCTryDefineFastNativeProperty( napi_env env, napi_value object, const napi_property_descriptor* descriptor) { diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index c5467503..365f612a 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -2,16 +2,23 @@ #ifdef TARGET_ENGINE_QUICKJS +#import + #include #include #include +#include +#include +#include +#include #include #include "CFunction.h" #include "ClassMember.h" #include "MetadataReader.h" #include "ObjCBridge.h" +#include "TypeConv.h" #include "mimalloc.h" #include "quicks-runtime.h" @@ -47,11 +54,57 @@ 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 { @@ -289,6 +342,513 @@ bool defineFastProperty(napi_env env, napi_value object, } // 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 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) { + 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; + 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)) { + return false; + } + *reinterpret_cast(result) = static_cast(wrapped); + return true; + } + + *reinterpret_cast(result) = + normalizeWrappedNativeObject(env, kind, wrapped); + return true; +} + +} // namespace + +bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContext* context = qjs_get_context(env); + if (context == nullptr) { + return false; + } + + JSValue jsValue = ToJSValue(value); + switch (kind) { + case mdTypeBool: + if (!JS_IsBool(jsValue)) { + return false; + } + *reinterpret_cast(result) = + JS_VALUE_GET_BOOL(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: { + double converted = 0.0; + if (!readQuickJSNumber(jsValue, &converted)) { + 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 (JS_IsString(jsValue)) { + size_t length = 0; + const char* str = JS_ToCStringLen(context, &length, jsValue); + if (str == nullptr) { + return false; + } + if (length != 1) { + JS_FreeCString(context, str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *reinterpret_cast(result) = static_cast(str[0]); + JS_FreeCString(context, str); + return true; + } + { + double converted = 0.0; + if (!readQuickJSNumber(jsValue, &converted)) { + return false; + } + *reinterpret_cast(result) = static_cast(converted); + return true; + } + + case mdTypeSLong: + case mdTypeSInt64: + return readQuickJSInt64(context, jsValue, + reinterpret_cast(result)); + + case mdTypeULong: + case mdTypeUInt64: + return readQuickJSUInt64(context, jsValue, + reinterpret_cast(result)); + + case mdTypeSelector: { + SEL* selector = reinterpret_cast(result); + if (JS_IsNull(jsValue) || JS_IsUndefined(jsValue)) { + *selector = nullptr; + return true; + } + if (!JS_IsString(jsValue)) { + return false; + } + size_t length = 0; + const char* selectorName = JS_ToCStringLen(context, &length, jsValue); + if (selectorName == nullptr) { + return false; + } + *selector = cachedSelectorForName(selectorName, length); + JS_FreeCString(context, selectorName); + return true; + } + + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (tryFastConvertQuickJSObjectArgument(env, kind, jsValue, 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) { diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index babb9a4c..017eb5ac 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -33,6 +33,14 @@ using ObjCNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, 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, @@ -71,6 +79,16 @@ 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; @@ -152,8 +170,35 @@ bool TryFastSetV8GeneratedObjCObjectReturnValue( #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 +#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_ENGINE_DIRECT #define NS_GSD_BACKEND_NAPI 0 #else #define NS_GSD_BACKEND_NAPI 1 @@ -163,6 +208,15 @@ bool TryFastSetV8GeneratedObjCObjectReturnValue( #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 @@ -176,6 +230,10 @@ bool TryFastSetV8GeneratedObjCObjectReturnValue( #define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 0 #endif +#ifndef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 0 +#endif + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" @@ -202,6 +260,15 @@ 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[] = { @@ -292,6 +359,26 @@ inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { 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()) { diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index dee44ae3..a0264f6f 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -72,6 +72,48 @@ bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, 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 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 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 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 +} + // Cleanup function to clear thread-local struct type caches void clearStructTypeCaches(); diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 5be6187d..8aee16f3 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -2110,6 +2110,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..7a33d960 100644 --- a/NativeScript/napi/jsc/jsc-api.h +++ b/NativeScript/napi/jsc/jsc-api.h @@ -15,6 +15,10 @@ #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{}; diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index d10018fc..cd1dfbf3 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -420,6 +420,24 @@ std::string makeV8WrapperName(DispatchKind kind, size_t 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"; @@ -1114,6 +1132,154 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, 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++) { + out << " if (!NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT(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 << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\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 writeV8Wrapper(std::ostringstream& out, DispatchKind kind, const std::string& wrapperName, const MDSignature* signature) { @@ -1533,6 +1699,8 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, preparedWrappersByKey; std::unordered_map objcNapiEntries; std::unordered_map cFunctionNapiEntries; + std::unordered_map objcEngineDirectEntries; + std::unordered_map cFunctionEngineDirectEntries; std::unordered_map objcV8Entries; std::unordered_map cFunctionV8Entries; std::unordered_map blockPreparedEntries; @@ -1565,6 +1733,8 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collidedDispatchIds.insert(dispatchId); objcNapiEntries.erase(dispatchId); cFunctionNapiEntries.erase(dispatchId); + objcEngineDirectEntries.erase(dispatchId); + cFunctionEngineDirectEntries.erase(dispatchId); objcV8Entries.erase(dispatchId); cFunctionV8Entries.erase(dispatchId); blockPreparedEntries.erase(dispatchId); @@ -1584,10 +1754,12 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, if (use.kind == DispatchKind::ObjCMethod) { wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); objcNapiEntries.emplace(dispatchId, wrapperKey); + objcEngineDirectEntries.emplace(dispatchId, wrapperKey); objcV8Entries.emplace(dispatchId, wrapperKey); } else if (use.kind == DispatchKind::CFunction) { wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); cFunctionNapiEntries.emplace(dispatchId, wrapperKey); + cFunctionEngineDirectEntries.emplace(dispatchId, wrapperKey); cFunctionV8Entries.emplace(dispatchId, wrapperKey); } else if (use.kind == DispatchKind::BlockInvoke) { preparedWrappersByKey.emplace(wrapperKey, @@ -1629,6 +1801,16 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, makeV8WrapperName(wrapper.second.first, v8WrapperIndex++)); } + std::unordered_map engineDirectWrapperNameByKey; + engineDirectWrapperNameByKey.reserve(wrappers.size()); + size_t engineDirectWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + engineDirectWrapperNameByKey.emplace( + wrapper.first, + makeEngineDirectWrapperName(wrapper.second.first, + engineDirectWrapperIndex++)); + } + std::unordered_map preparedWrapperNameByKey; preparedWrapperNameByKey.reserve(preparedWrappers.size()); size_t preparedWrapperIndex = 0; @@ -1650,6 +1832,23 @@ 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( @@ -1671,7 +1870,8 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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\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 1\n"; generated << "#endif\n"; @@ -1682,10 +1882,15 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, generated << "#if NS_GSD_BACKEND_V8\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH\n"; generated << "#define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 1\n"; generated << "#endif\n\n"; generated << "namespace nativescript {\n\n"; - generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI\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), @@ -1700,6 +1905,27 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, } 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 << "#elif NS_GSD_BACKEND_QUICKJS\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertQuickJSArgument\n"; + generated << "#elif NS_GSD_BACKEND_HERMES\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertHermesArgument\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 << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_V8\n"; for (const auto& wrapper : wrappers) { writeV8Wrapper(generated, wrapper.second.first, @@ -1707,7 +1933,8 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, } generated << "#endif\n\n"; - generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI\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"; @@ -1728,6 +1955,26 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, generated << "};\n\n"; generated << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "inline constexpr ObjCEngineDirectDispatchEntry " + "kGeneratedObjCEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionEngineDirectDispatchEntry " + "kGeneratedCFunctionEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_NAPI\n"; generated << "inline constexpr ObjCNapiDispatchEntry " "kGeneratedObjCNapiDispatchEntries[] = {\n"; diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index e7b8d5d7..ade3fb57 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -42,6 +42,9 @@ for arg in $@; do --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=}" ;; @@ -74,6 +77,12 @@ function effective_gsd_backend () { 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 From 2547d38d5bcde64acd42b7ffccce7930a569ca55 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 17:52:14 -0400 Subject: [PATCH 07/31] perf: add engine-direct native dispatch paths --- NativeScript/CMakeLists.txt | 1 + NativeScript/ffi/CFunction.mm | 11 + NativeScript/ffi/ClassMember.mm | 48 +- NativeScript/ffi/EngineDirectCall.h | 35 + NativeScript/ffi/EngineDirectCall.mm | 665 ++++++++++++ NativeScript/ffi/HermesFastNativeApi.h | 31 + NativeScript/ffi/HermesFastNativeApi.mm | 723 ++++++++++++- NativeScript/ffi/JSCFastNativeApi.mm | 954 ++++++++++++++++++ NativeScript/ffi/QuickJSFastNativeApi.mm | 790 ++++++++++++++- NativeScript/ffi/TypeConv.h | 26 + .../src/SignatureDispatchEmitter.cpp | 192 +++- 11 files changed, 3420 insertions(+), 56 deletions(-) create mode 100644 NativeScript/ffi/EngineDirectCall.h create mode 100644 NativeScript/ffi/EngineDirectCall.mm create mode 100644 NativeScript/ffi/HermesFastNativeApi.h diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index d715578c..1f9be3f6 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -198,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 diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 864b1971..3b1ea423 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -5,6 +5,8 @@ #include "Block.h" #include "ClassMember.h" #include "HermesFastCallbackInfo.h" +#include "HermesFastNativeApi.h" +#include "EngineDirectCall.h" #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -265,6 +267,15 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { args[i] = HermesFastArg(fastInfo, i); } + bool handledDirect = false; + napi_value directResult = TryCallHermesCFunctionFast( + env, + static_cast(reinterpret_cast(fastInfo->data)), + actualArgc, args, &handledDirect); + if (handledDirect) { + return directResult; + } + return jsCallDirect( env, static_cast(reinterpret_cast(fastInfo->data)), actualArgc, args); diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index df40875d..56896794 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -9,11 +9,14 @@ #include #include #include +#include #include #include "ClassBuilder.h" #include "Closure.h" +#include "EngineDirectCall.h" #include "Interop.h" #include "HermesFastCallbackInfo.h" +#include "HermesFastNativeApi.h" #include "MetadataReader.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" @@ -1091,6 +1094,12 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = ObjCBridgeState* bridgeState_; }; +inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + namespace { inline size_t alignUpSize(size_t value, size_t alignment) { @@ -1449,6 +1458,15 @@ explicit CifReturnStorage(Cif* cif) { args[i] = HermesFastArg(fastInfo, i); } + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), actualArgc, args, + EngineDirectMemberKind::Method, &handledDirect); + if (handledDirect) { + return directResult; + } + return jsCallDirect(env, static_cast(fastInfo->data), HermesFastThisArg(fastInfo), actualArgc, args); } @@ -1486,8 +1504,6 @@ explicit CifReturnStorage(Cif* cif) { 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* { @@ -1601,6 +1617,11 @@ explicit CifReturnStorage(Cif* cif) { return nullptr; } + std::optional roundTripCacheFrame; + if (generatedDispatchNeedsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + CifReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { napi_throw_error(env, "NativeScriptException", @@ -1811,6 +1832,14 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_value ObjCClassMember::jsGetter(napi_env env, napi_callback_info cbinfo) { #ifdef TARGET_ENGINE_HERMES if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), 0, nullptr, + EngineDirectMemberKind::Getter, &handledDirect); + if (handledDirect) { + return directResult; + } return jsGetterDirect(env, static_cast(fastInfo->data), HermesFastThisArg(fastInfo)); } @@ -1914,6 +1943,14 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } else { napi_get_undefined(env, &value); } + bool handledDirect = false; + napi_value directResult = TryCallHermesObjCMemberFast( + env, static_cast(fastInfo->data), + HermesFastThisArg(fastInfo), 1, &value, + EngineDirectMemberKind::Setter, &handledDirect); + if (handledDirect) { + return directResult; + } return jsSetterDirect(env, static_cast(fastInfo->data), HermesFastThisArg(fastInfo), value); } @@ -1943,14 +1980,17 @@ 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] = value; } diff --git a/NativeScript/ffi/EngineDirectCall.h b/NativeScript/ffi/EngineDirectCall.h new file mode 100644 index 00000000..a587c9b3 --- /dev/null +++ b/NativeScript/ffi/EngineDirectCall.h @@ -0,0 +1,35 @@ +#ifndef NS_ENGINE_DIRECT_CALL_H +#define NS_ENGINE_DIRECT_CALL_H + +#include +#include + +#include "MetadataReader.h" +#include "js_native_api.h" + +namespace nativescript { + +using metagen::MDSectionOffset; + +class ObjCClassMember; + +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); + +} // 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..75d8136a --- /dev/null +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -0,0 +1,665 @@ +#include "EngineDirectCall.h" + +#import +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "CFunction.h" +#include "Class.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "NativeScriptException.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" + +namespace nativescript { +namespace { + +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + +class RoundTripCacheFrameGuard { + public: + RoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) + : env_(env), bridgeState_(bridgeState) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~RoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; +}; + +class CifReturnStorage { + public: + explicit CifReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~CifReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeNSErrorOutMethodSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty()) { + return false; + } + if (!selectorEndsWith(selector, "error:")) { + return false; + } + + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isNSErrorOutMethodSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeNSErrorOutMethodSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeNSErrorOutMethodSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline void throwArgumentsCountError(napi_env env, size_t actualCount, + size_t expectedCount) { + std::string message = "Actual arguments count: \"" + + std::to_string(actualCount) + "\". Expected: \"" + + std::to_string(expectedCount) + "\"."; + napi_throw_error(env, "NativeScriptException", message.c_str()); +} + +inline bool isBlockFallbackSelector(SEL selector) { + const char* selectorName = sel_getName(selector); + return selectorName != nullptr && + (std::strcmp(selectorName, "methodWithSimpleBlock:") == 0 || + std::strcmp(selectorName, "methodRetainingBlock:") == 0 || + std::strcmp(selectorName, "methodWithBlock:") == 0 || + std::strcmp(selectorName, "methodWithComplexBlock:") == 0); +} + +id resolveSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + napi_status unwrapStatus = + self != nil ? napi_ok : napi_unwrap(env, jsThis, reinterpret_cast(&self)); + + if ((unwrapStatus != napi_ok || self == nil) && jsThis != nullptr) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, jsThis, kNativePointerProperty, + &nativePointerValue) == napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); + if (nativePointer != nullptr && nativePointer->data != nullptr) { + self = static_cast(nativePointer->data); + unwrapStatus = napi_ok; + } + } + } + + if (unwrapStatus == napi_ok && self != nil) { + return self; + } + + bool shouldUseClassFallback = false; + if (method != nullptr && method->cls != nullptr && + method->cls->nativeClass != nil) { + if (method->classMethod) { + shouldUseClassFallback = true; + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + napi_value definingConstructor = get_ref_value(env, method->cls->constructor); + if (definingConstructor != nullptr) { + bool isSameConstructor = false; + if (napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } + } + } + } else { + napi_valuetype jsType = napi_undefined; + if (napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + shouldUseClassFallback = true; + } + } + } + + if (shouldUseClassFallback) { + return static_cast(method->cls->nativeClass); + } + + napi_throw_error(env, "NativeScriptException", + "There was no native counterpart to the JavaScript object. " + "Native API was called with a likely plain object."); + return nil; +} + +Cif* resolveMethodDescriptorCif(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif** cacheSlot, + bool receiverIsClass, Class receiverClass) { + if (env == nullptr || member == nullptr || descriptor == nullptr || + cacheSlot == nullptr) { + return nullptr; + } + + Cif* cached = *cacheSlot; + if (cached != nullptr) { + return cached; + } + + Method runtimeMethod = receiverIsClass + ? class_getClassMethod(receiverClass, descriptor->selector) + : class_getInstanceMethod(receiverClass, descriptor->selector); + Cif* resolved = nullptr; + if (runtimeMethod != nullptr) { + resolved = member->bridgeState->getMethodCif(env, runtimeMethod); + } + if (resolved == nullptr) { + resolved = member->bridgeState->getMethodCif(env, descriptor->signatureOffset); + } + + *cacheSlot = resolved; + return resolved; +} + +inline bool receiverClassRequiresSuperCall(Class receiverClass) { + if (receiverClass == nil) { + return false; + } + + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + class_conformsToProtocol(receiverClass, @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +ObjCEngineDirectInvoker ensureObjCEngineDirectInvoker(Cif* cif, + MethodDescriptor* descriptor, + uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); +#ifdef TARGET_ENGINE_V8 + descriptor->v8Invoker = + reinterpret_cast(lookupObjCV8Invoker(descriptor->dispatchId)); +#endif + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureCFunctionEngineDirectInvoker(CFunction* function, + Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); +#ifdef TARGET_ENGINE_V8 + function->v8Invoker = + reinterpret_cast(lookupCFunctionV8Invoker(function->dispatchId)); +#endif + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +const napi_value* prepareInvocationArgs(napi_env env, Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + std::vector* paddedArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + paddedArgs->assign(cif->argc, jsUndefined); + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + std::memcpy(paddedArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return paddedArgs->data(); +} + +napi_value convertObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, + id self, bool receiverIsClass, + napi_value jsThis, void* rvalue, + bool propertyAccess) { + if (member == nullptr || descriptor == nullptr || cif == nullptr) { + return nullptr; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + return constructor; + } + + id classObject = receiverIsClass ? self : static_cast(object_getClass(self)); + return member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + + id obj = *reinterpret_cast(rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + + return member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + } + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + return cif->returnType->toJS(env, rvalue, member->returnOwned ? kReturnOwned : 0); +} + +napi_value convertCFunctionReturnValue(napi_env env, CFunction* function, + Cif* cif, void* rvalue) { + if (cif == nullptr) { + return nullptr; + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + return cif->returnType->toJS(env, rvalue, toJSFlags); +} + +bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return true; + } + + const char* name = bridgeState->metadata->getString(offset); + return name == nullptr || + std::strcmp(name, "dispatch_async") == 0 || + std::strcmp(name, "dispatch_get_current_queue") == 0 || + std::strcmp(name, "dispatch_get_global_queue") == 0 || + std::strcmp(name, "UIApplicationMain") == 0 || + std::strcmp(name, "NSApplicationMain") == 0; +} + +} // namespace + +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); + if (invoker == nullptr) { + return nullptr; + } + + 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 { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, invocationArgs, rvalue); + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return convertObjCReturnValue(env, member, descriptor, cif, self, + receiverIsClass, jsThis, rvalue, propertyAccess); +} + +napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (env == nullptr || bridgeState == nullptr || + isCompatOrMainCFunction(bridgeState, offset)) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic) { + return nullptr; + } + + CFunctionEngineDirectInvoker invoker = + ensureCFunctionEngineDirectInvoker(function, cif); + if (invoker == nullptr) { + return nullptr; + } + + 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 { + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, 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/HermesFastNativeApi.h b/NativeScript/ffi/HermesFastNativeApi.h new file mode 100644 index 00000000..6a71ab58 --- /dev/null +++ b/NativeScript/ffi/HermesFastNativeApi.h @@ -0,0 +1,31 @@ +#ifndef NS_HERMES_FAST_NATIVE_API_H +#define NS_HERMES_FAST_NATIVE_API_H + +#include + +#include "EngineDirectCall.h" +#include "MetadataReader.h" +#include "js_native_api.h" + +#ifdef TARGET_ENGINE_HERMES + +namespace nativescript { + +class ObjCClassMember; + +napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled); + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled); + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES + +#endif // NS_HERMES_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 6e7c5a78..959a83d1 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -1,17 +1,36 @@ -#include "TypeConv.h" +#include "HermesFastNativeApi.h" #ifdef TARGET_ENGINE_HERMES +#import +#include +#include + +#include #include +#include #include +#include #include +#include #include #include #include +#include "CFunction.h" +#include "Class.h" +#include "ClassBuilder.h" +#include "ClassMember.h" +#include "Interop.h" +#include "NativeScriptException.h" +#include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" + namespace nativescript { namespace { +constexpr const char* kNativePointerProperty = "__ns_native_ptr"; constexpr uint64_t kHermesFirstTaggedValue = 0xfff9000000000000ULL; constexpr uint64_t kHermesBoolETag = 0x1fff6ULL; constexpr uint64_t kHermesBoolBit = 1ULL << 46; @@ -30,6 +49,25 @@ inline double hermesRawToDouble(uint64_t raw) { return value; } +inline napi_value makeHermesRawValue(uint64_t raw) { + static thread_local uint64_t slots[8] = {}; + static thread_local unsigned int nextSlot = 0; + uint64_t* slot = &slots[nextSlot++ & 7]; + *slot = raw; + return reinterpret_cast(slot); +} + +inline napi_value makeHermesRawNumberValue(double value) { + uint64_t raw = 0; + std::memcpy(&raw, &value, sizeof(raw)); + return makeHermesRawValue(raw); +} + +inline napi_value makeHermesRawBoolValue(bool value) { + return makeHermesRawValue((kHermesBoolETag << 47) | + (value ? kHermesBoolBit : 0)); +} + SEL cachedSelectorForName(const char* selectorName, size_t length) { struct LastSelectorCacheEntry { std::string name; @@ -130,6 +168,474 @@ bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, return true; } +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && + (cif->generatedDispatchHasRoundTripCacheArgument || + cif->generatedDispatchUsesObjectReturnStorage); +} + +class HermesFastRoundTripCacheFrameGuard { + public: + HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) + : env_(env), bridgeState_(bridgeState) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + ~HermesFastRoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; +}; + +class HermesFastReturnStorage { + public: + explicit HermesFastReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~HermesFastReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +const napi_value* prepareHermesInvocationArgs(napi_env env, Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + napi_value* stackArgs, + size_t stackCapacity, + std::vector* heapArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + + if (cif->argc <= stackCapacity) { + for (unsigned int i = 0; i < cif->argc; i++) { + stackArgs[i] = i < actualArgc && rawArgs != nullptr ? rawArgs[i] : jsUndefined; + } + return stackArgs; + } + + heapArgs->assign(cif->argc, jsUndefined); + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + std::memcpy(heapArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return heapArgs->data(); +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeNSErrorOutMethodSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isNSErrorOutMethodSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeNSErrorOutMethodSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeNSErrorOutMethodSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline void throwArgumentsCountError(napi_env env, size_t actualCount, + size_t expectedCount) { + std::string message = "Actual arguments count: \"" + + std::to_string(actualCount) + "\". Expected: \"" + + std::to_string(expectedCount) + "\"."; + napi_throw_error(env, "NativeScriptException", message.c_str()); +} + +inline bool isBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + + napi_status unwrapStatus = napi_invalid_arg; + if (jsThis != nullptr) { + unwrapStatus = napi_unwrap(env, jsThis, reinterpret_cast(&self)); + if (unwrapStatus == napi_ok && self != nil) { + return self; + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &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 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* hermesMemberCif(napi_env env, ObjCClassMember* member, + EngineDirectMemberKind kind, + MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresHermesSuperCall(Class receiverClass) { + if (receiverClass == nil) { + return false; + } + + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + class_conformsToProtocol(receiverClass, @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +ObjCEngineDirectInvoker ensureHermesObjCEngineDirectInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureHermesCFunctionEngineDirectInvoker( + CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, + id self, bool receiverIsClass, + napi_value jsThis, void* rvalue, + bool propertyAccess) { + if (member == nullptr || descriptor == nullptr || cif == nullptr || + cif->returnType == nullptr) { + return nullptr; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + return constructor; + } + + id classObject = receiverIsClass ? self : static_cast(object_getClass(self)); + return member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + + id obj = *reinterpret_cast(rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + + return member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + } + + if (cif->returnType->kind == mdTypeAnyObject && receiverIsClass) { + id obj = *reinterpret_cast(rvalue); + Class receiverClass = static_cast(self); + if (obj != nil && + (receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + } + } + + if (cif->returnType->kind == mdTypeAnyObject || + cif->returnType->kind == mdTypeProtocolObject || + cif->returnType->kind == mdTypeClassObject) { + id obj = *reinterpret_cast(rvalue); + if (obj != nil && ![obj isKindOfClass:[NSString class]] && + ![obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSNull class]]) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); + if (cached == nullptr) { + cached = state->findCachedObjectWrapper(env, obj); + } + if (cached != nullptr) { + return cached; + } + } + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertHermesReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + return cif->returnType->toJS(env, rvalue, + member->returnOwned ? kReturnOwned : 0); +} + +napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, + Cif* cif, void* rvalue) { + if (cif == nullptr || cif->returnType == nullptr) { + return nullptr; + } + + napi_value fastResult = nullptr; + if (TryFastConvertHermesReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + return fastResult; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + return cif->returnType->toJS(env, rvalue, toJSFlags); +} + +bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { + if (bridgeState == nullptr) { + return true; + } + + const char* name = bridgeState->metadata->getString(offset); + return name == nullptr || + std::strcmp(name, "dispatch_async") == 0 || + std::strcmp(name, "dispatch_get_current_queue") == 0 || + std::strcmp(name, "dispatch_get_global_queue") == 0 || + std::strcmp(name, "UIApplicationMain") == 0 || + std::strcmp(name, "NSApplicationMain") == 0; +} + } // namespace bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, @@ -271,9 +777,9 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - return napi_get_boolean( - env, *reinterpret_cast(value) != 0, result) == - napi_ok; + *result = + makeHermesRawBoolValue(*reinterpret_cast(value) != 0); + return true; case mdTypeChar: { if (value == nullptr) { @@ -281,9 +787,11 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, } const int8_t raw = *reinterpret_cast(value); if (raw == 0 || raw == 1) { - return napi_get_boolean(env, raw == 1, result) == napi_ok; + *result = makeHermesRawBoolValue(raw == 1); + return true; } - return napi_create_int32(env, raw, result) == napi_ok; + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; } case mdTypeUChar: @@ -293,18 +801,20 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, } const uint8_t raw = *reinterpret_cast(value); if (raw == 0 || raw == 1) { - return napi_get_boolean(env, raw == 1, result) == napi_ok; + *result = makeHermesRawBoolValue(raw == 1); + return true; } - return napi_create_uint32(env, raw, result) == napi_ok; + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; } case mdTypeSShort: if (value == nullptr) { return false; } - return napi_create_int32( - env, *reinterpret_cast(value), result) == - napi_ok; + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; case mdTypeUShort: { if (value == nullptr) { @@ -316,24 +826,25 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, return napi_create_string_utf8(env, buffer, NAPI_AUTO_LENGTH, result) == napi_ok; } - return napi_create_uint32(env, raw, result) == napi_ok; + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; } case mdTypeSInt: if (value == nullptr) { return false; } - return napi_create_int32( - env, *reinterpret_cast(value), result) == - napi_ok; + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; case mdTypeUInt: if (value == nullptr) { return false; } - return napi_create_uint32( - env, *reinterpret_cast(value), result) == - napi_ok; + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; case mdTypeSLong: case mdTypeSInt64: { @@ -345,7 +856,8 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { return napi_create_bigint_int64(env, raw, result) == napi_ok; } - return napi_create_int64(env, raw, result) == napi_ok; + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; } case mdTypeULong: @@ -358,28 +870,191 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (raw > kMaxSafeInteger) { return napi_create_bigint_uint64(env, raw, result) == napi_ok; } - return napi_create_int64(env, static_cast(raw), result) == napi_ok; + *result = makeHermesRawNumberValue(static_cast(raw)); + return true; } case mdTypeFloat: if (value == nullptr) { return false; } - return napi_create_double( - env, *reinterpret_cast(value), result) == napi_ok; + *result = makeHermesRawNumberValue( + static_cast(*reinterpret_cast(value))); + return true; case mdTypeDouble: if (value == nullptr) { return false; } - return napi_create_double( - env, *reinterpret_cast(value), result) == napi_ok; + *result = makeHermesRawNumberValue( + *reinterpret_cast(value)); + return true; default: return false; } } +napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t actualArgc, + const napi_value* rawArgs, + EngineDirectMemberKind kind, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + if (env == nullptr || member == nullptr || member->bridgeState == nullptr) { + return nullptr; + } + + MethodDescriptor* descriptor = nullptr; + Cif* cif = hermesMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || + cif->returnType == nullptr) { + return nullptr; + } + + if (isNSErrorOutMethodSignature(descriptor, cif)) { + if (!cif->isVariadic && + (actualArgc > cif->argc || actualArgc + 1 < cif->argc)) { + throwArgumentsCountError(env, actualArgc, cif->argc); + if (handled != nullptr) { + *handled = true; + } + } + return nullptr; + } + + if (isBlockFallbackSelector(descriptor->selector)) { + return nullptr; + } + + ObjCEngineDirectInvoker invoker = + ensureHermesObjCEngineDirectInvoker(cif, descriptor, + descriptor->dispatchFlags); + if (invoker == nullptr) { + return nullptr; + } + + id self = resolveHermesSelf(env, jsThis, member); + if (self == nil) { + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = + receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresHermesSuperCall(receiverClass)) { + return nullptr; + } + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + HermesFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate return value storage for Objective-C call."); + if (handled != nullptr) { + *handled = true; + } + return nullptr; + } + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, member->bridgeState); + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, invocationArgs, rvalue); + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return makeHermesObjCReturnValue( + env, member, descriptor, cif, self, receiverIsClass, jsThis, rvalue, + kind != EngineDirectMemberKind::Method); +} + +napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* rawArgs, + bool* handled) { + if (handled != nullptr) { + *handled = false; + } + + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (env == nullptr || bridgeState == nullptr || + isCompatOrMainCFunction(bridgeState, offset)) { + return nullptr; + } + + CFunction* function = bridgeState->getCFunction(env, offset); + Cif* cif = function != nullptr ? function->cif : nullptr; + if (function == nullptr || cif == nullptr || cif->isVariadic || + cif->signatureHash == 0 || cif->returnType == nullptr) { + return nullptr; + } + + CFunctionEngineDirectInvoker invoker = + ensureHermesCFunctionEngineDirectInvoker(function, cif); + if (invoker == nullptr) { + return nullptr; + } + + napi_value stackPaddedArgs[16]; + std::vector heapPaddedArgs; + const napi_value* invocationArgs = prepareHermesInvocationArgs( + env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + + if (handled != nullptr) { + *handled = true; + } + + std::optional roundTripCacheFrame; + if (needsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, bridgeState); + } + + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (!didInvoke) { + return nullptr; + } + + return makeHermesCFunctionReturnValue(env, function, cif, cif->rvalue); +} + } // namespace nativescript #endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index b0668991..7dfe8ccc 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -4,6 +4,7 @@ #import +#include #include #include #include @@ -12,9 +13,13 @@ #include #include "CFunction.h" +#include "ClassBuilder.h" #include "ClassMember.h" +#include "EngineDirectCall.h" #include "MetadataReader.h" +#include "NativeScriptException.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" #include "TypeConv.h" #include "jsc-api.h" @@ -75,6 +80,673 @@ bool isCompatCFunction(napi_env env, void* data) { strcmp(name, "NSApplicationMain") == 0; } +class JSCFastReturnStorage { + public: + explicit JSCFastReturnStorage(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + if (size == 0) { + size = sizeof(void*); + } + + if (size <= kInlineSize) { + data_ = inlineBuffer_; + std::memset(data_, 0, size); + return; + } + + data_ = std::malloc(size); + if (data_ != nullptr) { + std::memset(data_, 0, size); + } + } + + ~JSCFastReturnStorage() { + if (data_ != nullptr && data_ != inlineBuffer_) { + std::free(data_); + } + } + + bool valid() const { return data_ != nullptr; } + void* get() const { return data_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* data_ = nullptr; +}; + +bool canMakeJSCRawReturnValue(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool makeJSCRawReturnValue(napi_env env, MDTypeKind kind, const void* value, + JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + switch (kind) { + case mdTypeVoid: + *result = JSValueMakeNull(ctx); + return true; + + case mdTypeBool: + if (value == nullptr) return false; + *result = JSValueMakeBoolean( + ctx, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: { + if (value == nullptr) return false; + const int8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) return false; + const uint8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JSValueMakeBoolean(ctx, raw == 1) + : JSValueMakeNumber(ctx, raw); + return true; + } + + case mdTypeSShort: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: { + if (value == nullptr) return false; + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[2] = {static_cast(raw), '\0'}; + *result = JSValueMakeString(ctx, ScopedJSString(buffer)); + } else { + *result = JSValueMakeNumber(ctx, raw); + } + return true; + } + + case mdTypeSInt: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) return false; + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_int64(env, raw, &bigint) == napi_ok) { + *result = ToJSValue(bigint); + return true; + } + } + *result = JSValueMakeNumber(ctx, static_cast(raw)); + return true; + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) return false; + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + if (raw > kMaxSafeInteger) { + napi_value bigint = nullptr; + if (napi_create_bigint_uint64(env, raw, &bigint) == napi_ok) { + *result = ToJSValue(bigint); + return true; + } + } + *result = JSValueMakeNumber(ctx, static_cast(raw)); + return true; + } + + case mdTypeFloat: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + case mdTypeDouble: + if (value == nullptr) return false; + *result = JSValueMakeNumber(ctx, *reinterpret_cast(value)); + return true; + + default: + return false; + } +} + +bool makeJSCNSStringValue(napi_env env, NSString* string, JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + if (string == nil) { + *result = JSValueMakeNull(env->context); + return true; + } + + NSUInteger length = [string length]; + std::vector chars(length > 0 ? length : 1); + if (length > 0) { + [string getCharacters:reinterpret_cast(chars.data()) + range:NSMakeRange(0, length)]; + } + + JSStringRef jsString = JSStringCreateWithCharacters( + reinterpret_cast(chars.data()), length); + if (jsString == nullptr) { + return false; + } + *result = JSValueMakeString(env->context, jsString); + JSStringRelease(jsString); + return true; +} + +bool makeJSCBoxedObjectValue(napi_env env, id obj, JSValueRef* result) { + if (env == nullptr || result == nullptr) { + return false; + } + + if (obj == nil || obj == [NSNull null]) { + *result = JSValueMakeNull(env->context); + return true; + } + + if ([obj isKindOfClass:[NSString class]]) { + return makeJSCNSStringValue(env, (NSString*)obj, result); + } + + if ([obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)obj) == CFBooleanGetTypeID()) { + *result = JSValueMakeBoolean(env->context, [obj boolValue]); + } else { + *result = JSValueMakeNumber(env->context, [obj doubleValue]); + } + return true; + } + + return false; +} + +id resolveJSCSelf(napi_env env, napi_value jsThis, ObjCClassMember* member) { + id self = nil; + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + + if (jsThis != nullptr) { + void* wrapped = nullptr; + if (nativescript_jsc_try_unwrap_native(env, jsThis, &wrapped) && + wrapped != nullptr) { + return static_cast(wrapped); + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + if (self == nil && jsThis != nullptr) { + napi_unwrap(env, jsThis, reinterpret_cast(&self)); + } + + if (self != nil) { + return self; + } + + if (member != nullptr && member->cls != nullptr && + member->cls->nativeClass != nil) { + if (member->classMethod) { + return static_cast(member->cls->nativeClass); + } + + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + return static_cast(member->cls->nativeClass); + } + } + + return nil; +} + +Cif* jscMemberCif(napi_env env, ObjCClassMember* member, + EngineDirectMemberKind kind, + MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresJSCSuperCall(Class receiverClass) { + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + receiverClass != nil && + class_conformsToProtocol(receiverClass, + @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeJSCNSErrorOutSignature(SEL selector, Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isJSCNSErrorOutSignature(MethodDescriptor* descriptor, Cif* cif) { + if (descriptor == nullptr) { + return computeJSCNSErrorOutSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeJSCNSErrorOutSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline bool isJSCBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +ObjCEngineDirectInvoker ensureJSCObjCEngineDirectInvoker( + Cif* cif, MethodDescriptor* descriptor, uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags); + descriptor->preparedInvoker = + reinterpret_cast(lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = + reinterpret_cast(lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = + reinterpret_cast(lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +CFunctionEngineDirectInvoker ensureJSCCFunctionEngineDirectInvoker( + CFunction* function, Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::CFunction, + function->dispatchFlags); + function->preparedInvoker = + reinterpret_cast(lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = + reinterpret_cast(lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +bool makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, napi_value jsThis, + void* rvalue, bool propertyAccess, + JSValueRef* result) { + if (env == nullptr || member == nullptr || descriptor == nullptr || + cif == nullptr || cif->returnType == nullptr || result == nullptr) { + return false; + } + + if (makeJSCRawReturnValue(env, cif->returnType->kind, rvalue, result)) { + return true; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + if (!propertyAccess && !receiverIsClass) { + napi_value constructor = jsThis; + napi_get_named_property(env, jsThis, "constructor", &constructor); + *result = ToJSValue(constructor); + return true; + } + + id classObject = receiverIsClass ? self : (id)object_getClass(self); + napi_value converted = + member->bridgeState->getObject(env, classObject, kUnownedObject, 0, nullptr); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *reinterpret_cast(rvalue); + napi_value converted = member->bridgeState->getObject( + env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + *result = converted != nullptr ? ToJSValue(converted) : JSValueMakeNull(env->context); + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeJSCNSStringValue( + env, *reinterpret_cast(rvalue), result); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (std::strcmp(selectorName, "string") == 0 || + std::strcmp(selectorName, "stringWithString:") == 0 || + std::strcmp(selectorName, "stringWithCapacity:") == 0)) { + napi_value converted = + member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; + } + } + + if (makeJSCBoxedObjectValue(env, obj, result)) { + return true; + } + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + *result = ToJSValue(fastResult); + return true; + } + + napi_value converted = + cif->returnType->toJS(env, rvalue, member->returnOwned ? kReturnOwned : 0); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; +} + +bool makeJSCCFunctionReturnValue(napi_env env, CFunction* function, Cif* cif, + void* rvalue, JSValueRef* result) { + if (env == nullptr || cif == nullptr || cif->returnType == nullptr || + result == nullptr) { + return false; + } + + if (makeJSCRawReturnValue(env, cif->returnType->kind, rvalue, result)) { + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeJSCNSStringValue( + env, *reinterpret_cast(rvalue), result); + } + if (cif->returnType->kind == mdTypeAnyObject && + makeJSCBoxedObjectValue(env, *reinterpret_cast(rvalue), result)) { + return true; + } + + napi_value fastResult = nullptr; + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, + &fastResult)) { + *result = ToJSValue(fastResult); + return true; + } + + uint32_t toJSFlags = kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= kReturnOwned; + } + napi_value converted = cif->returnType->toJS(env, rvalue, toJSFlags); + if (converted == nullptr) { + return false; + } + *result = ToJSValue(converted); + return true; +} + +bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, + napi_value jsThis, size_t argc, + const napi_value* argv, + EngineDirectMemberKind kind, + JSValueRef* result) { + if (env == nullptr || member == nullptr || member->bridgeState == nullptr || + result == nullptr) { + return false; + } + + MethodDescriptor* descriptor = nullptr; + Cif* cif = jscMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || + argc != cif->argc || cif->returnType == nullptr) { + return false; + } + + ObjCEngineDirectInvoker invoker = ensureJSCObjCEngineDirectInvoker( + cif, descriptor, descriptor->dispatchFlags); + if (invoker == nullptr) { + return false; + } + + if (isJSCNSErrorOutSignature(descriptor, cif) || + isJSCBlockFallbackSelector(descriptor->selector)) { + return false; + } + + id self = resolveJSCSelf(env, jsThis, member); + if (self == nil) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresJSCSuperCall(receiverClass)) { + return false; + } + + JSCFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, 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->signatureHash == 0 || argc != cif->argc || + cif->returnType == nullptr) { + return false; + } + + CFunctionEngineDirectInvoker invoker = + ensureJSCCFunctionEngineDirectInvoker(function, cif); + if (invoker == nullptr) { + return false; + } + + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, function->fnptr, 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 = @@ -130,6 +802,51 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, 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) { @@ -491,6 +1208,243 @@ bool tryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, } // namespace +bool TryFastConvertJSCBoolArgument(napi_env env, napi_value value, + uint8_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (!JSValueIsBoolean(env->context, jsValue)) { + return false; + } + *result = JSValueToBoolean(env->context, jsValue) ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertJSCDoubleArgument(napi_env env, napi_value value, + double* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (!JSValueIsNumber(env->context, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + double converted = JSValueToNumber(env->context, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *result = converted; + return true; +} + +bool TryFastConvertJSCFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsString(ctx, jsValue)) { + JSValueRef exception = nullptr; + JSStringRef str = JSValueToStringCopy(ctx, jsValue, &exception); + if (exception != nullptr || str == nullptr) { + env->last_exception = exception; + return false; + } + const size_t length = JSStringGetLength(str); + if (length != 1) { + JSStringRelease(str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *result = static_cast(JSStringGetCharactersPtr(str)[0]); + JSStringRelease(str); + return true; + } + + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertJSCInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNumber(ctx, jsValue)) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; + } + + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + *result = JSValueToInt64(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + + bool lossless = false; + return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertJSCUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSContextRef ctx = env->context; + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNumber(ctx, jsValue)) { + double converted = 0.0; + if (!TryFastConvertJSCDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; + } + + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + if (!JSValueIsBigInt(ctx, jsValue)) { + return false; + } + JSValueRef exception = nullptr; + *result = JSValueToUInt64(ctx, jsValue, &exception); + if (exception != nullptr) { + env->last_exception = exception; + return false; + } + return true; + } + + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertJSCSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValueRef jsValue = ToJSValue(value); + if (JSValueIsNull(env->context, jsValue) || + JSValueIsUndefined(env->context, jsValue)) { + *result = nullptr; + return true; + } + if (!JSValueIsString(env->context, jsValue)) { + return false; + } + + constexpr size_t kStackCapacity = 256; + char stackBuffer[kStackCapacity]; + std::vector heapBuffer; + const char* selectorName = nullptr; + size_t selectorLength = 0; + if (!readJSCStringUTF8(env, jsValue, &selectorName, &selectorLength, + stackBuffer, kStackCapacity, &heapBuffer)) { + return false; + } + *result = cachedSelectorForName(selectorName, selectorLength); + return true; +} + +bool TryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return tryFastConvertJSCObjectArgument(env, kind, ToJSValue(value), result); +} + bool TryFastConvertJSCArgument(napi_env env, MDTypeKind kind, napi_value value, void* result) { if (env == nullptr || value == nullptr || result == nullptr) { diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index 365f612a..f5858ef9 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -4,6 +4,7 @@ #import +#include #include #include @@ -15,9 +16,13 @@ #include #include "CFunction.h" +#include "ClassBuilder.h" #include "ClassMember.h" +#include "EngineDirectCall.h" #include "MetadataReader.h" +#include "NativeScriptException.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" #include "TypeConv.h" #include "mimalloc.h" #include "quicks-runtime.h" @@ -121,6 +126,30 @@ 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; + 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 readPointerData(JSContext* context, JSValue value, void** result) { if (result == nullptr) { return false; @@ -136,20 +165,46 @@ bool readPointerData(JSContext* context, JSValue value, void** result) { return true; } -bool isCompatCFunction(napi_env env, void* data) { - auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); - if (bridgeState == nullptr || data == nullptr) { - return true; +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); + } } - 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; -} + ~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: @@ -189,6 +244,665 @@ void close() { bool closed_ = false; }; +bool makeQuickJSRawReturnValue(JSContext* context, MDTypeKind kind, + const void* value, JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeVoid: + *result = JS_NULL; + return true; + + case mdTypeBool: + if (value == nullptr) return false; + *result = JS_NewBool(context, *reinterpret_cast(value) != 0); + return true; + + case mdTypeChar: { + if (value == nullptr) return false; + const int8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewInt32(context, raw); + return true; + } + + case mdTypeUChar: + case mdTypeUInt8: { + if (value == nullptr) return false; + const uint8_t raw = *reinterpret_cast(value); + *result = raw == 0 || raw == 1 ? JS_NewBool(context, raw == 1) + : JS_NewUint32(context, raw); + return true; + } + + case mdTypeSShort: + if (value == nullptr) return false; + *result = JS_NewInt32(context, *reinterpret_cast(value)); + return true; + + case mdTypeUShort: { + if (value == nullptr) return false; + const uint16_t raw = *reinterpret_cast(value); + if (raw >= 32 && raw <= 126) { + const char buffer[1] = {static_cast(raw)}; + *result = JS_NewStringLen(context, buffer, 1); + } else { + *result = JS_NewUint32(context, raw); + } + return !JS_IsException(*result); + } + + case mdTypeSInt: + if (value == nullptr) return false; + *result = JS_NewInt32(context, *reinterpret_cast(value)); + return true; + + case mdTypeUInt: + if (value == nullptr) return false; + *result = JS_NewUint32(context, *reinterpret_cast(value)); + return true; + + case mdTypeSLong: + case mdTypeSInt64: { + if (value == nullptr) return false; + const int64_t raw = *reinterpret_cast(value); + constexpr int64_t kMaxSafeInteger = 9007199254740991LL; + *result = raw > kMaxSafeInteger || raw < -kMaxSafeInteger + ? JS_NewBigInt64(context, raw) + : JS_NewInt64(context, raw); + return !JS_IsException(*result); + } + + case mdTypeULong: + case mdTypeUInt64: { + if (value == nullptr) return false; + const uint64_t raw = *reinterpret_cast(value); + constexpr uint64_t kMaxSafeInteger = 9007199254740991ULL; + *result = raw > kMaxSafeInteger + ? JS_NewBigUint64(context, raw) + : JS_NewInt64(context, static_cast(raw)); + return !JS_IsException(*result); + } + + case mdTypeFloat: + if (value == nullptr) return false; + *result = JS_NewFloat64(context, *reinterpret_cast(value)); + return !JS_IsException(*result); + + case mdTypeDouble: + if (value == nullptr) return false; + *result = JS_NewFloat64(context, *reinterpret_cast(value)); + return !JS_IsException(*result); + + default: + return false; + } +} + +bool makeQuickJSNSStringValue(JSContext* context, NSString* string, + JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + if (string == nil) { + *result = JS_NULL; + return true; + } + + NSUInteger length = [string length]; + std::vector chars(length > 0 ? length : 1); + if (length > 0) { + [string getCharacters:reinterpret_cast(chars.data()) + range:NSMakeRange(0, length)]; + } + + *result = JS_NewString16(context, + reinterpret_cast(chars.data()), + static_cast(length)); + return !JS_IsException(*result); +} + +bool makeQuickJSBoxedObjectValue(JSContext* context, id obj, JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + if (obj == nil || obj == [NSNull null]) { + *result = JS_NULL; + return true; + } + + if ([obj isKindOfClass:[NSString class]]) { + return makeQuickJSNSStringValue(context, (NSString*)obj, result); + } + + if ([obj isKindOfClass:[NSNumber class]] && + ![obj isKindOfClass:[NSDecimalNumber class]]) { + if (CFGetTypeID((CFTypeRef)obj) == CFBooleanGetTypeID()) { + *result = JS_NewBool(context, [obj boolValue]); + } else { + *result = JS_NewFloat64(context, [obj doubleValue]); + } + return !JS_IsException(*result); + } + + return false; +} + +bool duplicateQuickJSNapiResult(JSContext* context, napi_value value, + JSValue* result) { + if (context == nullptr || value == nullptr || result == nullptr) { + return false; + } + + *result = JS_DupValue(context, ToJSValue(value)); + return true; +} + +bool canMakeQuickJSRawReturnValue(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool isCompatCFunction(napi_env env, void* data) { + auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr || data == nullptr) { + return true; + } + + auto offset = static_cast(reinterpret_cast(data)); + const char* name = bridgeState->metadata->getString(offset); + return strcmp(name, "dispatch_async") == 0 || + strcmp(name, "dispatch_get_current_queue") == 0 || + strcmp(name, "dispatch_get_global_queue") == 0 || + strcmp(name, "UIApplicationMain") == 0 || + strcmp(name, "NSApplicationMain") == 0; +} + +id resolveQuickJSSelf(napi_env env, napi_value jsThis, + nativescript::ObjCClassMember* member) { + id self = nil; + auto* state = nativescript::ObjCBridgeState::InstanceData(env); + + if (jsThis != nullptr) { + void* wrapped = nullptr; + if (tryFastUnwrapQuickJSNativeObject(env, ToJSValue(jsThis), &wrapped) && + wrapped != nullptr) { + return static_cast(wrapped); + } + } + + if (state != nullptr && jsThis != nullptr) { + state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + } + + if (self == nil && jsThis != nullptr) { + napi_unwrap(env, jsThis, reinterpret_cast(&self)); + } + + if (self != nil) { + return self; + } + + if (member != nullptr && member->cls != nullptr && + member->cls->nativeClass != nil) { + if (member->classMethod) { + return static_cast(member->cls->nativeClass); + } + + napi_valuetype jsType = napi_undefined; + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { + return static_cast(member->cls->nativeClass); + } + } + + return nil; +} + +nativescript::Cif* quickJSMemberCif( + napi_env env, nativescript::ObjCClassMember* member, + nativescript::EngineDirectMemberKind kind, + nativescript::MethodDescriptor** descriptorOut) { + if (member == nullptr || descriptorOut == nullptr) { + return nullptr; + } + + switch (kind) { + case nativescript::EngineDirectMemberKind::Method: + if (!member->overloads.empty()) { + return nullptr; + } + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case nativescript::EngineDirectMemberKind::Getter: + *descriptorOut = &member->methodOrGetter; + if (member->cif == nullptr) { + member->cif = member->bridgeState->getMethodCif( + env, member->methodOrGetter.signatureOffset); + } + return member->cif; + + case nativescript::EngineDirectMemberKind::Setter: + *descriptorOut = &member->setter; + if (member->setterCif == nullptr) { + member->setterCif = member->bridgeState->getMethodCif( + env, member->setter.signatureOffset); + } + return member->setterCif; + } +} + +bool receiverClassRequiresQuickJSSuperCall(Class receiverClass) { + static thread_local Class lastReceiverClass = nil; + static thread_local bool lastRequiresSuperCall = false; + if (receiverClass == lastReceiverClass) { + return lastRequiresSuperCall; + } + + static thread_local std::unordered_map superCallCache; + auto cached = superCallCache.find(receiverClass); + if (cached != superCallCache.end()) { + lastReceiverClass = receiverClass; + lastRequiresSuperCall = cached->second; + return cached->second; + } + + const bool requiresSuperCall = + receiverClass != nil && + class_conformsToProtocol(receiverClass, + @protocol(ObjCBridgeClassBuilderProtocol)); + superCallCache.emplace(receiverClass, requiresSuperCall); + lastReceiverClass = receiverClass; + lastRequiresSuperCall = requiresSuperCall; + return requiresSuperCall; +} + +inline bool selectorEndsWith(SEL selector, const char* suffix) { + if (selector == nullptr || suffix == nullptr) { + return false; + } + const char* selectorName = sel_getName(selector); + if (selectorName == nullptr) { + return false; + } + + const size_t selectorLength = std::strlen(selectorName); + const size_t suffixLength = std::strlen(suffix); + return selectorLength >= suffixLength && + std::strcmp(selectorName + selectorLength - suffixLength, suffix) == 0; +} + +inline bool computeQuickJSNSErrorOutSignature(SEL selector, + nativescript::Cif* cif) { + if (cif == nullptr || cif->argc == 0 || cif->argTypes.empty() || + !selectorEndsWith(selector, "error:")) { + return false; + } + auto lastArgType = cif->argTypes[cif->argc - 1]; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; +} + +inline bool isQuickJSNSErrorOutSignature( + nativescript::MethodDescriptor* descriptor, nativescript::Cif* cif) { + if (descriptor == nullptr) { + return computeQuickJSNSErrorOutSignature(nullptr, cif); + } + + if (!descriptor->nserrorOutSignatureCached) { + descriptor->nserrorOutSignature = + computeQuickJSNSErrorOutSignature(descriptor->selector, cif); + descriptor->nserrorOutSignatureCached = true; + } + return descriptor->nserrorOutSignature; +} + +inline bool isQuickJSBlockFallbackSelector(SEL selector) { + return selector == @selector(methodWithSimpleBlock:) || + selector == @selector(methodRetainingBlock:) || + selector == @selector(methodWithBlock:) || + selector == @selector(methodWithComplexBlock:); +} + +nativescript::ObjCEngineDirectInvoker ensureQuickJSObjCEngineDirectInvoker( + nativescript::Cif* cif, nativescript::MethodDescriptor* descriptor, + uint8_t dispatchFlags) { + if (cif == nullptr || descriptor == nullptr || cif->signatureHash == 0) { + return nullptr; + } + + if (!descriptor->dispatchLookupCached || + descriptor->dispatchLookupSignatureHash != cif->signatureHash || + descriptor->dispatchLookupFlags != dispatchFlags) { + descriptor->dispatchLookupSignatureHash = cif->signatureHash; + descriptor->dispatchLookupFlags = dispatchFlags; + descriptor->dispatchId = nativescript::composeSignatureDispatchId( + cif->signatureHash, nativescript::SignatureCallKind::ObjCMethod, + dispatchFlags); + descriptor->preparedInvoker = reinterpret_cast( + nativescript::lookupObjCPreparedInvoker(descriptor->dispatchId)); + descriptor->napiInvoker = reinterpret_cast( + nativescript::lookupObjCNapiInvoker(descriptor->dispatchId)); + descriptor->engineDirectInvoker = reinterpret_cast( + nativescript::lookupObjCEngineDirectInvoker(descriptor->dispatchId)); + descriptor->dispatchLookupCached = true; + } + + return reinterpret_cast( + descriptor->engineDirectInvoker); +} + +nativescript::CFunctionEngineDirectInvoker +ensureQuickJSCFunctionEngineDirectInvoker(nativescript::CFunction* function, + nativescript::Cif* cif) { + if (function == nullptr || cif == nullptr || cif->signatureHash == 0) { + if (function != nullptr) { + function->dispatchLookupCached = true; + function->dispatchLookupSignatureHash = 0; + function->dispatchId = 0; + function->preparedInvoker = nullptr; + function->napiInvoker = nullptr; + function->engineDirectInvoker = nullptr; + function->v8Invoker = nullptr; + } + return nullptr; + } + + if (!function->dispatchLookupCached || + function->dispatchLookupSignatureHash != cif->signatureHash) { + function->dispatchLookupSignatureHash = cif->signatureHash; + function->dispatchId = nativescript::composeSignatureDispatchId( + cif->signatureHash, nativescript::SignatureCallKind::CFunction, + function->dispatchFlags); + function->preparedInvoker = reinterpret_cast( + nativescript::lookupCFunctionPreparedInvoker(function->dispatchId)); + function->napiInvoker = reinterpret_cast( + nativescript::lookupCFunctionNapiInvoker(function->dispatchId)); + function->engineDirectInvoker = reinterpret_cast( + nativescript::lookupCFunctionEngineDirectInvoker(function->dispatchId)); + function->dispatchLookupCached = true; + } + + return reinterpret_cast( + function->engineDirectInvoker); +} + +bool makeQuickJSObjCReturnValue( + JSContext* context, napi_env env, nativescript::ObjCClassMember* member, + nativescript::MethodDescriptor* descriptor, nativescript::Cif* cif, + id self, bool receiverIsClass, napi_value jsThis, void* rvalue, + bool propertyAccess, JSValue* result) { + if (context == nullptr || env == nullptr || member == nullptr || + descriptor == nullptr || cif == nullptr || cif->returnType == nullptr || + result == nullptr) { + return false; + } + + if (makeQuickJSRawReturnValue(context, cif->returnType->kind, rvalue, + result)) { + return true; + } + + const char* selectorName = sel_getName(descriptor->selector); + if (selectorName != nullptr && strcmp(selectorName, "class") == 0) { + QuickJSFastStackHandleScope scope(env); + napi_value converted = nullptr; + if (!propertyAccess && !receiverIsClass) { + converted = jsThis; + napi_get_named_property(env, jsThis, "constructor", &converted); + } else { + id classObject = receiverIsClass ? self : (id)object_getClass(self); + converted = + member->bridgeState->getObject(env, classObject, + nativescript::kUnownedObject, 0, nullptr); + } + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; + } + + if (cif->returnType->kind == mdTypeInstanceObject) { + QuickJSFastStackHandleScope scope(env); + napi_value constructor = jsThis; + if (!receiverIsClass) { + napi_get_named_property(env, jsThis, "constructor", &constructor); + } + id obj = *reinterpret_cast(rvalue); + napi_value converted = member->bridgeState->getObject( + env, obj, constructor, + member->returnOwned ? nativescript::kOwnedObject + : nativescript::kUnownedObject); + bool ok = false; + if (converted != nullptr) { + ok = duplicateQuickJSNapiResult(context, converted, result); + } else { + *result = JS_NULL; + ok = true; + } + scope.close(); + return ok; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeQuickJSNSStringValue( + context, *reinterpret_cast(rvalue), result); + } + + if (cif->returnType->kind == mdTypeAnyObject) { + id obj = *reinterpret_cast(rvalue); + if (receiverIsClass && obj != nil) { + Class receiverClass = static_cast(self); + if ((receiverClass == [NSString class] || + receiverClass == [NSMutableString class]) && + selectorName != nullptr && + (strcmp(selectorName, "string") == 0 || + strcmp(selectorName, "stringWithString:") == 0 || + strcmp(selectorName, "stringWithCapacity:") == 0)) { + QuickJSFastStackHandleScope scope(env); + napi_value converted = + member->bridgeState->getObject(env, obj, jsThis, + nativescript::kUnownedObject); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; + } + } + + if (makeQuickJSBoxedObjectValue(context, obj, result)) { + return true; + } + } + + QuickJSFastStackHandleScope scope(env); + napi_value fastResult = nullptr; + if (nativescript::TryFastConvertEngineReturnValue( + env, cif->returnType->kind, rvalue, &fastResult)) { + bool ok = duplicateQuickJSNapiResult(context, fastResult, result); + scope.close(); + return ok; + } + + napi_value converted = cif->returnType->toJS( + env, rvalue, member->returnOwned ? nativescript::kReturnOwned : 0); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; +} + +bool makeQuickJSCFunctionReturnValue(JSContext* context, napi_env env, + nativescript::CFunction* function, + nativescript::Cif* cif, void* rvalue, + JSValue* result) { + if (context == nullptr || env == nullptr || cif == nullptr || + cif->returnType == nullptr || result == nullptr) { + return false; + } + + if (makeQuickJSRawReturnValue(context, cif->returnType->kind, rvalue, + result)) { + return true; + } + + if (cif->returnType->kind == mdTypeNSStringObject) { + return makeQuickJSNSStringValue( + context, *reinterpret_cast(rvalue), result); + } + if (cif->returnType->kind == mdTypeAnyObject && + makeQuickJSBoxedObjectValue(context, *reinterpret_cast(rvalue), + result)) { + return true; + } + + QuickJSFastStackHandleScope scope(env); + napi_value fastResult = nullptr; + if (nativescript::TryFastConvertEngineReturnValue( + env, cif->returnType->kind, rvalue, &fastResult)) { + bool ok = duplicateQuickJSNapiResult(context, fastResult, result); + scope.close(); + return ok; + } + + uint32_t toJSFlags = nativescript::kCStringAsReference; + if (function != nullptr && (function->dispatchFlags & 1) != 0) { + toJSFlags |= nativescript::kReturnOwned; + } + napi_value converted = cif->returnType->toJS(env, rvalue, toJSFlags); + bool ok = duplicateQuickJSNapiResult(context, converted, result); + scope.close(); + return ok; +} + +bool tryCallQuickJSObjCEngineDirect( + JSContext* context, napi_env env, nativescript::ObjCClassMember* member, + napi_value jsThis, int argc, const napi_value* argv, + nativescript::EngineDirectMemberKind kind, JSValue* result) { + if (context == nullptr || env == nullptr || member == nullptr || + member->bridgeState == nullptr || argc < 0 || result == nullptr) { + return false; + } + + nativescript::MethodDescriptor* descriptor = nullptr; + nativescript::Cif* cif = quickJSMemberCif(env, member, kind, &descriptor); + if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || + static_cast(argc) != cif->argc || + cif->returnType == nullptr) { + return false; + } + + auto invoker = ensureQuickJSObjCEngineDirectInvoker( + cif, descriptor, descriptor->dispatchFlags); + if (invoker == nullptr) { + return false; + } + + if (isQuickJSNSErrorOutSignature(descriptor, cif) || + isQuickJSBlockFallbackSelector(descriptor->selector)) { + return false; + } + + id self = resolveQuickJSSelf(env, jsThis, member); + if (self == nil) { + return false; + } + + const bool receiverIsClass = object_isClass(self); + Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); + if (receiverClassRequiresQuickJSSuperCall(receiverClass)) { + return false; + } + + QuickJSFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + + void* rvalue = rvalueStorage.get(); + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, + descriptor->selector, 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->signatureHash == 0 || static_cast(argc) != cif->argc || + cif->returnType == nullptr) { + return false; + } + + auto invoker = ensureQuickJSCFunctionEngineDirectInvoker(function, cif); + if (invoker == nullptr) { + return false; + } + + bool didInvoke = false; + @try { + didInvoke = invoker(env, cif, function->fnptr, 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)); @@ -219,9 +933,56 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, 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 jsThis = reinterpret_cast(&effectiveThis); napi_value result = nullptr; switch (magic) { case kQuickJSFastObjCMethod: @@ -252,7 +1013,8 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, case kQuickJSFastCFunction: result = nativescript::CFunction::jsCallDirect( - env, static_cast(reinterpret_cast(data)), + env, + static_cast(reinterpret_cast(data)), static_cast(argc), napiArgs); break; diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index a0264f6f..07bf8ece 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -78,6 +78,32 @@ bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, // 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 diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index cd1dfbf3..acabcf55 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -959,6 +959,92 @@ void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, } } +const char* engineDirectConverterMacroForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT"; + case mdTypeChar: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT"; + case mdTypeUChar: + case mdTypeUInt8: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT"; + case mdTypeSShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT"; + case mdTypeUShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT"; + case mdTypeSInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT"; + case mdTypeUInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT"; + case mdTypeSLong: + case mdTypeSInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT"; + case mdTypeULong: + case mdTypeUInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT"; + case mdTypeFloat: + return "NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT"; + case mdTypeDouble: + return "NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT"; + case mdTypeSelector: + return "NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT"; + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return "NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT"; + default: + return "NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"; + } +} + +bool engineDirectConverterTakesKind(MDTypeKind kind) { + switch (kind) { + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return engineDirectConverterMacroForKind(kind) == + std::string("NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"); + } +} + +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index) { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + const char* converter = engineDirectConverterMacroForKind(type->kind); + out << " if (!" << converter << "(env, "; + if (engineDirectConverterTakesKind(type->kind)) { + out << "static_cast(" << static_cast(type->kind) + << "), "; + } + out << "argv[" << index << "], &arg" << index << ")) {\n"; + if (argKindMayNeedCleanup(type->kind)) { + out << " cif->argTypes[" << index << "]->toNative(env, argv[" << index + << "], &arg" << index << ", &shouldFree" << index + << ", &shouldFreeAny);\n"; + } else { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, argv[" << index + << "], &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; +} + void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, const std::string& wrapperName, const MDSignature* signature) { @@ -1234,20 +1320,7 @@ void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, } for (size_t i = 0; i < argTypes.size(); i++) { - out << " if (!NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT(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 << " bool ignoredShouldFree = false;\n"; - out << " bool ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i - << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; - } - out << " }\n"; + writeEngineDirectArgConversion(out, argTypeInfos[i], i); } std::ostringstream callExpr; @@ -1909,12 +1982,90 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeBool, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeChar, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeUInt8, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeSShort, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeUShort, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeSInt, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeUInt, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeSInt64, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeUInt64, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeFloat, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeDouble, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT(env, value, result) " + "TryFastConvertQuickJSArgument(env, mdTypeSelector, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT(env, kind, value, result) " + "TryFastConvertQuickJSArgument(env, kind, value, result)\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(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeBool, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeChar, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeUInt8, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeSShort, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeUShort, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeSInt, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeUInt, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeSInt64, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeUInt64, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeFloat, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeDouble, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT(env, value, result) " + "TryFastConvertHermesArgument(env, mdTypeSelector, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT(env, kind, value, result) " + "TryFastConvertHermesArgument(env, kind, value, result)\n"; generated << "#else\n"; generated << "#error \"No generated signature engine-direct converter selected\"\n"; generated << "#endif\n"; @@ -1924,6 +2075,19 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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"; From 3364db5568244cff2a1ba08b2af7ab0d920eed82 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Thu, 21 May 2026 19:30:27 -0400 Subject: [PATCH 08/31] perf: reduce direct native dispatch overhead --- NativeScript/ffi/EngineDirectCall.h | 18 + NativeScript/ffi/EngineDirectCall.mm | 411 ++++++++++++++++- NativeScript/ffi/HermesFastNativeApi.mm | 311 ++++++++----- NativeScript/ffi/JSCFastNativeApi.mm | 42 +- NativeScript/ffi/ObjCBridge.mm | 18 +- NativeScript/ffi/QuickJSFastNativeApi.mm | 418 ++++++++++++------ NativeScript/ffi/TypeConv.h | 65 +++ NativeScript/napi/quickjs/quickjs-api.c | 14 + benchmarks/objc-dispatch/README.md | 6 +- benchmarks/objc-dispatch/run.js | 26 +- .../src/SignatureDispatchEmitter.cpp | 104 ++--- 11 files changed, 1077 insertions(+), 356 deletions(-) diff --git a/NativeScript/ffi/EngineDirectCall.h b/NativeScript/ffi/EngineDirectCall.h index a587c9b3..84e4c02a 100644 --- a/NativeScript/ffi/EngineDirectCall.h +++ b/NativeScript/ffi/EngineDirectCall.h @@ -4,6 +4,8 @@ #include #include +#include + #include "MetadataReader.h" #include "js_native_api.h" @@ -11,7 +13,10 @@ namespace nativescript { using metagen::MDSectionOffset; +class CFunction; +class Cif; class ObjCClassMember; +struct MethodDescriptor; enum class EngineDirectMemberKind : uint8_t { Method, @@ -30,6 +35,19 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, 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 index 75d8136a..98608ab0 100644 --- a/NativeScript/ffi/EngineDirectCall.mm +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -93,6 +93,204 @@ explicit CifReturnStorage(Cif* cif) { 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; @@ -484,6 +682,196 @@ bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offse } // 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, @@ -559,9 +947,6 @@ napi_value TryCallObjCMemberEngineDirect(napi_env env, ObjCClassMember* member, ObjCEngineDirectInvoker invoker = ensureObjCEngineDirectInvoker(cif, descriptor, descriptor->dispatchFlags); - if (invoker == nullptr) { - return nullptr; - } std::vector paddedArgs; const napi_value* invocationArgs = @@ -589,8 +974,14 @@ napi_value TryCallObjCMemberEngineDirect(napi_env env, ObjCClassMember* member, void* rvalue = rvalueStorage.get(); bool didInvoke = false; @try { - didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, - descriptor->selector, invocationArgs, rvalue); + 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); @@ -628,9 +1019,6 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, CFunctionEngineDirectInvoker invoker = ensureCFunctionEngineDirectInvoker(function, cif); - if (invoker == nullptr) { - return nullptr; - } std::vector paddedArgs; const napi_value* invocationArgs = @@ -647,7 +1035,12 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, bool didInvoke = false; @try { - didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + 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); diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 959a83d1..7966d1e3 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -49,6 +49,24 @@ inline double hermesRawToDouble(uint64_t raw) { return value; } +inline bool readHermesFiniteNumber(napi_value value, double* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = *reinterpret_cast(value); + if (!isHermesNumber(raw)) { + return false; + } + + double converted = hermesRawToDouble(raw); + if (std::isnan(converted) || std::isinf(converted)) { + converted = 0.0; + } + *result = converted; + return true; +} + inline napi_value makeHermesRawValue(uint64_t raw) { static thread_local uint64_t slots[8] = {}; static thread_local unsigned int nextSlot = 0; @@ -638,114 +656,191 @@ bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offse } // namespace +bool TryFastConvertHermesBoolArgument(napi_env env, napi_value value, + uint8_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + const uint64_t raw = *reinterpret_cast(value); + if (!isHermesBool(raw)) { + return false; + } + *result = (raw & kHermesBoolBit) != 0 ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertHermesDoubleArgument(napi_env env, napi_value value, + double* result) { + return readHermesFiniteNumber(value, result); +} + +bool TryFastConvertHermesFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + return TryFastConvertNapiUInt16Argument(env, value, result); +} + +bool TryFastConvertHermesInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertHermesDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertHermesInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + + bool lossless = false; + return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (value == nullptr || result == nullptr) { + return false; + } + + double converted = 0.0; + if (readHermesFiniteNumber(value, &converted)) { + *result = static_cast(converted); + return true; + } + + bool lossless = false; + return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok; +} + +bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result) { + return tryFastConvertHermesSelectorArgument(env, value, result); +} + +bool TryFastConvertHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (tryFastUnwrapHermesObjectArgument(env, kind, value, result)) { + return true; + } + return false; +} + bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, napi_value value, void* result) { if (value == nullptr || result == nullptr) { return false; } - const uint64_t raw = *reinterpret_cast(value); switch (kind) { case mdTypeBool: - if (!isHermesBool(raw)) { - return false; - } - *reinterpret_cast(result) = - (raw & kHermesBoolBit) != 0 ? static_cast(1) : static_cast(0); - return true; - + 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: - case mdTypeFloat: - case mdTypeDouble: { - if (!isHermesNumber(raw)) { - return false; - } - double converted = hermesRawToDouble(raw); - 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 (isHermesNumber(raw)) { - double converted = hermesRawToDouble(raw); - if (std::isnan(converted) || std::isinf(converted)) { - converted = 0.0; - } - *reinterpret_cast(result) = static_cast(converted); - return true; - } - return TryFastConvertNapiUInt16Argument(env, value, - reinterpret_cast(result)); - + return TryFastConvertHermesUInt32Argument( + env, value, reinterpret_cast(result)); case mdTypeSLong: case mdTypeSInt64: - if (isHermesNumber(raw)) { - double converted = hermesRawToDouble(raw); - if (std::isnan(converted) || std::isinf(converted)) { - converted = 0.0; - } - *reinterpret_cast(result) = static_cast(converted); - return true; - } - { - bool lossless = false; - return napi_get_value_bigint_int64(env, value, reinterpret_cast(result), - &lossless) == napi_ok; - } - + return TryFastConvertHermesInt64Argument( + env, value, reinterpret_cast(result)); case mdTypeULong: case mdTypeUInt64: - if (isHermesNumber(raw)) { - double converted = hermesRawToDouble(raw); - if (std::isnan(converted) || std::isinf(converted)) { - converted = 0.0; - } - *reinterpret_cast(result) = static_cast(converted); - return true; - } - { - bool lossless = false; - return napi_get_value_bigint_uint64(env, value, reinterpret_cast(result), - &lossless) == napi_ok; - } - + 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( + return TryFastConvertHermesSelectorArgument( env, value, reinterpret_cast(result)); - case mdTypeClass: case mdTypeAnyObject: case mdTypeProtocolObject: @@ -753,7 +848,7 @@ bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, case mdTypeInstanceObject: case mdTypeNSStringObject: case mdTypeNSMutableStringObject: - if (tryFastUnwrapHermesObjectArgument(env, kind, value, result)) { + if (TryFastConvertHermesObjectArgument(env, kind, value, result)) { return true; } return TryFastConvertNapiArgument(env, kind, value, result); @@ -910,8 +1005,7 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, MethodDescriptor* descriptor = nullptr; Cif* cif = hermesMemberCif(env, member, kind, &descriptor); - if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || - cif->returnType == nullptr) { + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { return nullptr; } @@ -931,11 +1025,10 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, } ObjCEngineDirectInvoker invoker = - ensureHermesObjCEngineDirectInvoker(cif, descriptor, - descriptor->dispatchFlags); - if (invoker == nullptr) { - return nullptr; - } + cif->signatureHash != 0 + ? ensureHermesObjCEngineDirectInvoker(cif, descriptor, + descriptor->dispatchFlags) + : nullptr; id self = resolveHermesSelf(env, jsThis, member); if (self == nil) { @@ -979,8 +1072,14 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, void* rvalue = rvalueStorage.get(); bool didInvoke = false; @try { - didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, - descriptor->selector, invocationArgs, rvalue); + 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); @@ -1014,15 +1113,14 @@ napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, CFunction* function = bridgeState->getCFunction(env, offset); Cif* cif = function != nullptr ? function->cif : nullptr; if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->signatureHash == 0 || cif->returnType == nullptr) { + cif->returnType == nullptr) { return nullptr; } CFunctionEngineDirectInvoker invoker = - ensureHermesCFunctionEngineDirectInvoker(function, cif); - if (invoker == nullptr) { - return nullptr; - } + cif->signatureHash != 0 + ? ensureHermesCFunctionEngineDirectInvoker(function, cif) + : nullptr; napi_value stackPaddedArgs[16]; std::vector heapPaddedArgs; @@ -1040,7 +1138,12 @@ napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, bool didInvoke = false; @try { - didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + 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); diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index 7dfe8ccc..28169dfa 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -655,16 +655,16 @@ bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, MethodDescriptor* descriptor = nullptr; Cif* cif = jscMemberCif(env, member, kind, &descriptor); - if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || - argc != cif->argc || cif->returnType == nullptr) { + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { return false; } - ObjCEngineDirectInvoker invoker = ensureJSCObjCEngineDirectInvoker( - cif, descriptor, descriptor->dispatchFlags); - if (invoker == 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)) { @@ -690,8 +690,14 @@ bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, void* rvalue = rvalueStorage.get(); bool didInvoke = false; @try { - didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, - descriptor->selector, argv, rvalue); + 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); @@ -722,20 +728,24 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, CFunction* function = bridgeState->getCFunction(env, offset); Cif* cif = function != nullptr ? function->cif : nullptr; if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->signatureHash == 0 || argc != cif->argc || cif->returnType == nullptr) { return false; } - CFunctionEngineDirectInvoker invoker = - ensureJSCCFunctionEngineDirectInvoker(function, cif); - if (invoker == nullptr) { - return false; - } + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && argc == cif->argc; + CFunctionEngineDirectInvoker invoker = canUseGeneratedInvoker + ? ensureJSCCFunctionEngineDirectInvoker(function, cif) + : nullptr; bool didInvoke = false; @try { - didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + 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); diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index f1a21d5c..6fa741c3 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -952,13 +952,17 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat #ifdef TARGET_ENGINE_V8 napi_value result = object; #else - 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); + napi_value result = object; + const bool nativeIsArray = [nativeObject isKindOfClass:NSArray.class]; + if (nativeIsArray) { + napi_value factory = get_ref_value(env, createNativeProxy); + napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); + napi_value global; + napi_value args[3] = {object, nullptr, transferOwnershipFunc}; + napi_get_boolean(env, true, &args[1]); + napi_get_global(env, &global); + napi_call_function(env, global, factory, 3, args, &result); + } #endif napi_value nativePointer = Pointer::create(env, nativeObject); if (nativePointer != nullptr) { diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index f5858ef9..76e2a6ba 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -133,6 +133,13 @@ bool tryFastUnwrapQuickJSNativeObject(napi_env env, JSValue jsValue, } *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); @@ -150,21 +157,6 @@ bool tryFastUnwrapQuickJSNativeObject(napi_env env, JSValue jsValue, return *result != nullptr; } -bool readPointerData(JSContext* context, JSValue value, void** result) { - if (result == nullptr) { - return false; - } - - uint64_t raw = 0; - if (JS_ToBigUint64(context, &raw, value) != 0) { - *result = nullptr; - return false; - } - - *result = reinterpret_cast(static_cast(raw)); - return true; -} - class QuickJSFastReturnStorage { public: explicit QuickJSFastReturnStorage(nativescript::Cif* cif) { @@ -809,17 +801,16 @@ bool tryCallQuickJSObjCEngineDirect( nativescript::MethodDescriptor* descriptor = nullptr; nativescript::Cif* cif = quickJSMemberCif(env, member, kind, &descriptor); - if (cif == nullptr || cif->isVariadic || cif->signatureHash == 0 || - static_cast(argc) != cif->argc || - cif->returnType == nullptr) { + if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { return false; } - auto invoker = ensureQuickJSObjCEngineDirectInvoker( - cif, descriptor, descriptor->dispatchFlags); - if (invoker == 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)) { @@ -845,8 +836,14 @@ bool tryCallQuickJSObjCEngineDirect( void* rvalue = rvalueStorage.get(); bool didInvoke = false; @try { - didInvoke = invoker(env, cif, reinterpret_cast(objc_msgSend), self, - descriptor->selector, argv, rvalue); + 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); @@ -878,19 +875,24 @@ bool tryCallQuickJSCFunctionEngineDirect(JSContext* context, napi_env env, auto* function = bridgeState->getCFunction(env, offset); auto* cif = function != nullptr ? function->cif : nullptr; if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->signatureHash == 0 || static_cast(argc) != cif->argc || cif->returnType == nullptr) { return false; } - auto invoker = ensureQuickJSCFunctionEngineDirectInvoker(function, cif); - if (invoker == nullptr) { - return false; - } + const bool canUseGeneratedInvoker = + cif->signatureHash != 0 && static_cast(argc) == cif->argc; + auto invoker = canUseGeneratedInvoker + ? ensureQuickJSCFunctionEngineDirectInvoker(function, cif) + : nullptr; bool didInvoke = false; @try { - didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + 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); @@ -910,8 +912,10 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, return JS_UNDEFINED; } - void* data = nullptr; - if (!readPointerData(context, funcData[0], &data)) { + auto* externalInfo = static_cast( + JS_GetOpaque(funcData[0], env->runtime->externalClassId)); + void* data = externalInfo != nullptr ? externalInfo->data : nullptr; + if (data == nullptr) { return JS_UNDEFINED; } @@ -1041,9 +1045,33 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, return returnValue; } -JSValue makeFastFunction(JSContext* context, int kind, void* data) { - JSValue dataValue = JS_NewBigUint64( - context, static_cast(reinterpret_cast(data))); +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); @@ -1125,6 +1153,16 @@ inline bool readQuickJSNumber(JSValue value, double* result) { 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) { @@ -1251,6 +1289,13 @@ bool tryFastUnwrapQuickJSNativeObject(napi_env env, JSValue jsValue, } *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); @@ -1342,125 +1387,216 @@ bool tryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, } // namespace -bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, - napi_value value, void* result) { +bool TryFastConvertQuickJSBoolArgument(napi_env env, napi_value value, + uint8_t* result) { if (env == nullptr || value == nullptr || result == nullptr) { return false; } - JSContext* context = qjs_get_context(env); - if (context == nullptr) { + JSValue jsValue = ToJSValue(value); + if (!JS_IsBool(jsValue)) { + return false; + } + *result = JS_VALUE_GET_BOOL(jsValue) ? static_cast(1) + : static_cast(0); + return true; +} + +bool TryFastConvertQuickJSDoubleArgument(napi_env env, napi_value value, + double* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSFiniteNumber(ToJSValue(value), result); +} + +bool TryFastConvertQuickJSFloatArgument(napi_env env, napi_value value, + float* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt8Argument(napi_env env, napi_value value, + int8_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt8Argument(napi_env env, napi_value value, + uint8_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt16Argument(napi_env env, napi_value value, + int16_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt16Argument(napi_env env, napi_value value, + uint16_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + JSValue jsValue = ToJSValue(value); + if (JS_IsString(jsValue)) { + size_t length = 0; + const char* str = JS_ToCStringLen(env->context, &length, jsValue); + if (str == nullptr) { + return false; + } + if (length != 1) { + JS_FreeCString(env->context, str); + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + *result = static_cast(str[0]); + JS_FreeCString(env->context, str); + return true; + } + + double converted = 0.0; + if (!readQuickJSFiniteNumber(jsValue, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt32Argument(napi_env env, napi_value value, + int32_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSUInt32Argument(napi_env env, napi_value value, + uint32_t* result) { + double converted = 0.0; + if (!TryFastConvertQuickJSDoubleArgument(env, value, &converted)) { + return false; + } + *result = static_cast(converted); + return true; +} + +bool TryFastConvertQuickJSInt64Argument(napi_env env, napi_value value, + int64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSInt64(env->context, ToJSValue(value), result); +} + +bool TryFastConvertQuickJSUInt64Argument(napi_env env, napi_value value, + uint64_t* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return readQuickJSUInt64(env->context, ToJSValue(value), result); +} + +bool TryFastConvertQuickJSSelectorArgument(napi_env env, napi_value value, + SEL* result) { + if (env == nullptr || value == nullptr || result == nullptr) { return false; } JSValue jsValue = ToJSValue(value); + if (JS_IsNull(jsValue) || JS_IsUndefined(jsValue)) { + *result = nullptr; + return true; + } + if (!JS_IsString(jsValue)) { + return false; + } + + size_t length = 0; + const char* selectorName = JS_ToCStringLen(env->context, &length, jsValue); + if (selectorName == nullptr) { + return false; + } + *result = cachedSelectorForName(selectorName, length); + JS_FreeCString(env->context, selectorName); + return true; +} + +bool TryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + return tryFastConvertQuickJSObjectArgument(env, kind, ToJSValue(value), + result); +} + +bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + switch (kind) { case mdTypeBool: - if (!JS_IsBool(jsValue)) { - return false; - } - *reinterpret_cast(result) = - JS_VALUE_GET_BOOL(jsValue) ? static_cast(1) : static_cast(0); - return true; - + 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: - case mdTypeFloat: - case mdTypeDouble: { - double converted = 0.0; - if (!readQuickJSNumber(jsValue, &converted)) { - 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 (JS_IsString(jsValue)) { - size_t length = 0; - const char* str = JS_ToCStringLen(context, &length, jsValue); - if (str == nullptr) { - return false; - } - if (length != 1) { - JS_FreeCString(context, str); - napi_throw_type_error(env, nullptr, "Expected a single-character string."); - return false; - } - *reinterpret_cast(result) = static_cast(str[0]); - JS_FreeCString(context, str); - return true; - } - { - double converted = 0.0; - if (!readQuickJSNumber(jsValue, &converted)) { - return false; - } - *reinterpret_cast(result) = static_cast(converted); - return true; - } - + return TryFastConvertQuickJSUInt32Argument( + env, value, reinterpret_cast(result)); case mdTypeSLong: case mdTypeSInt64: - return readQuickJSInt64(context, jsValue, - reinterpret_cast(result)); - + return TryFastConvertQuickJSInt64Argument( + env, value, reinterpret_cast(result)); case mdTypeULong: case mdTypeUInt64: - return readQuickJSUInt64(context, jsValue, - reinterpret_cast(result)); - - case mdTypeSelector: { - SEL* selector = reinterpret_cast(result); - if (JS_IsNull(jsValue) || JS_IsUndefined(jsValue)) { - *selector = nullptr; - return true; - } - if (!JS_IsString(jsValue)) { - return false; - } - size_t length = 0; - const char* selectorName = JS_ToCStringLen(context, &length, jsValue); - if (selectorName == nullptr) { - return false; - } - *selector = cachedSelectorForName(selectorName, length); - JS_FreeCString(context, selectorName); - return true; - } - + 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: @@ -1468,7 +1604,7 @@ bool TryFastConvertQuickJSArgument(napi_env env, MDTypeKind kind, case mdTypeInstanceObject: case mdTypeNSStringObject: case mdTypeNSMutableStringObject: - if (tryFastConvertQuickJSObjectArgument(env, kind, jsValue, result)) { + if (TryFastConvertQuickJSObjectArgument(env, kind, value, result)) { return true; } return TryFastConvertNapiArgument(env, kind, value, result); @@ -1626,7 +1762,7 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, if (descriptor->method == nativescript::ObjCClassMember::jsCall && descriptor->data != nullptr) { JSValue function = - makeFastFunction(context, kQuickJSFastObjCMethod, descriptor->data); + makeFastFunction(env, kQuickJSFastObjCMethod, descriptor->data); return !JS_IsException(function) && defineFastProperty(env, object, descriptor, function, JS_UNDEFINED, JS_UNDEFINED); @@ -1636,7 +1772,7 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, descriptor->data != nullptr && !isCompatCFunction(env, descriptor->data)) { JSValue function = - makeFastFunction(context, kQuickJSFastCFunction, descriptor->data); + makeFastFunction(env, kQuickJSFastCFunction, descriptor->data); return !JS_IsException(function) && defineFastProperty(env, object, descriptor, function, JS_UNDEFINED, JS_UNDEFINED); @@ -1645,7 +1781,7 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, if (descriptor->getter == nativescript::ObjCClassMember::jsGetter && descriptor->data != nullptr) { JSValue getter = - makeFastFunction(context, kQuickJSFastObjCGetter, descriptor->data); + makeFastFunction(env, kQuickJSFastObjCGetter, descriptor->data); if (JS_IsException(getter)) { return false; } @@ -1653,13 +1789,13 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, JSValue setter = JS_UNDEFINED; if (descriptor->setter == nativescript::ObjCClassMember::jsSetter) { setter = - makeFastFunction(context, kQuickJSFastObjCSetter, descriptor->data); + makeFastFunction(env, kQuickJSFastObjCSetter, descriptor->data); if (JS_IsException(setter)) { return false; } } else if (descriptor->setter == nativescript::ObjCClassMember::jsReadOnlySetter) { - setter = makeFastFunction(context, kQuickJSFastObjCReadOnlySetter, + setter = makeFastFunction(env, kQuickJSFastObjCReadOnlySetter, descriptor->data); if (JS_IsException(setter)) { return false; diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/TypeConv.h index 07bf8ece..7f97f4e0 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -113,6 +113,32 @@ bool TryFastConvertJSCReturnValue(napi_env env, MDTypeKind kind, // 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 @@ -122,6 +148,32 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, // at the PinnedHermesValue slot supplied by Hermes' native trampoline. bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, napi_value value, void* result); +bool TryFastConvertHermesBoolArgument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertHermesInt8Argument(napi_env env, napi_value value, + int8_t* result); +bool TryFastConvertHermesUInt8Argument(napi_env env, napi_value value, + uint8_t* result); +bool TryFastConvertHermesInt16Argument(napi_env env, napi_value value, + int16_t* result); +bool TryFastConvertHermesUInt16Argument(napi_env env, napi_value value, + uint16_t* result); +bool TryFastConvertHermesInt32Argument(napi_env env, napi_value value, + int32_t* result); +bool TryFastConvertHermesUInt32Argument(napi_env env, napi_value value, + uint32_t* result); +bool TryFastConvertHermesInt64Argument(napi_env env, napi_value value, + int64_t* result); +bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, + uint64_t* result); +bool TryFastConvertHermesFloatArgument(napi_env env, napi_value value, + float* result); +bool TryFastConvertHermesDoubleArgument(napi_env env, napi_value value, + double* result); +bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, + SEL* result); +bool TryFastConvertHermesObjectArgument(napi_env env, MDTypeKind kind, + napi_value value, void* result); bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, const void* value, napi_value* result); #endif @@ -140,6 +192,19 @@ inline bool TryFastConvertEngineReturnValue(napi_env env, MDTypeKind kind, #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/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index a2c16177..c1d8ce0a 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -3420,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); @@ -3440,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, @@ -3502,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/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md index c146771e..6a2951bd 100644 --- a/benchmarks/objc-dispatch/README.md +++ b/benchmarks/objc-dispatch/README.md @@ -22,6 +22,10 @@ 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 @@ -36,8 +40,6 @@ Useful options: --legacy-repo /path/to/NativeScript/ios --destination "platform=iOS Simulator,id=" --napi-package-tgz /path/to/nativescript-ios.tgz ---napi-v8-napi-backend-package-tgz /path/to/nativescript-ios-v8-napi-backend.tgz --iterations 250000 --include-napi-gsd-off ---include-napi-v8-napi-backend ``` diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js index 29624763..62760a34 100644 --- a/benchmarks/objc-dispatch/run.js +++ b/benchmarks/objc-dispatch/run.js @@ -22,7 +22,6 @@ function parseArgs(argv) { iterations: 250000, warmupIterations: undefined, includeNapiGsdOff: false, - includeNapiV8NapiBackend: false, includeLegacyAotOff: false, legacyRepo: process.env.NS_LEGACY_IOS_REPO || defaultLegacyRepo, metadataPath: process.env.METADATA_PATH || defaultMetadataPath, @@ -31,7 +30,6 @@ function parseArgs(argv) { timeoutMs: 120000, buildTimeoutMs: 15 * 60 * 1000, napiPackageTgz: "", - napiV8NapiBackendPackageTgz: "", napiVariantLabel: "", skipBuild: false, compareResults: "" @@ -61,12 +59,9 @@ function parseArgs(argv) { 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-v8-napi-backend-package-tgz") args.napiV8NapiBackendPackageTgz = path.resolve(next()); - else if (arg.startsWith("--napi-v8-napi-backend-package-tgz=")) args.napiV8NapiBackendPackageTgz = path.resolve(arg.slice("--napi-v8-napi-backend-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-napi-v8-napi-backend") args.includeNapiV8NapiBackend = 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()); @@ -101,12 +96,8 @@ Options: --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-v8-napi-backend-package-tgz PATH - @nativescript/ios tgz built with TARGET_ENGINE=v8 and NS_GSD_BACKEND=napi --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-napi-v8-napi-backend - Also run V8 runtime compiled to use the N-API GSD/callback path --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 @@ -301,7 +292,7 @@ function labeledNapiVariant(options, variant) { } function napiVariantGroup(variant) { - const match = String(variant).match(/^(?:(.*)\s+)?(gsd-on|gsd-off|gsd-v8-napi-backend)$/); + const match = String(variant).match(/^(?:(.*)\s+)?(gsd-on|gsd-off)$/); if (!match) { return null; } @@ -399,16 +390,12 @@ function printComparisons(reports) { for (const group of napiGroups.values()) { const gsdOn = group.get("gsd-on"); const gsdOff = group.get("gsd-off"); - const v8NapiBackend = group.get("gsd-v8-napi-backend"); if (gsdOn && !napiGsdOn) { napiGsdOn = gsdOn; } if (gsdOn && gsdOff) { printPairComparison(gsdOn, gsdOff); } - if (gsdOn && v8NapiBackend) { - printPairComparison(gsdOn, v8NapiBackend); - } } const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); @@ -923,17 +910,6 @@ async function main() { } } else if (runtime === "napi-ios") { reports.push(await runNapiIOS(options, "gsd-on", undefined, labeledNapiVariant(options, "gsd-on"))); - if (options.includeNapiV8NapiBackend) { - if (!options.napiV8NapiBackendPackageTgz) { - throw new Error("--include-napi-v8-napi-backend requires --napi-v8-napi-backend-package-tgz"); - } - reports.push(await runNapiIOS( - options, - "gsd-v8-napi-backend", - options.napiV8NapiBackendPackageTgz, - labeledNapiVariant(options, "gsd-v8-napi-backend") - )); - } if (options.includeNapiGsdOff) { reports.push(await runNapiIOS(options, "gsd-off", undefined, labeledNapiVariant(options, "gsd-off"))); } diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index acabcf55..3a24c0ce 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -2011,61 +2011,61 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeBool, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeChar, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeUInt8, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeSShort, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeUShort, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeSInt, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeUInt, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeSInt64, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeUInt64, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeFloat, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeDouble, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT(env, value, result) " - "TryFastConvertQuickJSArgument(env, mdTypeSelector, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT(env, kind, value, result) " - "TryFastConvertQuickJSArgument(env, kind, value, result)\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(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeBool, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeChar, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeUInt8, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeSShort, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeUShort, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeSInt, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeUInt, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeSInt64, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeUInt64, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeFloat, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeDouble, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT(env, value, result) " - "TryFastConvertHermesArgument(env, mdTypeSelector, value, result)\n"; - generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT(env, kind, value, result) " - "TryFastConvertHermesArgument(env, kind, value, result)\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertHermesBoolArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesUInt8Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesUInt16Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesUInt32Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesUInt64Argument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesFloatArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesDoubleArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertHermesSelectorArgument\n"; + generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertHermesObjectArgument\n"; generated << "#else\n"; generated << "#error \"No generated signature engine-direct converter selected\"\n"; generated << "#endif\n"; From 5bec5ba233143a33b637199aa84f4a804648a8ef Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 22 May 2026 00:05:57 -0400 Subject: [PATCH 09/31] perf: tighten engine direct native dispatch --- NativeScript/ffi/Class.mm | 5 +- NativeScript/ffi/ClassBuilder.mm | 2 +- NativeScript/ffi/HermesFastNativeApi.mm | 52 ++++++++++++++++++++- NativeScript/ffi/Interop.mm | 2 +- NativeScript/ffi/JSCFastNativeApi.h | 4 ++ NativeScript/ffi/JSCFastNativeApi.mm | 11 ++++- NativeScript/ffi/ObjCBridge.h | 58 ++++++++++++++++++------ NativeScript/ffi/Protocol.mm | 3 +- NativeScript/ffi/QuickJSFastNativeApi.mm | 10 +++- NativeScript/ffi/TypeConv.mm | 3 +- NativeScript/napi/jsc/jsc-api.cpp | 22 +++++++++ NativeScript/napi/jsc/jsc-api.h | 2 + 12 files changed, 150 insertions(+), 24 deletions(-) diff --git a/NativeScript/ffi/Class.mm b/NativeScript/ffi/Class.mm index cd435952..f4ab722a 100644 --- a/NativeScript/ffi/Class.mm +++ b/NativeScript/ffi/Class.mm @@ -146,7 +146,7 @@ inline bool tryGetInteropPointerArg(napi_env env, napi_value value, void** out) ClassBuilder* builder = new ClassBuilder(env, argv[0]); // It gets lazily built when a static method is called. // builder->build(); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return nullptr; } @@ -747,8 +747,7 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value if (nativeClass != nil) { napi_wrap(env, constructor, (void*)nativeClass, nil, nil, nil); - bridgeState->classesByPointer[nativeClass] = this; - bridgeState->mdClassesByPointer[nativeClass] = metadataOffset; + bridgeState->registerRuntimeClass(this, nativeClass); } napi_get_named_property(env, constructor, "prototype", &prototype); diff --git a/NativeScript/ffi/ClassBuilder.mm b/NativeScript/ffi/ClassBuilder.mm index f11cfe5e..ac3ac914 100644 --- a/NativeScript/ffi/ClassBuilder.mm +++ b/NativeScript/ffi/ClassBuilder.mm @@ -1016,7 +1016,7 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat // Register the builder in the bridge state bridgeState = ObjCBridgeState::InstanceData(env); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return newConstructor; } diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 7966d1e3..70d7c668 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -43,6 +43,10 @@ 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)); @@ -175,10 +179,19 @@ bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, if (kind == mdTypeClass) { id nativeObject = static_cast(wrapped); + if (!object_isClass(nativeObject)) { + ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState != nullptr) { + id normalizedObject = bridgeState->nativeObjectForBridgeWrapper(wrapped); + if (normalizedObject != nil) { + nativeObject = normalizedObject; + } + } + } if (!object_isClass(nativeObject)) { return false; } - *reinterpret_cast(result) = static_cast(wrapped); + *reinterpret_cast(result) = static_cast(nativeObject); return true; } @@ -343,16 +356,51 @@ id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { id self = nil; ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + struct ReceiverCacheEntry { + napi_env env = nullptr; + ObjCClassMember* method = nullptr; + uint64_t rawValue = 0; + id self = nil; + bool classObject = false; + }; + + static thread_local ReceiverCacheEntry lastReceiver; + const uint64_t rawThis = hermesRawValueBits(jsThis); + if (rawThis != 0 && lastReceiver.env == env && + lastReceiver.method == method && lastReceiver.rawValue == rawThis && + lastReceiver.self != nil && + (lastReceiver.classObject || + (state != nullptr && state->hasObjectRef(lastReceiver.self)))) { + return lastReceiver.self; + } + + auto rememberReceiver = [&](id resolved) { + if (resolved == nil || rawThis == 0) { + return; + } + + lastReceiver.env = env; + lastReceiver.method = method; + lastReceiver.rawValue = rawThis; + lastReceiver.self = resolved; + lastReceiver.classObject = object_isClass(resolved); + }; + napi_status unwrapStatus = napi_invalid_arg; if (jsThis != nullptr) { unwrapStatus = napi_unwrap(env, jsThis, reinterpret_cast(&self)); if (unwrapStatus == napi_ok && self != nil) { + rememberReceiver(self); return self; } } if (state != nullptr && jsThis != nullptr) { state->tryResolveBridgedTypeConstructor(env, jsThis, &self); + if (self != nil) { + rememberReceiver(self); + return self; + } } if (self == nil && jsThis != nullptr) { @@ -368,6 +416,7 @@ id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { } if (self != nil) { + rememberReceiver(self); return self; } @@ -399,6 +448,7 @@ id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { } if (shouldUseClassFallback) { + rememberReceiver(static_cast(method->cls->nativeClass)); return static_cast(method->cls->nativeClass); } diff --git a/NativeScript/ffi/Interop.mm b/NativeScript/ffi/Interop.mm index c558a85d..9ff39883 100644 --- a/NativeScript/ffi/Interop.mm +++ b/NativeScript/ffi/Interop.mm @@ -590,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 index 8644d5b8..12e5e004 100644 --- a/NativeScript/ffi/JSCFastNativeApi.h +++ b/NativeScript/ffi/JSCFastNativeApi.h @@ -5,9 +5,13 @@ 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 index 28169dfa..2f9ba7de 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -18,6 +18,7 @@ #include "EngineDirectCall.h" #include "MetadataReader.h" #include "NativeScriptException.h" +#include "Object.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" #include "TypeConv.h" @@ -1125,6 +1126,11 @@ id normalizeWrappedNativeObject(napi_env env, MDTypeKind kind, void* wrapped) { 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) { @@ -1204,10 +1210,13 @@ bool tryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, 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(wrapped); + *reinterpret_cast(result) = static_cast(nativeObject); return true; } diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index 70c02b17..b2be9cef 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -97,6 +97,48 @@ class ObjCBridgeState { #endif } + inline void registerRuntimeClass(ObjCClass* bridgedClass, + Class runtimeClass) { + if (bridgedClass == nullptr || runtimeClass == nil) { + return; + } + + bridgedClass->nativeClass = runtimeClass; + classesByPointer[runtimeClass] = bridgedClass; + nativeObjectsByBridgeWrapper[bridgedClass] = (id)runtimeClass; + if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { + mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; + } + } + + inline void registerProtocolMetadata(Protocol* runtimeProtocol, + MDSectionOffset metadataOffset) { + if (runtimeProtocol == nil || metadataOffset == MD_SECTION_OFFSET_NULL) { + return; + } + + mdProtocolsByPointer[runtimeProtocol] = metadataOffset; + } + + inline void registerRuntimeProtocol(ObjCProtocol* bridgedProtocol, + Protocol* runtimeProtocol) { + if (bridgedProtocol == nullptr || runtimeProtocol == nil) { + return; + } + + nativeObjectsByBridgeWrapper[bridgedProtocol] = (id)runtimeProtocol; + registerProtocolMetadata(runtimeProtocol, bridgedProtocol->metadataOffset); + } + + inline id nativeObjectForBridgeWrapper(void* wrapped) const { + if (wrapped == nullptr) { + return nil; + } + + auto cached = nativeObjectsByBridgeWrapper.find(wrapped); + return cached != nativeObjectsByBridgeWrapper.end() ? cached->second : nil; + } + void registerVarGlobals(napi_env env, napi_value global); void registerEnumGlobals(napi_env env, napi_value global); void registerStructGlobals(napi_env env, napi_value global); @@ -412,19 +454,6 @@ class ObjCBridgeState { return name; }; - auto registerResolvedRuntimeClass = [&](ObjCClass* bridgedClass, - Class runtimeClass) { - if (bridgedClass == nullptr || runtimeClass == nil) { - return; - } - - bridgedClass->nativeClass = runtimeClass; - classesByPointer[runtimeClass] = bridgedClass; - if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { - mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; - } - }; - auto matchesConstructor = [&](ObjCClass* bridgedClass, ObjCClass** unresolvedMatch) -> bool { if (bridgedClass == nullptr || bridgedClass->constructor == nullptr) { @@ -485,7 +514,7 @@ class ObjCBridgeState { Class runtimeClass = objc_lookUpClass(candidateName.c_str()); if (runtimeClass != nil) { if (unresolvedConstructorMatch != nullptr) { - registerResolvedRuntimeClass(unresolvedConstructorMatch, runtimeClass); + registerRuntimeClass(unresolvedConstructorMatch, runtimeClass); } *out = runtimeClass; return true; @@ -703,6 +732,7 @@ class ObjCBridgeState { std::unordered_map classesByPointer; std::unordered_map mdClassesByPointer; std::unordered_map mdProtocolsByPointer; + std::unordered_map nativeObjectsByBridgeWrapper; std::unordered_map constructorsByPointer; std::unordered_map cifs; 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.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index 76e2a6ba..5c24af8c 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -1255,6 +1255,11 @@ id normalizeWrappedNativeObject(napi_env env, MDTypeKind kind, void* wrapped) { 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) { @@ -1373,10 +1378,13 @@ bool tryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, 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(wrapped); + *reinterpret_cast(result) = static_cast(nativeObject); return true; } diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/TypeConv.mm index cc50987a..66decdcb 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/TypeConv.mm @@ -1232,8 +1232,9 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { MDSectionOffset metadataOffset = findProtocolMetadataOffset(bridgeState->metadata, runtimeName); if (metadataOffset != MD_SECTION_OFFSET_NULL) { - bridgeState->mdProtocolsByPointer[runtimeProto] = metadataOffset; + bridgeState->registerProtocolMetadata(runtimeProto, metadataOffset); auto proto = bridgeState->getProtocol(env, metadataOffset); + bridgeState->registerRuntimeProtocol(proto, runtimeProto); if (proto != nullptr) { ::free(protocols); return get_ref_value(env, proto->constructor); diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 8aee16f3..49d1a76e 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -793,6 +793,14 @@ class WrapperInfo : public BaseInfoT { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); WrapperInfo* info{}; + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo != env->wrapper_info_cache.end()) { + info = static_cast(cachedInfo->second); + RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + *result = info; + return napi_ok; + } + bool hasOwnProperty = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol) != nullptr; @@ -800,6 +808,7 @@ class WrapperInfo : public BaseInfoT { if (hasOwnProperty) { CHECK_NAPI(Unwrap(env, object, &info)); RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + env->wrapper_info_cache[object] = info; *result = info; return napi_ok; } @@ -813,6 +822,13 @@ class WrapperInfo : public BaseInfoT { NativeInfo::SetNativeInfoKey(env->context, ToJSObject(env, object), info->_class, env->wrapper_info_symbol, info); + info->AddFinalizer([object](WrapperInfo* info) { + napi_env env = info->Env(); + if (env != nullptr) { + env->wrapper_info_cache.erase(object); + } + }); + env->wrapper_info_cache[object] = info; } *result = info; @@ -822,6 +838,12 @@ class WrapperInfo : public BaseInfoT { static napi_status Unwrap(napi_env env, napi_value object, WrapperInfo** result) { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo != env->wrapper_info_cache.end()) { + *result = static_cast(cachedInfo->second); + return napi_ok; + } + *result = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol); return napi_ok; diff --git a/NativeScript/napi/jsc/jsc-api.h b/NativeScript/napi/jsc/jsc-api.h index 7a33d960..36e91f1a 100644 --- a/NativeScript/napi/jsc/jsc-api.h +++ b/NativeScript/napi/jsc/jsc-api.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "js_native_api.h" @@ -24,6 +25,7 @@ struct napi_env__ { 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; From 5e5833b674df78ec268b6ea71d194ce29738ab2d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 22 May 2026 15:57:21 -0400 Subject: [PATCH 10/31] perf: optimize hermes generated dispatch --- NativeScript/ffi/Block.h | 1 + NativeScript/ffi/Block.mm | 417 ++++- NativeScript/ffi/CFunction.h | 3 + NativeScript/ffi/CFunction.mm | 59 +- NativeScript/ffi/Cif.h | 1 + NativeScript/ffi/ClassMember.h | 15 + NativeScript/ffi/ClassMember.mm | 79 +- NativeScript/ffi/EngineDirectCall.mm | 38 +- NativeScript/ffi/HermesFastCallbackInfo.h | 45 +- NativeScript/ffi/HermesFastNativeApi.h | 10 + NativeScript/ffi/HermesFastNativeApi.mm | 1422 +++++++++++++++-- NativeScript/ffi/ObjCBridge.h | 80 + NativeScript/ffi/ObjCBridge.mm | 39 + NativeScript/ffi/Object.mm | 148 ++ NativeScript/ffi/SignatureDispatch.h | 556 ++++++- NativeScript/ffi/TypeConv.h | 4 + benchmarks/objc-dispatch/run.js | 2 +- .../src/SignatureDispatchEmitter.cpp | 779 ++++++++- 18 files changed, 3381 insertions(+), 317 deletions(-) diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/Block.h index 4cd81962..cab8dc2c 100644 --- a/NativeScript/ffi/Block.h +++ b/NativeScript/ffi/Block.h @@ -20,6 +20,7 @@ class FunctionPointer { 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 612a7e50..f7835163 100644 --- a/NativeScript/ffi/Block.mm +++ b/NativeScript/ffi/Block.mm @@ -1,13 +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" @@ -73,6 +79,15 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) 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; } @@ -84,9 +99,17 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) 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; } @@ -94,6 +117,270 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) 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); @@ -454,93 +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); } - } - auto preparedInvoker = - ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::CFunction); - if (preparedInvoker != nullptr) { - preparedInvoker(ref->function, avalues, rvalue); - } else { - ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + std::vector heapArgs(actualArgc); + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, + heapArgs.data()); + return callFunctionPointerAsCFunctionDirect(env, ref, actualArgc, heapArgs.data()); } +#endif - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i])); - } - } + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); + + 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] = █ - - 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); +#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; } - } - BlockPreparedInvoker preparedInvoker = - ensureFunctionPointerPreparedInvoker(ref, SignatureCallKind::BlockInvoke); + napi_value stackArgs[16]; + if (actualArgc <= 16) { + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, stackArgs); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, stackArgs); + } - if (preparedInvoker != nullptr) { - preparedInvoker(block->invoke, avalues, rvalue); - } else { - ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + std::vector heapArgs(actualArgc); + copyHermesFunctionPointerFrameArgs(HermesFastArgsBase(fastInfo), actualArgc, + heapArgs.data()); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, heapArgs.data()); } +#endif - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i + 1])); - } - } + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); + + 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 42ac39ef..2584cdaa 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/CFunction.h @@ -23,6 +23,7 @@ class CFunction { ObjCBridgeState* bridgeState = nullptr; Cif* cif = nullptr; uint8_t dispatchFlags = 0; + bool skipEngineDirectFastPath = false; bool dispatchLookupCached = false; uint64_t dispatchLookupSignatureHash = 0; uint64_t dispatchId = 0; @@ -30,6 +31,8 @@ class CFunction { 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 3b1ea423..a6c48f76 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -20,6 +20,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; @@ -187,6 +196,8 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { function->napiInvoker = nullptr; function->engineDirectInvoker = nullptr; function->v8Invoker = nullptr; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = nullptr; } return; } @@ -206,6 +217,12 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { 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; } @@ -243,10 +260,12 @@ 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; @@ -255,30 +274,30 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { napi_value CFunction::jsCall(napi_env env, napi_callback_info cbinfo) { #ifdef TARGET_ENGINE_HERMES if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { - napi_value stackArgs[16]; - std::vector heapArgs; - napi_value* args = stackArgs; - const size_t actualArgc = fastInfo->argc; - if (actualArgc > 16) { - heapArgs.resize(actualArgc); - args = heapArgs.data(); - } - for (size_t i = 0; i < actualArgc; i++) { - args[i] = HermesFastArg(fastInfo, i); - } - + const size_t actualArgc = HermesFastArgc(fastInfo); + const auto offset = + static_cast(reinterpret_cast(HermesFastData(fastInfo))); bool handledDirect = false; - napi_value directResult = TryCallHermesCFunctionFast( - env, - static_cast(reinterpret_cast(fastInfo->data)), - actualArgc, args, &handledDirect); + napi_value directResult = TryCallHermesCFunctionFastFromFrame( + env, offset, actualArgc, HermesFastArgsBase(fastInfo), &handledDirect); if (handledDirect) { return directResult; } - return jsCallDirect( - env, static_cast(reinterpret_cast(fastInfo->data)), - actualArgc, args); + napi_value stackArgs[16]; + if (actualArgc <= 16) { + for (size_t i = 0; i < actualArgc; i++) { + stackArgs[i] = HermesFastArg(fastInfo, i); + } + + 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 diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/Cif.h index 3614d6ab..2ab69483 100644 --- a/NativeScript/ffi/Cif.h +++ b/NativeScript/ffi/Cif.h @@ -24,6 +24,7 @@ class Cif { bool generatedDispatchHasRoundTripCacheArgument = false; bool generatedDispatchUsesObjectReturnStorage = false; bool generatedDispatchSetsV8ReturnDirectly = false; + uint64_t hermesRawReturnSlot = 0; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/ClassMember.h index 62ac9628..feb81482 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/ClassMember.h @@ -35,8 +35,14 @@ class MethodDescriptor { 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() {} @@ -128,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 56896794..88150ad2 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -351,6 +351,12 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho 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; } @@ -440,6 +446,12 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, #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; } @@ -1096,8 +1108,8 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { return cif != nullptr && - (cif->generatedDispatchHasRoundTripCacheArgument || - cif->generatedDispatchUsesObjectReturnStorage); + cif->generatedDispatchUsesObjectReturnStorage && + cif->generatedDispatchHasRoundTripCacheArgument; } namespace { @@ -1446,29 +1458,32 @@ explicit CifReturnStorage(Cif* cif) { napi_value ObjCClassMember::jsCall(napi_env env, napi_callback_info cbinfo) { #ifdef TARGET_ENGINE_HERMES if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { - napi_value stackArgs[16]; - std::vector heapArgs; - napi_value* args = stackArgs; - const size_t actualArgc = fastInfo->argc; - if (actualArgc > 16) { - heapArgs.resize(actualArgc); - args = heapArgs.data(); - } - for (size_t i = 0; i < actualArgc; i++) { - args[i] = HermesFastArg(fastInfo, i); - } - + const size_t actualArgc = HermesFastArgc(fastInfo); bool handledDirect = false; - napi_value directResult = TryCallHermesObjCMemberFast( - env, static_cast(fastInfo->data), - HermesFastThisArg(fastInfo), actualArgc, args, + napi_value directResult = TryCallHermesObjCMemberFastFromFrame( + env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, HermesFastArgsBase(fastInfo), EngineDirectMemberKind::Method, &handledDirect); if (handledDirect) { return directResult; } - return jsCallDirect(env, static_cast(fastInfo->data), - HermesFastThisArg(fastInfo), actualArgc, args); + 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 @@ -1834,13 +1849,13 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { bool handledDirect = false; napi_value directResult = TryCallHermesObjCMemberFast( - env, static_cast(fastInfo->data), + env, static_cast(HermesFastData(fastInfo)), HermesFastThisArg(fastInfo), 0, nullptr, EngineDirectMemberKind::Getter, &handledDirect); if (handledDirect) { return directResult; } - return jsGetterDirect(env, static_cast(fastInfo->data), + return jsGetterDirect(env, static_cast(HermesFastData(fastInfo)), HermesFastThisArg(fastInfo)); } #endif @@ -1937,21 +1952,23 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_value ObjCClassMember::jsSetter(napi_env env, napi_callback_info cbinfo) { #ifdef TARGET_ENGINE_HERMES if (auto* fastInfo = TryGetHermesFastCallbackInfo(env, cbinfo)) { - napi_value value = nullptr; - if (fastInfo->argc > 0) { - value = HermesFastArg(fastInfo, 0); - } else { - napi_get_undefined(env, &value); - } + const size_t actualArgc = HermesFastArgc(fastInfo); bool handledDirect = false; - napi_value directResult = TryCallHermesObjCMemberFast( - env, static_cast(fastInfo->data), - HermesFastThisArg(fastInfo), 1, &value, + napi_value directResult = TryCallHermesObjCMemberFastFromFrame( + env, static_cast(HermesFastData(fastInfo)), + HermesFastThisArg(fastInfo), actualArgc, HermesFastArgsBase(fastInfo), EngineDirectMemberKind::Setter, &handledDirect); if (handledDirect) { return directResult; } - return jsSetterDirect(env, static_cast(fastInfo->data), + + 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 diff --git a/NativeScript/ffi/EngineDirectCall.mm b/NativeScript/ffi/EngineDirectCall.mm index 98608ab0..76b3ec0c 100644 --- a/NativeScript/ffi/EngineDirectCall.mm +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -28,8 +28,8 @@ inline bool needsRoundTripCacheFrame(Cif* cif) { return cif != nullptr && - (cif->generatedDispatchHasRoundTripCacheArgument || - cif->generatedDispatchUsesObjectReturnStorage); + cif->generatedDispatchUsesObjectReturnStorage && + cif->generatedDispatchHasRoundTripCacheArgument; } class RoundTripCacheFrameGuard { @@ -491,6 +491,12 @@ ObjCEngineDirectInvoker ensureObjCEngineDirectInvoker(Cif* cif, #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; } @@ -510,6 +516,8 @@ CFunctionEngineDirectInvoker ensureCFunctionEngineDirectInvoker(CFunction* funct function->napiInvoker = nullptr; function->engineDirectInvoker = nullptr; function->v8Invoker = nullptr; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = nullptr; } return nullptr; } @@ -528,6 +536,12 @@ CFunctionEngineDirectInvoker ensureCFunctionEngineDirectInvoker(CFunction* funct #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; } @@ -666,20 +680,6 @@ napi_value convertCFunctionReturnValue(napi_env env, CFunction* function, return cif->returnType->toJS(env, rvalue, toJSFlags); } -bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { - if (bridgeState == nullptr) { - return true; - } - - const char* name = bridgeState->metadata->getString(offset); - return name == nullptr || - std::strcmp(name, "dispatch_async") == 0 || - std::strcmp(name, "dispatch_get_current_queue") == 0 || - std::strcmp(name, "dispatch_get_global_queue") == 0 || - std::strcmp(name, "UIApplicationMain") == 0 || - std::strcmp(name, "NSApplicationMain") == 0; -} - } // namespace bool InvokeObjCMemberEngineDirectDynamic(napi_env env, Cif* cif, id self, @@ -1006,14 +1006,14 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, } ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); - if (env == nullptr || bridgeState == nullptr || - isCompatOrMainCFunction(bridgeState, offset)) { + if (env == nullptr || bridgeState == nullptr) { return nullptr; } CFunction* function = bridgeState->getCFunction(env, offset); Cif* cif = function != nullptr ? function->cif : nullptr; - if (function == nullptr || cif == nullptr || cif->isVariadic) { + if (function == nullptr || function->skipEngineDirectFastPath || + cif == nullptr || cif->isVariadic) { return nullptr; } diff --git a/NativeScript/ffi/HermesFastCallbackInfo.h b/NativeScript/ffi/HermesFastCallbackInfo.h index 26718648..a9934172 100644 --- a/NativeScript/ffi/HermesFastCallbackInfo.h +++ b/NativeScript/ffi/HermesFastCallbackInfo.h @@ -11,12 +11,20 @@ namespace nativescript { struct HermesFastCallbackInfo { - napi_env env = nullptr; - const uint64_t* thisArg = nullptr; - const uint64_t* argsBase = nullptr; - unsigned int argc = 0; - void* data = nullptr; - const uint64_t* newTarget = nullptr; + 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( @@ -26,25 +34,42 @@ inline const HermesFastCallbackInfo* TryGetHermesFastCallbackInfo( } auto* info = reinterpret_cast(cbinfo); - if (info->env != env || info->thisArg == nullptr || info->argsBase == nullptr) { + 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->thisArg)); + 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 >= info->argc) { + if (index >= HermesFastArgc(info)) { return nullptr; } return reinterpret_cast( - const_cast(info->argsBase - (index + 1))); + const_cast(info->frame->thisArgAndArgsBase - (index + 1))); } } // namespace nativescript diff --git a/NativeScript/ffi/HermesFastNativeApi.h b/NativeScript/ffi/HermesFastNativeApi.h index 6a71ab58..576c62b2 100644 --- a/NativeScript/ffi/HermesFastNativeApi.h +++ b/NativeScript/ffi/HermesFastNativeApi.h @@ -2,6 +2,7 @@ #define NS_HERMES_FAST_NATIVE_API_H #include +#include #include "EngineDirectCall.h" #include "MetadataReader.h" @@ -19,11 +20,20 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, 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 diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 70d7c668..805b5623 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -53,6 +52,11 @@ inline double hermesRawToDouble(uint64_t raw) { 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; @@ -63,30 +67,31 @@ inline bool readHermesFiniteNumber(napi_value value, double* result) { return false; } - double converted = hermesRawToDouble(raw); - if (std::isnan(converted) || std::isinf(converted)) { - converted = 0.0; - } - *result = converted; + *result = hermesRawDoubleIsFinite(raw) ? hermesRawToDouble(raw) : 0.0; return true; } -inline napi_value makeHermesRawValue(uint64_t raw) { - static thread_local uint64_t slots[8] = {}; +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++ & 7]; + uint64_t* slot = &slots[nextSlot++ & 63]; *slot = raw; return reinterpret_cast(slot); } -inline napi_value makeHermesRawNumberValue(double value) { +inline napi_value makeHermesRawNumberValue(Cif* cif, double value) { uint64_t raw = 0; std::memcpy(&raw, &value, sizeof(raw)); - return makeHermesRawValue(raw); + return makeHermesRawValue(cif, raw); } -inline napi_value makeHermesRawBoolValue(bool value) { - return makeHermesRawValue((kHermesBoolETag << 47) | +inline napi_value makeHermesRawBoolValue(Cif* cif, bool value) { + return makeHermesRawValue(cif, (kHermesBoolETag << 47) | (value ? kHermesBoolBit : 0)); } @@ -128,13 +133,30 @@ bool tryFastConvertHermesSelectorArgument(napi_env env, napi_value value, 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) { - *result = cachedSelectorForName(stackBuffer, length); + SEL selector = cachedSelectorForName(stackBuffer, length); + lastSelectorArgument = {env, rawValue, selector}; + *result = selector; return true; } @@ -159,19 +181,308 @@ bool tryFastConvertHermesSelectorArgument(napi_env env, napi_value value, heapBuffer.size(), &length) != napi_ok) { return false; } - *result = cachedSelectorForName(heapBuffer.data(), length); + 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; @@ -179,8 +490,9 @@ bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, if (kind == mdTypeClass) { id nativeObject = static_cast(wrapped); + ObjCBridgeState* bridgeState = nullptr; if (!object_isClass(nativeObject)) { - ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); + bridgeState = ObjCBridgeState::InstanceData(env); if (bridgeState != nullptr) { id normalizedObject = bridgeState->nativeObjectForBridgeWrapper(wrapped); if (normalizedObject != nil) { @@ -191,24 +503,29 @@ bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, if (!object_isClass(nativeObject)) { return false; } + rememberObjectArgument(nativeObject, bridgeState); *reinterpret_cast(result) = static_cast(nativeObject); return true; } - *reinterpret_cast(result) = static_cast(wrapped); + id nativeObject = static_cast(wrapped); + rememberObjectArgument(nativeObject, nullptr); + *reinterpret_cast(result) = nativeObject; return true; } inline bool needsRoundTripCacheFrame(Cif* cif) { return cif != nullptr && - (cif->generatedDispatchHasRoundTripCacheArgument || - cif->generatedDispatchUsesObjectReturnStorage); + cif->generatedDispatchUsesObjectReturnStorage && + cif->generatedDispatchHasRoundTripCacheArgument; } class HermesFastRoundTripCacheFrameGuard { public: - HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState) - : env_(env), bridgeState_(bridgeState) { + HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState, + bool enabled = true) + : env_(enabled ? env : nullptr), + bridgeState_(enabled ? bridgeState : nullptr) { if (bridgeState_ != nullptr) { bridgeState_->beginRoundTripCacheFrame(env_); } @@ -298,6 +615,16 @@ explicit HermesFastReturnStorage(Cif* cif) { 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; @@ -345,61 +672,161 @@ inline void throwArgumentsCountError(napi_env env, size_t actualCount, napi_throw_error(env, "NativeScriptException", message.c_str()); } -inline bool isBlockFallbackSelector(SEL selector) { +inline bool computeBlockFallbackSelector(SEL selector) { return selector == @selector(methodWithSimpleBlock:) || selector == @selector(methodRetainingBlock:) || selector == @selector(methodWithBlock:) || selector == @selector(methodWithComplexBlock:); } -id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { +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 = ObjCBridgeState::InstanceData(env); + ObjCBridgeState* state = + method != nullptr ? method->bridgeState : ObjCBridgeState::InstanceData(env); struct ReceiverCacheEntry { napi_env env = nullptr; ObjCClassMember* method = nullptr; uint64_t rawValue = 0; - id self = nil; - bool classObject = false; + 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 && - lastReceiver.self != nil && - (lastReceiver.classObject || - (state != nullptr && state->hasObjectRef(lastReceiver.self)))) { - return lastReceiver.self; + receiverCacheEntryIsValid(lastReceiver)) { + return lastReceiver.resolved; } - auto rememberReceiver = [&](id resolved) { - if (resolved == nil || rawThis == 0) { + 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.self = resolved; - lastReceiver.classObject = object_isClass(resolved); + 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) { - rememberReceiver(self); - return self; + return finishReceiver(self); } } if (state != nullptr && jsThis != nullptr) { state->tryResolveBridgedTypeConstructor(env, jsThis, &self); if (self != nil) { - rememberReceiver(self); - return self; + return finishReceiver(self); } } @@ -416,8 +843,7 @@ id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { } if (self != nil) { - rememberReceiver(self); - return self; + return finishReceiver(self); } bool shouldUseClassFallback = false; @@ -448,14 +874,13 @@ id resolveHermesSelf(napi_env env, napi_value jsThis, ObjCClassMember* method) { } if (shouldUseClassFallback) { - rememberReceiver(static_cast(method->cls->nativeClass)); - return static_cast(method->cls->nativeClass); + 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 nil; + return {}; } Cif* hermesMemberCif(napi_env env, ObjCClassMember* member, @@ -541,6 +966,10 @@ ObjCEngineDirectInvoker ensureHermesObjCEngineDirectInvoker( 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; } @@ -548,6 +977,24 @@ ObjCEngineDirectInvoker ensureHermesObjCEngineDirectInvoker( 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) { @@ -559,6 +1006,8 @@ CFunctionEngineDirectInvoker ensureHermesCFunctionEngineDirectInvoker( function->napiInvoker = nullptr; function->engineDirectInvoker = nullptr; function->v8Invoker = nullptr; + function->hermesDirectReturnInvoker = nullptr; + function->hermesFrameDirectReturnInvoker = nullptr; } return nullptr; } @@ -574,6 +1023,10 @@ CFunctionEngineDirectInvoker ensureHermesCFunctionEngineDirectInvoker( 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; } @@ -581,6 +1034,177 @@ CFunctionEngineDirectInvoker ensureHermesCFunctionEngineDirectInvoker( 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, @@ -591,8 +1215,13 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, return nullptr; } - const char* selectorName = sel_getName(descriptor->selector); - if (selectorName != nullptr && std::strcmp(selectorName, "class") == 0) { + 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); @@ -603,15 +1232,28 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, 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); + 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 = ObjCBridgeState::InstanceData(env); + ObjCBridgeState* state = member->bridgeState; if (state != nullptr) { napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); if (cached == nullptr) { @@ -623,32 +1265,28 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, } } + 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 && receiverIsClass) { - id obj = *reinterpret_cast(rvalue); - Class receiverClass = static_cast(self); - if (obj != nil && - (receiverClass == [NSString class] || - receiverClass == [NSMutableString class]) && - selectorName != nullptr && - (std::strcmp(selectorName, "string") == 0 || - std::strcmp(selectorName, "stringWithString:") == 0 || - std::strcmp(selectorName, "stringWithCapacity:") == 0)) { - return member->bridgeState->getObject(env, obj, jsThis, kUnownedObject); - } - } - if (cif->returnType->kind == mdTypeAnyObject || cif->returnType->kind == mdTypeProtocolObject || cif->returnType->kind == mdTypeClassObject) { id obj = *reinterpret_cast(rvalue); - if (obj != nil && ![obj isKindOfClass:[NSString class]] && - ![obj isKindOfClass:[NSNumber class]] && - ![obj isKindOfClass:[NSNull class]]) { - ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + 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) { napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); if (cached == nullptr) { @@ -661,10 +1299,12 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, } } - napi_value fastResult = nullptr; - if (TryFastConvertHermesReturnValue(env, 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; + } } return cif->returnType->toJS(env, rvalue, @@ -678,11 +1318,34 @@ napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, } napi_value fastResult = nullptr; - if (TryFastConvertHermesReturnValue(env, cif->returnType->kind, rvalue, + 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; @@ -690,21 +1353,116 @@ napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, return cif->returnType->toJS(env, rvalue, toJSFlags); } -bool isCompatOrMainCFunction(ObjCBridgeState* bridgeState, MDSectionOffset offset) { +} // 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 true; + return false; } - const char* name = bridgeState->metadata->getString(offset); - return name == nullptr || - std::strcmp(name, "dispatch_async") == 0 || - std::strcmp(name, "dispatch_get_current_queue") == 0 || - std::strcmp(name, "dispatch_get_global_queue") == 0 || - std::strcmp(name, "UIApplicationMain") == 0 || - std::strcmp(name, "NSApplicationMain") == 0; -} + if (value == nil && selector != @selector(class)) { + return napi_get_null(env, result) == napi_ok; + } -} // namespace + 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) { + napi_value cached = + bridgeState->getCachedHandleObject(env, static_cast(value)); + if (cached == nullptr) { + cached = bridgeState->findCachedObjectWrapper(env, value); + } + if (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) { + napi_value cached = + bridgeState->getCachedHandleObject(env, static_cast(value)); + if (cached == nullptr) { + cached = bridgeState->findCachedObjectWrapper(env, value); + } + if (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) { @@ -839,12 +1597,138 @@ bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, 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) { @@ -901,14 +1785,27 @@ bool TryFastConvertHermesArgument(napi_env env, MDTypeKind kind, 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, MDTypeKind kind, +bool TryFastConvertHermesReturnValue(napi_env env, Cif* cif, MDTypeKind kind, const void* value, napi_value* result) { if (env == nullptr || result == nullptr) { return false; @@ -923,7 +1820,8 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, return false; } *result = - makeHermesRawBoolValue(*reinterpret_cast(value) != 0); + makeHermesRawBoolValue( + cif, *reinterpret_cast(value) != 0); return true; case mdTypeChar: { @@ -932,10 +1830,10 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, } const int8_t raw = *reinterpret_cast(value); if (raw == 0 || raw == 1) { - *result = makeHermesRawBoolValue(raw == 1); + *result = makeHermesRawBoolValue(cif, raw == 1); return true; } - *result = makeHermesRawNumberValue(static_cast(raw)); + *result = makeHermesRawNumberValue(cif, static_cast(raw)); return true; } @@ -946,10 +1844,10 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, } const uint8_t raw = *reinterpret_cast(value); if (raw == 0 || raw == 1) { - *result = makeHermesRawBoolValue(raw == 1); + *result = makeHermesRawBoolValue(cif, raw == 1); return true; } - *result = makeHermesRawNumberValue(static_cast(raw)); + *result = makeHermesRawNumberValue(cif, static_cast(raw)); return true; } @@ -957,7 +1855,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - *result = makeHermesRawNumberValue( + *result = makeHermesRawNumberValue(cif, static_cast(*reinterpret_cast(value))); return true; @@ -966,12 +1864,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, return false; } const uint16_t raw = *reinterpret_cast(value); - if (raw >= 32 && raw <= 126) { - const char buffer[2] = {static_cast(raw), '\0'}; - return napi_create_string_utf8(env, buffer, NAPI_AUTO_LENGTH, - result) == napi_ok; - } - *result = makeHermesRawNumberValue(static_cast(raw)); + *result = makeHermesRawNumberValue(cif, static_cast(raw)); return true; } @@ -979,7 +1872,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - *result = makeHermesRawNumberValue( + *result = makeHermesRawNumberValue(cif, static_cast(*reinterpret_cast(value))); return true; @@ -987,7 +1880,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - *result = makeHermesRawNumberValue( + *result = makeHermesRawNumberValue(cif, static_cast(*reinterpret_cast(value))); return true; @@ -1001,7 +1894,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (raw > kMaxSafeInteger || raw < -kMaxSafeInteger) { return napi_create_bigint_int64(env, raw, result) == napi_ok; } - *result = makeHermesRawNumberValue(static_cast(raw)); + *result = makeHermesRawNumberValue(cif, static_cast(raw)); return true; } @@ -1015,7 +1908,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (raw > kMaxSafeInteger) { return napi_create_bigint_uint64(env, raw, result) == napi_ok; } - *result = makeHermesRawNumberValue(static_cast(raw)); + *result = makeHermesRawNumberValue(cif, static_cast(raw)); return true; } @@ -1023,7 +1916,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - *result = makeHermesRawNumberValue( + *result = makeHermesRawNumberValue(cif, static_cast(*reinterpret_cast(value))); return true; @@ -1031,7 +1924,7 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, if (value == nullptr) { return false; } - *result = makeHermesRawNumberValue( + *result = makeHermesRawNumberValue(cif, *reinterpret_cast(value)); return true; @@ -1040,11 +1933,16 @@ bool TryFastConvertHermesReturnValue(napi_env env, MDTypeKind kind, } } -napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, - napi_value jsThis, size_t actualArgc, - const napi_value* rawArgs, - EngineDirectMemberKind kind, - bool* handled) { +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; } @@ -1070,17 +1968,12 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, return nullptr; } - if (isBlockFallbackSelector(descriptor->selector)) { + if (isBlockFallbackSelector(descriptor)) { return nullptr; } - ObjCEngineDirectInvoker invoker = - cif->signatureHash != 0 - ? ensureHermesObjCEngineDirectInvoker(cif, descriptor, - descriptor->dispatchFlags) - : nullptr; - - id self = resolveHermesSelf(env, jsThis, member); + HermesResolvedSelf resolvedSelf = resolveHermesSelf(env, jsThis, member); + id self = resolvedSelf.self; if (self == nil) { if (handled != nullptr) { *handled = true; @@ -1088,17 +1981,127 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, return nullptr; } - const bool receiverIsClass = object_isClass(self); - Class receiverClass = - receiverIsClass ? static_cast(self) : object_getClass(self); - if (receiverClassRequiresHermesSuperCall(receiverClass)) { + const bool receiverIsClass = resolvedSelf.receiverIsClass; + if (resolvedSelf.requiresSuperCall) { return nullptr; } - napi_value stackPaddedArgs[16]; - std::vector heapPaddedArgs; - const napi_value* invocationArgs = prepareHermesInvocationArgs( - env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + 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 { + 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 { + 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()) { @@ -1114,21 +2117,29 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, *handled = true; } - std::optional roundTripCacheFrame; - if (needsRoundTripCacheFrame(cif)) { - roundTripCacheFrame.emplace(env, member->bridgeState); - } + 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(); 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, actualArgc, rawArgs, rvalue); + descriptor->dispatchFlags, dynamicArgc, dynamicArgs, rvalue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -1146,53 +2157,153 @@ napi_value TryCallHermesObjCMemberFast(napi_env env, ObjCClassMember* member, kind != EngineDirectMemberKind::Method); } -napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, - size_t actualArgc, - const napi_value* rawArgs, - bool* handled) { +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 || - isCompatOrMainCFunction(bridgeState, offset)) { + if (env == nullptr || bridgeState == nullptr) { return nullptr; } CFunction* function = bridgeState->getCFunction(env, offset); Cif* cif = function != nullptr ? function->cif : nullptr; - if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->returnType == nullptr) { + if (function == nullptr || function->skipEngineDirectFastPath || + cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { return nullptr; } - CFunctionEngineDirectInvoker invoker = + 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 { + 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 - ? ensureHermesCFunctionEngineDirectInvoker(function, cif) + ? ensureHermesCFunctionDirectReturnInvoker(function, cif) : nullptr; - napi_value stackPaddedArgs[16]; - std::vector heapPaddedArgs; - const napi_value* invocationArgs = prepareHermesInvocationArgs( - env, cif, actualArgc, rawArgs, stackPaddedArgs, 16, &heapPaddedArgs); + 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(); + 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; } - std::optional roundTripCacheFrame; - if (needsRoundTripCacheFrame(cif)) { - roundTripCacheFrame.emplace(env, bridgeState); - } + HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, needsRoundTripCacheFrame(cif)); bool didInvoke = false; + CFunctionEngineDirectInvoker invoker = + cif->signatureHash != 0 + ? ensureHermesCFunctionEngineDirectInvoker(function, cif) + : nullptr; @try { + const napi_value* invocationArgs = getPreparedInvocationArgs(); 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, actualArgc, rawArgs, cif->rvalue); + env, function, cif, dynamicArgc, dynamicArgs, cif->rvalue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -1208,6 +2319,21 @@ napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, 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/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index b2be9cef..9a51208a 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -185,11 +186,13 @@ class ObjCBridgeState { napi_delete_reference(env, it->second); handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); } napi_ref ref = nullptr; napi_create_reference(env, value, 0, &ref); handleObjectRefs[handleKey] = ref; + bumpHandleObjectRefsGeneration(); } inline napi_value getCachedHandleObject(napi_env env, void* handle) { if (handle == nullptr) { @@ -197,6 +200,47 @@ 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; @@ -206,8 +250,23 @@ class ObjCBridgeState { if (value == nullptr) { napi_delete_reference(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, + .generation = generation, + }; + recentHandleObjectRefs[nextRecentHandleObjectRefSlot++ & 7] = + lastHandleObjectRef; return value; } @@ -235,6 +294,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(); } @@ -724,6 +791,7 @@ class ObjCBridgeState { napi_ref referenceClass = nullptr; napi_ref functionReferenceClass = nullptr; napi_ref createNativeProxy = nullptr; + napi_ref createNativeFastArrayIndexes = nullptr; napi_ref createFastEnumeratorIterator = nullptr; napi_ref transferOwnershipToNative = nullptr; @@ -750,9 +818,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 { @@ -792,12 +869,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 6fa741c3..1f817217 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) { @@ -887,6 +890,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat deleteRef(referenceClass); deleteRef(functionReferenceClass); deleteRef(createNativeProxy); + deleteRef(createNativeFastArrayIndexes); deleteRef(createFastEnumeratorIterator); deleteRef(transferOwnershipToNative); @@ -954,7 +958,42 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat #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; diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/Object.mm index 79cbc585..a8771af2 100644 --- a/NativeScript/ffi/Object.mm +++ b/NativeScript/ffi/Object.mm @@ -190,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) { @@ -197,6 +232,38 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return target.class().superclass(); } + 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") { @@ -247,6 +314,9 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt } Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + if (typeof name === "string") { + boundMethods[name] = wrapper; + } try { Object.defineProperty(target, name, { value: wrapper, @@ -281,6 +351,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)) { @@ -302,12 +393,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); diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 017eb5ac..0e2bc1f4 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -17,6 +17,13 @@ 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, @@ -53,6 +60,434 @@ using CFunctionV8Invoker = 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) { + *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; @@ -96,8 +531,34 @@ struct ObjCV8DispatchEntry { }; 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; - CFunctionV8Invoker invoker; + CFunctionHermesFrameDirectReturnInvoker invoker; +}; + +struct BlockHermesFrameDirectReturnDispatchEntry { + uint64_t dispatchId; + BlockHermesFrameDirectReturnInvoker invoker; }; #endif @@ -234,6 +695,18 @@ bool TryFastSetV8GeneratedObjCObjectReturnValue( #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" @@ -278,6 +751,35 @@ inline constexpr CFunctionV8DispatchEntry } // 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 @@ -397,6 +899,58 @@ inline CFunctionV8Invoker lookupCFunctionV8Invoker(uint64_t 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 7f97f4e0..82246f19 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/TypeConv.h @@ -15,6 +15,8 @@ using namespace metagen; namespace nativescript { +class Cif; + typedef enum ConvertToJSFlags : uint32_t { kReturnOwned = 1 << 0, kBlockParam = 1 << 1, @@ -174,6 +176,8 @@ 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 diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js index 62760a34..6f1a2ed8 100644 --- a/benchmarks/objc-dispatch/run.js +++ b/benchmarks/objc-dispatch/run.js @@ -554,7 +554,7 @@ function launchAndCollect(udid, bundleId, options, env = {}) { const child = childProcess.spawn( "xcrun", - ["simctl", "launch", "--console", "--terminate-running-process", udid, bundleId], + ["simctl", "launch", "--terminate-running-process", udid, bundleId], { env: launchEnv } ); children.push(child); diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 3a24c0ce..65d5a449 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -420,6 +420,43 @@ std::string makeV8WrapperName(DispatchKind kind, size_t 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"; @@ -536,6 +573,28 @@ bool canSetV8ReturnDirectly(MDTypeKind kind) { } } +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: @@ -589,6 +648,83 @@ void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, } } +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: @@ -1017,34 +1153,94 @@ bool engineDirectConverterTakesKind(MDTypeKind kind) { } } +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 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 << "argv[" << index << "], &arg" << index << ")) {\n"; + out << argValue << ", &arg" << index << ")) {\n"; if (argKindMayNeedCleanup(type->kind)) { - out << " cif->argTypes[" << index << "]->toNative(env, argv[" << index - << "], &arg" << index << ", &shouldFree" << index + 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, argv[" << index - << "], &arg" << index + 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) { @@ -1353,6 +1549,323 @@ void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, 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 writeV8Wrapper(std::ostringstream& out, DispatchKind kind, const std::string& wrapperName, const MDSignature* signature) { @@ -1776,6 +2289,11 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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; @@ -1810,6 +2328,11 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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; @@ -1829,15 +2352,29 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, objcNapiEntries.emplace(dispatchId, wrapperKey); 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); + } } } @@ -1874,6 +2411,37 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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; @@ -1934,6 +2502,56 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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( @@ -1960,6 +2578,18 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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 || " @@ -2041,27 +2671,27 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " "TryFastConvertHermesArgument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " - "TryFastConvertHermesBoolArgument\n"; + "TryFastConvertHermesGeneratedBoolArgument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " - "TryFastConvertHermesInt8Argument\n"; + "TryFastConvertHermesGeneratedInt8Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " - "TryFastConvertHermesUInt8Argument\n"; + "TryFastConvertHermesGeneratedUInt8Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " - "TryFastConvertHermesInt16Argument\n"; + "TryFastConvertHermesGeneratedInt16Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " - "TryFastConvertHermesUInt16Argument\n"; + "TryFastConvertHermesGeneratedUInt16Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " - "TryFastConvertHermesInt32Argument\n"; + "TryFastConvertHermesGeneratedInt32Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " - "TryFastConvertHermesUInt32Argument\n"; + "TryFastConvertHermesGeneratedUInt32Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " - "TryFastConvertHermesInt64Argument\n"; + "TryFastConvertHermesGeneratedInt64Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " - "TryFastConvertHermesUInt64Argument\n"; + "TryFastConvertHermesGeneratedUInt64Argument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " - "TryFastConvertHermesFloatArgument\n"; + "TryFastConvertHermesGeneratedFloatArgument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " - "TryFastConvertHermesDoubleArgument\n"; + "TryFastConvertHermesGeneratedDoubleArgument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " "TryFastConvertHermesSelectorArgument\n"; generated << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " @@ -2090,6 +2720,69 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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, @@ -2139,6 +2832,58 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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"; From 8213dc4f0eb62ac4ff7edcf88ecb282c3d6ac2a1 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 00:50:27 -0400 Subject: [PATCH 11/31] feat: add Hermes JSI native API bridge --- .gitignore | 7 + NativeScript/CMakeLists.txt | 1 + NativeScript/NativeScript-Prefix.pch | 2 +- NativeScript/ffi/CFunction.mm | 3 + NativeScript/ffi/CallbackThreading.h | 163 ++ NativeScript/ffi/ClassBuilder.mm | 114 +- NativeScript/ffi/ClassMember.mm | 31 +- NativeScript/ffi/Closure.mm | 11 +- NativeScript/ffi/EngineDirectCall.mm | 18 +- NativeScript/ffi/HermesFastNativeApi.mm | 47 +- NativeScript/ffi/JSCFastNativeApi.mm | 146 +- NativeScript/ffi/ObjCBridge.h | 171 +- NativeScript/ffi/ObjCBridge.mm | 12 +- NativeScript/ffi/Object.mm | 37 +- NativeScript/ffi/QuickJSFastNativeApi.mm | 99 +- NativeScript/ffi/TypeConv.mm | 81 +- NativeScript/ffi/V8FastNativeApi.mm | 35 +- NativeScript/ffi/jsi/NativeApiJsi.h | 39 + NativeScript/ffi/jsi/NativeApiJsi.mm | 1978 +++++++++++++++++ .../ffi/jsi/NativeApiJsiReactNative.h | 80 + NativeScript/ffi/jsi/README.md | 37 + NativeScript/napi/hermes/jsr.cpp | 19 +- NativeScript/napi/hermes/jsr.h | 23 +- NativeScript/napi/jsc/jsc-api.cpp | 110 +- NativeScript/runtime/Runtime.cpp | 12 + NativeScript/runtime/modules/node/Process.cpp | 3 + NativeScript/runtime/modules/timers/Timers.mm | 30 +- package.json | 2 + packages/ios/package.json | 2 +- packages/react-native-ios-hermes/LICENSE | 201 ++ .../NativeScriptNativeApi.podspec | 52 + packages/react-native-ios-hermes/README.md | 31 + .../ios/NativeScriptNativeApiModule.h | 26 + .../ios/NativeScriptNativeApiModule.mm | 99 + .../ios/NativeScriptNativeApiModuleProvider.h | 9 + .../NativeScriptNativeApiModuleProvider.mm | 16 + packages/react-native-ios-hermes/package.json | 52 + .../react-native.config.js | 7 + .../src/NativeScriptNativeApi.ts | 11 + packages/react-native-ios-hermes/src/index.ts | 24 + scripts/build_all_ios.sh | 14 - scripts/build_nativescript.sh | 60 +- scripts/build_npm_ios.sh | 16 +- ...ild_react_native_ios_hermes_turbomodule.sh | 76 + ...est_react_native_ios_hermes_turbomodule.sh | 199 ++ .../Marshalling/TNSFunctionPointers.h | 1 + .../Marshalling/TNSFunctionPointers.m | 11 + test/runtime/fixtures/exported-symbols.txt | 1 + test/runtime/runner/app/tests/ApiTests.js | 19 +- .../tests/Marshalling/FunctionPointerTests.js | 15 + .../runner/app/tests/NativeApiJsiTests.js | 40 + test/runtime/runner/app/tests/index.js | 1 + 52 files changed, 4059 insertions(+), 235 deletions(-) create mode 100644 NativeScript/ffi/CallbackThreading.h create mode 100644 NativeScript/ffi/jsi/NativeApiJsi.h create mode 100644 NativeScript/ffi/jsi/NativeApiJsi.mm create mode 100644 NativeScript/ffi/jsi/NativeApiJsiReactNative.h create mode 100644 NativeScript/ffi/jsi/README.md create mode 100644 packages/react-native-ios-hermes/LICENSE create mode 100644 packages/react-native-ios-hermes/NativeScriptNativeApi.podspec create mode 100644 packages/react-native-ios-hermes/README.md create mode 100644 packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.h create mode 100644 packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.mm create mode 100644 packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.h create mode 100644 packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.mm create mode 100644 packages/react-native-ios-hermes/package.json create mode 100644 packages/react-native-ios-hermes/react-native.config.js create mode 100644 packages/react-native-ios-hermes/src/NativeScriptNativeApi.ts create mode 100644 packages/react-native-ios-hermes/src/index.ts create mode 100755 scripts/build_react_native_ios_hermes_turbomodule.sh create mode 100755 scripts/test_react_native_ios_hermes_turbomodule.sh create mode 100644 test/runtime/runner/app/tests/NativeApiJsiTests.js diff --git a/.gitignore b/.gitignore index dc351670..eb8b1df9 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-ios-hermes/dist/ +packages/react-native-ios-hermes/ios/vendor/ +packages/react-native-ios-hermes/metadata/ +packages/react-native-ios-hermes/native-api-jsi/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 1f9be3f6..c8eadce6 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -273,6 +273,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp + ffi/jsi/NativeApiJsi.mm ffi/HermesFastNativeApi.mm ) 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/CFunction.mm b/NativeScript/ffi/CFunction.mm index a6c48f76..8c528dc3 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -3,6 +3,7 @@ #include #include #include "Block.h" +#include "CallbackThreading.h" #include "ClassMember.h" #include "HermesFastCallbackInfo.h" #include "HermesFastNativeApi.h" @@ -376,6 +377,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && !isMainEntrypoint) { @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); bool invoked = engineDirectInvoker != nullptr ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue) : napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue); @@ -443,6 +445,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { #endif @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (preparedInvoker != nullptr) { preparedInvoker(func->fnptr, avalues, rvalue); } else { 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/ClassBuilder.mm b/NativeScript/ffi/ClassBuilder.mm index ac3ac914..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 diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index 88150ad2..e81efb10 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -12,6 +12,7 @@ #include #include #include "ClassBuilder.h" +#include "CallbackThreading.h" #include "Closure.h" #include "EngineDirectCall.h" #include "Interop.h" @@ -379,6 +380,7 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho } @try { + 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); @@ -461,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; } @@ -468,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 { @@ -483,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 } @@ -1107,9 +1116,7 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = }; inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { - return cif != nullptr && - cif->generatedDispatchUsesObjectReturnStorage && - cif->generatedDispatchHasRoundTripCacheArgument; + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } namespace { @@ -1707,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); } @@ -1928,6 +1944,15 @@ 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); } 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.mm b/NativeScript/ffi/EngineDirectCall.mm index 76b3ec0c..f3d2262f 100644 --- a/NativeScript/ffi/EngineDirectCall.mm +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -27,9 +27,7 @@ constexpr const char* kNativePointerProperty = "__ns_native_ptr"; inline bool needsRoundTripCacheFrame(Cif* cif) { - return cif != nullptr && - cif->generatedDispatchUsesObjectReturnStorage && - cif->generatedDispatchHasRoundTripCacheArgument; + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } class RoundTripCacheFrameGuard { @@ -603,11 +601,8 @@ napi_value convertObjCReturnValue(napi_env env, ObjCClassMember* member, if (obj != nil) { ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { return cached; } } @@ -641,11 +636,8 @@ napi_value convertObjCReturnValue(napi_env env, ObjCClassMember* member, ![obj isKindOfClass:[NSNull class]]) { ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { return cached; } } diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 805b5623..516fd66c 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -17,6 +17,7 @@ #include #include "CFunction.h" +#include "CallbackThreading.h" #include "Class.h" #include "ClassBuilder.h" #include "ClassMember.h" @@ -509,15 +510,19 @@ bool tryFastUnwrapHermesObjectArgument(napi_env env, MDTypeKind kind, } 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->generatedDispatchUsesObjectReturnStorage && - cif->generatedDispatchHasRoundTripCacheArgument; + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } class HermesFastRoundTripCacheFrameGuard { @@ -1255,11 +1260,8 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, if (obj != nil) { ObjCBridgeState* state = member->bridgeState; if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { return cached; } } @@ -1288,11 +1290,8 @@ napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, if (obj != nil && !recognizedFoundationObject) { ObjCBridgeState* state = member->bridgeState; if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, static_cast(obj)); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { return cached; } } @@ -1398,12 +1397,8 @@ bool TryFastSetHermesGeneratedObjCObjectReturnValue( } if (value != nil) { - napi_value cached = - bridgeState->getCachedHandleObject(env, static_cast(value)); - if (cached == nullptr) { - cached = bridgeState->findCachedObjectWrapper(env, value); - } - if (cached != nullptr) { + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + cached != nullptr) { *result = cached; return true; } @@ -1447,12 +1442,8 @@ bool TryFastSetHermesGeneratedObjCObjectReturnValue( } if (value != nil && !recognizedFoundationObject) { - napi_value cached = - bridgeState->getCachedHandleObject(env, static_cast(value)); - if (cached == nullptr) { - cached = bridgeState->findCachedObjectWrapper(env, value); - } - if (cached != nullptr) { + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + cached != nullptr) { *result = cached; return true; } @@ -2017,6 +2008,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( returnContext = &returnContextStorage; } @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (frameDirectReturnInvoker( env, cif, reinterpret_cast(objc_msgSend), self, descriptor->selector, returnContext, hermesArgsBase, @@ -2089,6 +2081,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( returnContext = &returnContextStorage; } @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (directReturnInvoker( env, cif, reinterpret_cast(objc_msgSend), self, descriptor->selector, returnContext, directArgs, @@ -2130,6 +2123,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( 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); @@ -2212,6 +2206,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( napi_value directResult = nullptr; @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (frameDirectReturnInvoker(env, cif, function->fnptr, hermesArgsBase, &directResult)) { return directResult; @@ -2270,6 +2265,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( const napi_value* directArgs = hasExactInvocationArgs ? exactInvocationArgs : getPreparedInvocationArgs(); + NativeCallRuntimeUnlockScope unlockRuntime(env); if (directReturnInvoker(env, cif, function->fnptr, directArgs, &directResult)) { return directResult; @@ -2296,6 +2292,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( : nullptr; @try { const napi_value* invocationArgs = getPreparedInvocationArgs(); + NativeCallRuntimeUnlockScope unlockRuntime(env); if (invoker != nullptr) { didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); } else { diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index 2f9ba7de..0a344575 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -16,6 +16,7 @@ #include "ClassBuilder.h" #include "ClassMember.h" #include "EngineDirectCall.h" +#include "Interop.h" #include "MetadataReader.h" #include "NativeScriptException.h" #include "Object.h" @@ -35,6 +36,33 @@ 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; @@ -553,8 +581,13 @@ bool makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *reinterpret_cast(rvalue); - napi_value converted = member->bridgeState->getObject( - env, obj, constructor, member->returnOwned ? kOwnedObject : kUnownedObject); + 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; } @@ -584,6 +617,16 @@ bool makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, } } + 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; } @@ -689,6 +732,8 @@ bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, } void* rvalue = rvalueStorage.get(); + JSCFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, cif); bool didInvoke = false; @try { if (invoker != nullptr) { @@ -740,6 +785,7 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, : nullptr; bool didInvoke = false; + JSCFastRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); @try { if (invoker != nullptr) { didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); @@ -791,6 +837,21 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, 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); } @@ -911,22 +972,38 @@ void finalizeFastFunction(JSObjectRef object) { JSClassRef fastFunctionClass() { static JSClassRef cls = [] { JSClassDefinition definition = kJSClassDefinitionEmpty; - definition.className = "NativeScriptFastNativeFunction"; - definition.initialize = initializeFastFunction; - definition.callAsFunction = callFastFunction; + definition.className = "NativeScriptFastNativeBinding"; definition.finalize = finalizeFastFunction; return JSClassCreate(&definition); }(); return cls; } -JSObjectRef makeFastFunction(napi_env env, JSCFastNativeKind kind, - void* data) { +JSObjectRef makeFastFunction(napi_env env, JSCFastNativeKind kind, void* data, + const char* name) { auto* binding = new JSCFastNativeBinding{env, kind, data}; - JSObjectRef function = JSObjectMake(env->context, fastFunctionClass(), binding); + 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; } @@ -1461,7 +1538,43 @@ bool TryFastConvertJSCObjectArgument(napi_env env, MDTypeKind kind, if (env == nullptr || value == nullptr || result == nullptr) { return false; } - return tryFastConvertJSCObjectArgument(env, kind, ToJSValue(value), result); + 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, @@ -1641,7 +1754,7 @@ bool TryFastConvertJSCArgument(napi_env env, MDTypeKind kind, napi_value value, case mdTypeInstanceObject: case mdTypeNSStringObject: case mdTypeNSMutableStringObject: - if (tryFastConvertJSCObjectArgument(env, kind, jsValue, result)) { + if (TryFastConvertJSCObjectArgument(env, kind, value, result)) { return true; } return TryFastConvertNapiArgument(env, kind, value, result); @@ -1803,7 +1916,8 @@ bool JSCTryDefineFastNativeProperty( if (descriptor->method == ObjCClassMember::jsCall && descriptor->data != nullptr) { JSObjectRef function = makeFastFunction( - env, JSCFastNativeKind::ObjCMethod, descriptor->data); + env, JSCFastNativeKind::ObjCMethod, descriptor->data, + descriptor->utf8name); return function != nullptr && defineProperty(env, object, descriptor, propertyName, function, nullptr, nullptr); @@ -1812,7 +1926,8 @@ bool JSCTryDefineFastNativeProperty( if (descriptor->method == CFunction::jsCall && descriptor->data != nullptr && !isCompatCFunction(env, descriptor->data)) { JSObjectRef function = makeFastFunction( - env, JSCFastNativeKind::CFunction, descriptor->data); + env, JSCFastNativeKind::CFunction, descriptor->data, + descriptor->utf8name); return function != nullptr && defineProperty(env, object, descriptor, propertyName, function, nullptr, nullptr); @@ -1821,14 +1936,15 @@ bool JSCTryDefineFastNativeProperty( if (descriptor->getter == ObjCClassMember::jsGetter && descriptor->data != nullptr) { JSObjectRef getter = makeFastFunction( - env, JSCFastNativeKind::ObjCGetter, descriptor->data); + env, JSCFastNativeKind::ObjCGetter, descriptor->data, + descriptor->utf8name); JSObjectRef setter = nullptr; if (descriptor->setter == ObjCClassMember::jsSetter) { setter = makeFastFunction(env, JSCFastNativeKind::ObjCSetter, - descriptor->data); + descriptor->data, descriptor->utf8name); } else if (descriptor->setter == ObjCClassMember::jsReadOnlySetter) { setter = makeFastFunction(env, JSCFastNativeKind::ObjCReadOnlySetter, - descriptor->data); + descriptor->data, descriptor->utf8name); } else if (descriptor->setter != nullptr) { return false; } diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index 9a51208a..12941649 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -42,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; @@ -167,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; @@ -175,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 && @@ -184,14 +202,34 @@ 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) { @@ -246,9 +284,9 @@ class ObjCBridgeState { 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 && @@ -262,13 +300,130 @@ class ObjCBridgeState { .bridgeState = this, .env = env, .handleKey = handleKey, - .ref = it->second, + .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; @@ -785,7 +940,9 @@ 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; diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index 1f817217..f504aab3 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -861,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) { @@ -1025,7 +1032,8 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); - cacheHandleObject(env, nativeObject, result); + cacheHandleObjectRef(nativeObject, ref); + cacheRecentObjectWrapper(env, nativeObject, result); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/Object.mm index a8771af2..4c532721 100644 --- a/NativeScript/ffi/Object.mm +++ b/NativeScript/ffi/Object.mm @@ -275,19 +275,21 @@ function bindTargetMethod(target, name) { if ((name === "isKindOfClass" || name === "isMemberOfClass")) { wrapper = function (cls, a1, a2, a3) { let resolvedClass = cls; - if (resolvedClass != null && typeof resolvedClass === "object") { + const resolvedClassType = typeof resolvedClass; + if (resolvedClass != null && + (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 (_) { } } @@ -622,7 +624,12 @@ 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)) { @@ -645,7 +652,11 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { } } - return handleCached; + if (isRawHandleRoundTrip) { + return handleCached; + } + + removeCachedHandleObject(env, (void*)obj); } if (napi_value existing = getNormalizedObjectRef(env, obj); existing != nullptr) { @@ -843,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]; } } @@ -852,6 +865,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, return false; } + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); [object release]; return true; } @@ -859,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/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index 5c24af8c..2ebfc994 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -19,6 +19,7 @@ #include "ClassBuilder.h" #include "ClassMember.h" #include "EngineDirectCall.h" +#include "Interop.h" #include "MetadataReader.h" #include "NativeScriptException.h" #include "ObjCBridge.h" @@ -122,6 +123,34 @@ 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; } @@ -686,10 +715,15 @@ bool makeQuickJSObjCReturnValue( napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *reinterpret_cast(rvalue); - napi_value converted = member->bridgeState->getObject( - env, obj, constructor, - member->returnOwned ? nativescript::kOwnedObject - : nativescript::kUnownedObject); + 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); @@ -726,6 +760,19 @@ bool makeQuickJSObjCReturnValue( } } + 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; } @@ -834,6 +881,8 @@ bool tryCallQuickJSObjCEngineDirect( } void* rvalue = rvalueStorage.get(); + QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, member->bridgeState, cif); bool didInvoke = false; @try { if (invoker != nullptr) { @@ -886,6 +935,8 @@ bool tryCallQuickJSCFunctionEngineDirect(JSContext* context, napi_env env, : nullptr; bool didInvoke = false; + QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + env, bridgeState, cif); @try { if (invoker != nullptr) { didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); @@ -1555,8 +1606,44 @@ bool TryFastConvertQuickJSObjectArgument(napi_env env, MDTypeKind kind, if (env == nullptr || value == nullptr || result == nullptr) { return false; } - return tryFastConvertQuickJSObjectArgument(env, kind, ToJSValue(value), - result); + 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, diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/TypeConv.mm index 66decdcb..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: @@ -2141,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()) { @@ -2640,6 +2682,7 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, } *res = (id)wrapped; + cacheRoundTrip(*res); return; break; @@ -2780,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 { @@ -3944,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.mm b/NativeScript/ffi/V8FastNativeApi.mm index feb5fa9a..6565a639 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -498,6 +498,11 @@ bool TryFastUnwrapV8ObjectArgument(napi_env env, v8::Local value, id* 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; } } @@ -510,6 +515,11 @@ bool TryFastUnwrapV8ObjectArgument(napi_env env, v8::Local value, id* 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; } } @@ -888,11 +898,8 @@ bool TryFastSetV8ObjectReturnValue(napi_env env, return true; } - napi_value cached = bridgeState->getCachedHandleObject(env, (void*)value); - if (cached == nullptr) { - cached = bridgeState->findCachedObjectWrapper(env, value); - } - if (cached != nullptr) { + if (napi_value cached = bridgeState->findCachedObjectWrapper(env, value); + cached != nullptr) { info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); return true; } @@ -1721,11 +1728,8 @@ void setObjCReturnValue(napi_env env, const v8::FunctionCallbackInfo& if (obj != nil) { ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, (void*)obj); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); return; } @@ -1760,11 +1764,8 @@ void setObjCReturnValue(napi_env env, const v8::FunctionCallbackInfo& ![obj isKindOfClass:[NSNumber class]] && ![obj isKindOfClass:[NSNull class]]) { ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); if (state != nullptr) { - napi_value cached = state->getCachedHandleObject(env, (void*)obj); - if (cached == nullptr) { - cached = state->findCachedObjectWrapper(env, obj); - } - if (cached != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { info.GetReturnValue().Set(v8impl::V8LocalValueFromJsValue(cached)); return; } @@ -1885,9 +1886,7 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf cif->generatedDispatchSetsV8ReturnDirectly; const bool generatedDispatchUsesObjectReturnStorage = !generatedDispatchSetsReturnDirectly && cif->generatedDispatchUsesObjectReturnStorage; - const bool needsRoundTripCache = - generatedDispatchUsesObjectReturnStorage && - cif->generatedDispatchHasRoundTripCacheArgument; + const bool needsRoundTripCache = cif->generatedDispatchHasRoundTripCacheArgument; std::optional roundTripCacheFrame; if (needsRoundTripCache) { roundTripCacheFrame.emplace(env, method->bridgeState); 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..41f0a9e7 --- /dev/null +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -0,0 +1,1978 @@ +#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 "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::Function; +using facebook::jsi::HostObject; +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; + +enum class NativeApiSymbolKind { + Class, + Function, + Protocol, + Enum, +}; + +struct NativeApiSymbol { + NativeApiSymbolKind kind; + MDSectionOffset offset = 0; + 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; +}; + +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; +} + +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; + } + + size_t classCount() const { return classNames_.size(); } + size_t functionCount() const { return functionNames_.size(); } + size_t protocolCount() const { return protocolNames_.size(); } + size_t enumCount() const { return enumNames_.size(); } + + const std::vector& classNames() const { return classNames_; } + const std::vector& functionNames() const { return functionNames_; } + const std::vector& protocolNames() const { return protocolNames_; } + const std::vector& enumNames() const { return enumNames_; } + std::shared_ptr scheduler() const { return scheduler_; } + + 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, readMembersForClass(symbol.offset)); + return inserted.first->second; + } + + 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) { + if (name == nullptr || name[0] == '\0') { + return; + } + + NativeApiSymbol symbol{ + .kind = kind, + .offset = offset, + .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::Protocol: + protocolNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Enum: + enumNames_.push_back(symbol.name); + break; + } + + symbolsByName_[symbol.name] = symbol; + if (kind == NativeApiSymbolKind::Class) { + classSymbolsByRuntimeName_[symbol.runtimeName] = std::move(symbol); + } + } + + void buildSymbolIndexes() { + if (metadata_ == nullptr) { + return; + } + + indexEnums(); + indexFunctions(); + indexProtocols(); + indexClasses(); + } + + 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); + } + addSymbol(NativeApiSymbolKind::Class, originalOffset, name, runtimeName); + + 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); + skipMember(flags, offset); + } + } + } + + 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::unique_ptr metadata_; + void* selfDl_ = nullptr; + std::unordered_map symbolsByName_; + std::unordered_map classSymbolsByRuntimeName_; + std::vector classNames_; + std::vector functionNames_; + std::vector protocolNames_; + std::vector enumNames_; + std::shared_ptr scheduler_; + mutable std::unordered_map> + membersByClassOffset_; +}; + +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::Protocol: + return "protocol"; + case NativeApiSymbolKind::Enum: + return "enum"; + } + 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; + +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)); + } + } + + return result; +} + +class NativeApiPointerHostObject final : public HostObject { + public: + NativeApiPointerHostObject(void* pointer, std::string kind = "pointer") + : pointer_(pointer), kind_(std::move(kind)) {} + + void* pointer() const { return pointer_; } + + 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 == "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, "toString"); + return names; + } + + private: + void* pointer_ = nullptr; + std::string kind_; +}; + +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); + }); + } + } + } + + 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; + } + } + + 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); + }); + } + + 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_; +}; + +struct NativeApiJsiType { + MDTypeKind kind = metagen::mdTypeVoid; + ffi_type* ffiType = &ffi_type_void; + bool supported = true; + bool returnOwned = 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_; +}; + +MDTypeKind stripTypeFlags(MDTypeKind kind) { + return static_cast((kind & ~metagen::mdTypeFlagNext) & + ~metagen::mdTypeFlagVariadic); +} + +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) { + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + return false; + 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) { + MDTypeKind rawKind = metadata->getTypeKind(*offset); + MDTypeKind kind = stripTypeFlags(rawKind); + *offset += sizeof(MDTypeKind); + skipMetadataJsiTypePayload(metadata, offset, kind); + + NativeApiJsiType type; + type.kind = kind; + type.ffiType = ffiTypeForJsiKind(kind); + type.supported = type.ffiType != nullptr && isSupportedJsiKind(kind); + return type; +} + +std::optional parseMetadataJsiSignature( + MDMetadataReader* metadata, MDSectionOffset signatureOffset, + unsigned int implicitArgumentCount, 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); + bool next = (returnKind & metagen::mdTypeFlagNext) != 0; + signature.variadic = (returnKind & metagen::mdTypeFlagVariadic) != 0; + signature.returnType = parseMetadataJsiType(metadata, &offset); + signature.returnType.returnOwned = returnOwned; + + while (next) { + MDTypeKind argKind = metadata->getTypeKind(offset); + next = (argKind & metagen::mdTypeFlagNext) != 0; + signature.argumentTypes.push_back(parseMetadataJsiType(metadata, &offset)); + } + + 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; +} + +const char* skipBalancedEncoding(const char* encoding, char open, char close) { + int depth = 0; + while (encoding != nullptr && *encoding != '\0') { + if (*encoding == open) { + depth++; + } else if (*encoding == close) { + depth--; + if (depth == 0) { + return encoding + 1; + } + } + 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; +} + +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)->pointer()); + } + } + throw facebook::jsi::JSError(runtime, + "Value cannot be converted to Objective-C object."); +} + +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 (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 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: + 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: + case metagen::mdTypeOpaquePointer: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + *static_cast(target) = pointerFromJsiValue(runtime, value, 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: + return static_cast(*static_cast(value)); + 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: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + return Object::createFromHostObject( + runtime, std::make_shared(pointer)); + } + default: + throw facebook::jsi::JSError(runtime, "Unsupported JSI return type."); + } +} + +void prepareJsiArguments(Runtime& runtime, 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]; + void* target = frame.storageAt(i, type.ffiType != nullptr ? type.ffiType->size + : sizeof(void*)); + convertJsiArgument(runtime, type, args[i], target, frame); + } +} + +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, + (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, *signature, args, count, frame); + + std::vector returnStorage( + std::max(signature->returnType.ffiType->size, sizeof(void*)), 0); + @try { + ffi_call(&signature->cif, FFI_FN(fnptr), returnStorage.data(), + frame.values()); + } @catch (NSException* exception) { + throw facebook::jsi::JSError( + runtime, std::string(exception.description.UTF8String ?: "")); + } + + return convertNativeReturnValue(runtime, bridge, signature->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, + (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, *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(signature->returnType.ffiType->size, sizeof(void*)), 0); + @try { +#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 + } @catch (NSException* exception) { + throw facebook::jsi::JSError( + runtime, std::string(exception.description.UTF8String ?: "")); + } + + return convertNativeReturnValue(runtime, bridge, signature->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 == "runOnUI") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUI"), 0, + [bridge](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto scheduler = bridge->scheduler(); + if (scheduler == nullptr) { + throw facebook::jsi::JSError( + runtime, + "NativeApiJsi was installed without a UI scheduler."); + } + + auto promiseCtor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + return promiseCtor.callAsConstructor( + runtime, + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUIPromise"), + 2, + [scheduler](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)); + auto reject = + std::make_shared(promiseArgs[1].asObject(promiseRuntime)); + scheduler->invokeOnUI([scheduler, resolve, reject, + &promiseRuntime]() { + try { + scheduler->invokeOnJS([resolve, &promiseRuntime]() { + resolve->asObject(promiseRuntime) + .asFunction(promiseRuntime) + .call(promiseRuntime); + }); + } catch (const std::exception& error) { + scheduler->invokeOnJS([reject, message = std::string(error.what()), + &promiseRuntime]() { + reject->asObject(promiseRuntime) + .asFunction(promiseRuntime) + .call(promiseRuntime, + String::createFromUtf8(promiseRuntime, message)); + }); + } + }); + + 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 (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); + }); + } + + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(9); + addPropertyName(runtime, names, "runtime"); + addPropertyName(runtime, names, "backend"); + addPropertyName(runtime, names, "metadata"); + addPropertyName(runtime, names, "hasScheduler"); + addPropertyName(runtime, names, "runOnUI"); + addPropertyName(runtime, names, "import"); + addPropertyName(runtime, names, "lookup"); + addPropertyName(runtime, names, "getClass"); + addPropertyName(runtime, names, "getFunction"); + 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, "protocols", + static_cast(bridge_->protocolCount())); + metadata.setProperty(runtime, "enums", + static_cast(bridge_->enumCount())); + + 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, "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()); + })); + 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 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); +} + +} // 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..ea66a067 --- /dev/null +++ b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h @@ -0,0 +1,80 @@ +#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 + +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; + } + invokeOnJS(std::move(task)); + } + + 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..867a0240 --- /dev/null +++ b/NativeScript/ffi/jsi/README.md @@ -0,0 +1,37 @@ +# 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. + +Current pure-JSI coverage is deliberately conservative: primitive numeric and +boolean values, C strings, Objective-C object/class/selector/pointer handles, +`alloc`/`new`, metadata-backed method/property lookup, and explicit selector +dispatch. Structs, blocks, callbacks, and complex typed arrays should stay on +the existing Node-API bridge until the JSI type layer has equivalent ownership +and lifetime handling. 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 49d1a76e..7b591ef8 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -598,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; } @@ -640,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); @@ -793,21 +832,15 @@ class WrapperInfo : public BaseInfoT { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); WrapperInfo* info{}; - auto cachedInfo = env->wrapper_info_cache.find(object); - if (cachedInfo != env->wrapper_info_cache.end()) { - info = static_cast(cachedInfo->second); - RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + info = GetCached(env, object); + if (info != nullptr) { *result = info; return napi_ok; } - bool hasOwnProperty = NativeInfo::GetNativeInfoKey( - env->context, ToJSObject(env, object), - env->wrapper_info_symbol) != nullptr; - - 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; @@ -838,18 +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 = env->wrapper_info_cache.find(object); - if (cachedInfo != env->wrapper_info_cache.end()) { - *result = static_cast(cachedInfo->second); + 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)"} {} }; @@ -1652,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; } } 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/package.json b/package.json index a67d5d18..5e6d5064 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "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-ios-hermes-turbomodule": "./scripts/build_react_native_ios_hermes_turbomodule.sh", + "test-rn-ios-hermes-turbomodule": "./scripts/test_react_native_ios_hermes_turbomodule.sh", "pack:ios": "./scripts/build_npm_ios.sh", "pack:macos": "./scripts/build_npm_macos.sh", "pack:visionos": "./scripts/build_npm_vision.sh", 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-ios-hermes/LICENSE b/packages/react-native-ios-hermes/LICENSE new file mode 100644 index 00000000..6f231e7c --- /dev/null +++ b/packages/react-native-ios-hermes/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-ios-hermes/NativeScriptNativeApi.podspec b/packages/react-native-ios-hermes/NativeScriptNativeApi.podspec new file mode 100644 index 00000000..a8ff1ef1 --- /dev/null +++ b/packages/react-native-ios-hermes/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-ios-hermes-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-ios-hermes/README.md b/packages/react-native-ios-hermes/README.md new file mode 100644 index 00000000..41810e25 --- /dev/null +++ b/packages/react-native-ios-hermes/README.md @@ -0,0 +1,31 @@ +# @nativescript/react-native-ios-hermes + +React Native TurboModule wrapper for the NativeScript Native API JSI bridge on +Hermes. + +The module exposes one small TurboModule whose `install()` method attaches the +NativeScript Native API host object to `globalThis.__nativeScriptNativeApi`. +The host object itself is pure JSI and is shared with the NativeScript Hermes +runtime. + +```ts +import NativeScriptNativeApi from "@nativescript/react-native-ios-hermes"; + +NativeScriptNativeApi.install(); + +const api = globalThis.__nativeScriptNativeApi; +const NSObject = api.getClass("NSObject"); +``` + +The published package must include generated NativeScript metadata and the +libffi xcframework. Build it from the repository root with: + +```sh +npm run build-rn-ios-hermes-turbomodule +``` + +To verify it inside a generated React Native iOS app: + +```sh +npm run test-rn-ios-hermes-turbomodule +``` diff --git a/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.h b/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.h new file mode 100644 index 00000000..77a68967 --- /dev/null +++ b/packages/react-native-ios-hermes/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-hermes/ios/NativeScriptNativeApiModule.mm b/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.mm new file mode 100644 index 00000000..74b045b6 --- /dev/null +++ b/packages/react-native-ios-hermes/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-hermes/ios/NativeScriptNativeApiModuleProvider.h b/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.h new file mode 100644 index 00000000..ea58c873 --- /dev/null +++ b/packages/react-native-ios-hermes/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-hermes/ios/NativeScriptNativeApiModuleProvider.mm b/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.mm new file mode 100644 index 00000000..997cc91f --- /dev/null +++ b/packages/react-native-ios-hermes/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-ios-hermes/package.json b/packages/react-native-ios-hermes/package.json new file mode 100644 index 00000000..76267765 --- /dev/null +++ b/packages/react-native-ios-hermes/package.json @@ -0,0 +1,52 @@ +{ + "name": "@nativescript/react-native-ios-hermes", + "version": "0.0.1", + "description": "React Native TurboModule for NativeScript Native API access on Hermes", + "keywords": [ + "NativeScript", + "React Native", + "TurboModule", + "Hermes", + "JSI", + "iOS" + ], + "repository": { + "type": "git", + "url": "https://github.com/NativeScript/napi-ios", + "directory": "packages/react-native-ios-hermes" + }, + "author": { + "name": "NativeScript Team", + "email": "oss@nativescript.org" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "react-native": "src/index.ts", + "types": "src/index.ts", + "files": [ + "src", + "ios", + "metadata", + "native-api-jsi", + "NativeScriptNativeApi.podspec", + "README.md", + "LICENSE" + ], + "peerDependencies": { + "react": "*", + "react-native": ">=0.80" + }, + "codegenConfig": { + "name": "NativeScriptNativeApiSpec", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "org.nativescript.nativeapi" + }, + "ios": { + "modulesProvider": { + "NativeScriptNativeApi": "NativeScriptNativeApiModuleProvider" + } + } + } +} diff --git a/packages/react-native-ios-hermes/react-native.config.js b/packages/react-native-ios-hermes/react-native.config.js new file mode 100644 index 00000000..22b2e13d --- /dev/null +++ b/packages/react-native-ios-hermes/react-native.config.js @@ -0,0 +1,7 @@ +module.exports = { + dependency: { + platforms: { + android: null, + }, + }, +}; diff --git a/packages/react-native-ios-hermes/src/NativeScriptNativeApi.ts b/packages/react-native-ios-hermes/src/NativeScriptNativeApi.ts new file mode 100644 index 00000000..6b15741b --- /dev/null +++ b/packages/react-native-ios-hermes/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-ios-hermes/src/index.ts b/packages/react-native-ios-hermes/src/index.ts new file mode 100644 index 00000000..d11adca8 --- /dev/null +++ b/packages/react-native-ios-hermes/src/index.ts @@ -0,0 +1,24 @@ +import NativeScriptNativeApi from './NativeScriptNativeApi'; + +export function install(metadataPath = ''): boolean { + return NativeScriptNativeApi.install(metadataPath); +} + +export function isInstalled(): boolean { + return NativeScriptNativeApi.isInstalled(); +} + +export function defaultMetadataPath(): string { + return NativeScriptNativeApi.defaultMetadataPath(); +} + +export function getRuntimeBackend(): string { + return NativeScriptNativeApi.getRuntimeBackend(); +} + +export default { + install, + isInstalled, + defaultMetadataPath, + getRuntimeBackend, +}; diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index 08a57490..8c4fa94f 100755 --- a/scripts/build_all_ios.sh +++ b/scripts/build_all_ios.sh @@ -56,20 +56,6 @@ if $EMBED_METADATA; then fi checkpoint "... All metadata generated!" -elif [[ "$TARGET_ENGINE" != "none" ]]; then - GSD_PLATFORM= - if $BUILD_SIMULATOR; then - GSD_PLATFORM=ios-sim - elif $BUILD_IPHONE; then - GSD_PLATFORM=ios - elif $BUILD_MACOS; then - GSD_PLATFORM=macos - fi - - if [ -n "$GSD_PLATFORM" ]; then - checkpoint "Generating signature dispatch bindings for $GSD_PLATFORM..." - npm run metagen "$GSD_PLATFORM" - fi fi "$SCRIPT_DIR/build_nativescript.sh" --no-vision "$@" diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index ade3fb57..3ec7ccec 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -17,6 +17,7 @@ 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 @@ -70,6 +71,22 @@ 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) @@ -93,34 +110,31 @@ function effective_gsd_backend () { esac } -function signature_dispatch_platform () { - if $BUILD_SIMULATOR; then - echo ios-sim - elif $BUILD_IPHONE; then - echo ios - elif $BUILD_MACOS || $BUILD_MACOS_CLI || $BUILD_MACOS_NODE_API; then - echo macos - elif $BUILD_VISION; then - echo visionos-sim - elif $BUILD_CATALYST; then - echo catalyst - fi +function 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 [ -f "$GENERATED_SIGNATURE_DISPATCH" ]; then + if [ -z "$platform" ]; then return fi - local platform - platform=$(signature_dispatch_platform) - if [ -z "$platform" ]; then + 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 @@ -128,12 +142,12 @@ function ensure_signature_dispatch_bindings () { "$SCRIPT_DIR/build_metadata_generator.sh" fi - checkpoint "Generating signature dispatch bindings for $platform..." - npm run metagen "$platform" + 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" } -ensure_signature_dispatch_bindings - DEV_TEAM=${DEVELOPMENT_TEAM:-} DIST=$(PWD)/dist mkdir -p $DIST @@ -157,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" ;; @@ -306,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" @@ -329,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 afe6a3a9..41587347 100755 --- a/scripts/build_npm_ios.sh +++ b/scripts/build_npm_ios.sh @@ -12,13 +12,27 @@ if [ ! -f "$PACKAGE_DIR/package.json" ]; then fi 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" @@ -39,7 +53,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_ios_hermes_turbomodule.sh b/scripts/build_react_native_ios_hermes_turbomodule.sh new file mode 100755 index 00000000..d29d945f --- /dev/null +++ b/scripts/build_react_native_ios_hermes_turbomodule.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +PACKAGE_DIR="packages/react-native-ios-hermes" +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 React Native iOS Hermes TurboModule package..." + +rm -rf "$PACKAGE_DIR/native-api-jsi" "$PACKAGE_DIR/metadata" "$PACKAGE_DIR/ios/vendor" +mkdir -p \ + "$PACKAGE_DIR/native-api-jsi/metadata/include" \ + "$PACKAGE_DIR/metadata" \ + "$PACKAGE_DIR/ios/vendor/libffi/include" \ + "$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 "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 "React Native TurboModule package staged." + exit 0 +fi + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +checkpoint "Packing React Native iOS Hermes TurboModule..." +( + cd "$PACKAGE_DIR" + npm pack --pack-destination "$REPO_ROOT/$OUTPUT_DIR" +) + +cp "$OUTPUT_DIR"/*.tgz "$PACK_DESTINATION/" + +checkpoint "React Native TurboModule npm package created." diff --git a/scripts/test_react_native_ios_hermes_turbomodule.sh b/scripts/test_react_native_ios_hermes_turbomodule.sh new file mode 100755 index 00000000..962e887c --- /dev/null +++ b/scripts/test_react_native_ios_hermes_turbomodule.sh @@ -0,0 +1,199 @@ +#!/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-ios-hermes-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 React Native iOS Hermes TurboModule tarball..." +"$SCRIPT_DIR/build_react_native_ios_hermes_turbomodule.sh" +TARBALL=$(ls -t "$REPO_ROOT/packages/react-native-ios-hermes/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 {SafeAreaView, Text} from 'react-native'; +import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; + +const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; + +function runSmoke(): string { + try { + const installed = NativeScriptNativeApi.install(); + const api = (globalThis as any).__nativeScriptNativeApi; + if (!installed || !api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + + const nsObject = api.getClass('NSObject'); + if (!nsObject || nsObject.available !== true) { + throw new Error('NSObject metadata lookup failed'); + } + + const summary = { + installed, + runtime: api.runtime, + backend: api.backend, + classes: api.metadata?.classes ?? 0, + metadataPath: NativeScriptNativeApi.defaultMetadataPath(), + turboBackend: NativeScriptNativeApi.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; + } +} + +const result = runSmoke(); + +export default function App(): React.JSX.Element { + 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 iOS Hermes 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"); From 54fd83e761626cd298bfbd04575c0e78fbc09850 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 01:27:51 -0400 Subject: [PATCH 12/31] fix(ios-hermes): dispatch RN native calls on main thread --- NativeScript/ffi/jsi/NativeApiJsi.mm | 182 +++++++++++++--- .../ffi/jsi/NativeApiJsiReactNative.h | 7 +- examples/react-native-ios-hermes-demo/App.tsx | 205 ++++++++++++++++++ .../react-native-ios-hermes-demo/README.md | 17 ++ package.json | 1 + packages/react-native-ios-hermes/README.md | 13 ++ packages/react-native-ios-hermes/package.json | 2 +- .../create_react_native_ios_hermes_demo.sh | 159 ++++++++++++++ ...est_react_native_ios_hermes_turbomodule.sh | 25 ++- 9 files changed, 570 insertions(+), 41 deletions(-) create mode 100644 examples/react-native-ios-hermes-demo/App.tsx create mode 100644 examples/react-native-ios-hermes-demo/README.md create mode 100755 scripts/create_react_native_ios_hermes_demo.sh diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index 41f0a9e7..36f70711 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -3,6 +3,7 @@ #ifdef TARGET_ENGINE_HERMES #import +#include #include #include #include @@ -45,6 +46,53 @@ using metagen::MDSectionOffset; using metagen::MDTypeKind; +thread_local bool gDispatchNativeCallsToUI = false; + +class ScopedNativeApiUINativeCallDispatch final { + public: + ScopedNativeApiUINativeCallDispatch() + : previous_(gDispatchNativeCallsToUI) { + gDispatchNativeCallsToUI = true; + } + + ~ScopedNativeApiUINativeCallDispatch() { + gDispatchNativeCallsToUI = previous_; + } + + private: + bool previous_ = false; +}; + +bool shouldDispatchNativeCallToUI() { + return gDispatchNativeCallsToUI && ![NSThread isMainThread]; +} + +template +void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { + NSString* exceptionDescription = nil; + auto run = [&]() { + @try { + invocation(); + } @catch (NSException* exception) { + exceptionDescription = [exception.description copy]; + } + }; + + if (shouldDispatchNativeCallToUI()) { + dispatch_sync(dispatch_get_main_queue(), ^{ + run(); + }); + } else { + run(); + } + + if (exceptionDescription != nil) { + std::string message = exceptionDescription.UTF8String ?: ""; + [exceptionDescription release]; + throw facebook::jsi::JSError(runtime, message); + } +} + enum class NativeApiSymbolKind { Class, Function, @@ -877,6 +925,20 @@ Value get(Runtime& runtime, const PropNameID& name) override { bool returnOwned = false; }; +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; @@ -1613,15 +1675,27 @@ Value callCFunction(Runtime& runtime, std::vector returnStorage( std::max(signature->returnType.ffiType->size, sizeof(void*)), 0); - @try { + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, [&]() { ffi_call(&signature->cif, FFI_FN(fnptr), returnStorage.data(), frame.values()); - } @catch (NSException* exception) { - throw facebook::jsi::JSError( - runtime, std::string(exception.description.UTF8String ?: "")); - } + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); - return convertNativeReturnValue(runtime, bridge, signature->returnType, + NativeApiJsiType returnType = signature->returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + return convertNativeReturnValue(runtime, bridge, returnType, returnStorage.data()); } @@ -1679,7 +1753,9 @@ Value callObjCSelector(Runtime& runtime, std::vector returnStorage( std::max(signature->returnType.ffiType->size, sizeof(void*)), 0); - @try { + 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; @@ -1690,12 +1766,22 @@ Value callObjCSelector(Runtime& runtime, ffi_call(&signature->cif, FFI_FN(objc_msgSend), returnStorage.data(), values.data()); #endif - } @catch (NSException* exception) { - throw facebook::jsi::JSError( - runtime, std::string(exception.description.UTF8String ?: "")); - } + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); - return convertNativeReturnValue(runtime, bridge, signature->returnType, + NativeApiJsiType returnType = signature->returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + return convertNativeReturnValue(runtime, bridge, returnType, returnStorage.data()); } @@ -1721,9 +1807,9 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (property == "runOnUI") { auto bridge = bridge_; return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "runOnUI"), 0, - [bridge](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { + 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( @@ -1731,6 +1817,23 @@ Value get(Runtime& runtime, const PropNameID& name) override { "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( @@ -1738,34 +1841,41 @@ Value get(Runtime& runtime, const PropNameID& name) override { Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "runOnUIPromise"), 2, - [scheduler](Runtime& promiseRuntime, const Value&, - const Value* promiseArgs, - size_t promiseArgc) -> Value { + [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)); - auto reject = - std::make_shared(promiseArgs[1].asObject(promiseRuntime)); - scheduler->invokeOnUI([scheduler, resolve, reject, - &promiseRuntime]() { - try { - scheduler->invokeOnJS([resolve, &promiseRuntime]() { - resolve->asObject(promiseRuntime) - .asFunction(promiseRuntime) - .call(promiseRuntime); + 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) { - scheduler->invokeOnJS([reject, message = std::string(error.what()), - &promiseRuntime]() { - reject->asObject(promiseRuntime) - .asFunction(promiseRuntime) - .call(promiseRuntime, - String::createFromUtf8(promiseRuntime, message)); - }); + reject->call( + *runtimePtr, + String::createFromUtf8(*runtimePtr, error.what())); } }); diff --git a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h index ea66a067..4e3f1de6 100644 --- a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h +++ b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h @@ -15,6 +15,8 @@ #if NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER +#include + namespace nativescript { class ReactNativeCallInvokerScheduler final : public NativeApiJsiScheduler { @@ -37,7 +39,10 @@ class ReactNativeCallInvokerScheduler final : public NativeApiJsiScheduler { uiInvoker_->invokeAsync(std::move(task)); return; } - invokeOnJS(std::move(task)); + auto heapTask = std::make_shared>(std::move(task)); + dispatch_async(dispatch_get_main_queue(), ^{ + (*heapTask)(); + }); } private: diff --git a/examples/react-native-ios-hermes-demo/App.tsx b/examples/react-native-ios-hermes-demo/App.tsx new file mode 100644 index 00000000..33369a98 --- /dev/null +++ b/examples/react-native-ios-hermes-demo/App.tsx @@ -0,0 +1,205 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import { + Platform, + Pressable, + SafeAreaView, + StyleSheet, + Text, + View, +} from 'react-native'; +import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; + +type NativeApiClass = Record; + +type NativeApiHost = { + backend: string; + metadata?: {classes?: number; functions?: number}; + runOnUI: (callback?: () => void) => Promise; + getClass(name: string): NativeApiClass | null; + import(path: string): boolean; +}; + +const uiUserInterfaceStyle = { + unspecified: 0, + light: 1, + dark: 2, +}; + +function requireNativeApi(): NativeApiHost { + NativeScriptNativeApi.install(); + 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 = requireNativeApi(); + + let nativeCallsRanOnMainThread = false; + await api.runOnUI(() => { + const NSThread = api.getClass('NSThread'); + const UIApplication = api.getClass('UIApplication'); + const UIColor = api.getClass('UIColor'); + + if (!NSThread || !UIApplication || !UIColor) { + throw new Error('UIKit/Foundation metadata was not available'); + } + + 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.invoke('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.invoke('setTintColor:', nativeAccent); + window.invoke('setBackgroundColor:', nativeBackdrop); + window.invoke('setOverrideUserInterfaceStyle:', uiUserInterfaceStyle.dark); + + const rootController = window.invoke('rootViewController'); + const rootView = rootController?.invoke('view'); + if (rootView) { + rootView.invoke('setTintColor:', nativeAccent); + rootView.invoke('setBackgroundColor:', nativeBackdrop); + } + }); + + return { + backend: api.backend, + turboBackend: NativeScriptNativeApi.getRuntimeBackend(), + classes: api.metadata?.classes ?? 0, + functions: api.metadata?.functions ?? 0, + 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 TurboModule + 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-ios-hermes-demo/README.md b/examples/react-native-ios-hermes-demo/README.md new file mode 100644 index 00000000..61b2d54f --- /dev/null +++ b/examples/react-native-ios-hermes-demo/README.md @@ -0,0 +1,17 @@ +# React Native iOS Hermes Demo + +This demo is generated into `build/react-native-ios-hermes-demo` so the +repository does not need to commit React Native boilerplate. + +Run it from the repository root: + +```sh +npm run demo-rn-ios-hermes-turbomodule +``` + +The generated app installs the local +`@nativescript/react-native-ios-hermes` tarball, enables Hermes and the New +Architecture, then launches an iOS simulator app. The app installs the +NativeScript Native API JSI host object 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/package.json b/package.json index 5e6d5064..c14ab315 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "build-node-api": "./scripts/build_all_react_native.sh", "build-rn-ios-hermes-turbomodule": "./scripts/build_react_native_ios_hermes_turbomodule.sh", "test-rn-ios-hermes-turbomodule": "./scripts/test_react_native_ios_hermes_turbomodule.sh", + "demo-rn-ios-hermes-turbomodule": "./scripts/create_react_native_ios_hermes_demo.sh", "pack:ios": "./scripts/build_npm_ios.sh", "pack:macos": "./scripts/build_npm_macos.sh", "pack:visionos": "./scripts/build_npm_vision.sh", diff --git a/packages/react-native-ios-hermes/README.md b/packages/react-native-ios-hermes/README.md index 41810e25..2310a89b 100644 --- a/packages/react-native-ios-hermes/README.md +++ b/packages/react-native-ios-hermes/README.md @@ -17,6 +17,19 @@ const api = globalThis.__nativeScriptNativeApi; const NSObject = api.getClass("NSObject"); ``` +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 api.runOnUI(() => { + const UIColor = api.getClass("UIColor"); + const UIApplication = api.getClass("UIApplication"); + UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; +}); +``` + The published package must include generated NativeScript metadata and the libffi xcframework. Build it from the repository root with: diff --git a/packages/react-native-ios-hermes/package.json b/packages/react-native-ios-hermes/package.json index 76267765..1fc32a51 100644 --- a/packages/react-native-ios-hermes/package.json +++ b/packages/react-native-ios-hermes/package.json @@ -34,7 +34,7 @@ ], "peerDependencies": { "react": "*", - "react-native": ">=0.80" + "react-native": ">=0.79" }, "codegenConfig": { "name": "NativeScriptNativeApiSpec", diff --git a/scripts/create_react_native_ios_hermes_demo.sh b/scripts/create_react_native_ios_hermes_demo.sh new file mode 100755 index 00000000..94d0b6c2 --- /dev/null +++ b/scripts/create_react_native_ios_hermes_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-ios-hermes-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-ios-hermes-demo/App.tsx" + +checkpoint "Building React Native iOS Hermes TurboModule tarball..." +"$SCRIPT_DIR/build_react_native_ios_hermes_turbomodule.sh" +TARBALL=$(ls -t "$REPO_ROOT/packages/react-native-ios-hermes/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 iOS Hermes TurboModule demo app is ready at $APP_DIR" diff --git a/scripts/test_react_native_ios_hermes_turbomodule.sh b/scripts/test_react_native_ios_hermes_turbomodule.sh index 962e887c..1eeac3eb 100755 --- a/scripts/test_react_native_ios_hermes_turbomodule.sh +++ b/scripts/test_react_native_ios_hermes_turbomodule.sh @@ -46,12 +46,13 @@ 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 NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; -function runSmoke(): string { +async function runSmoke(): Promise { try { const installed = NativeScriptNativeApi.install(); const api = (globalThis as any).__nativeScriptNativeApi; @@ -64,8 +65,18 @@ function runSmoke(): string { throw new Error('NSObject metadata lookup failed'); } + let nativeCallsRanOnMainThread = false; + await api.runOnUI(() => { + const NSThread = api.getClass('NSThread'); + 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, @@ -81,9 +92,17 @@ function runSmoke(): string { } } -const result = runSmoke(); - 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} From 3f64df89cdea9b8baecb1340f52f2dff0e3af32a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 01:53:05 -0400 Subject: [PATCH 13/31] fix(ios-hermes): install RN globals and preserve unichar returns --- NativeScript/ffi/HermesFastNativeApi.mm | 4 + NativeScript/ffi/SignatureDispatch.h | 18 ++++ NativeScript/ffi/V8FastNativeApi.mm | 14 ++- NativeScript/ffi/jsi/NativeApiJsi.mm | 100 ++++++++++++++++-- examples/react-native-ios-hermes-demo/App.tsx | 53 +++++----- .../react-native-ios-hermes-demo/README.md | 3 +- .../src/SignatureDispatchEmitter.cpp | 5 +- packages/react-native-ios-hermes/README.md | 13 +-- packages/react-native-ios-hermes/src/index.ts | 95 ++++++++++++++++- ...est_react_native_ios_hermes_turbomodule.sh | 10 +- 10 files changed, 263 insertions(+), 52 deletions(-) diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 516fd66c..4067ce01 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -1855,6 +1855,10 @@ bool TryFastConvertHermesReturnValue(napi_env env, Cif* cif, MDTypeKind kind, 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; } diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 0e2bc1f4..0170d630 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -444,6 +444,10 @@ inline bool SetHermesGeneratedInt16Return(Cif* cif, napi_value* result, 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; } @@ -615,6 +619,20 @@ inline void setV8DispatchUInt64ReturnValue( } } +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, diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/V8FastNativeApi.mm index 6565a639..e7a2b851 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/V8FastNativeApi.mm @@ -734,9 +734,19 @@ bool TryFastConvertV8ReturnValue(napi_env env, MDTypeKind kind, const void* valu *result = v8::Integer::New(isolate, *reinterpret_cast(value)); return true; - case mdTypeUShort: - *result = v8::Integer::NewFromUnsigned(isolate, *reinterpret_cast(value)); + 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)); diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index 36f70711..fb76bc78 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -103,6 +103,7 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { struct NativeApiSymbol { NativeApiSymbolKind kind; MDSectionOffset offset = 0; + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL; std::string name; std::string runtimeName; }; @@ -134,6 +135,18 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { 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; +} + class NativeApiJsiBridge { public: explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) @@ -192,7 +205,7 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) } auto inserted = membersByClassOffset_.emplace( - symbol.offset, readMembersForClass(symbol.offset)); + symbol.offset, readMembersForClassHierarchy(symbol)); return inserted.first->second; } @@ -259,7 +272,8 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) } void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, - const char* name, const char* runtimeName = nullptr) { + const char* name, const char* runtimeName = nullptr, + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL) { if (name == nullptr || name[0] == '\0') { return; } @@ -267,6 +281,7 @@ void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, NativeApiSymbol symbol{ .kind = kind, .offset = offset, + .superclassOffset = superclassOffset, .name = name, .runtimeName = runtimeName != nullptr ? runtimeName : name, }; @@ -288,6 +303,7 @@ void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, symbolsByName_[symbol.name] = symbol; if (kind == NativeApiSymbolKind::Class) { + classSymbolsByOffset_[symbol.offset] = symbol; classSymbolsByRuntimeName_[symbol.runtimeName] = std::move(symbol); } } @@ -380,7 +396,6 @@ void indexClasses() { if (runtimeNameOffset != MD_SECTION_OFFSET_NULL) { runtimeName = metadata_->resolveString(runtimeNameOffset); } - addSymbol(NativeApiSymbolKind::Class, originalOffset, name, runtimeName); while (hasProtocols) { auto protocolOffset = metadata_->getOffset(offset); @@ -390,6 +405,14 @@ void indexClasses() { 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) { @@ -483,10 +506,27 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { 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 protocolNames_; @@ -706,6 +746,13 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } } + + 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(); @@ -734,6 +781,15 @@ void set(Runtime& runtime, const PropNameID& name, const Value& value) override } } + 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); } @@ -888,6 +944,16 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } + 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(); } @@ -1417,7 +1483,17 @@ void convertJsiArgument(Runtime& runtime, const NativeApiJsiType& type, writeNumericArgument(runtime, value, target, "int16"); break; case metagen::mdTypeUShort: - writeNumericArgument(runtime, value, target, "uint16"); + 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"); @@ -1528,8 +1604,14 @@ Value convertNativeReturnValue(Runtime& runtime, return static_cast(*static_cast(value)); case metagen::mdTypeSShort: return static_cast(*static_cast(value)); - case metagen::mdTypeUShort: - 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: @@ -1978,6 +2060,12 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } + 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( diff --git a/examples/react-native-ios-hermes-demo/App.tsx b/examples/react-native-ios-hermes-demo/App.tsx index 33369a98..abc2ab84 100644 --- a/examples/react-native-ios-hermes-demo/App.tsx +++ b/examples/react-native-ios-hermes-demo/App.tsx @@ -9,14 +9,13 @@ import { } from 'react-native'; import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; -type NativeApiClass = Record; +declare const NSThread: any; +declare const UIApplication: any; +declare const UIColor: any; type NativeApiHost = { - backend: string; + backend?: string; metadata?: {classes?: number; functions?: number}; - runOnUI: (callback?: () => void) => Promise; - getClass(name: string): NativeApiClass | null; - import(path: string): boolean; }; const uiUserInterfaceStyle = { @@ -25,9 +24,11 @@ const uiUserInterfaceStyle = { dark: 2, }; -function requireNativeApi(): NativeApiHost { +function installNativeScriptGlobals(): NativeApiHost { NativeScriptNativeApi.install(); - const api = (globalThis as any).__nativeScriptNativeApi as NativeApiHost | undefined; + const api = (globalThis as any).__nativeScriptNativeApi as + | NativeApiHost + | undefined; if (!api) { throw new Error('NativeScript Native API JSI host object was not installed'); } @@ -39,41 +40,37 @@ async function applyUIKitTweaks() { throw new Error('This demo uses UIKit and must run on iOS'); } - const api = requireNativeApi(); + const api = installNativeScriptGlobals(); let nativeCallsRanOnMainThread = false; - await api.runOnUI(() => { - const NSThread = api.getClass('NSThread'); - const UIApplication = api.getClass('UIApplication'); - const UIColor = api.getClass('UIColor'); - - if (!NSThread || !UIApplication || !UIColor) { - throw new Error('UIKit/Foundation metadata was not available'); - } - + await NativeScriptNativeApi.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.invoke('keyWindow'); + 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.invoke('setTintColor:', nativeAccent); - window.invoke('setBackgroundColor:', nativeBackdrop); - window.invoke('setOverrideUserInterfaceStyle:', uiUserInterfaceStyle.dark); - - const rootController = window.invoke('rootViewController'); - const rootView = rootController?.invoke('view'); + 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.invoke('setTintColor:', nativeAccent); - rootView.invoke('setBackgroundColor:', nativeBackdrop); + rootView.tintColor = nativeAccent; + rootView.backgroundColor = nativeBackdrop; } }); diff --git a/examples/react-native-ios-hermes-demo/README.md b/examples/react-native-ios-hermes-demo/README.md index 61b2d54f..3acabac8 100644 --- a/examples/react-native-ios-hermes-demo/README.md +++ b/examples/react-native-ios-hermes-demo/README.md @@ -12,6 +12,7 @@ npm run demo-rn-ios-hermes-turbomodule The generated app installs the local `@nativescript/react-native-ios-hermes` tarball, enables Hermes and the New Architecture, then launches an iOS simulator app. The app installs the -NativeScript Native API JSI host object and uses `runOnUI` to execute a small +NativeScript Native API JSI host object, 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 65d5a449..c8a2c855 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -623,11 +623,14 @@ void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, break; case mdTypeUChar: case mdTypeUInt8: - case mdTypeUShort: 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, " diff --git a/packages/react-native-ios-hermes/README.md b/packages/react-native-ios-hermes/README.md index 2310a89b..c59e78d7 100644 --- a/packages/react-native-ios-hermes/README.md +++ b/packages/react-native-ios-hermes/README.md @@ -4,17 +4,16 @@ React Native TurboModule wrapper for the NativeScript Native API JSI bridge on Hermes. The module exposes one small TurboModule whose `install()` method attaches the -NativeScript Native API host object to `globalThis.__nativeScriptNativeApi`. -The host object itself is pure JSI and is shared with the NativeScript Hermes -runtime. +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 NativeScriptNativeApi from "@nativescript/react-native-ios-hermes"; NativeScriptNativeApi.install(); -const api = globalThis.__nativeScriptNativeApi; -const NSObject = api.getClass("NSObject"); +const object = NSObject.new(); ``` For UIKit work that must happen on the main thread, pass a callback to the JSI @@ -23,9 +22,7 @@ thread; NativeScript native calls made inside the callback are synchronously performed on UIKit's main thread. ```ts -await api.runOnUI(() => { - const UIColor = api.getClass("UIColor"); - const UIApplication = api.getClass("UIApplication"); +await NativeScriptNativeApi.runOnUI(() => { UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; }); ``` diff --git a/packages/react-native-ios-hermes/src/index.ts b/packages/react-native-ios-hermes/src/index.ts index d11adca8..07aedff7 100644 --- a/packages/react-native-ios-hermes/src/index.ts +++ b/packages/react-native-ios-hermes/src/index.ts @@ -1,7 +1,86 @@ import NativeScriptNativeApi from './NativeScriptNativeApi'; -export function install(metadataPath = ''): boolean { - return NativeScriptNativeApi.install(metadataPath); +type NativeApiHost = { + metadata?: { + classNames?: () => string[]; + functionNames?: () => string[]; + }; + runOnUI?: (callback?: () => void) => Promise; + [name: string]: unknown; +}; + +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]); + } + + return true; +} + +export function install( + metadataPath = '', + options: InstallOptions = {}, +): boolean { + const installed = NativeScriptNativeApi.install(metadataPath); + if (installed && options.globals !== false) { + installGlobals(); + } + return installed; } export function isInstalled(): boolean { @@ -16,9 +95,21 @@ 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); +} + export default { install, + installGlobals, isInstalled, defaultMetadataPath, getRuntimeBackend, + runOnUI, }; diff --git a/scripts/test_react_native_ios_hermes_turbomodule.sh b/scripts/test_react_native_ios_hermes_turbomodule.sh index 1eeac3eb..3b5018d2 100755 --- a/scripts/test_react_native_ios_hermes_turbomodule.sh +++ b/scripts/test_react_native_ios_hermes_turbomodule.sh @@ -50,6 +50,9 @@ import {useEffect, useState} from 'react'; import {SafeAreaView, Text} from 'react-native'; import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; +declare const NSObject: any; +declare const NSThread: any; + const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; async function runSmoke(): Promise { @@ -60,14 +63,13 @@ async function runSmoke(): Promise { throw new Error('NativeScript Native API JSI host object was not installed'); } - const nsObject = api.getClass('NSObject'); + const nsObject = NSObject; if (!nsObject || nsObject.available !== true) { - throw new Error('NSObject metadata lookup failed'); + throw new Error('NSObject global install failed'); } let nativeCallsRanOnMainThread = false; - await api.runOnUI(() => { - const NSThread = api.getClass('NSThread'); + await NativeScriptNativeApi.runOnUI(() => { nativeCallsRanOnMainThread = NSThread?.isMainThread === true; if (!nativeCallsRanOnMainThread) { throw new Error('runOnUI did not dispatch native calls to the main thread'); From cc0cafd47c9884395fe3037fbb97cd6d715960be Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 02:53:47 -0400 Subject: [PATCH 14/31] feat(react-native): simplify NativeScript package API --- .gitignore | 8 +- .../App.tsx | 14 +- examples/react-native-demo/README.md | 19 ++ .../react-native-ios-hermes-demo/README.md | 18 -- package.json | 6 +- packages/react-native-ios-hermes/README.md | 41 ---- .../LICENSE | 0 .../NativeScriptNativeApi.podspec | 2 +- packages/react-native/README.md | 69 +++++++ .../ios/NativeScriptNativeApiModule.h | 0 .../ios/NativeScriptNativeApiModule.mm | 0 .../ios/NativeScriptNativeApiModuleProvider.h | 0 .../NativeScriptNativeApiModuleProvider.mm | 0 .../package.json | 10 +- .../react-native.config.js | 0 .../src/NativeScriptNativeApi.ts | 0 packages/react-native/src/index.d.ts | 40 ++++ .../src/index.ts | 11 +- ...ild_react_native_ios_hermes_turbomodule.sh | 76 -------- scripts/build_react_native_turbomodule.sh | 184 ++++++++++++++++++ ...es_demo.sh => create_react_native_demo.sh} | 12 +- ...le.sh => test_react_native_turbomodule.sh} | 25 ++- 22 files changed, 355 insertions(+), 180 deletions(-) rename examples/{react-native-ios-hermes-demo => react-native-demo}/App.tsx (92%) create mode 100644 examples/react-native-demo/README.md delete mode 100644 examples/react-native-ios-hermes-demo/README.md delete mode 100644 packages/react-native-ios-hermes/README.md rename packages/{react-native-ios-hermes => react-native}/LICENSE (100%) rename packages/{react-native-ios-hermes => react-native}/NativeScriptNativeApi.podspec (96%) create mode 100644 packages/react-native/README.md rename packages/{react-native-ios-hermes => react-native}/ios/NativeScriptNativeApiModule.h (100%) rename packages/{react-native-ios-hermes => react-native}/ios/NativeScriptNativeApiModule.mm (100%) rename packages/{react-native-ios-hermes => react-native}/ios/NativeScriptNativeApiModuleProvider.h (100%) rename packages/{react-native-ios-hermes => react-native}/ios/NativeScriptNativeApiModuleProvider.mm (100%) rename packages/{react-native-ios-hermes => react-native}/package.json (85%) rename packages/{react-native-ios-hermes => react-native}/react-native.config.js (100%) rename packages/{react-native-ios-hermes => react-native}/src/NativeScriptNativeApi.ts (100%) create mode 100644 packages/react-native/src/index.d.ts rename packages/{react-native-ios-hermes => react-native}/src/index.ts (94%) delete mode 100755 scripts/build_react_native_ios_hermes_turbomodule.sh create mode 100755 scripts/build_react_native_turbomodule.sh rename scripts/{create_react_native_ios_hermes_demo.sh => create_react_native_demo.sh} (91%) rename scripts/{test_react_native_ios_hermes_turbomodule.sh => test_react_native_turbomodule.sh} (88%) diff --git a/.gitignore b/.gitignore index eb8b1df9..51e665b5 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,7 @@ NativeScript/ffi/GeneratedSignatureDispatch.inc NativeScript/ffi/GeneratedSignatureDispatch.inc.stamp # React Native TurboModule package staging -packages/react-native-ios-hermes/dist/ -packages/react-native-ios-hermes/ios/vendor/ -packages/react-native-ios-hermes/metadata/ -packages/react-native-ios-hermes/native-api-jsi/ +packages/react-native/dist/ +packages/react-native/ios/vendor/ +packages/react-native/metadata/ +packages/react-native/native-api-jsi/ diff --git a/examples/react-native-ios-hermes-demo/App.tsx b/examples/react-native-demo/App.tsx similarity index 92% rename from examples/react-native-ios-hermes-demo/App.tsx rename to examples/react-native-demo/App.tsx index abc2ab84..5426c2a7 100644 --- a/examples/react-native-ios-hermes-demo/App.tsx +++ b/examples/react-native-demo/App.tsx @@ -7,11 +7,7 @@ import { Text, View, } from 'react-native'; -import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; - -declare const NSThread: any; -declare const UIApplication: any; -declare const UIColor: any; +import NativeScript from '@nativescript/react-native'; type NativeApiHost = { backend?: string; @@ -25,7 +21,7 @@ const uiUserInterfaceStyle = { }; function installNativeScriptGlobals(): NativeApiHost { - NativeScriptNativeApi.install(); + NativeScript.init(); const api = (globalThis as any).__nativeScriptNativeApi as | NativeApiHost | undefined; @@ -43,7 +39,7 @@ async function applyUIKitTweaks() { const api = installNativeScriptGlobals(); let nativeCallsRanOnMainThread = false; - await NativeScriptNativeApi.runOnUI(() => { + await NativeScript.runOnUI(() => { nativeCallsRanOnMainThread = NSThread.isMainThread === true; if (!nativeCallsRanOnMainThread) { throw new Error('runOnUI did not dispatch native calls to the main thread'); @@ -76,7 +72,7 @@ async function applyUIKitTweaks() { return { backend: api.backend, - turboBackend: NativeScriptNativeApi.getRuntimeBackend(), + turboBackend: NativeScript.getRuntimeBackend(), classes: api.metadata?.classes ?? 0, functions: api.metadata?.functions ?? 0, nativeCallsRanOnMainThread, @@ -115,7 +111,7 @@ export default function App(): React.JSX.Element { return ( - NativeScript TurboModule + NativeScript for React Native Hermes JSI UIKit Demo {status} 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/examples/react-native-ios-hermes-demo/README.md b/examples/react-native-ios-hermes-demo/README.md deleted file mode 100644 index 3acabac8..00000000 --- a/examples/react-native-ios-hermes-demo/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# React Native iOS Hermes Demo - -This demo is generated into `build/react-native-ios-hermes-demo` so the -repository does not need to commit React Native boilerplate. - -Run it from the repository root: - -```sh -npm run demo-rn-ios-hermes-turbomodule -``` - -The generated app installs the local -`@nativescript/react-native-ios-hermes` tarball, enables Hermes and the New -Architecture, then launches an iOS simulator app. The app installs the -NativeScript Native API JSI host object, 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/package.json b/package.json index c14ab315..03639055 100644 --- a/package.json +++ b/package.json @@ -27,9 +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-ios-hermes-turbomodule": "./scripts/build_react_native_ios_hermes_turbomodule.sh", - "test-rn-ios-hermes-turbomodule": "./scripts/test_react_native_ios_hermes_turbomodule.sh", - "demo-rn-ios-hermes-turbomodule": "./scripts/create_react_native_ios_hermes_demo.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", diff --git a/packages/react-native-ios-hermes/README.md b/packages/react-native-ios-hermes/README.md deleted file mode 100644 index c59e78d7..00000000 --- a/packages/react-native-ios-hermes/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# @nativescript/react-native-ios-hermes - -React Native TurboModule wrapper for the NativeScript Native API JSI bridge on -Hermes. - -The module exposes one small TurboModule whose `install()` 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 NativeScriptNativeApi from "@nativescript/react-native-ios-hermes"; - -NativeScriptNativeApi.install(); - -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 NativeScriptNativeApi.runOnUI(() => { - UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; -}); -``` - -The published package must include generated NativeScript metadata and the -libffi xcframework. Build it from the repository root with: - -```sh -npm run build-rn-ios-hermes-turbomodule -``` - -To verify it inside a generated React Native iOS app: - -```sh -npm run test-rn-ios-hermes-turbomodule -``` diff --git a/packages/react-native-ios-hermes/LICENSE b/packages/react-native/LICENSE similarity index 100% rename from packages/react-native-ios-hermes/LICENSE rename to packages/react-native/LICENSE diff --git a/packages/react-native-ios-hermes/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec similarity index 96% rename from packages/react-native-ios-hermes/NativeScriptNativeApi.podspec rename to packages/react-native/NativeScriptNativeApi.podspec index a8ff1ef1..8aaa5f14 100644 --- a/packages/react-native-ios-hermes/NativeScriptNativeApi.podspec +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| 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-ios-hermes-v#{s.version}" } + s.source = { :git => "https://github.com/NativeScript/napi-ios.git", :tag => "react-native-v#{s.version}" } s.requires_arc = false s.source_files = [ 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-hermes/ios/NativeScriptNativeApiModule.h b/packages/react-native/ios/NativeScriptNativeApiModule.h similarity index 100% rename from packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.h rename to packages/react-native/ios/NativeScriptNativeApiModule.h diff --git a/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm similarity index 100% rename from packages/react-native-ios-hermes/ios/NativeScriptNativeApiModule.mm rename to packages/react-native/ios/NativeScriptNativeApiModule.mm diff --git a/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.h b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h similarity index 100% rename from packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.h rename to packages/react-native/ios/NativeScriptNativeApiModuleProvider.h diff --git a/packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.mm b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm similarity index 100% rename from packages/react-native-ios-hermes/ios/NativeScriptNativeApiModuleProvider.mm rename to packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm diff --git a/packages/react-native-ios-hermes/package.json b/packages/react-native/package.json similarity index 85% rename from packages/react-native-ios-hermes/package.json rename to packages/react-native/package.json index 1fc32a51..0108c004 100644 --- a/packages/react-native-ios-hermes/package.json +++ b/packages/react-native/package.json @@ -1,19 +1,18 @@ { - "name": "@nativescript/react-native-ios-hermes", + "name": "@nativescript/react-native", "version": "0.0.1", - "description": "React Native TurboModule for NativeScript Native API access on Hermes", + "description": "React Native TurboModule for NativeScript Native API access", "keywords": [ "NativeScript", "React Native", "TurboModule", - "Hermes", "JSI", "iOS" ], "repository": { "type": "git", "url": "https://github.com/NativeScript/napi-ios", - "directory": "packages/react-native-ios-hermes" + "directory": "packages/react-native" }, "author": { "name": "NativeScript Team", @@ -22,9 +21,10 @@ "license": "Apache-2.0", "main": "src/index.ts", "react-native": "src/index.ts", - "types": "src/index.ts", + "types": "src/index.d.ts", "files": [ "src", + "types", "ios", "metadata", "native-api-jsi", diff --git a/packages/react-native-ios-hermes/react-native.config.js b/packages/react-native/react-native.config.js similarity index 100% rename from packages/react-native-ios-hermes/react-native.config.js rename to packages/react-native/react-native.config.js diff --git a/packages/react-native-ios-hermes/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts similarity index 100% rename from packages/react-native-ios-hermes/src/NativeScriptNativeApi.ts rename to packages/react-native/src/NativeScriptNativeApi.ts diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts new file mode 100644 index 00000000..8879b4cf --- /dev/null +++ b/packages/react-native/src/index.d.ts @@ -0,0 +1,40 @@ +/// + +export type NativeApiHost = { + runtime?: string; + backend?: string; + metadata?: { + classes?: number; + functions?: number; + protocols?: number; + enums?: number; + classNames?: () => string[]; + functionNames?: () => 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-ios-hermes/src/index.ts b/packages/react-native/src/index.ts similarity index 94% rename from packages/react-native-ios-hermes/src/index.ts rename to packages/react-native/src/index.ts index 07aedff7..ce784a82 100644 --- a/packages/react-native-ios-hermes/src/index.ts +++ b/packages/react-native/src/index.ts @@ -9,7 +9,7 @@ type NativeApiHost = { [name: string]: unknown; }; -type InstallOptions = { +export type InstallOptions = { globals?: boolean; }; @@ -72,7 +72,7 @@ export function installGlobals(): boolean { return true; } -export function install( +export function init( metadataPath = '', options: InstallOptions = {}, ): boolean { @@ -83,6 +83,8 @@ export function install( return installed; } +export const install = init; + export function isInstalled(): boolean { return NativeScriptNativeApi.isInstalled(); } @@ -105,7 +107,8 @@ export function runOnUI(callback?: () => void): Promise { return run(callback); } -export default { +const NativeScript = { + init, install, installGlobals, isInstalled, @@ -113,3 +116,5 @@ export default { getRuntimeBackend, runOnUI, }; + +export default NativeScript; diff --git a/scripts/build_react_native_ios_hermes_turbomodule.sh b/scripts/build_react_native_ios_hermes_turbomodule.sh deleted file mode 100755 index d29d945f..00000000 --- a/scripts/build_react_native_ios_hermes_turbomodule.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -set -euo pipefail -source "$(dirname "$0")/build_utils.sh" - -PACKAGE_DIR="packages/react-native-ios-hermes" -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 React Native iOS Hermes TurboModule package..." - -rm -rf "$PACKAGE_DIR/native-api-jsi" "$PACKAGE_DIR/metadata" "$PACKAGE_DIR/ios/vendor" -mkdir -p \ - "$PACKAGE_DIR/native-api-jsi/metadata/include" \ - "$PACKAGE_DIR/metadata" \ - "$PACKAGE_DIR/ios/vendor/libffi/include" \ - "$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 "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 "React Native TurboModule package staged." - exit 0 -fi - -rm -rf "$OUTPUT_DIR" -mkdir -p "$OUTPUT_DIR" - -checkpoint "Packing React Native iOS Hermes TurboModule..." -( - cd "$PACKAGE_DIR" - npm pack --pack-destination "$REPO_ROOT/$OUTPUT_DIR" -) - -cp "$OUTPUT_DIR"/*.tgz "$PACK_DESTINATION/" - -checkpoint "React Native TurboModule 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_ios_hermes_demo.sh b/scripts/create_react_native_demo.sh similarity index 91% rename from scripts/create_react_native_ios_hermes_demo.sh rename to scripts/create_react_native_demo.sh index 94d0b6c2..ffcefee7 100755 --- a/scripts/create_react_native_ios_hermes_demo.sh +++ b/scripts/create_react_native_demo.sh @@ -5,7 +5,7 @@ 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-ios-hermes-demo"} +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} @@ -16,11 +16,11 @@ 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-ios-hermes-demo/App.tsx" +DEMO_APP_TSX="$REPO_ROOT/examples/react-native-demo/App.tsx" -checkpoint "Building React Native iOS Hermes TurboModule tarball..." -"$SCRIPT_DIR/build_react_native_ios_hermes_turbomodule.sh" -TARBALL=$(ls -t "$REPO_ROOT/packages/react-native-ios-hermes/dist"/*.tgz | head -n 1) +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" @@ -156,4 +156,4 @@ poll(); NODE fi -checkpoint "React Native iOS Hermes TurboModule demo app is ready at $APP_DIR" +checkpoint "React Native NativeScript demo app is ready at $APP_DIR" diff --git a/scripts/test_react_native_ios_hermes_turbomodule.sh b/scripts/test_react_native_turbomodule.sh similarity index 88% rename from scripts/test_react_native_ios_hermes_turbomodule.sh rename to scripts/test_react_native_turbomodule.sh index 3b5018d2..12067ec1 100755 --- a/scripts/test_react_native_ios_hermes_turbomodule.sh +++ b/scripts/test_react_native_turbomodule.sh @@ -5,7 +5,7 @@ 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-ios-hermes-smoke"} +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} @@ -15,9 +15,9 @@ MARKER="NATIVESCRIPT_RN_TURBO_SMOKE_PASS" BUNDLE_ID="org.reactjs.native.example.$APP_NAME" MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" -checkpoint "Building React Native iOS Hermes TurboModule tarball..." -"$SCRIPT_DIR/build_react_native_ios_hermes_turbomodule.sh" -TARBALL=$(ls -t "$REPO_ROOT/packages/react-native-ios-hermes/dist"/*.tgz | head -n 1) +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" @@ -48,28 +48,25 @@ const target = process.argv[2]; fs.writeFileSync(target, `import React from 'react'; import {useEffect, useState} from 'react'; import {SafeAreaView, Text} from 'react-native'; -import NativeScriptNativeApi from '@nativescript/react-native-ios-hermes'; - -declare const NSObject: any; -declare const NSThread: any; +import NativeScript from '@nativescript/react-native'; const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; async function runSmoke(): Promise { try { - const installed = NativeScriptNativeApi.install(); + 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 || nsObject.available !== true) { + if (!nsObject || typeof nsObject.alloc !== 'function') { throw new Error('NSObject global install failed'); } let nativeCallsRanOnMainThread = false; - await NativeScriptNativeApi.runOnUI(() => { + await NativeScript.runOnUI(() => { nativeCallsRanOnMainThread = NSThread?.isMainThread === true; if (!nativeCallsRanOnMainThread) { throw new Error('runOnUI did not dispatch native calls to the main thread'); @@ -82,8 +79,8 @@ async function runSmoke(): Promise { runtime: api.runtime, backend: api.backend, classes: api.metadata?.classes ?? 0, - metadataPath: NativeScriptNativeApi.defaultMetadataPath(), - turboBackend: NativeScriptNativeApi.getRuntimeBackend(), + metadataPath: NativeScript.defaultMetadataPath(), + turboBackend: NativeScript.getRuntimeBackend(), }; console.log(marker + ' ' + JSON.stringify(summary)); @@ -217,4 +214,4 @@ function poll() { poll(); NODE -checkpoint "React Native iOS Hermes TurboModule smoke test passed." +checkpoint "React Native NativeScript TurboModule smoke test passed." From ea9ddbcaf2af4f318826abb1f9621393baf2ee62 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 03:34:14 -0400 Subject: [PATCH 15/31] feat(react-native): expose native constants and enums --- NativeScript/ffi/jsi/NativeApiJsi.mm | 194 ++++++++++++++++++++++- NativeScript/ffi/jsi/README.md | 12 +- examples/react-native-demo/App.tsx | 19 ++- packages/react-native/src/index.d.ts | 3 + packages/react-native/src/index.ts | 12 ++ scripts/test_react_native_turbomodule.sh | 12 ++ 6 files changed, 237 insertions(+), 15 deletions(-) diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index fb76bc78..ab9d36f7 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -96,6 +96,7 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { enum class NativeApiSymbolKind { Class, Function, + Constant, Protocol, Enum, }; @@ -147,6 +148,8 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { return selector; } +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); + class NativeApiJsiBridge { public: explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) @@ -186,13 +189,29 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) : 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* findEnum(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Enum + ? symbol + : nullptr; + } + 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(); } 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_; } std::shared_ptr scheduler() const { return scheduler_; } @@ -293,6 +312,9 @@ void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, 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; @@ -313,12 +335,45 @@ void buildSymbolIndexes() { return; } + indexConstants(); indexEnums(); indexFunctions(); indexProtocols(); indexClasses(); } + 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) { @@ -529,6 +584,7 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { std::unordered_map classSymbolsByOffset_; std::vector classNames_; std::vector functionNames_; + std::vector constantNames_; std::vector protocolNames_; std::vector enumNames_; std::shared_ptr scheduler_; @@ -555,6 +611,8 @@ Value makeString(Runtime& runtime, const std::string& value) { return "class"; case NativeApiSymbolKind::Function: return "function"; + case NativeApiSymbolKind::Constant: + return "constant"; case NativeApiSymbolKind::Protocol: return "protocol"; case NativeApiSymbolKind::Enum: @@ -1704,6 +1762,91 @@ Value convertNativeReturnValue(Runtime& runtime, } } +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); + 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 NativeApiJsiSignature& signature, const Value* args, size_t count, NativeApiJsiArgumentFrame& frame) { @@ -2059,6 +2202,35 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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 (const NativeApiSymbol* classSymbol = bridge_->findClass(property)) { return Object::createFromHostObject( @@ -2077,12 +2249,20 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } + 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); + } + return Value::undefined(); } std::vector getPropertyNames(Runtime& runtime) override { std::vector names; - names.reserve(9); + names.reserve(11); addPropertyName(runtime, names, "runtime"); addPropertyName(runtime, names, "backend"); addPropertyName(runtime, names, "metadata"); @@ -2092,6 +2272,8 @@ Value get(Runtime& runtime, const PropNameID& name) override { addPropertyName(runtime, names, "lookup"); addPropertyName(runtime, names, "getClass"); addPropertyName(runtime, names, "getFunction"); + addPropertyName(runtime, names, "getConstant"); + addPropertyName(runtime, names, "getEnum"); return names; } @@ -2102,6 +2284,8 @@ Object metadataObject(Runtime& runtime) const { 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", @@ -2123,6 +2307,14 @@ Object metadataObject(Runtime& runtime) const { 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( diff --git a/NativeScript/ffi/jsi/README.md b/NativeScript/ffi/jsi/README.md index 867a0240..e32e5fcc 100644 --- a/NativeScript/ffi/jsi/README.md +++ b/NativeScript/ffi/jsi/README.md @@ -29,9 +29,9 @@ 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. -Current pure-JSI coverage is deliberately conservative: primitive numeric and -boolean values, C strings, Objective-C object/class/selector/pointer handles, -`alloc`/`new`, metadata-backed method/property lookup, and explicit selector -dispatch. Structs, blocks, callbacks, and complex typed arrays should stay on -the existing Node-API bridge until the JSI type layer has equivalent ownership -and lifetime handling. +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, including NativeScript-style global +lookup and main-thread native dispatch. Deeper interop features should be +validated against the full NativeScript bridge until equivalent ownership and +lifetime handling is present in the JSI layer. diff --git a/examples/react-native-demo/App.tsx b/examples/react-native-demo/App.tsx index 5426c2a7..fd0d29e3 100644 --- a/examples/react-native-demo/App.tsx +++ b/examples/react-native-demo/App.tsx @@ -11,13 +11,12 @@ import NativeScript from '@nativescript/react-native'; type NativeApiHost = { backend?: string; - metadata?: {classes?: number; functions?: number}; -}; - -const uiUserInterfaceStyle = { - unspecified: 0, - light: 1, - dark: 2, + metadata?: { + classes?: number; + functions?: number; + constants?: number; + enums?: number; + }; }; function installNativeScriptGlobals(): NativeApiHost { @@ -61,7 +60,7 @@ async function applyUIKitTweaks() { window.tintColor = nativeAccent; window.backgroundColor = nativeBackdrop; - window.overrideUserInterfaceStyle = uiUserInterfaceStyle.dark; + window.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark; const rootView = window.rootViewController?.view; if (rootView) { @@ -75,6 +74,10 @@ async function applyUIKitTweaks() { 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, }; } diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 8879b4cf..70110704 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -6,10 +6,13 @@ export type NativeApiHost = { 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; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index ce784a82..d6d89aef 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -4,6 +4,8 @@ type NativeApiHost = { metadata?: { classNames?: () => string[]; functionNames?: () => string[]; + constantNames?: () => string[]; + enumNames?: () => string[]; }; runOnUI?: (callback?: () => void) => Promise; [name: string]: unknown; @@ -69,6 +71,16 @@ export function installGlobals(): boolean { 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; } diff --git a/scripts/test_react_native_turbomodule.sh b/scripts/test_react_native_turbomodule.sh index 12067ec1..1bb7c173 100755 --- a/scripts/test_react_native_turbomodule.sh +++ b/scripts/test_react_native_turbomodule.sh @@ -65,6 +65,14 @@ async function runSmoke(): Promise { 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; @@ -79,6 +87,10 @@ async function runSmoke(): Promise { 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(), }; From bc1629ad5d657a9147ac528adec6c7ba246318fb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 04:15:50 -0400 Subject: [PATCH 16/31] feat(ffi): expand Hermes JSI interop coverage --- NativeScript/ffi/jsi/NativeApiJsi.mm | 2530 ++++++++++++++++++++++++-- NativeScript/ffi/jsi/README.md | 18 +- 2 files changed, 2422 insertions(+), 126 deletions(-) diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index ab9d36f7..a6f7f78c 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -11,15 +11,21 @@ #include #include +#include #include +#include #include #include #include +#include #include +#include #include #include #include +#include #include +#include #include #include "ffi.h" @@ -34,8 +40,10 @@ 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; @@ -47,6 +55,8 @@ using metagen::MDTypeKind; thread_local bool gDispatchNativeCallsToUI = false; +thread_local bool gExecutingDispatchedUINativeCall = false; +std::atomic gSynchronousNativeInvocationDepth{0}; class ScopedNativeApiUINativeCallDispatch final { public: @@ -67,10 +77,22 @@ 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) { @@ -80,7 +102,10 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { if (shouldDispatchNativeCallToUI()) { dispatch_sync(dispatch_get_main_queue(), ^{ + bool previous = gExecutingDispatchedUINativeCall; + gExecutingDispatchedUINativeCall = true; run(); + gExecutingDispatchedUINativeCall = previous; }); } else { run(); @@ -99,6 +124,8 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { Constant, Protocol, Enum, + Struct, + Union, }; struct NativeApiSymbol { @@ -120,6 +147,54 @@ void performNativeInvocation(Runtime& runtime, Invocation&& invocation) { 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; @@ -196,6 +271,13 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) : 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 @@ -203,18 +285,53 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) : 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 { @@ -228,6 +345,15 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) 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) { @@ -321,12 +447,46 @@ void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, 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()); + } } } @@ -340,6 +500,8 @@ void buildSymbolIndexes() { indexFunctions(); indexProtocols(); indexClasses(); + indexStructs(); + indexUnions(); } static void skipConstantValue(MDMetadataReader* metadata, @@ -479,6 +641,54 @@ void indexClasses() { } } + 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; @@ -587,9 +797,19 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { 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) { @@ -617,6 +837,10 @@ Value makeString(Runtime& runtime, const std::string& value) { return "protocol"; case NativeApiSymbolKind::Enum: return "enum"; + case NativeApiSymbolKind::Struct: + return "struct"; + case NativeApiSymbolKind::Union: + return "union"; } return "unknown"; } @@ -637,6 +861,7 @@ void addPropertyName(Runtime& runtime, std::vector& names, class NativeApiPointerHostObject; class NativeApiObjectHostObject; class NativeApiClassHostObject; +class NativeApiProtocolHostObject; Value callCFunction(Runtime& runtime, const std::shared_ptr& bridge, @@ -667,17 +892,36 @@ Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { 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") - : pointer_(pointer), kind_(std::move(kind)) {} + 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); @@ -687,6 +931,66 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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_; @@ -708,6 +1012,13 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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; } @@ -715,6 +1026,85 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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 { @@ -1042,11 +1432,66 @@ Value get(Runtime& runtime, const PropNameID& name) override { NativeApiSymbol symbol_; }; -struct NativeApiJsiType { - MDTypeKind kind = metagen::mdTypeVoid; - ffi_type* ffiType = &ffi_type_void; - bool supported = true; - bool returnOwned = false; +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) { @@ -1103,63 +1548,508 @@ explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(coun std::vector ownedObjects_; }; -MDTypeKind stripTypeFlags(MDTypeKind kind) { - return static_cast((kind & ~metagen::mdTypeFlagNext) & - ~metagen::mdTypeFlagVariadic); +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; } -ffi_type* ffiTypeForJsiKind(MDTypeKind kind) { - switch (kind) { +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 &ffi_type_sint8; + return "c"; case metagen::mdTypeUChar: case metagen::mdTypeUInt8: - case metagen::mdTypeBool: - return &ffi_type_uint8; + return "C"; case metagen::mdTypeSShort: - return &ffi_type_sint16; + return "s"; case metagen::mdTypeUShort: - return &ffi_type_uint16; + return "S"; case metagen::mdTypeSInt: - return &ffi_type_sint32; + return "i"; case metagen::mdTypeUInt: - return &ffi_type_uint32; + return "I"; case metagen::mdTypeSLong: case metagen::mdTypeSInt64: - return &ffi_type_sint64; + return "q"; case metagen::mdTypeULong: case metagen::mdTypeUInt64: - return &ffi_type_uint64; + return "Q"; case metagen::mdTypeFloat: - return &ffi_type_float; + return "f"; case metagen::mdTypeDouble: - return &ffi_type_double; - case metagen::mdTypeVoid: - return &ffi_type_void; + return "d"; case metagen::mdTypeString: + return "*"; case metagen::mdTypeAnyObject: case metagen::mdTypeProtocolObject: - case metagen::mdTypeClassObject: case metagen::mdTypeInstanceObject: case metagen::mdTypeNSStringObject: case metagen::mdTypeNSMutableStringObject: + return "@"; + case metagen::mdTypeClassObject: case metagen::mdTypeClass: + return "#"; case metagen::mdTypeSelector: - case metagen::mdTypePointer: - case metagen::mdTypeOpaquePointer: + return ":"; case metagen::mdTypeBlock: + return "@?"; case metagen::mdTypeFunctionPointer: - return &ffi_type_pointer; + 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 nullptr; + return "?"; } } -bool isSupportedJsiKind(MDTypeKind kind) { - switch (kind) { - case metagen::mdTypeBlock: - case metagen::mdTypeFunctionPointer: - return false; +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; } @@ -1220,22 +2110,232 @@ void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* off } NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, - MDSectionOffset* offset) { + MDSectionOffset* offset, + NativeApiJsiBridge* bridge) { MDTypeKind rawKind = metadata->getTypeKind(*offset); MDTypeKind kind = stripTypeFlags(rawKind); *offset += sizeof(MDTypeKind); - skipMetadataJsiTypePayload(metadata, offset, kind); 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, bool returnOwned = false) { + unsigned int implicitArgumentCount, NativeApiJsiBridge* bridge, + bool returnOwned = false) { if (metadata == nullptr || signatureOffset == MD_SECTION_OFFSET_NULL) { return std::nullopt; } @@ -1245,15 +2345,19 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset offset = signatureOffset; MDTypeKind returnKind = metadata->getTypeKind(offset); - bool next = (returnKind & metagen::mdTypeFlagNext) != 0; - signature.variadic = (returnKind & metagen::mdTypeFlagVariadic) != 0; - signature.returnType = parseMetadataJsiType(metadata, &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 = (argKind & metagen::mdTypeFlagNext) != 0; - signature.argumentTypes.push_back(parseMetadataJsiType(metadata, &offset)); + next = (rawTypeKind(argKind) & + static_cast(metagen::mdTypeFlagNext)) != 0; + signature.argumentTypes.push_back(parseMetadataJsiType(metadata, &offset, bridge)); } signature.ffiTypes.reserve(signature.argumentTypes.size() + @@ -1284,22 +2388,6 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, return encoding; } -const char* skipBalancedEncoding(const char* encoding, char open, char close) { - int depth = 0; - while (encoding != nullptr && *encoding != '\0') { - if (*encoding == open) { - depth++; - } else if (*encoding == close) { - depth--; - if (depth == 0) { - return encoding + 1; - } - } - encoding++; - } - return encoding; -} - NativeApiJsiType parseObjCEncodedJsiType(const char* encoding) { encoding = skipObjCTypeQualifiers(encoding); NativeApiJsiType type; @@ -1432,6 +2520,43 @@ 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()) { @@ -1460,15 +2585,91 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, 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()) { @@ -1488,6 +2689,25 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, 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); @@ -1511,7 +2731,125 @@ void writeNumericArgument(Runtime& runtime, const Value& value, void* target, *static_cast(target) = static_cast(number); } -void convertJsiArgument(Runtime& runtime, const NativeApiJsiType& type, +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)) { @@ -1631,11 +2969,59 @@ void convertJsiArgument(Runtime& runtime, const NativeApiJsiType& type, 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: + 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."); } @@ -1703,63 +3089,736 @@ Value convertNativeReturnValue(Runtime& runtime, 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 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 result; - } - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, object, type.returnOwned)); + 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)); } - 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))); + } + 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*); } - 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(); + Value sizeValue = object.getProperty(runtime, "sizeof"); + if (sizeValue.isNumber()) { + return static_cast(sizeValue.getNumber()); } - case metagen::mdTypePointer: - case metagen::mdTypeOpaquePointer: - case metagen::mdTypeBlock: - case metagen::mdTypeFunctionPointer: { - void* pointer = *static_cast(value); - if (pointer == nullptr) { - return Value::null(); + } + + 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 Object::createFromHostObject( - runtime, std::make_shared(pointer)); } - default: - throw facebook::jsi::JSError(runtime, "Unsupported JSI return type."); + 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, @@ -1833,7 +3892,7 @@ Value constantToValue(Runtime& runtime, } MDSectionOffset typeOffset = offset; - NativeApiJsiType type = parseMetadataJsiType(metadata, &typeOffset); + NativeApiJsiType type = parseMetadataJsiType(metadata, &typeOffset, bridge.get()); if (unsupportedJsiType(type)) { throw facebook::jsi::JSError( runtime, "Native constant type is not supported by pure JSI: " + @@ -1847,7 +3906,9 @@ Value constantToValue(Runtime& runtime, return convertNativeReturnValue(runtime, bridge, type, symbolPtr); } -void prepareJsiArguments(Runtime& runtime, const NativeApiJsiSignature& signature, +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()) { @@ -1859,10 +3920,102 @@ void prepareJsiArguments(Runtime& runtime, const NativeApiJsiSignature& signatur for (size_t i = 0; i < signature.argumentTypes.size(); i++) { const auto& type = signature.argumentTypes[i]; - void* target = frame.storageAt(i, type.ffiType != nullptr ? type.ffiType->size - : sizeof(void*)); - convertJsiArgument(runtime, type, args[i], target, frame); + 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, @@ -1885,7 +4038,7 @@ Value callCFunction(Runtime& runtime, metadata->signaturesOffset + metadata->getOffset(symbol.offset + sizeof(MDSectionOffset)); auto signature = parseMetadataJsiSignature( - metadata, signatureOffset, 0, + metadata, signatureOffset, 0, bridge.get(), (metadata->getFunctionFlag(symbol.offset + sizeof(MDSectionOffset) * 2) & metagen::mdFunctionReturnOwned) != 0); if (!signature || !signature->prepared || signature->variadic || @@ -1896,10 +4049,10 @@ Value callCFunction(Runtime& runtime, } NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); - prepareJsiArguments(runtime, *signature, args, count, frame); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); std::vector returnStorage( - std::max(signature->returnType.ffiType->size, sizeof(void*)), 0); + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); bool retainedReturn = false; performNativeInvocation(runtime, [&]() { @@ -1951,7 +4104,7 @@ Value callObjCSelector(Runtime& runtime, member->signatureOffset != MD_SECTION_OFFSET_NULL && member->signatureOffset != 0) { signature = parseMetadataJsiSignature( - bridge->metadata(), member->signatureOffset, 2, + bridge->metadata(), member->signatureOffset, 2, bridge.get(), (member->flags & metagen::mdMemberReturnOwned) != 0); } if (!signature) { @@ -1966,7 +4119,7 @@ Value callObjCSelector(Runtime& runtime, } NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); - prepareJsiArguments(runtime, *signature, args, count, frame); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); std::vector values; values.reserve(signature->argumentTypes.size() + 2); @@ -1977,7 +4130,7 @@ Value callObjCSelector(Runtime& runtime, } std::vector returnStorage( - std::max(signature->returnType.ffiType->size, sizeof(void*)), 0); + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); bool retainedReturn = false; performNativeInvocation(runtime, [&]() { @@ -2029,6 +4182,9 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (property == "hasScheduler") { return bridge_->scheduler() != nullptr; } + if (property == "interop") { + return createInteropObject(runtime, bridge_); + } if (property == "runOnUI") { auto bridge = bridge_; return Function::createFromHostFunction( @@ -2231,6 +4387,54 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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( @@ -2257,6 +4461,18 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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(); } @@ -2267,6 +4483,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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"); @@ -2274,6 +4491,9 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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; } @@ -2290,6 +4510,10 @@ Object metadataObject(Runtime& runtime) const { 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", @@ -2331,6 +4555,22 @@ Object metadataObject(Runtime& runtime) const { 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; } @@ -2345,12 +4585,58 @@ Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { 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 diff --git a/NativeScript/ffi/jsi/README.md b/NativeScript/ffi/jsi/README.md index e32e5fcc..3fbad151 100644 --- a/NativeScript/ffi/jsi/README.md +++ b/NativeScript/ffi/jsi/README.md @@ -31,7 +31,17 @@ 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, including NativeScript-style global -lookup and main-thread native dispatch. Deeper interop features should be -validated against the full NativeScript bridge until equivalent ownership and -lifetime handling is present in the JSI layer. +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. From b608d7c3fcd4252fc20e7d1680e44ef07ea6198a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 17:08:06 -0400 Subject: [PATCH 17/31] test(ffi): add React Native JSI compatibility suite --- NativeScript/ffi/jsi/NativeApiJsi.mm | 319 ++++++++++++--- package.json | 1 + .../ios/NativeScriptNativeApiModule.h | 1 + .../ios/NativeScriptNativeApiModule.mm | 31 ++ .../react-native/src/NativeScriptNativeApi.ts | 1 + packages/react-native/src/index.d.ts | 8 + packages/react-native/src/index.ts | 160 +++++++- scripts/test_react_native_ffi_compat.sh | 174 +++++++++ test/react-native/ffi-compat/App.tsx | 362 ++++++++++++++++++ 9 files changed, 996 insertions(+), 61 deletions(-) create mode 100755 scripts/test_react_native_ffi_compat.sh create mode 100644 test/react-native/ffi-compat/App.tsx diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index a6f7f78c..45da58a5 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -211,6 +211,32 @@ void finalize() { return jsifiedSelector; } +std::optional runtimeSelectorNameForProperty( + Class cls, bool staticMethod, const std::string& property) { + if (cls == nil || property.empty()) { + return std::nullopt; + } + + Class scan = staticMethod ? object_getClass(cls) : cls; + while (scan != Nil) { + unsigned int methodCount = 0; + Method* methods = class_copyMethodList(scan, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + SEL selector = method_getName(methods[i]); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName != nullptr && jsifySelector(selectorName) == property) { + std::string result(selectorName); + free(methods); + return result; + } + } + free(methods); + scan = class_getSuperclass(scan); + } + + return std::nullopt; +} + std::string setterSelectorForProperty(const std::string& property) { if (property.empty()) { return property; @@ -257,6 +283,20 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) return it != classSymbolsByRuntimeName_.end() ? &it->second : nullptr; } + const NativeApiSymbol* findClassForRuntimeClass(Class cls) const { + Class current = cls; + while (current != Nil) { + const char* name = class_getName(current); + if (name != nullptr) { + if (const NativeApiSymbol* symbol = findClass(name)) { + return symbol; + } + } + current = class_getSuperclass(current); + } + return nullptr; + } + const NativeApiSymbol* findFunction(const std::string& name) const { const NativeApiSymbol* symbol = find(name); return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Function @@ -1032,11 +1072,13 @@ Value get(Runtime& runtime, const PropNameID& name) override { class NativeApiReferenceHostObject final : public HostObject { public: NativeApiReferenceHostObject(std::shared_ptr bridge, - NativeApiJsiType type, void* data, bool ownsData) + NativeApiJsiType type, void* data, bool ownsData, + size_t byteLength = 0) : bridge_(std::move(bridge)), type_(std::move(type)), data_(data), - ownsData_(ownsData) {} + ownsData_(ownsData), + byteLength_(byteLength) {} ~NativeApiReferenceHostObject() override { if (ownsData_ && data_ != nullptr) { @@ -1047,11 +1089,25 @@ Value get(Runtime& runtime, const PropNameID& name) override { void* data() const { return data_; } const NativeApiJsiType& type() const { return type_; } - void ensureStorage(NativeApiJsiType type) { + void ensureStorage(NativeApiJsiType type, size_t elements = 1) { + size_t elementCount = std::max(elements, 1); + NativeApiJsiType storageType = std::move(type); + size_t stride = std::max(nativeSizeForType(storageType), 1); + size_t required = std::max(stride * elementCount, sizeof(void*)); if (data_ == nullptr) { - type_ = std::move(type); - data_ = calloc(1, std::max(nativeSizeForType(type_), sizeof(void*))); + type_ = std::move(storageType); + data_ = calloc(1, required); ownsData_ = true; + byteLength_ = required; + } else if (ownsData_ && byteLength_ < required) { + void* expanded = realloc(data_, required); + if (expanded == nullptr) { + throw std::bad_alloc(); + } + std::memset(static_cast(expanded) + byteLength_, 0, + required - byteLength_); + data_ = expanded; + byteLength_ = required; } } @@ -1071,6 +1127,7 @@ void ensureStorage(NativeApiJsiType type) { NativeApiJsiType type_; void* data_ = nullptr; bool ownsData_ = false; + size_t byteLength_ = 0; }; class NativeApiStructObjectHostObject final : public HostObject { @@ -1166,7 +1223,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (object_ != nil) { if (const NativeApiSymbol* symbol = - bridge_->findClass(object_getClassName(object_))) { + bridge_->findClassForRuntimeClass(object_getClass(object_))) { for (const auto& member : bridge_->membersForClass(*symbol)) { if ((member.flags & metagen::mdMemberStatic) != 0) { continue; @@ -1195,11 +1252,19 @@ Value get(Runtime& runtime, const PropNameID& name) override { } } - 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); + if (auto selectorName = + runtimeSelectorNameForProperty(object_getClass(object_), false, + property)) { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, object, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, object, false, + selectorName, nullptr, args, count); + }); } } @@ -1212,7 +1277,8 @@ void set(Runtime& runtime, const PropNameID& name, const Value& value) override throw facebook::jsi::JSError(runtime, "Cannot set property on nil object."); } - if (const NativeApiSymbol* symbol = bridge_->findClass(object_getClassName(object_))) { + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { for (const auto& member : bridge_->membersForClass(*symbol)) { if (!member.property || member.readonly || (member.flags & metagen::mdMemberStatic) != 0 || @@ -1253,7 +1319,7 @@ void set(Runtime& runtime, const PropNameID& name, const Value& value) override addPropertyName(runtime, names, "toString"); if (object_ != nil) { if (const NativeApiSymbol* symbol = - bridge_->findClass(object_getClassName(object_))) { + bridge_->findClassForRuntimeClass(object_getClass(object_))) { for (const auto& member : bridge_->membersForClass(*symbol)) { if ((member.flags & metagen::mdMemberStatic) == 0) { addPropertyName(runtime, names, member.name.c_str()); @@ -1394,11 +1460,18 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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); + if (auto selectorName = + runtimeSelectorNameForProperty(cls, true, property)) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, cls, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, static_cast(cls), + true, selectorName, nullptr, args, + count); + }); } } @@ -1432,15 +1505,30 @@ Value get(Runtime& runtime, const PropNameID& name) override { NativeApiSymbol symbol_; }; +Protocol* lookupProtocolByNativeName(const std::string& name) { + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol != nullptr) { + return protocol; + } + constexpr const char* suffix = "Protocol"; + size_t suffixLength = std::strlen(suffix); + if (name.size() > suffixLength && + name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - suffixLength).c_str()); + } + return protocol; +} + 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()); + Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { - protocol = objc_getProtocol(symbol_.name.c_str()); + protocol = lookupProtocolByNativeName(symbol_.name); } return protocol; } @@ -2718,6 +2806,39 @@ bool readNativePointerProperty(Runtime& runtime, const Object& object, throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); } +bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) { + if (pointer == nullptr || !value.isObject()) { + return false; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->pointer(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->object(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->nativeClass(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = + object.getHostObject(runtime)->nativeProtocol(); + return true; + } + return readNativePointerProperty(runtime, object, pointer); +} + template void writeNumericArgument(Runtime& runtime, const Value& value, void* target, const char* typeName) { @@ -2741,6 +2862,28 @@ Value convertNativeReturnValue(Runtime& runtime, const std::shared_ptr& bridge, const NativeApiJsiType& type, void* value); +std::optional parseArrayIndexProperty(const std::string& property) { + if (property.empty()) { + return std::nullopt; + } + size_t index = 0; + for (char c : property) { + if (!std::isdigit(static_cast(c))) { + return std::nullopt; + } + size_t digit = static_cast(c - '0'); + if (index > (std::numeric_limits::max() - digit) / 10) { + return std::nullopt; + } + index = (index * 10) + digit; + } + return index; +} + +size_t referenceElementStride(const NativeApiJsiType& type) { + return std::max(nativeSizeForType(type), 1); +} + void convertAggregateArgument(Runtime& runtime, const std::shared_ptr& bridge, const NativeApiJsiType& type, @@ -3138,6 +3281,11 @@ Value convertNativeReturnValue(Runtime& runtime, if (pointer == nullptr) { return Value::null(); } + if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, *type.elementType, pointer, false)); + } return Object::createFromHostObject( runtime, std::make_shared(pointer)); } @@ -3197,6 +3345,14 @@ Value convertNativeReturnValue(Runtime& runtime, } return convertNativeReturnValue(runtime, bridge_, type_, data_); } + if (auto index = parseArrayIndexProperty(property)) { + if (data_ == nullptr) { + return Value::undefined(); + } + void* slot = static_cast(data_) + + (*index * referenceElementStride(type_)); + return convertNativeReturnValue(runtime, bridge_, type_, slot); + } if (property == "toString") { void* data = data_; return Function::createFromHostFunction( @@ -3215,16 +3371,16 @@ Value convertNativeReturnValue(Runtime& runtime, const PropNameID& name, const Value& value) { std::string property = name.utf8(runtime); - if (property != "value") { + auto index = parseArrayIndexProperty(property); + if (property != "value" && !index) { return; } - if (data_ == nullptr) { - size_t size = nativeSizeForType(type_); - data_ = calloc(1, std::max(size, sizeof(void*))); - ownsData_ = true; - } + size_t slotIndex = index.value_or(0); + ensureStorage(type_, slotIndex + 1); + void* slot = static_cast(data_) + + (slotIndex * referenceElementStride(type_)); NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge_, type_, value, data_, frame); + convertJsiArgument(runtime, bridge_, type_, value, slot, frame); } Value NativeApiStructObjectHostObject::get(Runtime& runtime, @@ -3260,6 +3416,12 @@ Value convertNativeReturnValue(Runtime& runtime, continue; } void* fieldData = static_cast(data_) + field.offset; + if (field.type.kind == metagen::mdTypeStruct && + field.type.aggregateInfo != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge_, field.type.aggregateInfo, fieldData, false)); + } return convertNativeReturnValue(runtime, bridge_, field.type, fieldData); } } @@ -3333,6 +3495,7 @@ NativeApiJsiType primitiveInteropType(MDTypeKind kind) { case metagen::mdTypeDouble: case metagen::mdTypeString: case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: case metagen::mdTypeClass: case metagen::mdTypeSelector: case metagen::mdTypePointer: @@ -3368,6 +3531,30 @@ NativeApiJsiType primitiveInteropType(MDTypeKind kind) { } Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString()) { + std::string kindName = kindValue.asString(runtime).utf8(runtime); + if (kindName == "pointer") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "reference") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "class") { + return primitiveInteropType(metagen::mdTypeClass); + } + if (kindName == "selector") { + return primitiveInteropType(metagen::mdTypeSelector); + } + if (kindName == "protocol") { + return primitiveInteropType(metagen::mdTypeProtocolObject); + } + if (kindName == "block") { + return primitiveInteropType(metagen::mdTypeBlock); + } + if (kindName == "functionPointer") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } + } Value offsetValue = object.getProperty(runtime, "metadataOffset"); if (kindValue.isString() && offsetValue.isNumber()) { std::string kindName = kindValue.asString(runtime).utf8(runtime); @@ -3426,7 +3613,6 @@ Value makeAggregateConstructor(Runtime& runtime, 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", @@ -3551,6 +3737,7 @@ Object createInteropObject(Runtime& runtime, setType("unichar", metagen::mdTypeUShort); setType("id", metagen::mdTypeAnyObject); setType("class", metagen::mdTypeClass); + setType("protocol", metagen::mdTypeProtocolObject); setType("SEL", metagen::mdTypeSelector); setType("selector", metagen::mdTypeSelector); setType("pointer", metagen::mdTypePointer); @@ -3558,12 +3745,10 @@ Object createInteropObject(Runtime& runtime, 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 { + Function pointerConstructor = 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)) { @@ -3580,37 +3765,66 @@ Object createInteropObject(Runtime& runtime, 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 { + }); + pointerConstructor.setProperty(runtime, "kind", makeString(runtime, "pointer")); + pointerConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Pointer", pointerConstructor); + + Function referenceConstructor = 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 && + !(count == 1 && args[0].isNumber()) && interopTypeFromValue(runtime, bridge, args[0]).has_value(); if (hasType) { type = *interopTypeFromValue(runtime, bridge, args[0]); + } else if (count > 0 && args[0].isBool()) { + type = primitiveInteropType(metagen::mdTypeBool); + } else if (count > 0 && args[0].isNumber()) { + type = primitiveInteropType(metagen::mdTypeDouble); } - size_t size = hasType ? nativeSizeForType(type) : sizeof(void*); + size_t size = std::max(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); + void* pointer = nullptr; + if (readPointerLikeValue(runtime, args[1], &pointer)) { + data = pointer; + } else { + data = calloc(1, size); + ownsData = true; + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, args[1], data, frame); + } + } else { + data = calloc(1, size); + ownsData = true; } + } else if (count > 0) { + data = calloc(1, size); + ownsData = true; + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, args[0], data, frame); } + if (ownsData && data == nullptr) { + throw std::bad_alloc(); + } return Object::createFromHostObject( runtime, std::make_shared( - bridge, type, data, ownsData)); - })); + bridge, type, data, ownsData, + ownsData ? size : 0)); + }); + referenceConstructor.setProperty(runtime, "kind", + makeString(runtime, "reference")); + referenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Reference", referenceConstructor); interop.setProperty( runtime, "sizeof", @@ -4397,15 +4611,16 @@ Value get(Runtime& runtime, const PropNameID& name) override { readStringArg(runtime, args, count, 0, "name"); const NativeApiSymbol* symbol = bridge->findProtocol(protocolName); if (symbol == nullptr) { - Protocol* protocol = objc_getProtocol(protocolName.c_str()); + Protocol* protocol = lookupProtocolByNativeName(protocolName); if (protocol == nullptr) { return Value::null(); } + const char* runtimeName = protocol_getName(protocol); NativeApiSymbol runtimeSymbol{ .kind = NativeApiSymbolKind::Protocol, .offset = MD_SECTION_OFFSET_NULL, .name = protocolName, - .runtimeName = protocolName, + .runtimeName = runtimeName != nullptr ? runtimeName : protocolName, }; return Object::createFromHostObject( runtime, @@ -4635,8 +4850,6 @@ void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { runtime.global().setProperty(runtime, "interop", api.getProperty(runtime, "interop")); InstallAggregateGlobals(runtime, api, "protocolNames"); - InstallAggregateGlobals(runtime, api, "structNames"); - InstallAggregateGlobals(runtime, api, "unionNames"); } } // namespace nativescript diff --git a/package.json b/package.json index 03639055..2087ba01 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "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", + "test-rn-ffi": "./scripts/test_react_native_ffi_compat.sh", "demo-rn-turbomodule": "./scripts/create_react_native_demo.sh", "pack:ios": "./scripts/build_npm_ios.sh", "pack:macos": "./scripts/build_npm_macos.sh", diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.h b/packages/react-native/ios/NativeScriptNativeApiModule.h index 77a68967..540613fa 100644 --- a/packages/react-native/ios/NativeScriptNativeApiModule.h +++ b/packages/react-native/ios/NativeScriptNativeApiModule.h @@ -18,6 +18,7 @@ class NativeScriptNativeApiModule bool isInstalled(jsi::Runtime& runtime); std::string defaultMetadataPath(jsi::Runtime& runtime); std::string getRuntimeBackend(jsi::Runtime& runtime); + bool __writeTestMarker(jsi::Runtime& runtime, std::string content); private: std::shared_ptr jsInvoker_; diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm index 74b045b6..0ef3ef99 100644 --- a/packages/react-native/ios/NativeScriptNativeApiModule.mm +++ b/packages/react-native/ios/NativeScriptNativeApiModule.mm @@ -62,6 +62,32 @@ void writeSmokeMarkerIfRequested(const char* stage) { [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; } +bool writeSmokeMarkerContentIfRequested(const std::string& content) { + const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER"); + if (enabled == nullptr || enabled[0] == '\0') { + return false; + } + + NSString* path = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; + NSString* nativeContent = + [[NSString alloc] initWithBytes:content.data() + length:content.size() + encoding:NSUTF8StringEncoding]; + if (nativeContent == nil) { + nativeContent = @""; + } + + BOOL ok = [nativeContent writeToFile:path + atomically:YES + encoding:NSUTF8StringEncoding + error:nil]; +#if !__has_feature(objc_arc) + [nativeContent release]; +#endif + return ok == YES; +} + } // namespace namespace facebook::react { @@ -96,4 +122,9 @@ void writeSmokeMarkerIfRequested(const char* stage) { return "hermes-jsi"; } +bool NativeScriptNativeApiModule::__writeTestMarker(jsi::Runtime&, + std::string content) { + return writeSmokeMarkerContentIfRequested(content); +} + } // namespace facebook::react diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts index 6b15741b..96c9b268 100644 --- a/packages/react-native/src/NativeScriptNativeApi.ts +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -6,6 +6,7 @@ export interface Spec extends TurboModule { readonly isInstalled: () => boolean; readonly defaultMetadataPath: () => string; readonly getRuntimeBackend: () => string; + readonly __writeTestMarker: (content: string) => boolean; } export default TurboModuleRegistry.getEnforcing('NativeScriptNativeApi'); diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 70110704..8657e185 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -9,11 +9,19 @@ export type NativeApiHost = { constants?: number; protocols?: number; enums?: number; + structs?: number; + unions?: number; classNames?: () => string[]; functionNames?: () => string[]; constantNames?: () => string[]; + protocolNames?: () => string[]; enumNames?: () => string[]; + structNames?: () => string[]; + unionNames?: () => string[]; }; + getProtocol?: (name: string) => unknown; + getStruct?: (name: string) => unknown; + getUnion?: (name: string) => unknown; runOnUI?: (callback?: () => void) => Promise; [name: string]: unknown; }; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index d6d89aef..996c44ed 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -5,8 +5,14 @@ type NativeApiHost = { classNames?: () => string[]; functionNames?: () => string[]; constantNames?: () => string[]; + protocolNames?: () => string[]; enumNames?: () => string[]; + structNames?: () => string[]; + unionNames?: () => string[]; }; + getProtocol?: (name: string) => unknown; + getStruct?: (name: string) => unknown; + getUnion?: (name: string) => unknown; runOnUI?: (callback?: () => void) => Promise; [name: string]: unknown; }; @@ -34,24 +40,131 @@ function requireNativeApiHost(): NativeApiHost { function defineLazyNativeGlobal( name: string, resolve: (name: string) => unknown, + force = false, ) { - if (!name || name in globalThis) { + if (!name || (!force && Object.prototype.hasOwnProperty.call(globalThis, name))) { return; } - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: false, - get() { - const value = resolve(name); + try { + 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; + }, + }); + } catch { + const value = resolve(name); + if (value !== undefined) { Object.defineProperty(globalThis, name, { configurable: true, enumerable: false, writable: false, value, }); - return value; - }, + } + } +} + +function wrapAggregateConstructor(nativeConstructor: unknown): unknown { + if (typeof nativeConstructor !== 'function') { + return nativeConstructor; + } + + const aggregate = function NativeScriptAggregate(initialValue?: unknown) { + return nativeConstructor(initialValue); + }; + + for (const key of ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields']) { + try { + Object.defineProperty(aggregate, key, { + configurable: true, + enumerable: false, + writable: false, + value: (nativeConstructor as Record)[key], + }); + } catch { + // Best effort metadata copy for runtimes with stricter function objects. + } + } + + return aggregate; +} + +function wrapInteropFactory( + nativeFactory: unknown, + properties: Record, +): unknown { + if (typeof nativeFactory !== 'function') { + return nativeFactory; + } + + if ((nativeFactory as Record).__nativeScriptConstructable) { + return nativeFactory; + } + + const constructable = function NativeScriptInteropValue(...args: unknown[]) { + return (nativeFactory as (...args: unknown[]) => unknown)(...args); + }; + + for (const [key, value] of Object.entries(properties)) { + try { + Object.defineProperty(constructable, key, { + configurable: true, + enumerable: false, + writable: false, + value, + }); + } catch { + // Best effort metadata copy for runtimes with stricter function objects. + } + } + + Object.defineProperty(constructable, '__nativeScriptConstructable', { + configurable: false, + enumerable: false, + writable: false, + value: true, + }); + + return constructable; +} + +function installInteropConstructors(): void { + const interop = (globalThis as Record).interop as + | Record + | undefined; + if (!interop || typeof interop !== 'object') { + return; + } + + const sizeof = interop.sizeof; + const pointerType = (interop.types as Record | undefined) + ?.pointer; + let pointerSize: unknown = undefined; + if (typeof sizeof === 'function' && pointerType !== undefined) { + try { + pointerSize = sizeof(pointerType); + } catch { + pointerSize = undefined; + } + } + + interop.Pointer = wrapInteropFactory(interop.Pointer, { + kind: 'pointer', + sizeof: pointerSize, + }); + interop.Reference = wrapInteropFactory(interop.Reference, { + kind: 'reference', + sizeof: pointerSize, }); } @@ -76,11 +189,39 @@ export function installGlobals(): boolean { defineLazyNativeGlobal(name, (constantName) => api[constantName]); } + const protocolNames = api.metadata?.protocolNames?.() ?? []; + for (const name of protocolNames) { + defineLazyNativeGlobal( + name, + (protocolName) => api.getProtocol?.(protocolName) ?? api[protocolName], + ); + } + const enumNames = api.metadata?.enumNames?.() ?? []; for (const name of enumNames) { defineLazyNativeGlobal(name, (enumName) => api[enumName]); } + const structNames = api.metadata?.structNames?.() ?? []; + for (const name of structNames) { + defineLazyNativeGlobal( + name, + (structName) => + wrapAggregateConstructor(api.getStruct?.(structName) ?? api[structName]), + true, + ); + } + + const unionNames = api.metadata?.unionNames?.() ?? []; + for (const name of unionNames) { + defineLazyNativeGlobal( + name, + (unionName) => + wrapAggregateConstructor(api.getUnion?.(unionName) ?? api[unionName]), + true, + ); + } + return true; } @@ -89,6 +230,9 @@ export function init( options: InstallOptions = {}, ): boolean { const installed = NativeScriptNativeApi.install(metadataPath); + if (installed) { + installInteropConstructors(); + } if (installed && options.globals !== false) { installGlobals(); } diff --git a/scripts/test_react_native_ffi_compat.sh b/scripts/test_react_native_ffi_compat.sh new file mode 100755 index 00000000..058305fd --- /dev/null +++ b/scripts/test_react_native_ffi_compat.sh @@ -0,0 +1,174 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +RN_VERSION=${RN_FFI_COMPAT_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_FFI_COMPAT_CLI_VERSION:-20.1.3} +APP_NAME=${RN_FFI_COMPAT_APP_NAME:-NativeScriptNativeApiFfiCompat} +APP_ROOT=${RN_FFI_COMPAT_APP_ROOT:-"$REPO_ROOT/build/react-native-ffi-compat"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_FFI_COMPAT_FORCE_RECREATE:-1} +BUILD_TIMEOUT_SECONDS=${RN_FFI_COMPAT_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_FFI_COMPAT_LAUNCH_TIMEOUT_SECONDS:-120} +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER="NATIVESCRIPT_RN_FFI_COMPAT" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" +APP_TSX="$REPO_ROOT/test/react-native/ffi-compat/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 FFI compatibility 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 FFI compatibility app..." +( + cd "$APP_DIR" + npm install "$TARBALL" +) + +checkpoint "Installing FFI compatibility entrypoint..." +cp "$APP_TSX" "$APP_DIR/App.tsx" + +checkpoint "Installing CocoaPods for FFI compatibility 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 FFI compatibility 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 "FFI compatibility 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 FFI compatibility app and waiting for test 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(); +let lastContent = ''; +let lastPayload = null; + +function finish(payload) { + console.log(`${marker} ${JSON.stringify({markerFile, payload})}`); + if (!payload || payload.status !== 'pass') { + process.exit(1); + } + process.exit(0); +} + +function poll() { + if (fs.existsSync(markerFile)) { + const content = fs.readFileSync(markerFile, 'utf8').trim(); + if (content && content !== lastContent) { + lastContent = content; + try { + lastPayload = JSON.parse(content); + } catch (error) { + console.error(`Invalid ${marker} marker content at ${markerFile}: ${content}`); + process.exit(1); + } + console.log(`${marker} ${JSON.stringify({markerFile, payload: lastPayload})}`); + if (lastPayload.status !== 'running') { + finish(lastPayload); + } + } + } + + if (Date.now() - startedAt > timeoutMs) { + console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + if (lastPayload) { + console.error(`Last ${marker} payload: ${JSON.stringify(lastPayload)}`); + } + process.exit(1); + } + + setTimeout(poll, 2000); +} + +poll(); +NODE + +checkpoint "React Native NativeScript FFI compatibility suite passed." diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx new file mode 100644 index 00000000..dca451fa --- /dev/null +++ b/test/react-native/ffi-compat/App.tsx @@ -0,0 +1,362 @@ +import React, {useEffect, useState} from 'react'; +import {SafeAreaView, ScrollView, Text} from 'react-native'; +import NativeScript from '@nativescript/react-native'; +import NativeScriptNativeApi from '@nativescript/react-native/src/NativeScriptNativeApi'; + +type TestCase = { + name: string; + run: () => void | Promise; +}; + +type TestResult = { + name: string; + status: 'pass' | 'fail'; + error?: string; +}; + +const marker = 'NATIVESCRIPT_RN_FFI_COMPAT'; +let currentStep = 'startup'; +let lastGlobalAccess = ''; + +function g(name: string): any { + lastGlobalAccess = name; + return (globalThis as Record)[name]; +} + +function step(name: string, callback: () => T): T { + currentStep = name; + return callback(); +} + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +function assertEqual(actual: T, expected: T, message: string) { + if (!Object.is(actual, expected)) { + throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`); + } +} + +function assertClose(actual: number, expected: number, message: string) { + if (Math.abs(actual - expected) > 0.0001) { + throw new Error(`${message}: expected ${expected}, got ${actual}`); + } +} + +function ptrNumber(value: any): number { + assert(value && typeof value.toNumber === 'function', 'expected Pointer value'); + return value.toNumber(); +} + +function sameNativeHandle(a: any, b: any): boolean { + const interop = g('interop'); + return ptrNumber(interop.handleof(a)) === ptrNumber(interop.handleof(b)); +} + +function writeMarker(payload: unknown) { + const content = JSON.stringify(payload, null, 2); + const writer = (NativeScriptNativeApi as any).__writeTestMarker; + if (typeof writer === 'function') { + writer(content); + } + console.log(`${marker} ${content}`); +} + +function buildTests(): TestCase[] { + return [ + { + name: 'installs metadata-backed globals for classes, functions, constants, enums, protocols, and structs', + run() { + const api = g('__nativeScriptNativeApi'); + assert(api, 'Native API host object was not installed'); + assertEqual(api.runtime, 'jsi', 'runtime kind'); + assertEqual(api.backend, 'hermes', 'runtime backend'); + assert(api.metadata.classes > 0, 'class metadata should be loaded'); + assert(api.metadata.functions > 0, 'function metadata should be loaded'); + assert(api.metadata.constants > 0, 'constant metadata should be loaded'); + assert(api.metadata.enums > 0, 'enum metadata should be loaded'); + assert(api.metadata.protocols > 0, 'protocol metadata should be loaded'); + assert(api.metadata.structs > 0, 'struct metadata should be loaded'); + + assert(typeof g('NSObject').alloc === 'function', 'NSObject global missing'); + assert(typeof g('CGRectGetWidth') === 'function', 'CGRectGetWidth global missing'); + assert(typeof g('CGRect') === 'function', 'CGRect struct global missing'); + assert(g('NSObjectProtocol'), 'NSObjectProtocol global missing'); + assertEqual(g('NSURLErrorTimedOut'), -1001, 'NSURLErrorTimedOut constant'); + assertEqual(g('NSComparisonResult').Same, 0, 'NSComparisonResult enum'); + assertEqual(g('UIUserInterfaceStyle').Dark, 2, 'UIUserInterfaceStyle enum'); + }, + }, + { + name: 'dispatches Objective-C methods and properties with NativeScript-style calls', + run() { + const object = step('NSObject.alloc.init', () => g('NSObject').alloc().init()); + step('NSObject.respondsToSelector', () => + assert(object.respondsToSelector('init'), 'respondsToSelector failed'), + ); + step('NSObject.isKindOfClass', () => + assert(object.isKindOfClass(g('NSObject')), 'isKindOfClass failed'), + ); + step('NSObject.class', () => + assert(sameNativeHandle(object.class(), g('NSObject')), 'class() returned unexpected class'), + ); + + const array = step('NSMutableArray.alloc.init', () => + g('NSMutableArray').alloc().init(), + ); + step('NSMutableArray.addObject alpha', () => array.addObject('alpha')); + step('NSMutableArray.addObject beta', () => array.addObject('beta')); + step('NSMutableArray.count', () => assertEqual(array.count, 2, 'NSMutableArray count')); + step('NSMutableArray.objectAtIndex', () => + assertEqual(array.objectAtIndex(1), 'beta', 'NSMutableArray objectAtIndex'), + ); + }, + }, + { + name: 'marshals JavaScript arrays, objects, strings, and typed arrays into Foundation values', + run() { + const nsArray = g('NSArray').arrayWithArray(['a', 'b']); + assertEqual(nsArray.count, 2, 'NSArray arrayWithArray count'); + assertEqual(nsArray.objectAtIndex(0), 'a', 'NSArray first value'); + + const nsDict = g('NSDictionary').dictionaryWithDictionary({ + answer: 42, + label: 'native', + }); + assertEqual(nsDict.objectForKey('answer'), 42, 'NSDictionary numeric value'); + assertEqual(nsDict.objectForKey('label'), 'native', 'NSDictionary string value'); + + const bytes = new Uint8Array([65, 66, 67, 0]); + const data = g('NSData').dataWithBytesLength(bytes, bytes.byteLength); + const roundTrip = new Uint8Array(g('interop').bufferFromData(data)); + assertEqual(roundTrip[0], 65, 'NSData byte 0'); + assertEqual(roundTrip[1], 66, 'NSData byte 1'); + assertEqual(roundTrip[2], 67, 'NSData byte 2'); + }, + }, + { + name: 'marshals structs by constructor, fields, nested mutation, and C function calls', + run() { + const point = new (g('CGPoint'))({x: 10, y: 20}); + assertEqual(point.x, 10, 'CGPoint.x'); + assertEqual(point.y, 20, 'CGPoint.y'); + + const rect = new (g('CGRect'))({ + origin: {x: 1, y: 2}, + size: {width: 30, height: 40}, + }); + assertEqual(rect.origin.x, 1, 'CGRect.origin.x'); + assertEqual(rect.origin.y, 2, 'CGRect.origin.y'); + assertEqual(rect.size.width, 30, 'CGRect.size.width'); + assertEqual(rect.size.height, 40, 'CGRect.size.height'); + + rect.origin.y = 25; + rect.size.height = 45; + assertEqual(rect.origin.y, 25, 'nested CGRect origin mutation'); + assertEqual(rect.size.height, 45, 'nested CGRect size mutation'); + + const literal = new (g('CGRect'))({ + origin: {x: 3, y: 4}, + size: {width: 50, height: 60}, + }); + assert(g('CGRectContainsPoint')(literal, {x: 10, y: 10}), 'CGRectContainsPoint literal'); + assertClose(g('CGRectGetWidth')(literal), 50, 'CGRectGetWidth'); + }, + }, + { + name: 'supports NativeScript pointer and reference semantics', + run() { + const interop = g('interop'); + assertEqual( + interop.sizeof(interop.Pointer), + interop.sizeof(interop.types.pointer), + 'interop.Pointer sizeof', + ); + + const pointer = interop.alloc(4 * interop.sizeof(interop.types.int32)); + try { + const ref = new interop.Reference(interop.types.int32, pointer); + ref[0] = 123; + ref[1] = 456; + assertEqual(ref[0], 123, 'Reference index 0'); + assertEqual(ref[1], 456, 'Reference index 1'); + assertEqual(ptrNumber(interop.handleof(ref)), ptrNumber(pointer), 'Reference handle'); + + const lazy = new interop.Reference(7); + assertEqual(lazy.value, 7, 'lazy Reference initial value'); + lazy.value = 9; + assertEqual(lazy.value, 9, 'lazy Reference assigned value'); + } finally { + interop.free(pointer); + } + }, + }, + { + name: 'supports C strings through interop.handleof and stringFromCString', + run() { + const interop = g('interop'); + const ptr = interop.handleof('hello'); + assertEqual(interop.stringFromCString(ptr), 'hello', 'stringFromCString'); + assertEqual(interop.stringFromCString(ptr, 2), 'he', 'stringFromCString length'); + }, + }, + { + name: 'wraps protocol values with stable native handles', + run() { + const interop = g('interop'); + const nsObjectProtocol = g('NSObjectProtocol'); + const lookup = g('NSProtocolFromString')('NSObject'); + assert(nsObjectProtocol, 'NSObjectProtocol global missing'); + assert(lookup, 'NSProtocolFromString returned null'); + assertEqual( + ptrNumber(interop.handleof(lookup)), + ptrNumber(interop.handleof(nsObjectProtocol)), + 'protocol handle round trip', + ); + assert(g('NSObject').conformsToProtocol(nsObjectProtocol), 'NSObject protocol conformance'); + }, + }, + { + name: 'invokes blocks and exposes pointer parameters as References', + run() { + const seen: string[] = []; + const array = g('NSArray').arrayWithArray(['a', 'b', 'c']); + array.enumerateObjectsUsingBlock((value: string, index: number, stop: any) => { + seen.push(`${index}:${value}`); + if (index === 1) { + stop.value = true; + } + }); + assertEqual(seen.join(','), '0:a,1:b', 'enumerateObjectsUsingBlock stop reference'); + }, + }, + { + name: 'invokes C function pointer callbacks on the native caller thread', + run() { + let callbackRan = false; + let callbackThreadHash = 0; + const queue = g('dispatch_get_global_queue')(0, 0); + g('dispatch_sync_f')(queue, null, () => { + callbackRan = true; + callbackThreadHash = g('NSThread').currentThread.hash; + }); + assert(callbackRan, 'dispatch_sync_f callback did not run'); + assert( + callbackThreadHash !== g('NSThread').currentThread.hash, + 'dispatch_sync_f callback should run on the dispatch queue thread', + ); + }, + }, + { + name: 'invokes Objective-C block callbacks inside dispatch_async_and_wait', + run() { + let callbackRan = false; + const queue = g('dispatch_get_global_queue')(0, 0); + g('dispatch_async_and_wait')(queue, () => { + callbackRan = true; + }); + assert(callbackRan, 'dispatch_async_and_wait block did not run'); + }, + }, + { + name: 'runs UIKit native calls through runOnUI main-thread dispatch', + async run() { + let mainThread = false; + await NativeScript.runOnUI(() => { + mainThread = g('NSThread').isMainThread === true; + const color = g('UIColor').colorWithRedGreenBlueAlpha(0.1, 0.2, 0.3, 1); + assert(color, 'UIColor construction failed on UI thread'); + }); + assert(mainThread, 'runOnUI did not execute native calls on main thread'); + }, + }, + ]; +} + +async function runCompatibilitySuite() { + writeMarker({ + marker, + status: 'running', + current: 'before NativeScript.init', + passed: 0, + total: 0, + results: [], + }); + NativeScript.init(); + + const tests = buildTests(); + const results: TestResult[] = []; + writeMarker({ + marker, + status: 'running', + current: 'initialized', + passed: 0, + total: tests.length, + results, + backend: NativeScript.getRuntimeBackend(), + }); + + for (const test of tests) { + try { + writeMarker({ + marker, + status: 'running', + current: test.name, + passed: results.filter((result) => result.status === 'pass').length, + total: tests.length, + results, + backend: NativeScript.getRuntimeBackend(), + }); + await test.run(); + results.push({name: test.name, status: 'pass'}); + } catch (error) { + results.push({ + name: test.name, + status: 'fail', + error: + error instanceof Error + ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}` + : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`, + }); + break; + } + } + + const failed = results.find((result) => result.status === 'fail'); + const payload = { + marker, + status: failed ? 'fail' : 'pass', + passed: results.filter((result) => result.status === 'pass').length, + total: tests.length, + results, + backend: NativeScript.getRuntimeBackend(), + }; + writeMarker(payload); + if (failed) { + throw new Error(failed.error); + } + return payload; +} + +export default function App(): React.JSX.Element { + const [text, setText] = useState('Running NativeScript RN FFI compatibility tests...'); + + useEffect(() => { + runCompatibilitySuite() + .then((payload) => setText(JSON.stringify(payload, null, 2))) + .catch((error) => { + setText(error instanceof Error ? error.message : String(error)); + }); + }, []); + + return ( + + + {text} + + + ); +} From c4ef2a78adb153b821a4d52e9ee3f0e8885028a8 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 17:43:08 -0400 Subject: [PATCH 18/31] feat(react-native): support JS-defined UIKit views --- .../NativeScriptNativeApi.podspec | 2 + packages/react-native/README.md | 60 ++++ .../Fabric/NativeScriptUIViewComponentView.h | 4 + .../Fabric/NativeScriptUIViewComponentView.mm | 67 ++++ .../react-native/ios/NativeScriptUIView.h | 7 + .../react-native/ios/NativeScriptUIView.mm | 75 +++++ .../ios/NativeScriptUIViewManager.mm | 18 ++ packages/react-native/package.json | 5 +- .../src/NativeScriptUIViewNativeComponent.ts | 10 + packages/react-native/src/index.d.ts | 36 +++ packages/react-native/src/index.ts | 291 ++++++++++++++++++ test/react-native/ffi-compat/App.tsx | 176 ++++++++++- 12 files changed, 749 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h create mode 100644 packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm create mode 100644 packages/react-native/ios/NativeScriptUIView.h create mode 100644 packages/react-native/ios/NativeScriptUIView.mm create mode 100644 packages/react-native/ios/NativeScriptUIViewManager.mm create mode 100644 packages/react-native/src/NativeScriptUIViewNativeComponent.ts diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec index 8aaa5f14..d65543a7 100644 --- a/packages/react-native/NativeScriptNativeApi.podspec +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -1,6 +1,7 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +fabric_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" 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" @@ -19,6 +20,7 @@ Pod::Spec.new do |s| "ios/**/*.{h,mm}", "native-api-jsi/**/*.{h,mm}" ] + s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled s.public_header_files = "ios/**/*.h" s.resource_bundles = { "NativeScriptNativeApi" => ["metadata/*.nsmd"] diff --git a/packages/react-native/README.md b/packages/react-native/README.md index b7571da7..ca9ed9db 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -27,6 +27,66 @@ await NativeScript.runOnUI(() => { }); ``` +## Defining native UIKit views in JS + +Use `defineUIKitView()` to turn a NativeScript-created `UIView` tree into a +normal React Native component. The package owns the RN host view; your +definition owns the UIKit subtree. `create`, `update`, `mounted`, and `dispose` +run through the NativeScript UI dispatcher, so UIKit calls are safe and use the +same globals and iOS SDK types as NativeScript. + +```tsx +import NativeScript, {defineUIKitView} from "@nativescript/react-native"; +import type {UIKitViewRef} from "@nativescript/react-native"; + +NativeScript.init(); + +type BadgeProps = { + title: string; + tone?: "blue" | "green"; +}; + +export const NativeBadge = defineUIKitView({ + displayName: "NativeBadge", + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + label.textColor = UIColor.whiteColor; + label.autoresizingMask = + UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + view.addSubview(label); + return view; + }, + update(view, props) { + view.backgroundColor = + props.tone === "green" ? UIColor.systemGreenColor : UIColor.systemBlueColor; + view.layer.cornerRadius = 12; + view.clipsToBounds = true; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, +}); + +; +``` + +Forward a ref when you need imperative access: + +```tsx +const badgeRef = useRef>(null); + +await badgeRef.current?.runOnUI((view) => { + view.alpha = 0.8; +}); +``` + +React Native view props such as `style`, `testID`, accessibility props, responder +props, and `pointerEvents` go to the host component. Your own props go to the +UIKit definition; use `nativeProps(props)` when a plugin prop should also affect +the RN host. + The published package includes generated NativeScript metadata, the libffi xcframework, and generated iOS SDK TypeScript declarations. Build it from the repository root with: diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h new file mode 100644 index 00000000..c6567eb5 --- /dev/null +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h @@ -0,0 +1,4 @@ +#import + +@interface NativeScriptUIViewComponentView : RCTViewComponentView +@end diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm new file mode 100644 index 00000000..69f41075 --- /dev/null +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm @@ -0,0 +1,67 @@ +#import "NativeScriptUIViewComponentView.h" + +#import +#import +#import + +#import "NativeScriptUIView.h" + +using namespace facebook::react; + +@implementation NativeScriptUIViewComponentView { + NativeScriptUIView* _containerView; +} + ++ (void)load { + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _containerView = [[NativeScriptUIView alloc] initWithFrame:self.bounds]; + _containerView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.contentView = _containerView; + } + + return self; +} + +- (void)dealloc { + [_containerView release]; + [super dealloc]; +} + +- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps { + const auto oldViewProps = std::static_pointer_cast(_props); + const auto newViewProps = std::static_pointer_cast(props); + const std::string oldNativeViewHandle = oldViewProps->nativeViewHandle; + const std::string newNativeViewHandle = newViewProps->nativeViewHandle; + + [super updateProps:props oldProps:oldProps]; + + if (oldNativeViewHandle != newNativeViewHandle) { + NSString* nativeViewHandle = newNativeViewHandle.empty() + ? nil + : [NSString stringWithUTF8String:newNativeViewHandle.c_str()]; + _containerView.nativeViewHandle = nativeViewHandle; + } +} + +- (void)prepareForRecycle { + [super prepareForRecycle]; + _containerView.nativeViewHandle = nil; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider { + return concreteComponentDescriptorProvider(); +} + +@end + +Class NativeScriptUIViewCls(void) { + return NativeScriptUIViewComponentView.class; +} diff --git a/packages/react-native/ios/NativeScriptUIView.h b/packages/react-native/ios/NativeScriptUIView.h new file mode 100644 index 00000000..82d09405 --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIView.h @@ -0,0 +1,7 @@ +#import + +@interface NativeScriptUIView : UIView + +@property(nonatomic, copy) NSString* nativeViewHandle; + +@end diff --git a/packages/react-native/ios/NativeScriptUIView.mm b/packages/react-native/ios/NativeScriptUIView.mm new file mode 100644 index 00000000..16b6ae89 --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIView.mm @@ -0,0 +1,75 @@ +#import "NativeScriptUIView.h" + +static UIView* NativeScriptUIViewFromHandle(NSString* handle) { + if (handle == nil || handle.length == 0) { + return nil; + } + + const char* text = handle.UTF8String; + if (text == nullptr || text[0] == '\0') { + return nil; + } + + char* end = nullptr; + unsigned long long address = strtoull(text, &end, 0); + if (address == 0 || end == text) { + return nil; + } + + id object = reinterpret_cast(static_cast(address)); + if (object == nil || ![object isKindOfClass:UIView.class]) { + return nil; + } + + return static_cast(object); +} + +@implementation NativeScriptUIView { + UIView* _nativeView; +} + +- (void)dealloc { + [_nativeView removeFromSuperview]; + [_nativeView release]; + [_nativeViewHandle release]; + [super dealloc]; +} + +- (void)setNativeViewHandle:(NSString*)nativeViewHandle { + if ((_nativeViewHandle == nativeViewHandle) || + [_nativeViewHandle isEqualToString:nativeViewHandle]) { + return; + } + + [_nativeViewHandle release]; + _nativeViewHandle = [nativeViewHandle copy]; + [self setNativeView:NativeScriptUIViewFromHandle(_nativeViewHandle)]; +} + +- (void)setNativeView:(UIView*)nativeView { + if (_nativeView == nativeView) { + return; + } + + [_nativeView removeFromSuperview]; + [_nativeView release]; + _nativeView = nil; + + if (nativeView == nil) { + return; + } + + _nativeView = [nativeView retain]; + [_nativeView removeFromSuperview]; + _nativeView.frame = self.bounds; + _nativeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_nativeView]; + [self setNeedsLayout]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _nativeView.frame = self.bounds; +} + +@end diff --git a/packages/react-native/ios/NativeScriptUIViewManager.mm b/packages/react-native/ios/NativeScriptUIViewManager.mm new file mode 100644 index 00000000..ed7af13d --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIViewManager.mm @@ -0,0 +1,18 @@ +#import + +#import "NativeScriptUIView.h" + +@interface NativeScriptUIViewManager : RCTViewManager +@end + +@implementation NativeScriptUIViewManager + +RCT_EXPORT_MODULE(NativeScriptUIView) + +- (UIView*)view { + return [[[NativeScriptUIView alloc] initWithFrame:CGRectZero] autorelease]; +} + +RCT_EXPORT_VIEW_PROPERTY(nativeViewHandle, NSString) + +@end diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 0108c004..9834dc64 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -38,12 +38,15 @@ }, "codegenConfig": { "name": "NativeScriptNativeApiSpec", - "type": "modules", + "type": "all", "jsSrcsDir": "src", "android": { "javaPackageName": "org.nativescript.nativeapi" }, "ios": { + "componentProvider": { + "NativeScriptUIView": "NativeScriptUIViewComponentView" + }, "modulesProvider": { "NativeScriptNativeApi": "NativeScriptNativeApiModuleProvider" } diff --git a/packages/react-native/src/NativeScriptUIViewNativeComponent.ts b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts new file mode 100644 index 00000000..281e0607 --- /dev/null +++ b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts @@ -0,0 +1,10 @@ +import type {HostComponent, ViewProps} from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export interface NativeProps extends ViewProps { + nativeViewHandle?: string; +} + +export default codegenNativeComponent( + 'NativeScriptUIView', +) as HostComponent; diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 8657e185..7d24ded4 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -1,5 +1,12 @@ /// +import type { + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, +} from 'react'; +import type {ViewProps} from 'react-native'; + export type NativeApiHost = { runtime?: string; backend?: string; @@ -30,6 +37,31 @@ export type InstallOptions = { globals?: boolean; }; +export type UIKitViewDefinition = { + displayName?: string; + create: (props: Readonly) => NativeView; + update?: ( + view: NativeView, + props: Readonly, + previousProps?: Readonly, + ) => void; + mounted?: (view: NativeView, props: Readonly) => void; + dispose?: (view: NativeView, props: Readonly) => void; + nativeProps?: ( + props: Readonly, + ) => Partial | undefined; +}; + +export type UIKitViewRef = { + readonly nativeView: NativeView | null; + runOnUI: (callback: (view: NativeView) => void) => Promise; +}; + +export type UIKitViewComponent = + ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes> + >; + export function init(metadataPath?: string, options?: InstallOptions): boolean; export const install: typeof init; export function installGlobals(): boolean; @@ -37,6 +69,9 @@ export function isInstalled(): boolean; export function defaultMetadataPath(): string; export function getRuntimeBackend(): string; export function runOnUI(callback?: () => void): Promise; +export function defineUIKitView( + definition: UIKitViewDefinition, +): UIKitViewComponent; declare const NativeScript: { init: typeof init; @@ -44,6 +79,7 @@ declare const NativeScript: { installGlobals: typeof installGlobals; isInstalled: typeof isInstalled; defaultMetadataPath: typeof defaultMetadataPath; + defineUIKitView: typeof defineUIKitView; getRuntimeBackend: typeof getRuntimeBackend; runOnUI: typeof runOnUI; }; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 996c44ed..baa5685e 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1,4 +1,18 @@ +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import type { + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, +} from 'react'; +import type {ViewProps} from 'react-native'; import NativeScriptNativeApi from './NativeScriptNativeApi'; +import NativeScriptUIViewNativeComponent from './NativeScriptUIViewNativeComponent'; type NativeApiHost = { metadata?: { @@ -21,6 +35,31 @@ export type InstallOptions = { globals?: boolean; }; +export type UIKitViewDefinition = { + displayName?: string; + create: (props: Readonly) => NativeView; + update?: ( + view: NativeView, + props: Readonly, + previousProps?: Readonly, + ) => void; + mounted?: (view: NativeView, props: Readonly) => void; + dispose?: (view: NativeView, props: Readonly) => void; + nativeProps?: ( + props: Readonly, + ) => Partial | undefined; +}; + +export type UIKitViewRef = { + readonly nativeView: NativeView | null; + runOnUI: (callback: (view: NativeView) => void) => Promise; +}; + +export type UIKitViewComponent = + ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes> + >; + const nativeApiGlobalName = '__nativeScriptNativeApi'; function nativeApiHost(): NativeApiHost | undefined { @@ -37,6 +76,121 @@ function requireNativeApiHost(): NativeApiHost { return api; } +const hostViewPropNames = new Set([ + 'accessible', + 'accessibilityActions', + 'accessibilityElementsHidden', + 'accessibilityHint', + 'accessibilityIgnoresInvertColors', + 'accessibilityLabel', + 'accessibilityLanguage', + 'accessibilityLiveRegion', + 'accessibilityRole', + 'accessibilityState', + 'accessibilityValue', + 'accessibilityViewIsModal', + 'children', + 'collapsable', + 'focusable', + 'hitSlop', + 'id', + 'importantForAccessibility', + 'nativeID', + 'needsOffscreenAlphaCompositing', + 'onAccessibilityAction', + 'onAccessibilityEscape', + 'onAccessibilityTap', + 'onLayout', + 'onMagicTap', + 'onMoveShouldSetResponder', + 'onMoveShouldSetResponderCapture', + 'onResponderEnd', + 'onResponderGrant', + 'onResponderMove', + 'onResponderReject', + 'onResponderRelease', + 'onResponderStart', + 'onResponderTerminate', + 'onResponderTerminationRequest', + 'onStartShouldSetResponder', + 'onStartShouldSetResponderCapture', + 'pointerEvents', + 'removeClippedSubviews', + 'renderToHardwareTextureAndroid', + 'shouldRasterizeIOS', + 'style', + 'testID', +]); + +function splitUIKitViewProps( + props: Props & ViewProps, + definition: UIKitViewDefinition, +): { + nativeProps: ViewProps; + pluginProps: Props & ViewProps; +} { + const nativeProps: Record = {}; + const pluginProps: Record = {}; + + for (const [key, value] of Object.entries(props)) { + if ( + hostViewPropNames.has(key) || + key.startsWith('accessibility') || + key.startsWith('aria-') + ) { + nativeProps[key] = value; + } else { + pluginProps[key] = value; + } + } + + Object.assign(nativeProps, definition.nativeProps?.(props)); + + return { + nativeProps: nativeProps as ViewProps, + pluginProps: pluginProps as Props & ViewProps, + }; +} + +function nativeHandleForUIKitView(view: unknown): string { + const interop = (globalThis as Record).interop; + if (!interop || typeof interop.handleof !== 'function') { + throw new Error('NativeScript interop globals are not installed'); + } + + const pointer = interop.handleof(view); + if (!pointer) { + throw new Error('UIKit view definition returned a value without a native handle'); + } + + if (typeof pointer.toHexString === 'function') { + const text = pointer.toHexString(); + if (typeof text === 'string' && text.length > 0) { + return text; + } + } + + if (typeof pointer.address === 'string' && pointer.address.length > 0) { + return pointer.address; + } + + if (typeof pointer.address === 'number') { + return String(pointer.address); + } + + if (typeof pointer.toNumber === 'function') { + return String(pointer.toNumber()); + } + + throw new Error('UIKit view native handle could not be read'); +} + +function ensureNativeScriptInstalled(): void { + if (!isInstalled()) { + init(); + } +} + function defineLazyNativeGlobal( name: string, resolve: (name: string) => unknown, @@ -263,12 +417,149 @@ export function runOnUI(callback?: () => void): Promise { return run(callback); } +export function defineUIKitView( + definition: UIKitViewDefinition, +): UIKitViewComponent { + const Component = forwardRef, Props & ViewProps>( + function NativeScriptUIKitView(props, ref) { + const {nativeProps, pluginProps} = splitUIKitViewProps(props, definition); + const viewRef = useRef(null); + const propsRef = useRef(pluginProps); + const previousPropsRef = useRef | undefined>(); + const mountedRef = useRef(false); + const disposedRef = useRef(false); + const [nativeViewHandle, setNativeViewHandle] = useState(); + const [error, setError] = useState(null); + + propsRef.current = pluginProps; + + useImperativeHandle( + ref, + () => ({ + get nativeView() { + return viewRef.current; + }, + runOnUI(callback) { + return runOnUI(() => { + if (viewRef.current == null) { + throw new Error('UIKit view has not been created yet'); + } + callback(viewRef.current); + }); + }, + }), + [], + ); + + useEffect(() => { + disposedRef.current = false; + let cancelled = false; + + ensureNativeScriptInstalled(); + + runOnUI(() => { + const currentProps = propsRef.current; + const nativeView = definition.create(currentProps); + if (cancelled || disposedRef.current) { + definition.dispose?.(nativeView, currentProps); + const maybeView = nativeView as Record; + if (typeof maybeView.removeFromSuperview === 'function') { + maybeView.removeFromSuperview(); + } + return undefined; + } + viewRef.current = nativeView; + definition.update?.(nativeView, currentProps, undefined); + previousPropsRef.current = currentProps; + return undefined; + }) + .then(() => { + if (cancelled || viewRef.current == null) { + return; + } + setNativeViewHandle(nativeHandleForUIKitView(viewRef.current)); + }) + .catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + + return () => { + cancelled = true; + disposedRef.current = true; + const nativeView = viewRef.current; + viewRef.current = null; + mountedRef.current = false; + if (nativeView == null) { + return; + } + runOnUI(() => { + definition.dispose?.(nativeView, propsRef.current); + const maybeView = nativeView as Record; + if (typeof maybeView.removeFromSuperview === 'function') { + maybeView.removeFromSuperview(); + } + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }; + }, [definition]); + + useEffect(() => { + const nativeView = viewRef.current; + if (nativeView == null) { + return; + } + + const previousProps = previousPropsRef.current; + const currentProps = propsRef.current; + previousPropsRef.current = currentProps; + + runOnUI(() => { + definition.update?.(nativeView, currentProps, previousProps); + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }, [definition, pluginProps]); + + useEffect(() => { + const nativeView = viewRef.current; + if (nativeViewHandle == null || nativeView == null || mountedRef.current) { + return; + } + + mountedRef.current = true; + runOnUI(() => { + if (!disposedRef.current) { + definition.mounted?.(nativeView, propsRef.current); + } + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }, [definition, nativeViewHandle]); + + if (error) { + throw error; + } + + return React.createElement(NativeScriptUIViewNativeComponent, { + ...nativeProps, + collapsable: false, + nativeViewHandle, + }); + }, + ); + + Component.displayName = definition.displayName ?? 'NativeScriptUIKitView'; + return Component; +} + const NativeScript = { init, install, installGlobals, isInstalled, defaultMetadataPath, + defineUIKitView, getRuntimeBackend, runOnUI, }; diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx index dca451fa..be9fd56d 100644 --- a/test/react-native/ffi-compat/App.tsx +++ b/test/react-native/ffi-compat/App.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useState} from 'react'; import {SafeAreaView, ScrollView, Text} from 'react-native'; -import NativeScript from '@nativescript/react-native'; +import NativeScript, {defineUIKitView} from '@nativescript/react-native'; import NativeScriptNativeApi from '@nativescript/react-native/src/NativeScriptNativeApi'; type TestCase = { @@ -17,6 +17,8 @@ type TestResult = { const marker = 'NATIVESCRIPT_RN_FFI_COMPAT'; let currentStep = 'startup'; let lastGlobalAccess = ''; +const uikitPluginIdentifier = 'NativeScriptUIKitPluginView'; +const uikitPluginLabelTag = 101; function g(name: string): any { lastGlobalAccess = name; @@ -65,6 +67,124 @@ function writeMarker(payload: unknown) { console.log(`${marker} ${content}`); } +function waitFor( + read: () => T | undefined | null | false, + message: string, + timeoutMs = 5000, +): Promise { + const startedAt = Date.now(); + return new Promise((resolve, reject) => { + function poll() { + const value = read(); + if (value) { + resolve(value); + return; + } + if (Date.now() - startedAt > timeoutMs) { + reject(new Error(message)); + return; + } + setTimeout(poll, 50); + } + poll(); + }); +} + +function waitForAsync( + read: () => Promise, + message: string, + timeoutMs = 5000, +): Promise { + const startedAt = Date.now(); + return new Promise((resolve, reject) => { + async function poll() { + const value = await read(); + if (value) { + resolve(value); + return; + } + if (Date.now() - startedAt > timeoutMs) { + reject(new Error(message)); + return; + } + setTimeout(poll, 50); + } + poll().catch(reject); + }); +} + +async function waitForUIKitPluginAttachment(): Promise { + await waitForAsync( + async () => { + let attached = false; + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + attached = Boolean(view?.superview && view?.window); + }); + return attached; + }, + 'JS-defined UIKit view was not attached to the RN tree', + ); +} + +const NativeScriptUIKitTestView = defineUIKitView<{ + title: string; + tint: 'blue' | 'green'; +}>({ + displayName: 'NativeScriptUIKitTestView', + create(props) { + const view = g('UIView').alloc().initWithFrame( + new (g('CGRect'))({ + origin: {x: 0, y: 0}, + size: {width: 160, height: 48}, + }), + ); + view.accessibilityIdentifier = uikitPluginIdentifier; + + const label = g('UILabel').alloc().initWithFrame( + new (g('CGRect'))({ + origin: {x: 8, y: 8}, + size: {width: 144, height: 32}, + }), + ); + label.tag = uikitPluginLabelTag; + label.textAlignment = g('NSTextAlignment').Center; + label.textColor = g('UIColor').whiteColor; + view.addSubview(label); + + (globalThis as any).__nativeScriptUIKitPlugin = { + created: true, + disposed: false, + title: '', + tint: props.tint, + view, + }; + return view; + }, + mounted(view, props) { + (globalThis as any).__nativeScriptUIKitPlugin.mounted = true; + (globalThis as any).__nativeScriptUIKitPlugin.nativeHandle = g( + 'interop', + ).handleof(view).address; + (globalThis as any).__nativeScriptUIKitPlugin.title = props.title; + }, + update(view, props) { + view.backgroundColor = + props.tint === 'green' ? g('UIColor').greenColor : g('UIColor').blueColor; + const label = view.viewWithTag(uikitPluginLabelTag); + label.text = props.title; + (globalThis as any).__nativeScriptUIKitPlugin.title = props.title; + (globalThis as any).__nativeScriptUIKitPlugin.tint = props.tint; + }, + dispose() { + const state = (globalThis as any).__nativeScriptUIKitPlugin; + if (state) { + state.disposed = true; + state.view = null; + } + }, +}); + function buildTests(): TestCase[] { return [ { @@ -273,6 +393,51 @@ function buildTests(): TestCase[] { assert(mainThread, 'runOnUI did not execute native calls on main thread'); }, }, + { + name: 'mounts JS-defined UIKit views through the React Native host component', + async run() { + await waitFor( + () => + (globalThis as any).__nativeScriptUIKitPlugin?.mounted === true && + (globalThis as any).__nativeScriptUIKitPlugin?.title === + 'Initial UIKit title', + 'JS-defined UIKit view did not mount', + ); + + await waitForUIKitPluginAttachment(); + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + assert(view?.superview, 'JS-defined UIKit view has no host superview'); + assert(view?.window, 'JS-defined UIKit view has no window'); + const label = view.viewWithTag(uikitPluginLabelTag); + assertEqual(label.text, 'Initial UIKit title', 'initial UIKit label text'); + }); + + const setTitle = (globalThis as any).__setNativeScriptUIKitTitle; + const setTint = (globalThis as any).__setNativeScriptUIKitTint; + assert(typeof setTitle === 'function', 'UIKit title setter was not installed'); + assert(typeof setTint === 'function', 'UIKit tint setter was not installed'); + setTitle('Updated UIKit title'); + setTint('green'); + + await waitFor( + () => + (globalThis as any).__nativeScriptUIKitPlugin?.title === + 'Updated UIKit title' && + (globalThis as any).__nativeScriptUIKitPlugin?.tint === 'green', + 'JS-defined UIKit view did not receive prop updates', + ); + + await waitForUIKitPluginAttachment(); + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + assert(view?.superview, 'updated JS-defined UIKit view has no host superview'); + assert(view?.window, 'updated JS-defined UIKit view has no window'); + const label = view.viewWithTag(uikitPluginLabelTag); + assertEqual(label.text, 'Updated UIKit title', 'updated UIKit label text'); + }); + }, + }, ]; } @@ -343,8 +508,12 @@ async function runCompatibilitySuite() { export default function App(): React.JSX.Element { const [text, setText] = useState('Running NativeScript RN FFI compatibility tests...'); + const [uikitTitle, setUIKitTitle] = useState('Initial UIKit title'); + const [uikitTint, setUIKitTint] = useState<'blue' | 'green'>('blue'); useEffect(() => { + (globalThis as any).__setNativeScriptUIKitTitle = setUIKitTitle; + (globalThis as any).__setNativeScriptUIKitTint = setUIKitTint; runCompatibilitySuite() .then((payload) => setText(JSON.stringify(payload, null, 2))) .catch((error) => { @@ -356,6 +525,11 @@ export default function App(): React.JSX.Element { {text} + ); From 6e3d7b71ea351bd6567cc3a247b007f5310e9a5f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 19:06:38 -0400 Subject: [PATCH 19/31] fix(runtime): harden console formatting --- .../runtime/modules/console/Console.cpp | 159 +++++++++++++++--- 1 file changed, 135 insertions(+), 24 deletions(-) diff --git a/NativeScript/runtime/modules/console/Console.cpp b/NativeScript/runtime/modules/console/Console.cpp index fa7ae1ff..903d9ac4 100644 --- a/NativeScript/runtime/modules/console/Console.cpp +++ b/NativeScript/runtime/modules/console/Console.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "ffi/NativeScriptException.h" #include "js_native_api.h" @@ -34,6 +35,64 @@ extern "C" void NSLog(CFStringRef format, ...); namespace nativescript { +namespace { + +bool readStringValue(napi_env env, napi_value value, std::string& result) { + if (value == nullptr) { + return false; + } + + size_t length = 0; + if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok) { + return false; + } + + std::vector buffer(length + 1); + size_t copied = 0; + if (napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), + &copied) != napi_ok) { + return false; + } + + result.assign(buffer.data(), copied); + return true; +} + +bool throwPendingException(napi_env env, const std::string& message) { + bool isPending = false; + if (napi_is_exception_pending(env, &isPending) != napi_ok || !isPending) { + return false; + } + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + + if (exception != nullptr && + napi_util::is_of_type(env, exception, napi_object)) { + throw NativeScriptException(env, exception, message); + } + + throw NativeScriptException(env, message); +} + +bool coerceToString(napi_env env, napi_value value, std::string& result) { + napi_value stringValue = nullptr; + if (napi_coerce_to_string(env, value, &stringValue) != napi_ok || + stringValue == nullptr) { + throwPendingException(env, "Error converting console argument to string"); + return false; + } + + if (!readStringValue(env, stringValue, result)) { + throwPendingException(env, "Error reading console argument string"); + return false; + } + + return true; +} + +} // namespace + JS_CLASS_INIT(Console::Init) { napi_value Console, console; @@ -65,17 +124,38 @@ JS_METHOD(Console::Constructor) { } std::string transformJSObject(napi_env env, napi_value object) { - napi_value toStringFunc; + napi_value toStringFunc = nullptr; bool hasToString = false; // Check if the object has a toString method - napi_has_named_property(env, object, "toString", &hasToString); + if (napi_has_named_property(env, object, "toString", &hasToString) != + napi_ok) { + throwPendingException(env, "Error reading console object toString"); + return "[object Object]"; + } + if (hasToString) { - napi_get_named_property(env, object, "toString", &toStringFunc); - if (napi_util::is_of_type(env, toStringFunc, napi_function)) { - napi_value result; - napi_call_function(env, object, toStringFunc, 0, nullptr, &result); - auto value = napi_util::get_cxx_string(env, result); + if (napi_get_named_property(env, object, "toString", &toStringFunc) != + napi_ok) { + throwPendingException(env, "Error reading console object toString"); + return "[object Object]"; + } + + if (toStringFunc != nullptr && + napi_util::is_of_type(env, toStringFunc, napi_function)) { + napi_value result = nullptr; + if (napi_call_function(env, object, toStringFunc, 0, nullptr, &result) != + napi_ok || + result == nullptr) { + throwPendingException(env, "Error converting console object to string"); + return "[object Object]"; + } + + std::string value; + if (!coerceToString(env, result, value)) { + return "[object Object]"; + } + auto hasCustomToStringImplementation = value.find("[object Object]") == std::string::npos; if (hasCustomToStringImplementation) return value; @@ -88,29 +168,48 @@ std::string transformJSObject(napi_env env, napi_value object) { std::string buildStringFromArg(napi_env env, napi_value val, napi_value inspectSymbol) { napi_valuetype type; - napi_typeof(env, val, &type); + if (napi_typeof(env, val, &type) != napi_ok) { + throwPendingException(env, "Error reading console argument type"); + return ""; + } if (type == napi_function) { - napi_value funcString; - napi_coerce_to_string(env, val, &funcString); - return napi_util::get_string_value(env, funcString); + std::string funcString; + if (coerceToString(env, val, funcString)) { + return funcString; + } + return ""; } else if (napi_util::is_array(env, val)) { napi_value cachedSelf = val; // Get array length - uint32_t arrayLength; - napi_get_array_length(env, val, &arrayLength); + uint32_t arrayLength = 0; + if (napi_get_array_length(env, val, &arrayLength) != napi_ok) { + throwPendingException(env, "Error reading console array length"); + return "[]"; + } std::stringstream arrayStr; arrayStr << "["; for (uint32_t i = 0; i < arrayLength; i++) { - napi_value propertyValue; - napi_get_element(env, val, i, &propertyValue); + napi_value propertyValue = nullptr; + if (napi_get_element(env, val, i, &propertyValue) != napi_ok || + propertyValue == nullptr) { + throwPendingException(env, "Error reading console array element"); + arrayStr << ""; + if (i != arrayLength - 1) { + arrayStr << ", "; + } + continue; + } // Check for circular reference bool isStrictEqual = false; - napi_strict_equals(env, propertyValue, cachedSelf, &isStrictEqual); + if (napi_strict_equals(env, propertyValue, cachedSelf, &isStrictEqual) != + napi_ok) { + throwPendingException(env, "Error comparing console array element"); + } if (isStrictEqual) { arrayStr << "[Circular]"; @@ -132,20 +231,32 @@ std::string buildStringFromArg(napi_env env, napi_value val, napi_status getInspectStatus = napi_get_property(env, val, inspectSymbol, &inspectFunc); if (getInspectStatus == napi_ok && + inspectFunc != nullptr && napi_util::is_of_type(env, inspectFunc, napi_function)) { - napi_value inspectedValue; - napi_call_function(env, val, inspectFunc, 0, nullptr, &inspectedValue); + napi_value inspectedValue = nullptr; + if (napi_call_function(env, val, inspectFunc, 0, nullptr, + &inspectedValue) != napi_ok || + inspectedValue == nullptr) { + throwPendingException(env, "Error inspecting console object"); + return "[object Object]"; + } return buildStringFromArg(env, inspectedValue, inspectSymbol); + } else if (getInspectStatus != napi_ok) { + throwPendingException(env, "Error reading console inspect function"); } return transformJSObject(env, val); } else if (type == napi_symbol) { - napi_value symString; - napi_coerce_to_string(env, val, &symString); - return "Symbol(" + napi_util::get_cxx_string(env, symString) + ")"; + std::string symString; + if (coerceToString(env, val, symString)) { + return symString; + } + return "Symbol()"; } else { - napi_value defaultToString; - napi_coerce_to_string(env, val, &defaultToString); - return napi_util::get_string_value(env, defaultToString); + std::string defaultToString; + if (coerceToString(env, val, defaultToString)) { + return defaultToString; + } + return ""; } } From abc20cbe1e9a8a5df79bd84d355736c5f7c4bc39 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 19:40:13 -0400 Subject: [PATCH 20/31] feat(react-native): add Expo config plugin --- examples/expo-demo/App.tsx | 122 ++++++++++++++++++ examples/expo-demo/README.md | 17 +++ examples/expo-demo/app.config.js | 11 ++ packages/react-native/README.md | 61 +++++++++ packages/react-native/app.plugin.js | 1 + packages/react-native/package.json | 8 ++ .../plugin/withNativeScriptReactNative.js | 52 ++++++++ 7 files changed, 272 insertions(+) create mode 100644 examples/expo-demo/App.tsx create mode 100644 examples/expo-demo/README.md create mode 100644 examples/expo-demo/app.config.js create mode 100644 packages/react-native/app.plugin.js create mode 100644 packages/react-native/plugin/withNativeScriptReactNative.js diff --git a/examples/expo-demo/App.tsx b/examples/expo-demo/App.tsx new file mode 100644 index 00000000..657b6aac --- /dev/null +++ b/examples/expo-demo/App.tsx @@ -0,0 +1,122 @@ +import React, {useEffect, useMemo, useState} from 'react'; +import {Pressable, ScrollView, Text, View} from 'react-native'; +import NativeScript, {defineUIKitView} from '@nativescript/react-native'; + +NativeScript.init(); + +type BadgeProps = { + title: string; + tone: 'blue' | 'green'; +}; + +const NativeBadge = defineUIKitView({ + displayName: 'NativeBadge', + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + view.clipsToBounds = true; + + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + label.textColor = UIColor.whiteColor; + label.autoresizingMask = + UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + view.addSubview(label); + + return view; + }, + mounted(view) { + view.layer.cornerRadius = 14; + }, + update(view, props) { + view.backgroundColor = + props.tone === 'green' ? UIColor.systemGreenColor : UIColor.systemBlueColor; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, +}); + +async function readNativeSummary() { + const api = (globalThis as any).__nativeScriptNativeApi; + let ranOnMainThread = false; + + await NativeScript.runOnUI(() => { + ranOnMainThread = NSThread.isMainThread === true; + UIApplication.sharedApplication.keyWindow.tintColor = + UIColor.systemPinkColor; + }); + + return { + backend: api?.backend, + classes: api?.metadata?.classes ?? 0, + constants: api?.metadata?.constants ?? 0, + enums: api?.metadata?.enums ?? 0, + ranOnMainThread, + timeoutConstant: NSURLErrorTimedOut, + darkStyle: UIUserInterfaceStyle.Dark, + }; +} +export default function App() { + const [tone, setTone] = useState<'blue' | 'green'>('blue'); + const [summary, setSummary] = useState('Loading NativeScript...'); + + useEffect(() => { + readNativeSummary() + .then((value) => setSummary(JSON.stringify(value, null, 2))) + .catch((error) => { + setSummary(error instanceof Error ? error.message : String(error)); + }); + }, []); + + const title = useMemo( + () => (tone === 'blue' ? 'UIKit view from Expo' : 'Updated from React state'), + [tone], + ); + + return ( + + + + NativeScript Expo + + + Define UIKit views directly in JavaScript and mount them in an Expo + development build. + + + + + + setTone((value) => (value === 'blue' ? 'green' : 'blue'))} + style={{ + alignItems: 'center', + borderRadius: 8, + backgroundColor: '#111827', + minHeight: 48, + justifyContent: 'center', + paddingHorizontal: 16, + }}> + + Toggle Native Badge + + + + + {summary} + + + ); +} diff --git a/examples/expo-demo/README.md b/examples/expo-demo/README.md new file mode 100644 index 00000000..5ebc12c6 --- /dev/null +++ b/examples/expo-demo/README.md @@ -0,0 +1,17 @@ +# NativeScript Expo Demo + +This example is meant to be copied into a generated Expo app after installing +`@nativescript/react-native`. + +```sh +npx create-expo-app NativeScriptExpoDemo --template blank-typescript +cd NativeScriptExpoDemo +npm install /path/to/nativescript-react-native-0.0.1.tgz +cp /path/to/napi-ios/examples/expo-demo/app.config.js ./app.config.js +cp /path/to/napi-ios/examples/expo-demo/App.tsx ./App.tsx +npx expo prebuild --platform ios +npx expo run:ios +``` + +The package config plugin enables the iOS New Architecture and Hermes during +prebuild. This custom native module cannot run inside Expo Go. diff --git a/examples/expo-demo/app.config.js b/examples/expo-demo/app.config.js new file mode 100644 index 00000000..70135997 --- /dev/null +++ b/examples/expo-demo/app.config.js @@ -0,0 +1,11 @@ +module.exports = { + expo: { + name: 'NativeScript Expo Demo', + slug: 'nativescript-expo-demo', + scheme: 'nativescriptexpodemo', + ios: { + bundleIdentifier: 'org.nativescript.expo.demo', + }, + plugins: ['@nativescript/react-native'], + }, +}; diff --git a/packages/react-native/README.md b/packages/react-native/README.md index ca9ed9db..bb88da7c 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -127,3 +127,64 @@ npm run test-rn-turbomodule UIColor.systemPinkColor; }); ``` + +## Using the package in an Expo app + +Expo Go cannot load this package because it contains custom native code. Use an +Expo development build, EAS Build, or `npx expo run:ios`. + +1. Install the package: + + ```sh + npx expo install @nativescript/react-native + ``` + + When testing a local tarball: + + ```sh + npm install /path/to/nativescript-react-native-0.0.1.tgz + ``` + +2. Add the config plugin to `app.json` or `app.config.js`: + + ```json + { + "expo": { + "plugins": ["@nativescript/react-native"] + } + } + ``` + + The plugin configures iOS for Hermes and the React Native New Architecture, + which are required by this JSI TurboModule. + +3. Prebuild and run the iOS development build: + + ```sh + npx expo prebuild --platform ios + npx expo run:ios + ``` + +4. Initialize NativeScript in app code before using native APIs: + + ```tsx + import NativeScript, {defineUIKitView} from "@nativescript/react-native"; + + NativeScript.init(); + + const NativeBadge = defineUIKitView<{title: string}, UIView>({ + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + view.addSubview(label); + return view; + }, + update(view, props) { + view.backgroundColor = UIColor.systemBlueColor; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, + }); + ``` diff --git a/packages/react-native/app.plugin.js b/packages/react-native/app.plugin.js new file mode 100644 index 00000000..9247126c --- /dev/null +++ b/packages/react-native/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/withNativeScriptReactNative'); diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 9834dc64..4ba07227 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -23,6 +23,8 @@ "react-native": "src/index.ts", "types": "src/index.d.ts", "files": [ + "app.plugin.js", + "plugin", "src", "types", "ios", @@ -33,9 +35,15 @@ "LICENSE" ], "peerDependencies": { + "expo": "*", "react": "*", "react-native": ">=0.79" }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + }, "codegenConfig": { "name": "NativeScriptNativeApiSpec", "type": "all", diff --git a/packages/react-native/plugin/withNativeScriptReactNative.js b/packages/react-native/plugin/withNativeScriptReactNative.js new file mode 100644 index 00000000..24f1a5cc --- /dev/null +++ b/packages/react-native/plugin/withNativeScriptReactNative.js @@ -0,0 +1,52 @@ +const pkg = require('../package.json'); + +const DEFAULTS = { + ios: { + hermes: true, + newArchitecture: true, + }, +}; + +function readBoolean(value, fallback) { + return typeof value === 'boolean' ? value : fallback; +} + +function normalizeOptions(options = {}) { + const ios = options.ios || {}; + return { + ios: { + hermes: readBoolean( + ios.hermes, + readBoolean(options.hermes, DEFAULTS.ios.hermes), + ), + newArchitecture: readBoolean( + ios.newArchitecture, + readBoolean(options.newArchitecture, DEFAULTS.ios.newArchitecture), + ), + }, + }; +} + +function withNativeScriptReactNative(config, options) { + const normalized = normalizeOptions(options); + + config.ios = config.ios || {}; + + if (normalized.ios.hermes) { + config.ios.jsEngine = 'hermes'; + } + + if (normalized.ios.newArchitecture) { + // Expo SDKs have accepted both the root and iOS-scoped keys over time. + // Set both so CNG and existing native projects agree on the required RN mode. + config.newArchEnabled = true; + config.ios.newArchEnabled = true; + } + + return config; +} + +module.exports = withNativeScriptReactNative; +module.exports.default = withNativeScriptReactNative; +module.exports.withNativeScriptReactNative = withNativeScriptReactNative; +module.exports.pkg = pkg; From de6be51fb9880a4b3725dd77c9fa6a7e6b8fb765 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 19:59:23 -0400 Subject: [PATCH 21/31] ci(npm): support react-native trusted publishing --- .github/workflows/npm_trusted_release.yml | 113 ++++++++++++++-------- scripts/get-npm-tag.js | 1 + 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/.github/workflows/npm_trusted_release.yml b/.github/workflows/npm_trusted_release.yml index 04c54be1..4d215a4a 100644 --- a/.github/workflows/npm_trusted_release.yml +++ b/.github/workflows/npm_trusted_release.yml @@ -1,18 +1,20 @@ -name: NPM Trusted Release (iOS engines) +name: NPM Trusted Release -# Publishes one or more engine-specific NativeScript iOS runtime packages +# Publishes one or more NativeScript iOS npm packages # (@nativescript/ios-v8, @nativescript/ios-hermes, @nativescript/ios-jsc, -# @nativescript/ios-quickjs) via npm trusted publishing (OIDC). +# @nativescript/ios-quickjs, @nativescript/react-native) via npm trusted +# publishing (OIDC). # # Each package must be configured on npmjs.com with a trusted publisher that # points at this repository + workflow + environment. With `engine: all`, the -# workflow fans out across all four engines via a matrix. +# workflow fans out across the four iOS engine packages via a matrix; use +# `engine: react-native` to publish @nativescript/react-native. on: workflow_dispatch: inputs: engine: - description: "Engine to release (or 'all' to publish every engine)" + description: "Package to release (engine package, react-native, or 'all' for every iOS engine)" required: true type: choice default: v8 @@ -21,6 +23,7 @@ on: - hermes - jsc - quickjs + - react-native - all release-type: description: "Version bump (patch/minor/major publish to 'latest'; prerelease uses 'preid' as the dist-tag)" @@ -44,7 +47,7 @@ on: default: true concurrency: - # Avoid overlapping publishes on the same ref/engine selection. + # Avoid overlapping publishes on the same ref/package selection. group: npm-trusted-release-${{ github.ref }}-${{ inputs.engine }} cancel-in-progress: false @@ -53,34 +56,32 @@ env: jobs: matrix: - name: Resolve engine matrix + name: Resolve package matrix runs-on: ubuntu-latest outputs: - engines: ${{ steps.compute.outputs.engines }} + targets: ${{ steps.compute.outputs.targets }} steps: - name: Compute matrix id: compute run: | if [ "${{ inputs.engine }}" = "all" ]; then - echo 'engines=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" + echo 'targets=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" else - echo "engines=[\"${{ inputs.engine }}\"]" >> "$GITHUB_OUTPUT" + echo "targets=[\"${{ inputs.engine }}\"]" >> "$GITHUB_OUTPUT" fi build: - name: Build ${{ matrix.engine }} + name: Build ${{ matrix.target }} needs: matrix runs-on: macos-26 strategy: fail-fast: false matrix: - engine: ${{ fromJson(needs.matrix.outputs.engines) }} + target: ${{ fromJson(needs.matrix.outputs.targets) }} outputs: - # Per-engine outputs aren't natively supported with matrices, so each job + # Per-target outputs aren't natively supported with matrices, so each job # uploads its computed metadata alongside the tarball artifact. placeholder: noop - env: - IOS_VARIANT: ios-${{ matrix.engine }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 @@ -122,7 +123,19 @@ jobs: set -euo pipefail release_type='${{ inputs.release-type }}' preid='${{ inputs.preid }}' - pkg_dir="packages/${IOS_VARIANT}" + target='${{ matrix.target }}' + if [ "$target" = "react-native" ]; then + pkg_dir="packages/react-native" + package_name="@nativescript/react-native" + tarball_basename="nativescript-react-native" + npm_tag_target="react-native" + else + pkg_dir="packages/ios-${target}" + package_name="@nativescript/ios-${target}" + tarball_basename="nativescript-ios-${target}" + npm_tag_target="ios-${target}" + echo "IOS_VARIANT=ios-${target}" >> "$GITHUB_ENV" + fi pushd "$pkg_dir" >/dev/null if [ "$release_type" = "prerelease" ]; then @@ -133,40 +146,54 @@ jobs: NPM_VERSION=$(node -e "console.log(require('./package.json').version)") popd >/dev/null - NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "$IOS_VARIANT") + NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "$npm_tag_target") echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT" echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT" - echo "Resolved $IOS_VARIANT@$NPM_VERSION (tag: $NPM_TAG)" - - name: Build (--${{ matrix.engine }}) - run: ./scripts/build_all_ios.sh --${{ matrix.engine }} + echo "PACKAGE_DIR=$pkg_dir" >> "$GITHUB_OUTPUT" + echo "PACKAGE_NAME=$package_name" >> "$GITHUB_OUTPUT" + echo "TARBALL_BASENAME=$tarball_basename" >> "$GITHUB_OUTPUT" + echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG)" + - name: Build iOS engine (--${{ matrix.target }}) + if: ${{ matrix.target != 'react-native' }} + run: ./scripts/build_all_ios.sh --${{ matrix.target }} + - name: Build @nativescript/react-native + if: ${{ matrix.target == 'react-native' }} + run: | + ./scripts/build_all_react_native.sh + ./scripts/build_react_native_turbomodule.sh - name: Record metadata shell: bash run: | - mkdir -p packages/${IOS_VARIANT}/dist - cat > packages/${IOS_VARIANT}/dist/release-meta.json < "$package_dir/dist/release-meta.json" <&2 exit 1 @@ -212,9 +239,11 @@ jobs: NPM_VERSION=$(node -e "console.log(require('./$meta').version)") NPM_TAG=$(node -e "console.log(require('./$meta').tag)") PACKAGE_NAME=$(node -e "console.log(require('./$meta').package_name)") + TARBALL=$(node -e "console.log(require('./$meta').tarball)") echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT" echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT" echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT" + echo "TARBALL=$TARBALL" >> "$GITHUB_OUTPUT" - name: Publish package (OIDC trusted publishing) if: ${{ vars.USE_NPM_TOKEN != 'true' }} shell: bash @@ -222,12 +251,13 @@ jobs: NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }} NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} + TARBALL: ${{ steps.meta.outputs.TARBALL }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: "" run: | set -euo pipefail - TARBALL="packages/ios-${{ matrix.engine }}/dist/nativescript-ios-${{ matrix.engine }}-${NPM_VERSION}.tgz" - PUBLISH_ARGS=("$TARBALL" --tag "$NPM_TAG" --access public --provenance) + TARBALL_PATH="npm-package/${{ matrix.target }}/${TARBALL}" + PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) fi @@ -245,12 +275,13 @@ jobs: NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }} NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} + TARBALL: ${{ steps.meta.outputs.TARBALL }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} run: | set -euo pipefail - TARBALL="packages/ios-${{ matrix.engine }}/dist/nativescript-ios-${{ matrix.engine }}-${NPM_VERSION}.tgz" - PUBLISH_ARGS=("$TARBALL" --tag "$NPM_TAG" --access public --provenance) + TARBALL_PATH="npm-package/${{ matrix.target }}/${TARBALL}" + PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) fi @@ -268,10 +299,10 @@ jobs: steps: - name: Print summary run: | - echo "Engine selection: ${{ inputs.engine }}" + echo "Package selection: ${{ inputs.engine }}" echo "Release type: ${{ inputs.release-type }}" echo "Preid: ${{ inputs.preid }}" echo "Dry run: ${{ inputs.dry-run }}" - echo "Engines: ${{ needs.matrix.outputs.engines }}" + echo "Targets: ${{ needs.matrix.outputs.targets }}" echo "Build result: ${{ needs.build.result }}" echo "Publish result: ${{ needs.publish.result }}" diff --git a/scripts/get-npm-tag.js b/scripts/get-npm-tag.js index 7555e2a7..046c37ee 100644 --- a/scripts/get-npm-tag.js +++ b/scripts/get-npm-tag.js @@ -16,6 +16,7 @@ if (!currentVersion) { "ios-node-api": "../packages/ios-node-api/package.json", "macos-node-api": "../packages/macos-node-api/package.json", "objc-node-api": "../packages/objc-node-api/package.json", + "react-native": "../packages/react-native/package.json", }; if (!packageJsonByTarget[target]) { From be1abb2d1e4f5cc0ee7a7a354597b4f9f7ec3f5b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 22:19:34 -0400 Subject: [PATCH 22/31] fix(hermes): stabilize native callback dispatch --- NativeScript/CMakeLists.txt | 26 ++- NativeScript/ffi/CFunction.mm | 4 +- NativeScript/ffi/CallbackThreading.h | 39 ++-- NativeScript/ffi/Cif.mm | 4 + NativeScript/ffi/ClassMember.mm | 55 +++--- NativeScript/ffi/HermesFastNativeApi.mm | 64 +++++- NativeScript/ffi/jsi/NativeApiJsi.mm | 68 ++++++- NativeScript/runtime/modules/timers/Timers.mm | 39 +++- .../src/SignatureDispatchEmitter.cpp | 75 ++++--- .../react-native/ios/NativeScriptUIView.mm | 2 +- packages/react-native/src/index.ts | 26 +++ scripts/build_nativescript.sh | 7 +- scripts/create_react_native_demo.sh | 129 ++---------- scripts/react_native_app_utils.sh | 187 ++++++++++++++++++ scripts/run-tests-macos.js | 13 ++ scripts/test_react_native_ffi_compat.sh | 105 +--------- scripts/test_react_native_turbomodule.sh | 130 ++---------- 17 files changed, 541 insertions(+), 432 deletions(-) create mode 100644 scripts/react_native_app_utils.sh diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index c8eadce6..c80c2458 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -409,20 +409,32 @@ elseif(TARGET_ENGINE_JSC) target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_JSC) endif() +set(NS_GSD_BACKEND_V8_VALUE 0) +set(NS_GSD_BACKEND_JSC_VALUE 0) +set(NS_GSD_BACKEND_QUICKJS_VALUE 0) +set(NS_GSD_BACKEND_HERMES_VALUE 0) +set(NS_GSD_BACKEND_NAPI_VALUE 0) + 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) + set(NS_GSD_BACKEND_V8_VALUE 1) 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) + set(NS_GSD_BACKEND_JSC_VALUE 1) 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) + set(NS_GSD_BACKEND_QUICKJS_VALUE 1) 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) + set(NS_GSD_BACKEND_HERMES_VALUE 1) 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) + set(NS_GSD_BACKEND_NAPI_VALUE 1) endif() +target_compile_definitions(${NAME} PRIVATE + NS_GSD_BACKEND_V8=${NS_GSD_BACKEND_V8_VALUE} + NS_GSD_BACKEND_JSC=${NS_GSD_BACKEND_JSC_VALUE} + NS_GSD_BACKEND_QUICKJS=${NS_GSD_BACKEND_QUICKJS_VALUE} + NS_GSD_BACKEND_HERMES=${NS_GSD_BACKEND_HERMES_VALUE} + NS_GSD_BACKEND_NAPI=${NS_GSD_BACKEND_NAPI_VALUE} +) + set(FRAMEWORK_VERSION_VALUE "${VERSION}") if(TARGET_PLATFORM_MACOS) # macOS framework consumers (including Xcode's copy/sign phases) expect diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 8c528dc3..38ef35a4 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -344,7 +344,9 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { auto preparedInvoker = reinterpret_cast(func->preparedInvoker); auto napiInvoker = reinterpret_cast(func->napiInvoker); auto engineDirectInvoker = - reinterpret_cast(func->engineDirectInvoker); + !cif->skipGeneratedNapiDispatch + ? reinterpret_cast(func->engineDirectInvoker) + : nullptr; MDFunctionFlag functionFlags = bridgeState->metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); diff --git a/NativeScript/ffi/CallbackThreading.h b/NativeScript/ffi/CallbackThreading.h index 6a3d3b1a..63c17990 100644 --- a/NativeScript/ffi/CallbackThreading.h +++ b/NativeScript/ffi/CallbackThreading.h @@ -21,26 +21,27 @@ inline thread_local int native_caller_thread_callback_depth = 0; } // namespace detail -inline bool shouldInvokeCallbackOnNativeCallerThread() { +inline bool isNativeCallRuntimeUnlockedForCallbacks() { #if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) - return true; + return detail::native_call_unlocked_runtime_count.load( + std::memory_order_acquire) > 0; #else return false; #endif } -inline bool isNativeCallRuntimeUnlockedForCallbacks() { +inline bool isNativeCallerThreadCallbackActive() { #if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) - return detail::native_call_unlocked_runtime_count.load( - std::memory_order_acquire) > 0; + return detail::native_caller_thread_callback_depth > 0; #else return false; #endif } -inline bool isNativeCallerThreadCallbackActive() { +inline bool shouldInvokeCallbackOnNativeCallerThread() { #if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) - return detail::native_caller_thread_callback_depth > 0; + return isNativeCallRuntimeUnlockedForCallbacks() || + isNativeCallerThreadCallbackActive(); #else return false; #endif @@ -114,15 +115,19 @@ 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; - } + if (isNativeCallerThreadCallbackActive() || + js_current_env_lock_depth(env_) > 0) { + napi_open_handle_scope(env_, &napiHandleScope_); + return; + } + + auto it = JSR::env_to_jsr_cache.find(env_); + if (it != JSR::env_to_jsr_cache.end() && it->second != nullptr) { + jsr_ = it->second; + jsr_->lock(); + detail::native_caller_thread_callback_depth += 1; + napi_open_handle_scope(env_, &napiHandleScope_); + return; } #endif #if defined(ENABLE_JS_RUNTIME) @@ -139,7 +144,7 @@ class NativeCallbackScope final { } if (jsr_ != nullptr) { detail::native_caller_thread_callback_depth -= 1; - jsr_->js_mutex.unlock(); + jsr_->unlock(); } #endif } diff --git a/NativeScript/ffi/Cif.mm b/NativeScript/ffi/Cif.mm index 9d8ba085..db8ae6dd 100644 --- a/NativeScript/ffi/Cif.mm +++ b/NativeScript/ffi/Cif.mm @@ -64,6 +64,10 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptrkind) { case mdTypeUChar: case mdTypeUInt8: + case mdTypeString: + case mdTypePointer: + case mdTypeStruct: + case mdTypeArray: case mdTypeBlock: case mdTypeFunctionPointer: case mdTypeVector: diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/ClassMember.mm index e81efb10..174e21a7 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/ClassMember.mm @@ -363,11 +363,14 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho } } - ObjCEngineDirectInvoker engineInvoker = - descriptor != nullptr - ? reinterpret_cast(descriptor->engineDirectInvoker) - : lookupObjCEngineDirectInvoker(composeSignatureDispatchId( - cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); + ObjCEngineDirectInvoker engineInvoker = nullptr; + if (!cif->skipGeneratedNapiDispatch) { + engineInvoker = + descriptor != nullptr + ? reinterpret_cast(descriptor->engineDirectInvoker) + : lookupObjCEngineDirectInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); + } ObjCNapiInvoker invoker = engineInvoker == nullptr && !cif->skipGeneratedNapiDispatch ? (descriptor != nullptr @@ -1474,23 +1477,6 @@ explicit CifReturnStorage(Cif* cif) { 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 @@ -1567,6 +1553,20 @@ explicit CifReturnStorage(Cif* cif) { method->cif = selectedCif; } + if (selectedCif != nullptr && !selectedCif->isVariadic && + selectedCif->argc != actualArgc) { + Method runtimeMethod = receiverIsClass + ? class_getClassMethod(receiverClass, selectedMethod->selector) + : class_getInstanceMethod(receiverClass, selectedMethod->selector); + if (runtimeMethod != nullptr) { + Cif* runtimeCif = method->bridgeState->getMethodCif(env, runtimeMethod); + if (runtimeCif != nullptr && runtimeCif->argc == actualArgc) { + selectedCif = runtimeCif; + method->cif = selectedCif; + } + } + } + if (!method->overloads.empty()) { struct Candidate { MethodDescriptor* descriptor; @@ -1871,8 +1871,6 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa if (handledDirect) { return directResult; } - return jsGetterDirect(env, static_cast(HermesFastData(fastInfo)), - HermesFastThisArg(fastInfo)); } #endif @@ -1986,15 +1984,6 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa 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 diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 4067ce01..4299bf7d 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -525,6 +525,50 @@ inline bool needsRoundTripCacheFrame(Cif* cif) { return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } +inline bool canUseHermesFrameArgument(MDTypeKind kind) { + switch (kind) { + 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: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + case mdTypeClass: + case mdTypeSelector: + return true; + default: + return false; + } +} + +inline bool canUseHermesFrameArguments(Cif* cif) { + if (cif == nullptr) { + return false; + } + + for (const auto& argType : cif->argTypes) { + if (argType == nullptr || !canUseHermesFrameArgument(argType->kind)) { + return false; + } + } + + return true; +} + class HermesFastRoundTripCacheFrameGuard { public: HermesFastRoundTripCacheFrameGuard(napi_env env, ObjCBridgeState* bridgeState, @@ -1952,6 +1996,10 @@ napi_value TryCallHermesObjCMemberFastImpl( return nullptr; } + if (hermesArgsBase != nullptr && !canUseHermesFrameArguments(cif)) { + return nullptr; + } + if (isNSErrorOutMethodSignature(descriptor, cif)) { if (!cif->isVariadic && (actualArgc > cif->argc || actualArgc + 1 < cif->argc)) { @@ -1963,6 +2011,10 @@ napi_value TryCallHermesObjCMemberFastImpl( return nullptr; } + if (!cif->isVariadic && actualArgc != cif->argc) { + return nullptr; + } + if (isBlockFallbackSelector(descriptor)) { return nullptr; } @@ -2118,7 +2170,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); ObjCEngineDirectInvoker invoker = - cif->signatureHash != 0 + !cif->skipGeneratedNapiDispatch && cif->signatureHash != 0 ? ensureHermesObjCEngineDirectInvoker(cif, descriptor, descriptor->dispatchFlags) : nullptr; @@ -2193,6 +2245,14 @@ napi_value TryCallHermesCFunctionFastImpl( return nullptr; } + if (hermesArgsBase != nullptr && !canUseHermesFrameArguments(cif)) { + return nullptr; + } + + if (actualArgc != cif->argc) { + return nullptr; + } + const bool hasExactHermesFrameArgs = hermesArgsBase != nullptr && actualArgc == cif->argc; CFunctionHermesFrameDirectReturnInvoker frameDirectReturnInvoker = @@ -2291,7 +2351,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( bool didInvoke = false; CFunctionEngineDirectInvoker invoker = - cif->signatureHash != 0 + !cif->skipGeneratedNapiDispatch && cif->signatureHash != 0 ? ensureHermesCFunctionEngineDirectInvoker(function, cif) : nullptr; @try { diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/jsi/NativeApiJsi.mm index 45da58a5..321ddb61 100644 --- a/NativeScript/ffi/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/jsi/NativeApiJsi.mm @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -56,7 +55,7 @@ thread_local bool gDispatchNativeCallsToUI = false; thread_local bool gExecutingDispatchedUINativeCall = false; -std::atomic gSynchronousNativeInvocationDepth{0}; +thread_local int gSynchronousNativeInvocationDepth = 0; class ScopedNativeApiUINativeCallDispatch final { public: @@ -80,11 +79,11 @@ bool shouldDispatchNativeCallToUI() { class ScopedNativeApiSynchronousInvocation final { public: ScopedNativeApiSynchronousInvocation() { - gSynchronousNativeInvocationDepth.fetch_add(1, std::memory_order_acq_rel); + gSynchronousNativeInvocationDepth += 1; } ~ScopedNativeApiSynchronousInvocation() { - gSynchronousNativeInvocationDepth.fetch_sub(1, std::memory_order_acq_rel); + gSynchronousNativeInvocationDepth -= 1; } }; @@ -1873,8 +1872,7 @@ void invoke(void* ret, void* args[]) { auto call = [&]() { invokeOnCurrentThread(ret, args, &error); }; bool direct = std::this_thread::get_id() == bridge_->jsThreadId() || gExecutingDispatchedUINativeCall || - gSynchronousNativeInvocationDepth.load( - std::memory_order_acquire) > 0; + gSynchronousNativeInvocationDepth > 0; if (direct) { call(); } else if (auto scheduler = bridge_->scheduler()) { @@ -3657,6 +3655,46 @@ Object createPointer(Runtime& runtime, void* pointer, bool adopted = false) { adopted)); } +void installInteropHasInstance(Runtime& runtime, Function& constructor, + const char* kind) { + Value symbolCtorValue = runtime.global().getProperty(runtime, "Symbol"); + if (!symbolCtorValue.isObject()) { + return; + } + + Object symbolCtor = symbolCtorValue.asObject(runtime); + Value hasInstanceValue = symbolCtor.getProperty(runtime, "hasInstance"); + if (!hasInstanceValue.isSymbol()) { + return; + } + + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty( + runtime, "value", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.hasInstance"), 1, + [kind = std::string(kind)](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + return false; + } + + Object object = args[0].asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + return kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == kind; + })); + defineProperty.call(runtime, objectCtor, constructor, hasInstanceValue, + descriptor); + } catch (const std::exception&) { + } +} + Class classFromJsiValue(Runtime& runtime, const Value& value) { if (value.isString()) { std::string name = value.asString(runtime).utf8(runtime); @@ -3766,6 +3804,10 @@ Object createInteropObject(Runtime& runtime, } return createPointer(runtime, pointer); }); + Object pointerPrototype(runtime); + pointerPrototype.setProperty(runtime, "constructor", pointerConstructor); + pointerConstructor.setProperty(runtime, "prototype", pointerPrototype); + installInteropHasInstance(runtime, pointerConstructor, "pointer"); pointerConstructor.setProperty(runtime, "kind", makeString(runtime, "pointer")); pointerConstructor.setProperty(runtime, "sizeof", static_cast(sizeof(void*))); @@ -3820,6 +3862,10 @@ Object createInteropObject(Runtime& runtime, bridge, type, data, ownsData, ownsData ? size : 0)); }); + Object referencePrototype(runtime); + referencePrototype.setProperty(runtime, "constructor", referenceConstructor); + referenceConstructor.setProperty(runtime, "prototype", referencePrototype); + installInteropHasInstance(runtime, referenceConstructor, "reference"); referenceConstructor.setProperty(runtime, "kind", makeString(runtime, "reference")); referenceConstructor.setProperty(runtime, "sizeof", @@ -4846,9 +4892,13 @@ void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { ? config.globalName : "__nativeScriptNativeApi"; Object api = CreateNativeApiJSI(runtime, config); - runtime.global().setProperty(runtime, globalName, api); - runtime.global().setProperty(runtime, "interop", - api.getProperty(runtime, "interop")); + Object global = runtime.global(); + global.setProperty(runtime, globalName, api); + + Value existingInterop = global.getProperty(runtime, "interop"); + if (existingInterop.isUndefined() || existingInterop.isNull()) { + global.setProperty(runtime, "interop", api.getProperty(runtime, "interop")); + } InstallAggregateGlobals(runtime, api, "protocolNames"); } diff --git a/NativeScript/runtime/modules/timers/Timers.mm b/NativeScript/runtime/modules/timers/Timers.mm index e15168e3..86b62164 100644 --- a/NativeScript/runtime/modules/timers/Timers.mm +++ b/NativeScript/runtime/modules/timers/Timers.mm @@ -9,6 +9,7 @@ #include #include #include "Timers.h" +#include "ffi/CallbackThreading.h" static std::atomic gActiveTimers{0}; struct TimerToken; @@ -212,7 +213,23 @@ bool MarkTimerInactive(NSTimerHandle* handle) { return didDeactivate; } -void AddTimerToMainRunLoop(NSTimer* timer) { +bool shouldAvoidMainQueueSyncWhileHoldingHermesLock(napi_env env) { +#ifdef TARGET_ENGINE_HERMES + if ([NSThread isMainThread]) { + return false; + } + + // A native-caller-thread callback already owns the Hermes JS lock. A + // synchronous hop to main can deadlock if the main run loop is draining jobs. + return nativescript::isNativeCallerThreadCallbackActive() || + (env != nullptr && js_current_env_lock_depth(env) > 0); +#else + (void)env; + return false; +#endif +} + +void AddTimerToMainRunLoop(napi_env env, NSTimer* timer) { if (timer == nil) { return; } @@ -228,6 +245,15 @@ void AddTimerToMainRunLoop(NSTimer* timer) { return; } + if (shouldAvoidMainQueueSyncWhileHoldingHermesLock(env)) { + [timer retain]; + dispatch_async(dispatch_get_main_queue(), ^{ + addTimer(); + [timer release]; + }); + return; + } + dispatch_sync(dispatch_get_main_queue(), addTimer); } @@ -254,6 +280,13 @@ void DisposeTimerHandle(napi_env callEnv, NSTimerHandle* handle, bool invalidate if ([NSThread isMainThread]) { disposeTimer(); + } else if (shouldAvoidMainQueueSyncWhileHoldingHermesLock( + callEnv != nullptr ? callEnv : handle->env)) { + [handle retain]; + dispatch_async(dispatch_get_main_queue(), ^{ + disposeTimer(); + [handle release]; + }); } else { dispatch_sync(dispatch_get_main_queue(), disposeTimer); } @@ -427,7 +460,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { // Drop creator ownership. Remaining ownership is the timer association. [handle release]; - AddTimerToMainRunLoop(timer); + AddTimerToMainRunLoop(env, timer); return result; } @@ -529,7 +562,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { // Drop creator ownership. Remaining ownership is the timer association. [handle release]; - AddTimerToMainRunLoop(timer); + AddTimerToMainRunLoop(env, timer); return result; } diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index c8a2c855..0d3095de 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -744,6 +744,40 @@ bool argKindMayNeedCleanup(MDTypeKind kind) { } } +enum class HermesDirectReturnCallSite { + FastCallback, + Frame, +}; + +bool canUseHermesDirectReturnWrapper(DispatchKind kind, + const MDSignature* signature, + HermesDirectReturnCallSite callSite) { + if (callSite == HermesDirectReturnCallSite::FastCallback && + kind == DispatchKind::BlockInvoke) { + return false; + } + + if (signature == nullptr || signature->returnType == nullptr) { + return false; + } + + const bool canSetReturnDirectly = + kind == DispatchKind::ObjCMethod + ? canSetHermesObjCReturnDirectly(signature->returnType->kind) + : canSetHermesReturnDirectly(signature->returnType->kind); + if (!canSetReturnDirectly) { + return false; + } + + for (const auto* arg : signature->arguments) { + if (arg == nullptr || argKindMayNeedCleanup(arg->kind)) { + return false; + } + } + + return true; +} + std::string makeWrapperShapeKey(DispatchKind kind, const MDSignature* signature) { if (signature == nullptr) { @@ -1555,16 +1589,8 @@ void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, 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) { + if (!canUseHermesDirectReturnWrapper( + kind, signature, HermesDirectReturnCallSite::FastCallback)) { return; } @@ -1707,15 +1733,8 @@ 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) { + if (!canUseHermesDirectReturnWrapper(kind, signature, + HermesDirectReturnCallSite::Frame)) { return; } @@ -2355,9 +2374,12 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, objcNapiEntries.emplace(dispatchId, wrapperKey); objcEngineDirectEntries.emplace(dispatchId, wrapperKey); objcV8Entries.emplace(dispatchId, wrapperKey); - if (signature->returnType != nullptr && - canSetHermesObjCReturnDirectly(signature->returnType->kind)) { + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::FastCallback)) { objcHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { objcHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); } } else if (use.kind == DispatchKind::CFunction) { @@ -2365,17 +2387,20 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, cFunctionNapiEntries.emplace(dispatchId, wrapperKey); cFunctionEngineDirectEntries.emplace(dispatchId, wrapperKey); cFunctionV8Entries.emplace(dispatchId, wrapperKey); - if (signature->returnType != nullptr && - canSetHermesReturnDirectly(signature->returnType->kind)) { + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::FastCallback)) { cFunctionHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { 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)) { + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { blockHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); } } diff --git a/packages/react-native/ios/NativeScriptUIView.mm b/packages/react-native/ios/NativeScriptUIView.mm index 16b6ae89..ae0430c8 100644 --- a/packages/react-native/ios/NativeScriptUIView.mm +++ b/packages/react-native/ios/NativeScriptUIView.mm @@ -12,7 +12,7 @@ char* end = nullptr; unsigned long long address = strtoull(text, &end, 0); - if (address == 0 || end == text) { + if (address == 0 || end == text || (end != nullptr && *end != '\0')) { return nil; } diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index baa5685e..5a021cd0 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -269,6 +269,32 @@ function wrapInteropFactory( return (nativeFactory as (...args: unknown[]) => unknown)(...args); }; + try { + const nativePrototype = (nativeFactory as {prototype?: unknown}).prototype; + if ( + nativePrototype && + (typeof nativePrototype === 'object' || typeof nativePrototype === 'function') + ) { + constructable.prototype = nativePrototype; + } + } catch { + // Keep construction working even if the host function exposes a fixed prototype. + } + + try { + const hasInstance = Symbol.hasInstance; + const nativeHasInstance = (nativeFactory as Record)[hasInstance]; + if (typeof nativeHasInstance === 'function') { + Object.defineProperty(constructable, hasInstance, { + configurable: true, + enumerable: false, + value: nativeHasInstance, + }); + } + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + for (const [key, value] of Object.entries(properties)) { try { Object.defineProperty(constructable, key, { diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 3ec7ccec..161f55df 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -114,8 +114,11 @@ 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" + local generator_hash + generator_hash=$(find ./metadata-generator/src ./metadata-generator/include ./metadata-generator/CMakeLists.txt \ + -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}') + printf "platform=%s\nbackend=%s\ntarget_engine=%s\nmetadata_size=%s\ngenerator_hash=%s\n" \ + "$platform" "$backend" "$TARGET_ENGINE" "$METADATA_SIZE" "$generator_hash" } function ensure_signature_dispatch_bindings () { diff --git a/scripts/create_react_native_demo.sh b/scripts/create_react_native_demo.sh index ffcefee7..dc89a20b 100755 --- a/scripts/create_react_native_demo.sh +++ b/scripts/create_react_native_demo.sh @@ -1,6 +1,7 @@ #!/bin/bash set -euo pipefail source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" RN_VERSION=${RN_DEMO_VERSION:-0.85.3} RN_CLI_VERSION=${RN_DEMO_CLI_VERSION:-20.1.3} @@ -18,142 +19,34 @@ 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) +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) 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" -) +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native demo app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "demo app" 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 -) +rn_install_pods "$APP_DIR" "demo app" 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 +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "demo app" +APP_BUNDLE="$RN_APP_BUNDLE" 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 + MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") + rn_wait_for_marker_file "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" fi checkpoint "React Native NativeScript demo app is ready at $APP_DIR" diff --git a/scripts/react_native_app_utils.sh b/scripts/react_native_app_utils.sh new file mode 100644 index 00000000..5d1bc133 --- /dev/null +++ b/scripts/react_native_app_utils.sh @@ -0,0 +1,187 @@ +#!/bin/bash +set -euo pipefail + +# Shared helpers for generated React Native apps used by smoke tests, FFI +# compatibility tests, and local demos. Source build_utils.sh before this file. + +function rn_build_turbo_tarball() { + checkpoint "Building @nativescript/react-native TurboModule tarball..." + "$SCRIPT_DIR/build_react_native_turbomodule.sh" +} + +function rn_latest_turbo_tarball() { + ls -t "$REPO_ROOT/packages/react-native/dist"/*.tgz | head -n 1 +} + +function rn_create_app_if_missing() { + local app_dir="$1" + local app_root="$2" + local app_name="$3" + local rn_version="$4" + local rn_cli_version="$5" + local label="$6" + + if [[ ! -d "$app_dir" ]]; then + checkpoint "Creating $label ($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 +} + +function rn_install_turbo_tarball() { + local app_dir="$1" + local tarball="$2" + local label="$3" + + checkpoint "Installing local TurboModule tarball into $label..." + ( + cd "$app_dir" + npm install "$tarball" + ) +} + +function rn_install_pods() { + local app_dir="$1" + local label="$2" + + checkpoint "Installing CocoaPods for $label..." + ( + 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 + ) +} + +function rn_select_ios_simulator() { + 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 +} + +function rn_require_ios_simulator() { + local udid + if ! udid=$(rn_select_ios_simulator); then + udid="" + fi + if [[ -z "$udid" ]]; then + echo "No available iOS simulator found." >&2 + exit 1 + fi + echo "$udid" +} + +function rn_build_ios_app() { + local app_dir="$1" + local app_root="$2" + local app_name="$3" + local configuration="$4" + local udid="$5" + local timeout_seconds="$6" + local label="$7" + + checkpoint "Building $label 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" & + local build_pid=$! + + local seconds_waited=0 + while kill -0 "$build_pid" >/dev/null 2>&1; do + if [[ "$seconds_waited" -ge "$timeout_seconds" ]]; then + kill "$build_pid" >/dev/null 2>&1 || true + echo "$label build timed out after ${timeout_seconds}s." >&2 + exit 1 + fi + sleep 5 + seconds_waited=$((seconds_waited + 5)) + done + wait "$build_pid" + + RN_APP_BUNDLE=$(find "$app_dir/ios/build/DerivedData/Build/Products/$configuration-iphonesimulator" -maxdepth 1 -name "$app_name.app" -print -quit) + if [[ -z "$RN_APP_BUNDLE" ]]; then + echo "Built app bundle not found." >&2 + exit 1 + fi +} + +function rn_launch_app_with_marker() { + local udid="$1" + local app_bundle="$2" + local bundle_id="$3" + local marker_file_name="$4" + + xcrun simctl install "$udid" "$app_bundle" + local data_container + data_container=$(xcrun simctl get_app_container "$udid" "$bundle_id" data) + local 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" >/dev/null + + echo "$marker_file" +} + +function rn_wait_for_marker_file() { + local marker_file="$1" + local marker="$2" + local timeout_seconds="$3" + + node - "$marker_file" "$marker" "$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 +} diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 30e4c11b..7e2b70da 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -94,6 +94,10 @@ const junitPrefix = "TKUnit: "; const junitEndTag = ""; const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); +const generatedRuntimeBuildOutputs = new Set([ + path.join(nativeScriptSourceRoot, "ffi", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "GeneratedSignatureDispatch.inc.stamp") +]); function parseArgs() { const args = process.argv.slice(2).filter(Boolean); @@ -136,6 +140,9 @@ function getPathStats(targetPath) { while (queue.length > 0) { const currentPath = queue.pop(); + if (generatedRuntimeBuildOutputs.has(currentPath)) { + continue; + } let stats; try { stats = fs.lstatSync(currentPath); @@ -469,6 +476,12 @@ function ensureMacOSRuntimeArtifactsBuilt() { const cachePath = path.join(__dirname, "../dist", "intermediates", "macos", "CMakeCache.txt"); const sourceInputs = [ nativeScriptSourceRoot, + path.join(metadataGeneratorRoot, "src"), + path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "CMakeLists.txt"), + metadataGeneratorBinary, + metadataGeneratorBuildStepScript, + path.join(__dirname, "build_metadata_generator.sh"), path.join(__dirname, "build_nativescript.sh") ]; diff --git a/scripts/test_react_native_ffi_compat.sh b/scripts/test_react_native_ffi_compat.sh index 058305fd..61c88233 100755 --- a/scripts/test_react_native_ffi_compat.sh +++ b/scripts/test_react_native_ffi_compat.sh @@ -1,6 +1,7 @@ #!/bin/bash set -euo pipefail source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" RN_VERSION=${RN_FFI_COMPAT_VERSION:-0.85.3} RN_CLI_VERSION=${RN_FFI_COMPAT_CLI_VERSION:-20.1.3} @@ -16,112 +17,26 @@ MARKER="NATIVESCRIPT_RN_FFI_COMPAT" MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" APP_TSX="$REPO_ROOT/test/react-native/ffi-compat/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) +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) if [[ "$FORCE_RECREATE" == "1" ]]; then rm -rf "$APP_DIR" fi -if [[ ! -d "$APP_DIR" ]]; then - checkpoint "Creating React Native FFI compatibility 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 FFI compatibility app..." -( - cd "$APP_DIR" - npm install "$TARBALL" -) +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native FFI compatibility app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "FFI compatibility app" checkpoint "Installing FFI compatibility entrypoint..." cp "$APP_TSX" "$APP_DIR/App.tsx" -checkpoint "Installing CocoaPods for FFI compatibility 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 FFI compatibility 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 "FFI compatibility 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 +rn_install_pods "$APP_DIR" "FFI compatibility app" +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "FFI compatibility app" +APP_BUNDLE="$RN_APP_BUNDLE" checkpoint "Launching FFI compatibility app and waiting for test 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" +MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") node - "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" <<'NODE' const fs = require('fs'); diff --git a/scripts/test_react_native_turbomodule.sh b/scripts/test_react_native_turbomodule.sh index 1bb7c173..bd1cdd0b 100755 --- a/scripts/test_react_native_turbomodule.sh +++ b/scripts/test_react_native_turbomodule.sh @@ -1,6 +1,7 @@ #!/bin/bash set -euo pipefail source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" RN_VERSION=${RN_VERSION:-0.85.3} RN_CLI_VERSION=${RN_CLI_VERSION:-20.1.3} @@ -15,30 +16,15 @@ 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) +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) 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" -) +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native smoke app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "smoke app" checkpoint "Writing smoke app entrypoint..." node - "$APP_DIR/App.tsx" <<'NODE' @@ -123,107 +109,13 @@ export default function App(): React.JSX.Element { `); 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 +rn_install_pods "$APP_DIR" "smoke app" +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "smoke app" +APP_BUNDLE="$RN_APP_BUNDLE" 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 +MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") +rn_wait_for_marker_file "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" checkpoint "React Native NativeScript TurboModule smoke test passed." From 387abcea6d935f01dd4b55b3cf0b28315ad28c49 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 23 May 2026 23:53:32 -0400 Subject: [PATCH 23/31] fix: address CodeRabbit review comments --- .github/workflows/npm_trusted_release.yml | 89 ++++++++++++----- NativeScript/ffi/CFunction.mm | 96 +++++++++++++++---- NativeScript/ffi/Class.mm | 4 +- NativeScript/ffi/EngineDirectCall.mm | 13 ++- NativeScript/ffi/HermesFastCallbackInfo.h | 4 + NativeScript/ffi/HermesFastNativeApi.mm | 28 ++++-- NativeScript/ffi/JSCFastNativeApi.h | 4 + NativeScript/ffi/JSCFastNativeApi.mm | 17 ++-- NativeScript/ffi/ObjCBridge.h | 2 +- NativeScript/ffi/ObjCBridge.mm | 2 +- NativeScript/ffi/QuickJSFastNativeApi.mm | 21 ++-- NativeScript/ffi/SignatureDispatch.h | 16 ++-- NativeScript/ffi/TypeConv.mm | 39 ++++---- .../ffi/jsi/NativeApiJsiReactNative.h | 14 +-- benchmarks/objc-dispatch/run.js | 20 +++- examples/react-native-demo/App.tsx | 29 +++++- .../src/SignatureDispatchEmitter.cpp | 1 + .../Fabric/NativeScriptUIViewComponentView.mm | 4 - 18 files changed, 292 insertions(+), 111 deletions(-) diff --git a/.github/workflows/npm_trusted_release.yml b/.github/workflows/npm_trusted_release.yml index 4d215a4a..3c4541ec 100644 --- a/.github/workflows/npm_trusted_release.yml +++ b/.github/workflows/npm_trusted_release.yml @@ -58,22 +58,35 @@ jobs: matrix: name: Resolve package matrix runs-on: ubuntu-latest + permissions: {} outputs: targets: ${{ steps.compute.outputs.targets }} steps: - name: Compute matrix id: compute + env: + ENGINE: ${{ inputs.engine }} run: | - if [ "${{ inputs.engine }}" = "all" ]; then - echo 'targets=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" - else - echo "targets=[\"${{ inputs.engine }}\"]" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + case "$ENGINE" in + all) + echo 'targets=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" + ;; + v8|hermes|jsc|quickjs|react-native) + printf 'targets=["%s"]\n' "$ENGINE" >> "$GITHUB_OUTPUT" + ;; + *) + echo "Unsupported engine: $ENGINE" >&2 + exit 1 + ;; + esac build: name: Build ${{ matrix.target }} needs: matrix runs-on: macos-26 + permissions: + contents: read strategy: fail-fast: false matrix: @@ -119,11 +132,15 @@ jobs: - name: Bump version id: bump shell: bash + env: + RELEASE_TYPE: ${{ inputs.release-type }} + PREID: ${{ inputs.preid }} + TARGET: ${{ matrix.target }} run: | set -euo pipefail - release_type='${{ inputs.release-type }}' - preid='${{ inputs.preid }}' - target='${{ matrix.target }}' + release_type="$RELEASE_TYPE" + preid="$PREID" + target="$TARGET" if [ "$target" = "react-native" ]; then pkg_dir="packages/react-native" package_name="@nativescript/react-native" @@ -155,7 +172,9 @@ jobs: echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG)" - name: Build iOS engine (--${{ matrix.target }}) if: ${{ matrix.target != 'react-native' }} - run: ./scripts/build_all_ios.sh --${{ matrix.target }} + env: + TARGET: ${{ matrix.target }} + run: ./scripts/build_all_ios.sh "--${TARGET}" - name: Build @nativescript/react-native if: ${{ matrix.target == 'react-native' }} run: | @@ -163,18 +182,25 @@ jobs: ./scripts/build_react_native_turbomodule.sh - name: Record metadata shell: bash + env: + TARGET: ${{ matrix.target }} + PACKAGE_DIR: ${{ steps.bump.outputs.PACKAGE_DIR }} + PACKAGE_NAME: ${{ steps.bump.outputs.PACKAGE_NAME }} + NPM_VERSION: ${{ steps.bump.outputs.NPM_VERSION }} + NPM_TAG: ${{ steps.bump.outputs.NPM_TAG }} + TARBALL_BASENAME: ${{ steps.bump.outputs.TARBALL_BASENAME }} run: | set -euo pipefail - package_dir="${{ steps.bump.outputs.PACKAGE_DIR }}" - tarball_file="${{ steps.bump.outputs.TARBALL_BASENAME }}-${{ steps.bump.outputs.NPM_VERSION }}.tgz" + package_dir="$PACKAGE_DIR" + tarball_file="${TARBALL_BASENAME}-${NPM_VERSION}.tgz" mkdir -p "$package_dir/dist" cat > "$package_dir/dist/release-meta.json" <&2 exit 1 @@ -252,11 +280,12 @@ jobs: NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} TARBALL: ${{ steps.meta.outputs.TARBALL }} + TARGET: ${{ matrix.target }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: "" run: | set -euo pipefail - TARBALL_PATH="npm-package/${{ matrix.target }}/${TARBALL}" + TARBALL_PATH="npm-package/${TARGET}/${TARBALL}" PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) @@ -276,11 +305,12 @@ jobs: NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} TARBALL: ${{ steps.meta.outputs.TARBALL }} + TARGET: ${{ matrix.target }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} run: | set -euo pipefail - TARBALL_PATH="npm-package/${{ matrix.target }}/${TARBALL}" + TARBALL_PATH="npm-package/${TARGET}/${TARBALL}" PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) @@ -296,13 +326,22 @@ jobs: - build - publish runs-on: ubuntu-latest + permissions: {} steps: - name: Print summary + env: + PACKAGE_SELECTION: ${{ inputs.engine }} + RELEASE_TYPE: ${{ inputs.release-type }} + PREID: ${{ inputs.preid }} + DRY_RUN: ${{ inputs.dry-run }} + TARGETS: ${{ needs.matrix.outputs.targets }} + BUILD_RESULT: ${{ needs.build.result }} + PUBLISH_RESULT: ${{ needs.publish.result }} run: | - echo "Package selection: ${{ inputs.engine }}" - echo "Release type: ${{ inputs.release-type }}" - echo "Preid: ${{ inputs.preid }}" - echo "Dry run: ${{ inputs.dry-run }}" - echo "Targets: ${{ needs.matrix.outputs.targets }}" - echo "Build result: ${{ needs.build.result }}" - echo "Publish result: ${{ needs.publish.result }}" + echo "Package selection: $PACKAGE_SELECTION" + echo "Release type: $RELEASE_TYPE" + echo "Preid: $PREID" + echo "Dry run: $DRY_RUN" + echo "Targets: $TARGETS" + echo "Build result: $BUILD_RESULT" + echo "Publish result: $PUBLISH_RESULT" diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 38ef35a4..9a3610f2 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -1,6 +1,9 @@ #include "CFunction.h" #include +#include +#include #include +#include #include #include "Block.h" #include "CallbackThreading.h" @@ -30,6 +33,62 @@ inline bool isCompatOrMainCFunctionName(const char* name) { strcmp(name, "NSApplicationMain") == 0; } +class CFunctionInvocationFrame final { + public: + explicit CFunctionInvocationFrame(Cif* cif) + : avalues_(cif != nullptr ? cif->argc : 0, nullptr), + argumentStorage_(cif != nullptr ? cif->argc : 0, nullptr) { + if (cif == nullptr) { + return; + } + + const size_t rvalueLength = cif->rvalueLength > 0 ? cif->rvalueLength : 1; + rvalue_ = std::malloc(rvalueLength); + if (rvalue_ == nullptr) { + return; + } + + valid_ = true; + for (unsigned int i = 0; i < cif->argc; i++) { + ffi_type* argType = + cif->cif.arg_types != nullptr ? cif->cif.arg_types[i] : nullptr; + const size_t argLength = + argType != nullptr && argType->size > 0 ? argType->size : 1; + void* storage = std::malloc(argLength); + if (storage == nullptr) { + valid_ = false; + return; + } + argumentStorage_[i] = storage; + avalues_[i] = storage; + } + } + + ~CFunctionInvocationFrame() { + for (void* storage : argumentStorage_) { + if (storage != nullptr) { + std::free(storage); + } + } + if (rvalue_ != nullptr) { + std::free(rvalue_); + } + } + + CFunctionInvocationFrame(const CFunctionInvocationFrame&) = delete; + CFunctionInvocationFrame& operator=(const CFunctionInvocationFrame&) = delete; + + bool isValid() const { return valid_ && rvalue_ != nullptr; } + void* rvalue() const { return rvalue_; } + void** avalues() { return avalues_.empty() ? nullptr : avalues_.data(); } + + private: + bool valid_ = false; + void* rvalue_ = nullptr; + std::vector avalues_; + std::vector argumentStorage_; +}; + inline bool unwrapCompatNativeHandleForCFunction(napi_env env, napi_value value, void** out) { if (value == nullptr || out == nullptr) { return false; @@ -374,6 +433,13 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; + auto invocationFrame = std::make_shared(cif); + if (!invocationFrame->isValid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function invocation storage."); + return nullptr; + } + void* rvalue = invocationFrame->rvalue(); if ((engineDirectInvoker != nullptr || (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && @@ -381,8 +447,8 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { @try { NativeCallRuntimeUnlockScope unlockRuntime(env); bool invoked = engineDirectInvoker != nullptr - ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue) - : napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue); + ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, rvalue) + : napiInvoker(env, cif, func->fnptr, invocationArgs, rvalue); if (!invoked) { return nullptr; } @@ -395,38 +461,32 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { } napi_value fastResult = nullptr; - if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, cif->rvalue, + if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, &fastResult)) { return fastResult; } - return cif->returnType->toJS(env, cif->rvalue, toJSFlags); + return cif->returnType->toJS(env, rvalue, toJSFlags); } - void* avalues[cif->argc]; - void* rvalue = cif->rvalue; + void** avalues = invocationFrame->avalues(); bool shouldFreeAny = false; - bool shouldFree[cif->argc]; + std::vector shouldFree(cif->argc, 0); 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, invocationArgs[i], avalues[i], &shouldFree[i], + bool argShouldFree = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], &argShouldFree, &shouldFreeAny); + shouldFree[i] = argShouldFree ? 1 : 0; } } #ifdef ENABLE_JS_RUNTIME if (isMainEntrypoint) { - void** avaluesPtr = new void*[cif->argc]; - memcpy(avaluesPtr, avalues, cif->argc * sizeof(void*)); - - Tasks::Register([env, cif, func, preparedInvoker, rvalue, avaluesPtr]() { - void* avalues[cif->argc]; - memcpy(avalues, avaluesPtr, cif->argc * sizeof(void*)); - delete[] avaluesPtr; - + Tasks::Register([env, cif, func, preparedInvoker, invocationFrame]() { + void** avalues = invocationFrame->avalues(); + void* rvalue = invocationFrame->rvalue(); @try { if (preparedInvoker != nullptr) { preparedInvoker(func->fnptr, avalues, rvalue); diff --git a/NativeScript/ffi/Class.mm b/NativeScript/ffi/Class.mm index f4ab722a..12aea823 100644 --- a/NativeScript/ffi/Class.mm +++ b/NativeScript/ffi/Class.mm @@ -599,14 +599,14 @@ static napi_value fillStack(napi_env env, napi_callback_info cbinfo) { objects:self->stackbuf count:16]; - for (NSUInteger index = 0; index < count; index++) { + for (uint32_t index = 0; index < static_cast(count); index++) { id obj = self->state.itemsPtr[index]; napi_value jsObj = bridgeState->getObject(env, obj); napi_set_element(env, stackArray, index, jsObj); } napi_value result; - napi_create_int32(env, count, &result); + napi_create_uint32(env, static_cast(count), &result); return result; } diff --git a/NativeScript/ffi/EngineDirectCall.mm b/NativeScript/ffi/EngineDirectCall.mm index f3d2262f..f5fd1f76 100644 --- a/NativeScript/ffi/EngineDirectCall.mm +++ b/NativeScript/ffi/EngineDirectCall.mm @@ -1026,12 +1026,19 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, } bool didInvoke = false; + CifReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function return storage."); + return nullptr; + } + void* rvalue = rvalueStorage.get(); @try { if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + didInvoke = invoker(env, cif, function->fnptr, invocationArgs, rvalue); } else { didInvoke = InvokeCFunctionEngineDirectDynamic( - env, function, cif, actualArgc, rawArgs, cif->rvalue); + env, function, cif, actualArgc, rawArgs, rvalue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -1044,7 +1051,7 @@ napi_value TryCallCFunctionEngineDirect(napi_env env, MDSectionOffset offset, return nullptr; } - return convertCFunctionReturnValue(env, function, cif, cif->rvalue); + return convertCFunctionReturnValue(env, function, cif, rvalue); } } // namespace nativescript diff --git a/NativeScript/ffi/HermesFastCallbackInfo.h b/NativeScript/ffi/HermesFastCallbackInfo.h index a9934172..93665e3b 100644 --- a/NativeScript/ffi/HermesFastCallbackInfo.h +++ b/NativeScript/ffi/HermesFastCallbackInfo.h @@ -52,6 +52,10 @@ inline void* HermesFastData(const HermesFastCallbackInfo* info) { } inline napi_value HermesFastThisArg(const HermesFastCallbackInfo* info) { + if (info == nullptr || info->frame == nullptr || + info->frame->thisArgAndArgsBase == nullptr) { + return nullptr; + } return reinterpret_cast( const_cast(info->frame->thisArgAndArgsBase)); } diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/HermesFastNativeApi.mm index 4299bf7d..64631805 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/HermesFastNativeApi.mm @@ -226,9 +226,9 @@ bool tryFastConvertHermesStringToNSStringArgument(napi_env env, } *result = - [[NSMutableString alloc] + [[[NSMutableString alloc] initWithCharacters:reinterpret_cast(utf16Buffer) - length:utf16Length]; + length:utf16Length] autorelease]; return true; } @@ -1606,7 +1606,8 @@ bool TryFastConvertHermesInt64Argument(napi_env env, napi_value value, } bool lossless = false; - return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok; + return napi_get_value_bigint_int64(env, value, result, &lossless) == napi_ok && + lossless; } bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, @@ -1622,7 +1623,8 @@ bool TryFastConvertHermesUInt64Argument(napi_env env, napi_value value, } bool lossless = false; - return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok; + return napi_get_value_bigint_uint64(env, value, result, &lossless) == napi_ok && + lossless; } bool TryFastConvertHermesSelectorArgument(napi_env env, napi_value value, @@ -1636,7 +1638,9 @@ bool TryFastConvertHermesObjectArgument(napi_env env, MDTypeKind kind, return false; } - if (kind != mdTypeClass) { + const bool isNSStringKind = + kind == mdTypeNSStringObject || kind == mdTypeNSMutableStringObject; + if (kind != mdTypeClass && !isNSStringKind) { const uint64_t raw = hermesRawValueBits(value); if (isHermesBool(raw)) { *reinterpret_cast(result) = @@ -2354,17 +2358,25 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( !cif->skipGeneratedNapiDispatch && cif->signatureHash != 0 ? ensureHermesCFunctionEngineDirectInvoker(function, cif) : nullptr; + HermesFastReturnStorage returnStorage(cif); + void* perCallRValue = returnStorage.get(); + if (!returnStorage.valid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function return storage."); + return nullptr; + } @try { const napi_value* invocationArgs = getPreparedInvocationArgs(); NativeCallRuntimeUnlockScope unlockRuntime(env); if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, invocationArgs, cif->rvalue); + didInvoke = + invoker(env, cif, function->fnptr, invocationArgs, perCallRValue); } 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); + env, function, cif, dynamicArgc, dynamicArgs, perCallRValue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -2377,7 +2389,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( return nullptr; } - return makeHermesCFunctionReturnValue(env, function, cif, cif->rvalue); + return makeHermesCFunctionReturnValue(env, function, cif, perCallRValue); } napi_value TryCallHermesCFunctionFast(napi_env env, MDSectionOffset offset, diff --git a/NativeScript/ffi/JSCFastNativeApi.h b/NativeScript/ffi/JSCFastNativeApi.h index 12e5e004..1c4010ce 100644 --- a/NativeScript/ffi/JSCFastNativeApi.h +++ b/NativeScript/ffi/JSCFastNativeApi.h @@ -3,6 +3,8 @@ #include "js_native_api.h" +#ifdef __cplusplus + namespace nativescript { #ifdef TARGET_ENGINE_JSC @@ -14,4 +16,6 @@ bool JSCTryDefineFastNativeProperty(napi_env env, napi_value object, } // namespace nativescript +#endif // __cplusplus + #endif // NS_JSC_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index 0a344575..f3da1e84 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -182,7 +182,7 @@ bool makeJSCRawReturnValue(napi_env env, MDTypeKind kind, const void* value, JSContextRef ctx = env->context; switch (kind) { case mdTypeVoid: - *result = JSValueMakeNull(ctx); + *result = JSValueMakeUndefined(ctx); return true; case mdTypeBool: @@ -786,12 +786,17 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, bool didInvoke = false; JSCFastRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); + JSCFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + void* rvalue = rvalueStorage.get(); @try { if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + didInvoke = invoker(env, cif, function->fnptr, argv, rvalue); } else { didInvoke = InvokeCFunctionEngineDirectDynamic( - env, function, cif, argc, argv, cif->rvalue); + env, function, cif, argc, argv, rvalue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -800,8 +805,7 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, return false; } - return didInvoke && - makeJSCCFunctionReturnValue(env, function, cif, cif->rvalue, result); + return didInvoke && makeJSCCFunctionReturnValue(env, function, cif, rvalue, result); } void initializeFastFunction(JSContextRef ctx, JSObjectRef object) { @@ -918,6 +922,7 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, } return directResult != nullptr ? directResult : JSValueMakeUndefined(ctx); } + env->last_exception = nullptr; napi_value result = nullptr; @@ -1774,7 +1779,7 @@ bool TryFastConvertJSCReturnValue(napi_env env, MDTypeKind kind, JSValueRef jsValue = nullptr; switch (kind) { case mdTypeVoid: - jsValue = JSValueMakeNull(ctx); + jsValue = JSValueMakeUndefined(ctx); break; case mdTypeBool: diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/ObjCBridge.h index 12941649..9008da0b 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/ObjCBridge.h @@ -212,7 +212,7 @@ class ObjCBridgeState { handleObjectRefs[handleKey] = HandleObjectRef{ref, true}; bumpHandleObjectRefsGeneration(); } - inline void cacheHandleObjectRef(void* handle, napi_ref ref) { + inline void cacheHandleObjectRef(napi_env env, void* handle, napi_ref ref) { if (handle == nullptr || ref == nullptr) { return; } diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/ObjCBridge.mm index f504aab3..0f911c5a 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/ObjCBridge.mm @@ -1032,7 +1032,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); - cacheHandleObjectRef(nativeObject, ref); + cacheHandleObjectRef(env, nativeObject, ref); cacheRecentObjectWrapper(env, nativeObject, result); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index 2ebfc994..781e063b 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -273,7 +273,7 @@ bool makeQuickJSRawReturnValue(JSContext* context, MDTypeKind kind, switch (kind) { case mdTypeVoid: - *result = JS_NULL; + *result = JS_UNDEFINED; return true; case mdTypeBool: @@ -937,12 +937,17 @@ bool tryCallQuickJSCFunctionEngineDirect(JSContext* context, napi_env env, bool didInvoke = false; QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, cif); + QuickJSFastReturnStorage rvalueStorage(cif); + if (!rvalueStorage.valid()) { + return false; + } + void* rvalue = rvalueStorage.get(); @try { if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, argv, cif->rvalue); + didInvoke = invoker(env, cif, function->fnptr, argv, rvalue); } else { didInvoke = nativescript::InvokeCFunctionEngineDirectDynamic( - env, function, cif, static_cast(argc), argv, cif->rvalue); + env, function, cif, static_cast(argc), argv, rvalue); } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; @@ -952,8 +957,8 @@ QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( } return didInvoke && - makeQuickJSCFunctionReturnValue(context, env, function, cif, - cif->rvalue, result); + makeQuickJSCFunctionReturnValue(context, env, function, cif, rvalue, + result); } JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, @@ -1035,6 +1040,10 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, } return directReturn; } + if (JS_HasException(context)) { + JSValue staleException = JS_GetException(context); + JS_FreeValue(context, staleException); + } QuickJSFastStackHandleScope scope(env); @@ -1723,7 +1732,7 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, JSValue jsValue = JS_UNDEFINED; switch (kind) { case mdTypeVoid: - jsValue = JS_NULL; + jsValue = JS_UNDEFINED; break; case mdTypeBool: diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/SignatureDispatch.h index 0170d630..21aa9bf0 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/SignatureDispatch.h @@ -146,9 +146,11 @@ inline bool readHermesDispatchFiniteNumber(napi_value value, double* result) { return false; } - *result = hermesDispatchRawDoubleIsFinite(raw) - ? hermesDispatchRawToDouble(raw) - : 0.0; + if (!hermesDispatchRawDoubleIsFinite(raw)) { + return false; + } + + *result = hermesDispatchRawToDouble(raw); return true; } @@ -157,9 +159,11 @@ inline bool readHermesDispatchFiniteNumberRaw(uint64_t raw, double* result) { return false; } - *result = hermesDispatchRawDoubleIsFinite(raw) - ? hermesDispatchRawToDouble(raw) - : 0.0; + if (!hermesDispatchRawDoubleIsFinite(raw)) { + return false; + } + + *result = hermesDispatchRawToDouble(raw); return true; } diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/TypeConv.mm index 7e4c04b2..d4e15462 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/TypeConv.mm @@ -2176,19 +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) { - if (object_isClass(obj)) { - if (napi_value constructor = findRegisteredClassConstructor(env, (Class)obj); - constructor != nullptr) { - return constructor; - } - } + 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 normalizePtr = [](void* ptr) -> uintptr_t { + return normalizeRuntimePointer(reinterpret_cast(ptr)); + }; auto protocolIt = bridgeState->mdProtocolsByPointer.find((Protocol*)obj); if (protocolIt != bridgeState->mdProtocolsByPointer.end()) { @@ -2197,18 +2197,15 @@ 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); - if (objNormalized != objPtr) { - for (const auto& entry : bridgeState->mdProtocolsByPointer) { - if (normalizePtr((void*)entry.first) != objNormalized) { - continue; - } + for (const auto& entry : bridgeState->mdProtocolsByPointer) { + if (normalizePtr((void*)entry.first) != objNormalized) { + continue; + } - auto proto = bridgeState->getProtocol(env, entry.second); - if (proto != nullptr) { - return get_ref_value(env, proto->constructor); - } + auto proto = bridgeState->getProtocol(env, entry.second); + if (proto != nullptr) { + return get_ref_value(env, proto->constructor); } } } diff --git a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h index 4e3f1de6..0de3a7e2 100644 --- a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h +++ b/NativeScript/ffi/jsi/NativeApiJsiReactNative.h @@ -16,6 +16,7 @@ #if NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER #include +#include namespace nativescript { @@ -24,14 +25,15 @@ class ReactNativeCallInvokerScheduler final : public NativeApiJsiScheduler { ReactNativeCallInvokerScheduler( std::shared_ptr jsInvoker, std::shared_ptr uiInvoker) - : jsInvoker_(std::move(jsInvoker)), uiInvoker_(std::move(uiInvoker)) {} + : jsInvoker_(std::move(jsInvoker)), uiInvoker_(std::move(uiInvoker)) { + if (!jsInvoker_) { + throw std::invalid_argument( + "NativeScript React Native JSI scheduler requires a JS CallInvoker"); + } + } void invokeOnJS(std::function task) override { - if (jsInvoker_) { - jsInvoker_->invokeAsync(std::move(task)); - return; - } - task(); + jsInvoker_->invokeAsync(std::move(task)); } void invokeOnUI(std::function task) override { diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js index 6f1a2ed8..7d8be2dc 100644 --- a/benchmarks/objc-dispatch/run.js +++ b/benchmarks/objc-dispatch/run.js @@ -5,6 +5,7 @@ const childProcess = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); +const { pathToFileURL } = require("url"); const repoRoot = path.resolve(__dirname, "../.."); const benchmarkFile = path.join(__dirname, "objc-dispatch-benchmarks.js"); @@ -458,7 +459,7 @@ function runNapiNode(options, variant) { } function pathToFileUrl(filePath) { - return new URL(`file://${filePath}`).href; + return pathToFileURL(filePath).href; } function destinationToUdid(destination) { @@ -572,6 +573,9 @@ function launchAndCollect(udid, bundleId, options, env = {}) { }, options.timeoutMs); function settleWithReport(report) { + if (settled) { + return; + } settled = true; clearTimeout(timeout); for (const activeChild of children) { @@ -608,16 +612,28 @@ function launchAndCollect(udid, bundleId, options, env = {}) { } }); } - child.on("exit", (code) => { + child.on("exit", (code, signal) => { if (!settled) { if (output.includes(marker)) { try { settleWithReport(parseBenchmarkOutput(output)); + return; } catch (_) { // The unified log stream may still deliver the actual message after // simctl launch exits. } } + if (code !== 0 || signal) { + settled = true; + clearTimeout(timeout); + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + reject(new Error( + `simctl launch for ${bundleId} exited with code ${code ?? "unknown"}${signal ? ` (signal ${signal})` : ""} before emitting ${marker}.\n${output}` + )); + } } }); }); diff --git a/examples/react-native-demo/App.tsx b/examples/react-native-demo/App.tsx index fd0d29e3..f03cf6b4 100644 --- a/examples/react-native-demo/App.tsx +++ b/examples/react-native-demo/App.tsx @@ -30,6 +30,32 @@ function installNativeScriptGlobals(): NativeApiHost { return api; } +function getActiveUIKitWindow() { + const app = UIApplication.sharedApplication; + const scenes = app.connectedScenes?.allObjects; + if (scenes) { + for (let i = 0; i < scenes.count; i++) { + const scene = scenes.objectAtIndex(i); + if ( + scene instanceof UIWindowScene && + scene.activationState === UISceneActivationState.ForegroundActive + ) { + const windows = scene.windows; + for (let j = 0; j < windows.count; j++) { + const window = windows.objectAtIndex(j); + if (window.isKeyWindow) { + return window; + } + } + if (windows.count > 0) { + return windows.objectAtIndex(0); + } + } + } + } + return app.keyWindow; +} + async function applyUIKitTweaks() { if (Platform.OS !== 'ios') { throw new Error('This demo uses UIKit and must run on iOS'); @@ -44,8 +70,7 @@ async function applyUIKitTweaks() { throw new Error('runOnUI did not dispatch native calls to the main thread'); } - const app = UIApplication.sharedApplication; - const window = app.keyWindow; + const window = getActiveUIKitWindow(); if (!window) { throw new Error('No key UIWindow is available yet'); } diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 0d3095de..4548e6f6 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -654,6 +654,7 @@ void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, void writeHermesDirectReturnValue(std::ostringstream& out, DispatchKind dispatchKind, MDTypeKind kind, const std::string& valueExpr) { + // Emits an open failure branch; the caller appends cleanup and `return false`. switch (kind) { case mdTypeVoid: out << " if (!SetHermesGeneratedVoidReturn(env, result)) {\n"; diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm index 69f41075..fea3fb76 100644 --- a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm @@ -12,10 +12,6 @@ @implementation NativeScriptUIViewComponentView { NativeScriptUIView* _containerView; } -+ (void)load { - [super load]; -} - - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); From df472eb5e3df9f9c5cc9231124254f859fc68d6e Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 00:53:27 -0400 Subject: [PATCH 24/31] fix: address follow-up CodeRabbit comments --- NativeScript/ffi/CFunction.mm | 67 ++++++++++++-- NativeScript/ffi/JSCFastNativeApi.mm | 100 ++++++++++++++------- NativeScript/ffi/QuickJSFastNativeApi.mm | 108 ++++++++++++++++------- 3 files changed, 206 insertions(+), 69 deletions(-) diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/CFunction.mm index 9a3610f2..ea5b4e94 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/CFunction.mm @@ -1,5 +1,6 @@ #include "CFunction.h" #include +#include #include #include #include @@ -33,6 +34,50 @@ inline bool isCompatOrMainCFunctionName(const char* name) { strcmp(name, "NSApplicationMain") == 0; } +class CFunctionReturnStorage final { + public: + explicit CFunctionReturnStorage(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) { + rvalue_ = inlineBuffer_; + std::memset(rvalue_, 0, size); + return; + } + + rvalue_ = std::malloc(size); + if (rvalue_ != nullptr) { + std::memset(rvalue_, 0, size); + } + } + + ~CFunctionReturnStorage() { + if (rvalue_ != nullptr && rvalue_ != inlineBuffer_) { + std::free(rvalue_); + } + } + + CFunctionReturnStorage(const CFunctionReturnStorage&) = delete; + CFunctionReturnStorage& operator=(const CFunctionReturnStorage&) = delete; + + bool isValid() const { return rvalue_ != nullptr; } + void* rvalue() const { return rvalue_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* rvalue_ = nullptr; +}; + class CFunctionInvocationFrame final { public: explicit CFunctionInvocationFrame(Cif* cif) @@ -433,17 +478,18 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; - auto invocationFrame = std::make_shared(cif); - if (!invocationFrame->isValid()) { - napi_throw_error(env, "NativeScriptException", - "Unable to allocate C function invocation storage."); - return nullptr; - } - void* rvalue = invocationFrame->rvalue(); if ((engineDirectInvoker != nullptr || (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && !isMainEntrypoint) { + CFunctionReturnStorage returnStorage(cif); + if (!returnStorage.isValid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function return storage."); + return nullptr; + } + + void* rvalue = returnStorage.rvalue(); @try { NativeCallRuntimeUnlockScope unlockRuntime(env); bool invoked = engineDirectInvoker != nullptr @@ -468,6 +514,13 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { return cif->returnType->toJS(env, rvalue, toJSFlags); } + auto invocationFrame = std::make_shared(cif); + if (!invocationFrame->isValid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function invocation storage."); + return nullptr; + } + void* rvalue = invocationFrame->rvalue(); void** avalues = invocationFrame->avalues(); bool shouldFreeAny = false; diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/JSCFastNativeApi.mm index f3da1e84..359a580d 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/JSCFastNativeApi.mm @@ -36,6 +36,12 @@ CFunction = 5, }; +enum class JSCEngineDirectResult { + NotHandled, + Handled, + Failed, +}; + inline bool needsRoundTripCacheFrame(Cif* cif) { return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } @@ -687,20 +693,18 @@ bool makeJSCCFunctionReturnValue(napi_env env, CFunction* function, Cif* cif, return true; } -bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, - napi_value jsThis, size_t argc, - const napi_value* argv, - EngineDirectMemberKind kind, - JSValueRef* result) { +JSCEngineDirectResult 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; + return JSCEngineDirectResult::NotHandled; } MethodDescriptor* descriptor = nullptr; Cif* cif = jscMemberCif(env, member, kind, &descriptor); if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { - return false; + return JSCEngineDirectResult::NotHandled; } const bool canUseGeneratedInvoker = @@ -712,23 +716,23 @@ bool tryCallJSCObjCEngineDirect(napi_env env, ObjCClassMember* member, if (isJSCNSErrorOutSignature(descriptor, cif) || isJSCBlockFallbackSelector(descriptor->selector)) { - return false; + return JSCEngineDirectResult::NotHandled; } id self = resolveJSCSelf(env, jsThis, member); if (self == nil) { - return false; + return JSCEngineDirectResult::NotHandled; } const bool receiverIsClass = object_isClass(self); Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); if (receiverClassRequiresJSCSuperCall(receiverClass)) { - return false; + return JSCEngineDirectResult::NotHandled; } JSCFastReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { - return false; + return JSCEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); @@ -748,34 +752,44 @@ JSCFastRoundTripCacheFrameGuard roundTripCacheFrame( std::string message = exception.description.UTF8String; NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); - return false; + return JSCEngineDirectResult::Failed; } - return didInvoke && - makeJSCObjCReturnValue(env, member, descriptor, cif, self, - receiverIsClass, jsThis, rvalue, - kind != EngineDirectMemberKind::Method, result); + if (!didInvoke) { + if (invoker == nullptr && env->last_exception != nullptr) { + return JSCEngineDirectResult::Failed; + } + return JSCEngineDirectResult::NotHandled; + } + + if (!makeJSCObjCReturnValue(env, member, descriptor, cif, self, + receiverIsClass, jsThis, rvalue, + kind != EngineDirectMemberKind::Method, result)) { + return JSCEngineDirectResult::Failed; + } + + return JSCEngineDirectResult::Handled; } -bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, - size_t argc, const napi_value* argv, - JSValueRef* result) { +JSCEngineDirectResult tryCallJSCCFunctionEngineDirect( + napi_env env, MDSectionOffset offset, size_t argc, const napi_value* argv, + JSValueRef* result) { if (env == nullptr || result == nullptr) { - return false; + return JSCEngineDirectResult::NotHandled; } ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); if (bridgeState == nullptr || isCompatCFunction(env, reinterpret_cast( static_cast(offset)))) { - return false; + return JSCEngineDirectResult::NotHandled; } 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; + return JSCEngineDirectResult::NotHandled; } const bool canUseGeneratedInvoker = @@ -788,7 +802,7 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, JSCFastRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); JSCFastReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { - return false; + return JSCEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); @try { @@ -802,10 +816,21 @@ bool tryCallJSCCFunctionEngineDirect(napi_env env, MDSectionOffset offset, std::string message = exception.description.UTF8String; NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); - return false; + return JSCEngineDirectResult::Failed; + } + + if (!didInvoke) { + if (invoker == nullptr && env->last_exception != nullptr) { + return JSCEngineDirectResult::Failed; + } + return JSCEngineDirectResult::NotHandled; + } + + if (!makeJSCCFunctionReturnValue(env, function, cif, rvalue, result)) { + return JSCEngineDirectResult::Failed; } - return didInvoke && makeJSCCFunctionReturnValue(env, function, cif, rvalue, result); + return JSCEngineDirectResult::Handled; } void initializeFastFunction(JSContextRef ctx, JSObjectRef object) { @@ -879,16 +904,16 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, } napi_value jsThis = ToNapi(effectiveThis); JSValueRef directResult = nullptr; - bool didUseDirectResult = false; + JSCEngineDirectResult directCallResult = JSCEngineDirectResult::NotHandled; switch (binding->kind) { case JSCFastNativeKind::ObjCMethod: - didUseDirectResult = tryCallJSCObjCEngineDirect( + directCallResult = tryCallJSCObjCEngineDirect( env, static_cast(binding->data), jsThis, argumentCount, argv, EngineDirectMemberKind::Method, &directResult); break; case JSCFastNativeKind::ObjCGetter: - didUseDirectResult = tryCallJSCObjCEngineDirect( + directCallResult = tryCallJSCObjCEngineDirect( env, static_cast(binding->data), jsThis, 0, nullptr, EngineDirectMemberKind::Getter, &directResult); break; @@ -896,13 +921,13 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, JSValueRef undefined = JSValueMakeUndefined(ctx); napi_value value = argumentCount > 0 ? ToNapi(arguments[0]) : ToNapi(undefined); - didUseDirectResult = tryCallJSCObjCEngineDirect( + directCallResult = tryCallJSCObjCEngineDirect( env, static_cast(binding->data), jsThis, 1, &value, EngineDirectMemberKind::Setter, &directResult); break; } case JSCFastNativeKind::CFunction: - didUseDirectResult = tryCallJSCCFunctionEngineDirect( + directCallResult = tryCallJSCCFunctionEngineDirect( env, static_cast( reinterpret_cast(binding->data)), @@ -912,7 +937,7 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, break; } - if (didUseDirectResult) { + if (directCallResult == JSCEngineDirectResult::Handled) { if (env->last_exception != nullptr) { if (exception != nullptr) { *exception = env->last_exception; @@ -922,6 +947,19 @@ JSValueRef callFastFunction(JSContextRef ctx, JSObjectRef function, } return directResult != nullptr ? directResult : JSValueMakeUndefined(ctx); } + + if (directCallResult == JSCEngineDirectResult::Failed) { + if (env->last_exception == nullptr) { + napi_throw_error(env, "NativeScriptException", + "NativeScript fast native call failed."); + } + if (exception != nullptr) { + *exception = env->last_exception; + } + env->last_exception = nullptr; + return JSValueMakeUndefined(ctx); + } + env->last_exception = nullptr; napi_value result = nullptr; diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/QuickJSFastNativeApi.mm index 781e063b..ff0bf1d3 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/QuickJSFastNativeApi.mm @@ -123,6 +123,22 @@ kQuickJSFastCFunction = 5, }; +enum class QuickJSEngineDirectResult { + NotHandled, + Handled, + Failed, +}; + +JSValue throwQuickJSPendingException(JSContext* context, const char* message) { + if (context == nullptr) { + return JS_EXCEPTION; + } + if (JS_HasException(context)) { + return JS_Throw(context, JS_GetException(context)); + } + return JS_ThrowInternalError(context, "%s", message); +} + inline bool needsRoundTripCacheFrame(nativescript::Cif* cif) { return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; } @@ -837,19 +853,19 @@ bool makeQuickJSCFunctionReturnValue(JSContext* context, napi_env env, return ok; } -bool tryCallQuickJSObjCEngineDirect( +QuickJSEngineDirectResult 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; + return QuickJSEngineDirectResult::NotHandled; } nativescript::MethodDescriptor* descriptor = nullptr; nativescript::Cif* cif = quickJSMemberCif(env, member, kind, &descriptor); if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { - return false; + return QuickJSEngineDirectResult::NotHandled; } const bool canUseGeneratedInvoker = @@ -861,23 +877,23 @@ bool tryCallQuickJSObjCEngineDirect( if (isQuickJSNSErrorOutSignature(descriptor, cif) || isQuickJSBlockFallbackSelector(descriptor->selector)) { - return false; + return QuickJSEngineDirectResult::NotHandled; } id self = resolveQuickJSSelf(env, jsThis, member); if (self == nil) { - return false; + return QuickJSEngineDirectResult::NotHandled; } const bool receiverIsClass = object_isClass(self); Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); if (receiverClassRequiresQuickJSSuperCall(receiverClass)) { - return false; + return QuickJSEngineDirectResult::NotHandled; } QuickJSFastReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { - return false; + return QuickJSEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); @@ -897,35 +913,44 @@ QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( std::string message = exception.description.UTF8String; nativescript::NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); - return false; + return QuickJSEngineDirectResult::Failed; + } + + if (!didInvoke) { + if (invoker == nullptr && JS_HasException(context)) { + return QuickJSEngineDirectResult::Failed; + } + return QuickJSEngineDirectResult::NotHandled; + } + + if (!makeQuickJSObjCReturnValue( + context, env, member, descriptor, cif, self, receiverIsClass, + jsThis, rvalue, kind != nativescript::EngineDirectMemberKind::Method, + result)) { + return QuickJSEngineDirectResult::Failed; } - return didInvoke && - makeQuickJSObjCReturnValue( - context, env, member, descriptor, cif, self, receiverIsClass, - jsThis, rvalue, kind != nativescript::EngineDirectMemberKind::Method, - result); + return QuickJSEngineDirectResult::Handled; } -bool tryCallQuickJSCFunctionEngineDirect(JSContext* context, napi_env env, - MDSectionOffset offset, int argc, - const napi_value* argv, - JSValue* result) { +QuickJSEngineDirectResult 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; + return QuickJSEngineDirectResult::NotHandled; } auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); if (bridgeState == nullptr || isCompatCFunction(env, reinterpret_cast( static_cast(offset)))) { - return false; + return QuickJSEngineDirectResult::NotHandled; } 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; + return QuickJSEngineDirectResult::NotHandled; } const bool canUseGeneratedInvoker = @@ -939,7 +964,7 @@ QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, cif); QuickJSFastReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { - return false; + return QuickJSEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); @try { @@ -953,12 +978,22 @@ QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( std::string message = exception.description.UTF8String; nativescript::NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); - return false; + return QuickJSEngineDirectResult::Failed; } - return didInvoke && - makeQuickJSCFunctionReturnValue(context, env, function, cif, rvalue, - result); + if (!didInvoke) { + if (invoker == nullptr && JS_HasException(context)) { + return QuickJSEngineDirectResult::Failed; + } + return QuickJSEngineDirectResult::NotHandled; + } + + if (!makeQuickJSCFunctionReturnValue(context, env, function, cif, rvalue, + result)) { + return QuickJSEngineDirectResult::Failed; + } + + return QuickJSEngineDirectResult::Handled; } JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, @@ -995,16 +1030,17 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, napi_value jsThis = reinterpret_cast(&effectiveThis); JSValue directReturn = JS_UNDEFINED; - bool didUseDirectReturn = false; + QuickJSEngineDirectResult directResult = + QuickJSEngineDirectResult::NotHandled; switch (magic) { case kQuickJSFastObjCMethod: - didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + directResult = tryCallQuickJSObjCEngineDirect( context, env, static_cast(data), jsThis, argc, napiArgs, nativescript::EngineDirectMemberKind::Method, &directReturn); break; case kQuickJSFastObjCGetter: - didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + directResult = tryCallQuickJSObjCEngineDirect( context, env, static_cast(data), jsThis, 0, nullptr, nativescript::EngineDirectMemberKind::Getter, &directReturn); @@ -1014,14 +1050,14 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, napi_value value = argc > 0 ? reinterpret_cast(&argv[0]) : reinterpret_cast(&undefined); - didUseDirectReturn = tryCallQuickJSObjCEngineDirect( + directResult = tryCallQuickJSObjCEngineDirect( context, env, static_cast(data), jsThis, 1, &value, nativescript::EngineDirectMemberKind::Setter, &directReturn); break; } case kQuickJSFastCFunction: - didUseDirectReturn = tryCallQuickJSCFunctionEngineDirect( + directResult = tryCallQuickJSCFunctionEngineDirect( context, env, static_cast(reinterpret_cast(data)), argc, napiArgs, &directReturn); @@ -1030,7 +1066,7 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, break; } - if (didUseDirectReturn) { + if (directResult == QuickJSEngineDirectResult::Handled) { if (useGlobalValue) { JS_FreeValue(context, effectiveThis); } @@ -1040,6 +1076,16 @@ JSValue callFastNative(JSContext* context, JSValueConst thisValue, int argc, } return directReturn; } + + if (directResult == QuickJSEngineDirectResult::Failed) { + if (useGlobalValue) { + JS_FreeValue(context, effectiveThis); + } + JS_FreeValue(context, directReturn); + return throwQuickJSPendingException( + context, "NativeScript fast native call failed."); + } + if (JS_HasException(context)) { JSValue staleException = JS_GetException(context); JS_FreeValue(context, staleException); From bbe4745be83ffde2bb263ad476dcf030113f92f0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 01:38:25 -0400 Subject: [PATCH 25/31] refactor: organize ffi backend layers --- .gitignore | 4 +- NativeScript/CMakeLists.txt | 89 +- NativeScript/cli/main.cpp | 4 +- .../ffi/{ => hermes}/HermesFastCallbackInfo.h | 0 .../ffi/{ => hermes}/HermesFastNativeApi.h | 0 .../ffi/{ => hermes}/HermesFastNativeApi.mm | 18 +- .../ffi/{ => hermes}/jsi/NativeApiJsi.h | 0 .../ffi/{ => hermes}/jsi/NativeApiJsi.mm | 0 .../jsi/NativeApiJsiReactNative.h | 0 NativeScript/ffi/{ => hermes}/jsi/README.md | 0 NativeScript/ffi/{ => jsc}/JSCFastNativeApi.h | 0 .../ffi/{ => jsc}/JSCFastNativeApi.mm | 16 +- NativeScript/ffi/{ => napi}/AutoreleasePool.h | 0 .../ffi/{ => napi}/AutoreleasePool.mm | 0 NativeScript/ffi/{ => napi}/Block.h | 0 NativeScript/ffi/{ => napi}/Block.mm | 0 NativeScript/ffi/{ => napi}/CFunction.h | 0 NativeScript/ffi/{ => napi}/CFunction.mm | 29 +- .../ffi/{ => napi}/CallbackThreading.h | 0 NativeScript/ffi/{ => napi}/Cif.h | 0 NativeScript/ffi/{ => napi}/Cif.mm | 0 NativeScript/ffi/{ => napi}/Class.h | 0 NativeScript/ffi/{ => napi}/Class.mm | 0 NativeScript/ffi/{ => napi}/ClassBuilder.h | 0 NativeScript/ffi/{ => napi}/ClassBuilder.mm | 0 NativeScript/ffi/{ => napi}/ClassMember.h | 0 NativeScript/ffi/{ => napi}/ClassMember.mm | 6 +- NativeScript/ffi/{ => napi}/Closure.h | 0 NativeScript/ffi/{ => napi}/Closure.mm | 2 +- NativeScript/ffi/{ => napi}/Enum.h | 0 NativeScript/ffi/{ => napi}/Enum.mm | 0 NativeScript/ffi/{ => napi}/InlineFunctions.h | 0 .../ffi/{ => napi}/InlineFunctions.mm | 0 NativeScript/ffi/{ => napi}/Interop.h | 0 NativeScript/ffi/{ => napi}/Interop.mm | 0 NativeScript/ffi/{ => napi}/JSObject.h | 0 NativeScript/ffi/{ => napi}/JSObject.mm | 0 .../ffi/{ => napi}/NativeScriptException.h | 0 .../ffi/{ => napi}/NativeScriptException.mm | 0 NativeScript/ffi/{ => napi}/ObjCBridge.h | 0 NativeScript/ffi/{ => napi}/ObjCBridge.mm | 0 NativeScript/ffi/{ => napi}/Object.h | 0 NativeScript/ffi/{ => napi}/Object.mm | 0 NativeScript/ffi/{ => napi}/ObjectRef.h | 0 NativeScript/ffi/{ => napi}/ObjectRef.mm | 0 NativeScript/ffi/{ => napi}/Protocol.h | 0 NativeScript/ffi/{ => napi}/Protocol.mm | 0 NativeScript/ffi/{ => napi}/Struct.h | 0 NativeScript/ffi/{ => napi}/Struct.mm | 0 NativeScript/ffi/{ => napi}/TypeConv.h | 0 NativeScript/ffi/{ => napi}/TypeConv.mm | 2 +- NativeScript/ffi/{ => napi}/Util.h | 0 NativeScript/ffi/{ => napi}/Util.mm | 0 NativeScript/ffi/{ => napi}/Variable.h | 0 NativeScript/ffi/{ => napi}/Variable.mm | 0 NativeScript/ffi/{ => napi}/node_api_util.h | 0 .../ffi/{ => quickjs}/QuickJSFastNativeApi.h | 0 .../ffi/{ => quickjs}/QuickJSFastNativeApi.mm | 14 +- .../ffi/{ => shared}/EngineDirectCall.h | 0 .../ffi/{ => shared}/EngineDirectCall.mm | 16 +- .../ffi/{ => shared}/SignatureDispatch.h | 2 +- NativeScript/ffi/{ => shared}/Tasks.cpp | 0 NativeScript/ffi/{ => shared}/Tasks.h | 0 NativeScript/ffi/{ => v8}/V8FastNativeApi.h | 0 NativeScript/ffi/{ => v8}/V8FastNativeApi.mm | 16 +- NativeScript/napi/jsc/jsc-api.cpp | 2 +- NativeScript/napi/quickjs/quickjs-api.c | 2 +- NativeScript/napi/v8/v8-api.cpp | 2 +- NativeScript/napi/v8/v8_inspector/Utils.cpp | 2 +- .../v8_inspector/ns-v8-tracing-agent-impl.cpp | 2 +- NativeScript/runtime/NativeScript.mm | 4 +- NativeScript/runtime/Runtime.cpp | 2 +- NativeScript/runtime/Util.h | 2 +- .../runtime/modules/console/Console.cpp | 2 +- .../runtime/modules/module/ModuleInternal.cpp | 2 +- NativeScript/runtime/modules/timers/Timers.mm | 2 +- .../runtime/modules/worker/MessageV8.cpp | 2 +- NativeScript/runtime/modules/worker/Worker.mm | 2 +- .../runtime/modules/worker/WorkerImpl.h | 2 +- .../runtime/modules/worker/WorkerImpl.mm | 2 +- metadata-generator/CMakeLists.txt | 6 + .../build-step-metadata-generator.py | 2 +- .../src/SignatureDispatchEmitter.cpp | 2401 +---------------- .../SignatureDispatchEmitter/EngineDirect.cpp | 363 +++ .../src/SignatureDispatchEmitter/Hermes.cpp | 548 ++++ .../src/SignatureDispatchEmitter/Napi.cpp | 428 +++ .../src/SignatureDispatchEmitter/Shared.cpp | 552 ++++ .../src/SignatureDispatchEmitter/Shared.h | 96 + .../src/SignatureDispatchEmitter/V8.cpp | 500 ++++ scripts/build_nativescript.sh | 2 +- scripts/build_react_native_turbomodule.sh | 6 +- scripts/metagen.js | 2 +- scripts/run-tests-macos.js | 4 +- 93 files changed, 2649 insertions(+), 2511 deletions(-) rename NativeScript/ffi/{ => hermes}/HermesFastCallbackInfo.h (100%) rename NativeScript/ffi/{ => hermes}/HermesFastNativeApi.h (100%) rename NativeScript/ffi/{ => hermes}/HermesFastNativeApi.mm (99%) rename NativeScript/ffi/{ => hermes}/jsi/NativeApiJsi.h (100%) rename NativeScript/ffi/{ => hermes}/jsi/NativeApiJsi.mm (100%) rename NativeScript/ffi/{ => hermes}/jsi/NativeApiJsiReactNative.h (100%) rename NativeScript/ffi/{ => hermes}/jsi/README.md (100%) rename NativeScript/ffi/{ => jsc}/JSCFastNativeApi.h (100%) rename NativeScript/ffi/{ => jsc}/JSCFastNativeApi.mm (99%) rename NativeScript/ffi/{ => napi}/AutoreleasePool.h (100%) rename NativeScript/ffi/{ => napi}/AutoreleasePool.mm (100%) rename NativeScript/ffi/{ => napi}/Block.h (100%) rename NativeScript/ffi/{ => napi}/Block.mm (100%) rename NativeScript/ffi/{ => napi}/CFunction.h (100%) rename NativeScript/ffi/{ => napi}/CFunction.mm (97%) rename NativeScript/ffi/{ => napi}/CallbackThreading.h (100%) rename NativeScript/ffi/{ => napi}/Cif.h (100%) rename NativeScript/ffi/{ => napi}/Cif.mm (100%) rename NativeScript/ffi/{ => napi}/Class.h (100%) rename NativeScript/ffi/{ => napi}/Class.mm (100%) rename NativeScript/ffi/{ => napi}/ClassBuilder.h (100%) rename NativeScript/ffi/{ => napi}/ClassBuilder.mm (100%) rename NativeScript/ffi/{ => napi}/ClassMember.h (100%) rename NativeScript/ffi/{ => napi}/ClassMember.mm (99%) rename NativeScript/ffi/{ => napi}/Closure.h (100%) rename NativeScript/ffi/{ => napi}/Closure.mm (99%) rename NativeScript/ffi/{ => napi}/Enum.h (100%) rename NativeScript/ffi/{ => napi}/Enum.mm (100%) rename NativeScript/ffi/{ => napi}/InlineFunctions.h (100%) rename NativeScript/ffi/{ => napi}/InlineFunctions.mm (100%) rename NativeScript/ffi/{ => napi}/Interop.h (100%) rename NativeScript/ffi/{ => napi}/Interop.mm (100%) rename NativeScript/ffi/{ => napi}/JSObject.h (100%) rename NativeScript/ffi/{ => napi}/JSObject.mm (100%) rename NativeScript/ffi/{ => napi}/NativeScriptException.h (100%) rename NativeScript/ffi/{ => napi}/NativeScriptException.mm (100%) rename NativeScript/ffi/{ => napi}/ObjCBridge.h (100%) rename NativeScript/ffi/{ => napi}/ObjCBridge.mm (100%) rename NativeScript/ffi/{ => napi}/Object.h (100%) rename NativeScript/ffi/{ => napi}/Object.mm (100%) rename NativeScript/ffi/{ => napi}/ObjectRef.h (100%) rename NativeScript/ffi/{ => napi}/ObjectRef.mm (100%) rename NativeScript/ffi/{ => napi}/Protocol.h (100%) rename NativeScript/ffi/{ => napi}/Protocol.mm (100%) rename NativeScript/ffi/{ => napi}/Struct.h (100%) rename NativeScript/ffi/{ => napi}/Struct.mm (100%) rename NativeScript/ffi/{ => napi}/TypeConv.h (100%) rename NativeScript/ffi/{ => napi}/TypeConv.mm (99%) rename NativeScript/ffi/{ => napi}/Util.h (100%) rename NativeScript/ffi/{ => napi}/Util.mm (100%) rename NativeScript/ffi/{ => napi}/Variable.h (100%) rename NativeScript/ffi/{ => napi}/Variable.mm (100%) rename NativeScript/ffi/{ => napi}/node_api_util.h (100%) rename NativeScript/ffi/{ => quickjs}/QuickJSFastNativeApi.h (100%) rename NativeScript/ffi/{ => quickjs}/QuickJSFastNativeApi.mm (99%) rename NativeScript/ffi/{ => shared}/EngineDirectCall.h (100%) rename NativeScript/ffi/{ => shared}/EngineDirectCall.mm (99%) rename NativeScript/ffi/{ => shared}/SignatureDispatch.h (99%) rename NativeScript/ffi/{ => shared}/Tasks.cpp (100%) rename NativeScript/ffi/{ => shared}/Tasks.h (100%) rename NativeScript/ffi/{ => v8}/V8FastNativeApi.h (100%) rename NativeScript/ffi/{ => v8}/V8FastNativeApi.mm (99%) create mode 100644 metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp create mode 100644 metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp create mode 100644 metadata-generator/src/SignatureDispatchEmitter/Napi.cpp create mode 100644 metadata-generator/src/SignatureDispatchEmitter/Shared.cpp create mode 100644 metadata-generator/src/SignatureDispatchEmitter/Shared.h create mode 100644 metadata-generator/src/SignatureDispatchEmitter/V8.cpp diff --git a/.gitignore b/.gitignore index 51e665b5..9d05b325 100644 --- a/.gitignore +++ b/.gitignore @@ -65,8 +65,8 @@ packages/*/types SwiftBindgen # Generated Objective-C/C dispatch wrappers -NativeScript/ffi/GeneratedSignatureDispatch.inc -NativeScript/ffi/GeneratedSignatureDispatch.inc.stamp +NativeScript/ffi/shared/GeneratedSignatureDispatch.inc +NativeScript/ffi/shared/GeneratedSignatureDispatch.inc.stamp # React Native TurboModule package staging packages/react-native/dist/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index c80c2458..d6463659 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -175,35 +175,69 @@ message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND}) # Set up sources include_directories( ./ + ffi/shared + ffi/v8 + ffi/hermes + ffi/jsc + ffi/quickjs ../metadata-generator/include napi/common libffi/${LIBFFI_BUILD}/include ) +set(FFI_SHARED_SOURCE_FILES + ffi/shared/Tasks.cpp +) + +set(FFI_NAPI_SOURCE_FILES + ffi/napi/AutoreleasePool.mm + ffi/napi/Protocol.mm + ffi/napi/ObjCBridge.mm + ffi/napi/Block.mm + ffi/napi/Class.mm + ffi/napi/Closure.mm + ffi/napi/ClassMember.mm + ffi/napi/Cif.mm + ffi/napi/TypeConv.mm + ffi/napi/Util.mm + ffi/napi/Struct.mm + ffi/napi/ObjectRef.mm + ffi/napi/JSObject.mm + ffi/napi/Enum.mm + ffi/napi/Variable.mm + ffi/napi/Object.mm + ffi/napi/CFunction.mm + ffi/napi/Interop.mm + ffi/napi/InlineFunctions.mm + ffi/napi/ClassBuilder.mm + ffi/napi/NativeScriptException.mm +) + +set(FFI_DIRECT_SOURCE_FILES + ffi/shared/EngineDirectCall.mm +) + +set(FFI_V8_SOURCE_FILES + ffi/v8/V8FastNativeApi.mm +) + +set(FFI_HERMES_SOURCE_FILES + ffi/hermes/jsi/NativeApiJsi.mm + ffi/hermes/HermesFastNativeApi.mm +) + +set(FFI_QUICKJS_SOURCE_FILES + ffi/quickjs/QuickJSFastNativeApi.mm +) + +set(FFI_JSC_SOURCE_FILES + ffi/jsc/JSCFastNativeApi.mm +) + set(SOURCE_FILES - ffi/AutoreleasePool.mm - ffi/Protocol.mm - ffi/ObjCBridge.mm - ffi/Block.mm - ffi/Class.mm - ffi/Closure.mm - ffi/ClassMember.mm - ffi/Cif.mm - ffi/TypeConv.mm - ffi/Util.mm - ffi/Struct.mm - ffi/ObjectRef.mm - ffi/JSObject.mm - ffi/Enum.mm - ffi/Variable.mm - ffi/Object.mm - ffi/CFunction.mm - ffi/EngineDirectCall.mm - ffi/Interop.mm - ffi/InlineFunctions.mm - ffi/ClassBuilder.mm - ffi/NativeScriptException.mm - ffi/Tasks.cpp + ${FFI_SHARED_SOURCE_FILES} + ${FFI_NAPI_SOURCE_FILES} + ${FFI_DIRECT_SOURCE_FILES} ) if(ENABLE_JS_RUNTIME) @@ -248,7 +282,7 @@ if(ENABLE_JS_RUNTIME) napi/v8/v8-module-loader.cpp napi/v8/jsr.cpp napi/v8/SimpleAllocator.cpp - ffi/V8FastNativeApi.mm + ${FFI_V8_SOURCE_FILES} ) elseif(TARGET_ENGINE_HERMES) @@ -273,8 +307,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp - ffi/jsi/NativeApiJsi.mm - ffi/HermesFastNativeApi.mm + ${FFI_HERMES_SOURCE_FILES} ) elseif(TARGET_ENGINE_QUICKJS) @@ -307,7 +340,7 @@ if(ENABLE_JS_RUNTIME) # napi napi/quickjs/quickjs-api.c napi/quickjs/jsr.cpp - ffi/QuickJSFastNativeApi.mm + ${FFI_QUICKJS_SOURCE_FILES} ) elseif(TARGET_ENGINE_JSC) @@ -320,7 +353,7 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/jsc/jsc-api.cpp napi/jsc/jsr.cpp - ffi/JSCFastNativeApi.mm + ${FFI_JSC_SOURCE_FILES} ) endif() else() diff --git a/NativeScript/cli/main.cpp b/NativeScript/cli/main.cpp index 5f7a6c35..9fe7ce70 100644 --- a/NativeScript/cli/main.cpp +++ b/NativeScript/cli/main.cpp @@ -5,12 +5,12 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "runtime/Bundle.h" #include "runtime/Runtime.h" #include "runtime/RuntimeConfig.h" #include "segappend.h" -#include "ffi/Tasks.h" +#include "ffi/shared/Tasks.h" #include "BundleLoader.h" using namespace nativescript; diff --git a/NativeScript/ffi/HermesFastCallbackInfo.h b/NativeScript/ffi/hermes/HermesFastCallbackInfo.h similarity index 100% rename from NativeScript/ffi/HermesFastCallbackInfo.h rename to NativeScript/ffi/hermes/HermesFastCallbackInfo.h diff --git a/NativeScript/ffi/HermesFastNativeApi.h b/NativeScript/ffi/hermes/HermesFastNativeApi.h similarity index 100% rename from NativeScript/ffi/HermesFastNativeApi.h rename to NativeScript/ffi/hermes/HermesFastNativeApi.h diff --git a/NativeScript/ffi/HermesFastNativeApi.mm b/NativeScript/ffi/hermes/HermesFastNativeApi.mm similarity index 99% rename from NativeScript/ffi/HermesFastNativeApi.mm rename to NativeScript/ffi/hermes/HermesFastNativeApi.mm index 64631805..7c779679 100644 --- a/NativeScript/ffi/HermesFastNativeApi.mm +++ b/NativeScript/ffi/hermes/HermesFastNativeApi.mm @@ -16,16 +16,16 @@ #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 "ffi/napi/CFunction.h" +#include "ffi/napi/CallbackThreading.h" +#include "ffi/napi/Class.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "ffi/napi/Interop.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" -#include "TypeConv.h" +#include "ffi/napi/TypeConv.h" namespace nativescript { namespace { diff --git a/NativeScript/ffi/jsi/NativeApiJsi.h b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h similarity index 100% rename from NativeScript/ffi/jsi/NativeApiJsi.h rename to NativeScript/ffi/hermes/jsi/NativeApiJsi.h diff --git a/NativeScript/ffi/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm similarity index 100% rename from NativeScript/ffi/jsi/NativeApiJsi.mm rename to NativeScript/ffi/hermes/jsi/NativeApiJsi.mm diff --git a/NativeScript/ffi/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h similarity index 100% rename from NativeScript/ffi/jsi/NativeApiJsiReactNative.h rename to NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h diff --git a/NativeScript/ffi/jsi/README.md b/NativeScript/ffi/hermes/jsi/README.md similarity index 100% rename from NativeScript/ffi/jsi/README.md rename to NativeScript/ffi/hermes/jsi/README.md diff --git a/NativeScript/ffi/JSCFastNativeApi.h b/NativeScript/ffi/jsc/JSCFastNativeApi.h similarity index 100% rename from NativeScript/ffi/JSCFastNativeApi.h rename to NativeScript/ffi/jsc/JSCFastNativeApi.h diff --git a/NativeScript/ffi/JSCFastNativeApi.mm b/NativeScript/ffi/jsc/JSCFastNativeApi.mm similarity index 99% rename from NativeScript/ffi/JSCFastNativeApi.mm rename to NativeScript/ffi/jsc/JSCFastNativeApi.mm index 359a580d..300dcbcb 100644 --- a/NativeScript/ffi/JSCFastNativeApi.mm +++ b/NativeScript/ffi/jsc/JSCFastNativeApi.mm @@ -12,17 +12,17 @@ #include #include -#include "CFunction.h" -#include "ClassBuilder.h" -#include "ClassMember.h" +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" #include "EngineDirectCall.h" -#include "Interop.h" +#include "ffi/napi/Interop.h" #include "MetadataReader.h" -#include "NativeScriptException.h" -#include "Object.h" -#include "ObjCBridge.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/Object.h" +#include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" -#include "TypeConv.h" +#include "ffi/napi/TypeConv.h" #include "jsc-api.h" namespace nativescript { diff --git a/NativeScript/ffi/AutoreleasePool.h b/NativeScript/ffi/napi/AutoreleasePool.h similarity index 100% rename from NativeScript/ffi/AutoreleasePool.h rename to NativeScript/ffi/napi/AutoreleasePool.h diff --git a/NativeScript/ffi/AutoreleasePool.mm b/NativeScript/ffi/napi/AutoreleasePool.mm similarity index 100% rename from NativeScript/ffi/AutoreleasePool.mm rename to NativeScript/ffi/napi/AutoreleasePool.mm diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/napi/Block.h similarity index 100% rename from NativeScript/ffi/Block.h rename to NativeScript/ffi/napi/Block.h diff --git a/NativeScript/ffi/Block.mm b/NativeScript/ffi/napi/Block.mm similarity index 100% rename from NativeScript/ffi/Block.mm rename to NativeScript/ffi/napi/Block.mm diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/napi/CFunction.h similarity index 100% rename from NativeScript/ffi/CFunction.h rename to NativeScript/ffi/napi/CFunction.h diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/napi/CFunction.mm similarity index 97% rename from NativeScript/ffi/CFunction.mm rename to NativeScript/ffi/napi/CFunction.mm index ea5b4e94..3460149d 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/napi/CFunction.mm @@ -15,8 +15,8 @@ #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" -#include "ffi/NativeScriptException.h" -#include "ffi/Tasks.h" +#include "NativeScriptException.h" +#include "Tasks.h" #ifdef ENABLE_JS_RUNTIME #include "jsr.h" #endif @@ -25,6 +25,17 @@ namespace { +size_t getCifReturnStorageSize(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + return size != 0 ? size : sizeof(void*); +} + inline bool isCompatOrMainCFunctionName(const char* name) { return name == nullptr || strcmp(name, "dispatch_async") == 0 || @@ -37,16 +48,7 @@ inline bool isCompatOrMainCFunctionName(const char* name) { class CFunctionReturnStorage final { public: explicit CFunctionReturnStorage(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*); - } + const size_t size = getCifReturnStorageSize(cif); if (size <= kInlineSize) { rvalue_ = inlineBuffer_; @@ -87,8 +89,7 @@ explicit CFunctionInvocationFrame(Cif* cif) return; } - const size_t rvalueLength = cif->rvalueLength > 0 ? cif->rvalueLength : 1; - rvalue_ = std::malloc(rvalueLength); + rvalue_ = std::malloc(getCifReturnStorageSize(cif)); if (rvalue_ == nullptr) { return; } diff --git a/NativeScript/ffi/CallbackThreading.h b/NativeScript/ffi/napi/CallbackThreading.h similarity index 100% rename from NativeScript/ffi/CallbackThreading.h rename to NativeScript/ffi/napi/CallbackThreading.h diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/napi/Cif.h similarity index 100% rename from NativeScript/ffi/Cif.h rename to NativeScript/ffi/napi/Cif.h diff --git a/NativeScript/ffi/Cif.mm b/NativeScript/ffi/napi/Cif.mm similarity index 100% rename from NativeScript/ffi/Cif.mm rename to NativeScript/ffi/napi/Cif.mm diff --git a/NativeScript/ffi/Class.h b/NativeScript/ffi/napi/Class.h similarity index 100% rename from NativeScript/ffi/Class.h rename to NativeScript/ffi/napi/Class.h diff --git a/NativeScript/ffi/Class.mm b/NativeScript/ffi/napi/Class.mm similarity index 100% rename from NativeScript/ffi/Class.mm rename to NativeScript/ffi/napi/Class.mm diff --git a/NativeScript/ffi/ClassBuilder.h b/NativeScript/ffi/napi/ClassBuilder.h similarity index 100% rename from NativeScript/ffi/ClassBuilder.h rename to NativeScript/ffi/napi/ClassBuilder.h diff --git a/NativeScript/ffi/ClassBuilder.mm b/NativeScript/ffi/napi/ClassBuilder.mm similarity index 100% rename from NativeScript/ffi/ClassBuilder.mm rename to NativeScript/ffi/napi/ClassBuilder.mm diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/napi/ClassMember.h similarity index 100% rename from NativeScript/ffi/ClassMember.h rename to NativeScript/ffi/napi/ClassMember.h diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/napi/ClassMember.mm similarity index 99% rename from NativeScript/ffi/ClassMember.mm rename to NativeScript/ffi/napi/ClassMember.mm index 174e21a7..8b355490 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/napi/ClassMember.mm @@ -23,9 +23,9 @@ #include "SignatureDispatch.h" #include "TypeConv.h" #include "Util.h" -#include "ffi/Block.h" -#include "ffi/Class.h" -#include "ffi/NativeScriptException.h" +#include "Block.h" +#include "Class.h" +#include "NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" diff --git a/NativeScript/ffi/Closure.h b/NativeScript/ffi/napi/Closure.h similarity index 100% rename from NativeScript/ffi/Closure.h rename to NativeScript/ffi/napi/Closure.h diff --git a/NativeScript/ffi/Closure.mm b/NativeScript/ffi/napi/Closure.mm similarity index 99% rename from NativeScript/ffi/Closure.mm rename to NativeScript/ffi/napi/Closure.mm index 621ddca0..7f7ec936 100644 --- a/NativeScript/ffi/Closure.mm +++ b/NativeScript/ffi/napi/Closure.mm @@ -6,7 +6,7 @@ #include "ObjCBridge.h" #include "TypeConv.h" #include "Util.h" -#include "ffi/NativeScriptException.h" +#include "NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #ifdef ENABLE_JS_RUNTIME diff --git a/NativeScript/ffi/Enum.h b/NativeScript/ffi/napi/Enum.h similarity index 100% rename from NativeScript/ffi/Enum.h rename to NativeScript/ffi/napi/Enum.h diff --git a/NativeScript/ffi/Enum.mm b/NativeScript/ffi/napi/Enum.mm similarity index 100% rename from NativeScript/ffi/Enum.mm rename to NativeScript/ffi/napi/Enum.mm diff --git a/NativeScript/ffi/InlineFunctions.h b/NativeScript/ffi/napi/InlineFunctions.h similarity index 100% rename from NativeScript/ffi/InlineFunctions.h rename to NativeScript/ffi/napi/InlineFunctions.h diff --git a/NativeScript/ffi/InlineFunctions.mm b/NativeScript/ffi/napi/InlineFunctions.mm similarity index 100% rename from NativeScript/ffi/InlineFunctions.mm rename to NativeScript/ffi/napi/InlineFunctions.mm diff --git a/NativeScript/ffi/Interop.h b/NativeScript/ffi/napi/Interop.h similarity index 100% rename from NativeScript/ffi/Interop.h rename to NativeScript/ffi/napi/Interop.h diff --git a/NativeScript/ffi/Interop.mm b/NativeScript/ffi/napi/Interop.mm similarity index 100% rename from NativeScript/ffi/Interop.mm rename to NativeScript/ffi/napi/Interop.mm diff --git a/NativeScript/ffi/JSObject.h b/NativeScript/ffi/napi/JSObject.h similarity index 100% rename from NativeScript/ffi/JSObject.h rename to NativeScript/ffi/napi/JSObject.h diff --git a/NativeScript/ffi/JSObject.mm b/NativeScript/ffi/napi/JSObject.mm similarity index 100% rename from NativeScript/ffi/JSObject.mm rename to NativeScript/ffi/napi/JSObject.mm diff --git a/NativeScript/ffi/NativeScriptException.h b/NativeScript/ffi/napi/NativeScriptException.h similarity index 100% rename from NativeScript/ffi/NativeScriptException.h rename to NativeScript/ffi/napi/NativeScriptException.h diff --git a/NativeScript/ffi/NativeScriptException.mm b/NativeScript/ffi/napi/NativeScriptException.mm similarity index 100% rename from NativeScript/ffi/NativeScriptException.mm rename to NativeScript/ffi/napi/NativeScriptException.mm diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/napi/ObjCBridge.h similarity index 100% rename from NativeScript/ffi/ObjCBridge.h rename to NativeScript/ffi/napi/ObjCBridge.h diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/napi/ObjCBridge.mm similarity index 100% rename from NativeScript/ffi/ObjCBridge.mm rename to NativeScript/ffi/napi/ObjCBridge.mm diff --git a/NativeScript/ffi/Object.h b/NativeScript/ffi/napi/Object.h similarity index 100% rename from NativeScript/ffi/Object.h rename to NativeScript/ffi/napi/Object.h diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/napi/Object.mm similarity index 100% rename from NativeScript/ffi/Object.mm rename to NativeScript/ffi/napi/Object.mm diff --git a/NativeScript/ffi/ObjectRef.h b/NativeScript/ffi/napi/ObjectRef.h similarity index 100% rename from NativeScript/ffi/ObjectRef.h rename to NativeScript/ffi/napi/ObjectRef.h diff --git a/NativeScript/ffi/ObjectRef.mm b/NativeScript/ffi/napi/ObjectRef.mm similarity index 100% rename from NativeScript/ffi/ObjectRef.mm rename to NativeScript/ffi/napi/ObjectRef.mm diff --git a/NativeScript/ffi/Protocol.h b/NativeScript/ffi/napi/Protocol.h similarity index 100% rename from NativeScript/ffi/Protocol.h rename to NativeScript/ffi/napi/Protocol.h diff --git a/NativeScript/ffi/Protocol.mm b/NativeScript/ffi/napi/Protocol.mm similarity index 100% rename from NativeScript/ffi/Protocol.mm rename to NativeScript/ffi/napi/Protocol.mm diff --git a/NativeScript/ffi/Struct.h b/NativeScript/ffi/napi/Struct.h similarity index 100% rename from NativeScript/ffi/Struct.h rename to NativeScript/ffi/napi/Struct.h diff --git a/NativeScript/ffi/Struct.mm b/NativeScript/ffi/napi/Struct.mm similarity index 100% rename from NativeScript/ffi/Struct.mm rename to NativeScript/ffi/napi/Struct.mm diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/napi/TypeConv.h similarity index 100% rename from NativeScript/ffi/TypeConv.h rename to NativeScript/ffi/napi/TypeConv.h diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/napi/TypeConv.mm similarity index 99% rename from NativeScript/ffi/TypeConv.mm rename to NativeScript/ffi/napi/TypeConv.mm index d4e15462..6a94b32d 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/napi/TypeConv.mm @@ -8,7 +8,7 @@ #include "MetadataReader.h" #include "ObjCBridge.h" #include "ffi.h" -#include "ffi/Struct.h" +#include "Struct.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" diff --git a/NativeScript/ffi/Util.h b/NativeScript/ffi/napi/Util.h similarity index 100% rename from NativeScript/ffi/Util.h rename to NativeScript/ffi/napi/Util.h diff --git a/NativeScript/ffi/Util.mm b/NativeScript/ffi/napi/Util.mm similarity index 100% rename from NativeScript/ffi/Util.mm rename to NativeScript/ffi/napi/Util.mm diff --git a/NativeScript/ffi/Variable.h b/NativeScript/ffi/napi/Variable.h similarity index 100% rename from NativeScript/ffi/Variable.h rename to NativeScript/ffi/napi/Variable.h diff --git a/NativeScript/ffi/Variable.mm b/NativeScript/ffi/napi/Variable.mm similarity index 100% rename from NativeScript/ffi/Variable.mm rename to NativeScript/ffi/napi/Variable.mm diff --git a/NativeScript/ffi/node_api_util.h b/NativeScript/ffi/napi/node_api_util.h similarity index 100% rename from NativeScript/ffi/node_api_util.h rename to NativeScript/ffi/napi/node_api_util.h diff --git a/NativeScript/ffi/QuickJSFastNativeApi.h b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.h similarity index 100% rename from NativeScript/ffi/QuickJSFastNativeApi.h rename to NativeScript/ffi/quickjs/QuickJSFastNativeApi.h diff --git a/NativeScript/ffi/QuickJSFastNativeApi.mm b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm similarity index 99% rename from NativeScript/ffi/QuickJSFastNativeApi.mm rename to NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm index ff0bf1d3..75f7e382 100644 --- a/NativeScript/ffi/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm @@ -15,16 +15,16 @@ #include #include -#include "CFunction.h" -#include "ClassBuilder.h" -#include "ClassMember.h" +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" #include "EngineDirectCall.h" -#include "Interop.h" +#include "ffi/napi/Interop.h" #include "MetadataReader.h" -#include "NativeScriptException.h" -#include "ObjCBridge.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" -#include "TypeConv.h" +#include "ffi/napi/TypeConv.h" #include "mimalloc.h" #include "quicks-runtime.h" diff --git a/NativeScript/ffi/EngineDirectCall.h b/NativeScript/ffi/shared/EngineDirectCall.h similarity index 100% rename from NativeScript/ffi/EngineDirectCall.h rename to NativeScript/ffi/shared/EngineDirectCall.h diff --git a/NativeScript/ffi/EngineDirectCall.mm b/NativeScript/ffi/shared/EngineDirectCall.mm similarity index 99% rename from NativeScript/ffi/EngineDirectCall.mm rename to NativeScript/ffi/shared/EngineDirectCall.mm index f5fd1f76..ecd09a86 100644 --- a/NativeScript/ffi/EngineDirectCall.mm +++ b/NativeScript/ffi/shared/EngineDirectCall.mm @@ -11,15 +11,15 @@ #include #include -#include "CFunction.h" -#include "Class.h" -#include "ClassBuilder.h" -#include "ClassMember.h" -#include "Interop.h" -#include "NativeScriptException.h" -#include "ObjCBridge.h" +#include "ffi/napi/CFunction.h" +#include "ffi/napi/Class.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "ffi/napi/Interop.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" -#include "TypeConv.h" +#include "ffi/napi/TypeConv.h" namespace nativescript { namespace { diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/shared/SignatureDispatch.h similarity index 99% rename from NativeScript/ffi/SignatureDispatch.h rename to NativeScript/ffi/shared/SignatureDispatch.h index 21aa9bf0..34f856fe 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/shared/SignatureDispatch.h @@ -9,7 +9,7 @@ #include #include -#include "Cif.h" +#include "ffi/napi/Cif.h" #include "js_native_api.h" #ifdef TARGET_ENGINE_V8 #include diff --git a/NativeScript/ffi/Tasks.cpp b/NativeScript/ffi/shared/Tasks.cpp similarity index 100% rename from NativeScript/ffi/Tasks.cpp rename to NativeScript/ffi/shared/Tasks.cpp diff --git a/NativeScript/ffi/Tasks.h b/NativeScript/ffi/shared/Tasks.h similarity index 100% rename from NativeScript/ffi/Tasks.h rename to NativeScript/ffi/shared/Tasks.h diff --git a/NativeScript/ffi/V8FastNativeApi.h b/NativeScript/ffi/v8/V8FastNativeApi.h similarity index 100% rename from NativeScript/ffi/V8FastNativeApi.h rename to NativeScript/ffi/v8/V8FastNativeApi.h diff --git a/NativeScript/ffi/V8FastNativeApi.mm b/NativeScript/ffi/v8/V8FastNativeApi.mm similarity index 99% rename from NativeScript/ffi/V8FastNativeApi.mm rename to NativeScript/ffi/v8/V8FastNativeApi.mm index e7a2b851..70914a3d 100644 --- a/NativeScript/ffi/V8FastNativeApi.mm +++ b/NativeScript/ffi/v8/V8FastNativeApi.mm @@ -19,15 +19,15 @@ #include #include -#include "CFunction.h" -#include "ClassBuilder.h" -#include "ClassMember.h" -#include "Interop.h" -#include "Object.h" -#include "ObjCBridge.h" +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "ffi/napi/Interop.h" +#include "ffi/napi/Object.h" +#include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" -#include "TypeConv.h" -#include "ffi/NativeScriptException.h" +#include "ffi/napi/TypeConv.h" +#include "ffi/napi/NativeScriptException.h" #include "v8-api.h" namespace nativescript { diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 7b591ef8..522090cc 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -15,7 +15,7 @@ #include #include -#include "ffi/JSCFastNativeApi.h" +#include "ffi/jsc/JSCFastNativeApi.h" struct napi_callback_info__ { napi_value newTarget; diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index c1d8ce0a..744269df 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -4,7 +4,7 @@ #include #include "js_native_api.h" -#include "ffi/QuickJSFastNativeApi.h" +#include "ffi/quickjs/QuickJSFastNativeApi.h" #include "libbf.h" #include "quicks-runtime.h" diff --git a/NativeScript/napi/v8/v8-api.cpp b/NativeScript/napi/v8/v8-api.cpp index eec2772d..ec49f746 100644 --- a/NativeScript/napi/v8/v8-api.cpp +++ b/NativeScript/napi/v8/v8-api.cpp @@ -9,7 +9,7 @@ #define NAPI_EXPERIMENTAL -#include "ffi/V8FastNativeApi.h" +#include "ffi/v8/V8FastNativeApi.h" #include "js_native_api.h" #include "v8-api.h" #include "v8-module-loader.h" diff --git a/NativeScript/napi/v8/v8_inspector/Utils.cpp b/NativeScript/napi/v8/v8_inspector/Utils.cpp index e1e6722b..70108a09 100644 --- a/NativeScript/napi/v8/v8_inspector/Utils.cpp +++ b/NativeScript/napi/v8/v8_inspector/Utils.cpp @@ -1,7 +1,7 @@ #include "Utils.h" #include "JsV8InspectorClient.h" -#include "Util.h" +#include "ffi/napi/Util.h" using namespace v8; using namespace std; diff --git a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp index f8a1da91..e9a8a094 100644 --- a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp +++ b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp @@ -14,7 +14,7 @@ #include #include "JsV8InspectorClient.h" -#include "NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "Runtime.h" namespace tns { diff --git a/NativeScript/runtime/NativeScript.mm b/NativeScript/runtime/NativeScript.mm index 20d0689b..b1e9fa2c 100644 --- a/NativeScript/runtime/NativeScript.mm +++ b/NativeScript/runtime/NativeScript.mm @@ -1,8 +1,8 @@ #include "NativeScript.h" #include "Runtime.h" #include "RuntimeConfig.h" -#include "ffi/NativeScriptException.h" -#include "ffi/Tasks.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/shared/Tasks.h" #include "js_native_api.h" #include "jsr.h" diff --git a/NativeScript/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index 4f3841fc..a6a653bc 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -13,7 +13,7 @@ #include "v8-api.h" #endif // TARGET_ENGINE_V8 #ifdef TARGET_ENGINE_HERMES -#include "ffi/jsi/NativeApiJsi.h" +#include "ffi/hermes/jsi/NativeApiJsi.h" #endif // TARGET_ENGINE_HERMES #include diff --git a/NativeScript/runtime/Util.h b/NativeScript/runtime/Util.h index 7699cde9..ef2b7622 100644 --- a/NativeScript/runtime/Util.h +++ b/NativeScript/runtime/Util.h @@ -1,6 +1,6 @@ #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "jsr_common.h" #include "native_api_util.h" #include "robin_hood.h" diff --git a/NativeScript/runtime/modules/console/Console.cpp b/NativeScript/runtime/modules/console/Console.cpp index 903d9ac4..2ac59e39 100644 --- a/NativeScript/runtime/modules/console/Console.cpp +++ b/NativeScript/runtime/modules/console/Console.cpp @@ -4,7 +4,7 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "js_native_api.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" diff --git a/NativeScript/runtime/modules/module/ModuleInternal.cpp b/NativeScript/runtime/modules/module/ModuleInternal.cpp index 993bed31..2b508fa8 100644 --- a/NativeScript/runtime/modules/module/ModuleInternal.cpp +++ b/NativeScript/runtime/modules/module/ModuleInternal.cpp @@ -15,7 +15,7 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" #include "runtime/Util.h" diff --git a/NativeScript/runtime/modules/timers/Timers.mm b/NativeScript/runtime/modules/timers/Timers.mm index 86b62164..1b65c3ca 100644 --- a/NativeScript/runtime/modules/timers/Timers.mm +++ b/NativeScript/runtime/modules/timers/Timers.mm @@ -9,7 +9,7 @@ #include #include #include "Timers.h" -#include "ffi/CallbackThreading.h" +#include "ffi/napi/CallbackThreading.h" static std::atomic gActiveTimers{0}; struct TimerToken; diff --git a/NativeScript/runtime/modules/worker/MessageV8.cpp b/NativeScript/runtime/modules/worker/MessageV8.cpp index 183149d8..f736b7f2 100644 --- a/NativeScript/runtime/modules/worker/MessageV8.cpp +++ b/NativeScript/runtime/modules/worker/MessageV8.cpp @@ -10,7 +10,7 @@ #include "MessageV8.h" -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "v8-api.h" using namespace v8; diff --git a/NativeScript/runtime/modules/worker/Worker.mm b/NativeScript/runtime/modules/worker/Worker.mm index ce39e2ad..e4747afe 100644 --- a/NativeScript/runtime/modules/worker/Worker.mm +++ b/NativeScript/runtime/modules/worker/Worker.mm @@ -2,7 +2,7 @@ #include #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.h b/NativeScript/runtime/modules/worker/WorkerImpl.h index 842c81a8..dc6898cf 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.h +++ b/NativeScript/runtime/modules/worker/WorkerImpl.h @@ -5,7 +5,7 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "js_native_api_types.h" #include "native_api_util.h" #include "runtime/modules/worker/ConcurrentMap.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.mm b/NativeScript/runtime/modules/worker/WorkerImpl.mm index aba8b4e4..6af0d585 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.mm +++ b/NativeScript/runtime/modules/worker/WorkerImpl.mm @@ -1,7 +1,7 @@ #include #include #include -#include "ffi/NativeScriptException.h" +#include "ffi/napi/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/metadata-generator/CMakeLists.txt b/metadata-generator/CMakeLists.txt index a6aea0d4..ada4ad98 100644 --- a/metadata-generator/CMakeLists.txt +++ b/metadata-generator/CMakeLists.txt @@ -17,11 +17,17 @@ endif(NOT DEFINED METADATA_BINARY_ARCH) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src ) set(EXEC_SOURCE_FILES src/main.cpp src/SignatureDispatchEmitter.cpp + src/SignatureDispatchEmitter/EngineDirect.cpp + src/SignatureDispatchEmitter/Hermes.cpp + src/SignatureDispatchEmitter/Napi.cpp + src/SignatureDispatchEmitter/Shared.cpp + src/SignatureDispatchEmitter/V8.cpp src/Umbrella.cpp src/IR/Category.cpp src/IR/Class.cpp diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 4939377b..7e9532e3 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -172,7 +172,7 @@ def is_nativescript_source_root(search_path): strict_includes = env_or_none("NS_DEBUG_METADATA_STRICT_INCLUDES") or env_or_none("TNS_DEBUG_METADATA_STRICT_INCLUDES") signature_bindings_cpp_path = env_or_none("NS_SIGNATURE_BINDINGS_CPP_PATH") or env_or_none("TNS_SIGNATURE_BINDINGS_CPP_PATH") if signature_bindings_cpp_path is None: - default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "GeneratedSignatureDispatch.inc") + default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "shared", "GeneratedSignatureDispatch.inc") if os.path.isdir(os.path.dirname(default_signature_bindings_path)): signature_bindings_cpp_path = default_signature_bindings_path diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 4548e6f6..da50820e 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -1,2264 +1,17 @@ #include "SignatureDispatchEmitter.h" +#include "SignatureDispatchEmitter/Shared.h" #include -#include #include -#include #include #include -#include #include #include #include namespace metagen { -namespace { -enum class DispatchKind : uint8_t { - ObjCMethod = 1, - CFunction = 2, - BlockInvoke = 3, -}; - -struct SignatureUse { - DispatchKind kind; - MDSectionOffset signatureOffset; - uint8_t flags; -}; - -constexpr uint64_t kFNV64OffsetBasis = 14695981039346656037ull; -constexpr uint64_t kFNV64Prime = 1099511628211ull; - -uint64_t hashBytesFnv1a(const void* data, size_t size, - uint64_t seed = kFNV64OffsetBasis) { - const auto* bytes = static_cast(data); - uint64_t hash = seed; - for (size_t i = 0; i < size; i++) { - hash ^= static_cast(bytes[i]); - hash *= kFNV64Prime; - } - return hash; -} - -uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind kind, - uint8_t flags) { - const uint8_t kindByte = static_cast(kind); - uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); - hash = hashBytesFnv1a(&flags, sizeof(flags), hash); - return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); -} - -using SignatureMap = std::unordered_map; - -MDTypeKind canonicalizeSignatureTypeKind(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return mdTypeAnyObject; - default: - return kind; - } -} - -template -void appendIntegral(uint64_t* hash, std::string* key, T value) { - using Unsigned = typename std::make_unsigned::type; - Unsigned unsignedValue = static_cast(value); - for (size_t i = 0; i < sizeof(Unsigned); i++) { - const uint8_t byte = - static_cast((unsignedValue >> (i * 8)) & 0xFF); - *hash = hashBytesFnv1a(&byte, sizeof(byte), *hash); - if (key != nullptr) { - key->push_back(static_cast(byte)); - } - } -} - -bool appendCanonicalSignature( - const MDSignature* signature, MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::unordered_set* activeSignatures, uint64_t* hash, - std::string* key); - -bool appendCanonicalType(const MDTypeInfo* type, const SignatureMap& signatures, - std::unordered_set* activeSignatures, - uint64_t* hash, std::string* key) { - if (type == nullptr || hash == nullptr) { - return false; - } - - appendIntegral(hash, key, 0xB0); - const MDTypeKind rawKind = type->kind; - const MDTypeKind canonicalKind = canonicalizeSignatureTypeKind(rawKind); - appendIntegral(hash, key, static_cast(canonicalKind)); - - switch (rawKind) { - case mdTypeArray: - case mdTypeVector: - case mdTypeExtVector: - case mdTypeComplex: - appendIntegral(hash, key, type->arraySize); - if (!appendCanonicalType(type->elementType, signatures, activeSignatures, - hash, key)) { - return false; - } - break; - - case mdTypeStruct: - appendIntegral(hash, key, type->structOffset); - break; - - case mdTypePointer: - if (!appendCanonicalType(type->pointeeType, signatures, activeSignatures, - hash, key)) { - return false; - } - break; - - case mdTypeBlock: - case mdTypeFunctionPointer: { - const MDSectionOffset nestedSignatureOffset = type->signatureOffset; - auto nestedIt = signatures.find(nestedSignatureOffset); - if (nestedSignatureOffset == MD_SECTION_OFFSET_NULL || - nestedIt == signatures.end() || nestedIt->second == nullptr) { - break; - } - - if (!appendCanonicalSignature(nestedIt->second, nestedSignatureOffset, - signatures, activeSignatures, hash, key)) { - return false; - } - break; - } - - default: - break; - } - - appendIntegral(hash, key, 0xBF); - return true; -} - -bool appendCanonicalSignature( - const MDSignature* signature, MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::unordered_set* activeSignatures, uint64_t* hash, - std::string* key) { - if (signature == nullptr || hash == nullptr || activeSignatures == nullptr) { - return false; - } - - const bool trackRecursion = signatureOffset != MD_SECTION_OFFSET_NULL; - if (trackRecursion) { - if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { - appendIntegral(hash, key, 0xEE); - return true; - } - activeSignatures->insert(signatureOffset); - } - - appendIntegral(hash, key, 0xA0); - appendIntegral(hash, key, signature->isVariadic ? 1 : 0); - - if (!appendCanonicalType(signature->returnType, signatures, activeSignatures, - hash, key)) { - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return false; - } - - uint32_t argCount = 0; - for (const auto* arg : signature->arguments) { - if (!appendCanonicalType(arg, signatures, activeSignatures, hash, key)) { - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return false; - } - argCount++; - } - - appendIntegral(hash, key, argCount); - appendIntegral(hash, key, 0xAF); - - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return true; -} - -uint64_t signatureHash(const MDSignature* signature, - MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::string* canonicalKeyOut) { - if (signature == nullptr) { - return 0; - } - - uint64_t hash = kFNV64OffsetBasis; - std::unordered_set activeSignatures; - if (!appendCanonicalSignature(signature, signatureOffset, signatures, - &activeSignatures, &hash, canonicalKeyOut)) { - return 0; - } - return hash; -} - -bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, - bool allowVoid = false); - -bool mapPointerPointeeToCpp(const MDTypeInfo* type, std::string* out) { - if (type == nullptr || out == nullptr) { - return false; - } - - switch (type->kind) { - case mdTypeVoid: - *out = "void"; - return true; - case mdTypePointer: { - std::string nested; - if (!mapPointerPointeeToCpp(type->pointeeType, &nested)) { - return false; - } - *out = nested + "*"; - return true; - } - default: - return mapTypeToCpp(type, out, false); - } -} - -bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid) { - if (type == nullptr || out == nullptr) { - return false; - } - - switch (type->kind) { - case mdTypeVoid: - if (!allowVoid) { - return false; - } - *out = "void"; - return true; - - case mdTypeBool: - *out = "uint8_t"; - return true; - - case mdTypeChar: - *out = "int8_t"; - return true; - - case mdTypeUChar: - case mdTypeUInt8: - *out = "uint8_t"; - return true; - - case mdTypeSShort: - *out = "int16_t"; - return true; - - case mdTypeUShort: - *out = "uint16_t"; - return true; - - case mdTypeSInt: - *out = "int32_t"; - return true; - - case mdTypeUInt: - *out = "uint32_t"; - return true; - - case mdTypeSLong: - case mdTypeSInt64: - *out = "int64_t"; - return true; - - case mdTypeULong: - case mdTypeUInt64: - *out = "uint64_t"; - return true; - - case mdTypeFloat: - *out = "float"; - return true; - - case mdTypeDouble: - *out = "double"; - return true; - - case mdTypeString: - *out = "char*"; - return true; - - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - *out = "id"; - return true; - - case mdTypeClass: - *out = "Class"; - return true; - - case mdTypeSelector: - *out = "SEL"; - return true; - - case mdTypePointer: { - std::string pointee; - if (!mapPointerPointeeToCpp(type->pointeeType, &pointee)) { - return false; - } - *out = pointee + "*"; - return true; - } - - case mdTypeOpaquePointer: - case mdTypeBlock: - case mdTypeFunctionPointer: - *out = "void*"; - return true; - - default: - return false; - } -} - -bool isSignatureSupported(const MDSignature* signature) { - if (signature == nullptr || signature->isVariadic) { - return false; - } - // Keep generated dispatch focused on hot paths and avoid huge wrappers. - if (signature->arguments.size() > 8) { - return false; - } - - std::string unused; - if (!mapTypeToCpp(signature->returnType, &unused, true)) { - return false; - } - - for (const auto* arg : signature->arguments) { - if (!mapTypeToCpp(arg, &unused, false)) { - return false; - } - } - - return true; -} - -std::string toHexLiteral(uint64_t value) { - std::ostringstream stream; - stream << "0x" << std::hex << std::setw(16) << std::setfill('0') << value - << "ULL"; - return stream.str(); -} - -std::string toBase36(size_t value) { - std::ostringstream stream; - if (value == 0) { - stream << '0'; - return stream.str(); - } - - std::string digits; - while (value > 0) { - const size_t digit = value % 36; - digits.push_back( - static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10))); - value /= 36; - } - std::reverse(digits.begin(), digits.end()); - stream << digits; - return stream.str(); -} - -std::string makeNapiWrapperName(DispatchKind kind, size_t index) { - std::ostringstream stream; - 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(); -} - -bool isFastDirectNapiKind(MDTypeKind kind) { - switch (kind) { - 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 isFastManagedNapiKind(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - case mdTypeSelector: - return true; - default: - return false; - } -} - -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) { - // Emits an open failure branch; the caller appends cleanup and `return false`. - 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: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - case mdTypeClass: - case mdTypeSelector: - return false; - default: - return !isFastDirectNapiKind(kind); - } -} - -enum class HermesDirectReturnCallSite { - FastCallback, - Frame, -}; - -bool canUseHermesDirectReturnWrapper(DispatchKind kind, - const MDSignature* signature, - HermesDirectReturnCallSite callSite) { - if (callSite == HermesDirectReturnCallSite::FastCallback && - kind == DispatchKind::BlockInvoke) { - return false; - } - - if (signature == nullptr || signature->returnType == nullptr) { - return false; - } - - const bool canSetReturnDirectly = - kind == DispatchKind::ObjCMethod - ? canSetHermesObjCReturnDirectly(signature->returnType->kind) - : canSetHermesReturnDirectly(signature->returnType->kind); - if (!canSetReturnDirectly) { - return false; - } - - for (const auto* arg : signature->arguments) { - if (arg == nullptr || argKindMayNeedCleanup(arg->kind)) { - return false; - } - } - - return true; -} - -std::string makeWrapperShapeKey(DispatchKind kind, - const MDSignature* signature) { - if (signature == nullptr) { - return {}; - } - - std::string returnType; - if (!mapTypeToCpp(signature->returnType, &returnType, true)) { - return {}; - } - - std::ostringstream key; - key << static_cast(kind) << "|" << returnType << "|"; - for (const auto* arg : signature->arguments) { - std::string argType; - if (!mapTypeToCpp(arg, &argType, false)) { - return {}; - } - - if (isFastDirectNapiKind(arg->kind)) { - key << "F" << static_cast(arg->kind); - } else if (isFastManagedNapiKind(arg->kind)) { - key << "H" << static_cast(arg->kind); - } else { - key << "M" << argType; - } - key << "|"; - } - - return key.str(); -} - -void writeFastNapiArgConversion(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 (napi_get_value_int32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\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 (napi_get_value_uint32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\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 (napi_get_value_int32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\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 (!TryFastConvertNapiUInt16Argument(env, argv[" << index - << "], &arg" << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSInt: { - out << " if (napi_get_value_int32(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeUInt: { - out << " if (napi_get_value_uint32(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSLong: - case mdTypeSInt64: { - out << " if (napi_get_value_int64(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - out << " bool lossless" << index << " = false;\n"; - out << " if (napi_get_value_bigint_int64(env, argv[" << index - << "], &arg" << index << ", &lossless" << index - << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " }\n"; - break; - } - case mdTypeULong: - case mdTypeUInt64: { - out << " bool lossless" << index << " = false;\n"; - out << " if (napi_get_value_bigint_uint64(env, argv[" << index - << "], &arg" << index << ", &lossless" << index - << ") != napi_ok) {\n"; - out << " int64_t signedValue" << index << " = 0;\n"; - out << " if (napi_get_value_int64(env, argv[" << index - << "], &signedValue" << index << ") != napi_ok) {\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 (napi_get_value_double(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\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 (napi_get_value_double(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index - << ")) {\n"; - out << " arg" << index << " = 0.0;\n"; - out << " }\n"; - break; - } - case mdTypeBool: { - out << " bool boolValue" << index << " = false;\n"; - out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(boolValue" << index - << " ? 1 : 0);\n"; - break; - } - default: - out << failCleanup; - out << " return false;\n"; - break; - } -} - -void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, - size_t index, bool hasCleanupArgs) { - const char* failCleanup = hasCleanupArgs ? " cleanupManagedArgs();\n" : ""; - if (type == nullptr) { - out << failCleanup; - out << " return false;\n"; - return; - } - - switch (type->kind) { - case mdTypeChar: { - out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeUChar: - case mdTypeUInt8: { - out << " uint32_t tmpArg" << index << " = 0;\n"; - out << " if (!info[" << index << "]->Uint32Value(context).To(&tmpArg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeSShort: { - out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeUShort: { - out << " if (!TryFastConvertV8UInt16Argument(env, info[" << index - << "], &arg" << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSInt: { - out << " if (!info[" << index << "]->Int32Value(context).To(&arg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeUInt: { - out << " if (!info[" << index << "]->Uint32Value(context).To(&arg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSLong: - case mdTypeSInt64: { - out << " if (info[" << index << "]->IsBigInt()) {\n"; - out << " bool lossless" << index << " = false;\n"; - out << " arg" << index << " = info[" << index - << "].As()->Int64Value(&lossless" << index << ");\n"; - out << " } else if (!info[" << index - << "]->IntegerValue(context).To(&arg" << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeULong: - case mdTypeUInt64: { - out << " if (info[" << index << "]->IsBigInt()) {\n"; - out << " bool lossless" << index << " = false;\n"; - out << " arg" << index << " = info[" << index - << "].As()->Uint64Value(&lossless" << index << ");\n"; - out << " } else {\n"; - out << " int64_t signedValue" << index << " = 0;\n"; - out << " if (!info[" << index - << "]->IntegerValue(context).To(&signedValue" << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(signedValue" - << index << ");\n"; - out << " }\n"; - break; - } - case mdTypeFloat: { - out << " double tmpArg" << index << " = 0.0;\n"; - out << " if (!info[" << index << "]->NumberValue(context).To(&tmpArg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeDouble: { - out << " if (!info[" << index << "]->NumberValue(context).To(&arg" - << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index - << ")) {\n"; - out << " arg" << index << " = 0.0;\n"; - out << " }\n"; - break; - } - case mdTypeBool: { - out << " if (!info[" << index << "]->IsBoolean()) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(info[" << index - << "]->BooleanValue(info.GetIsolate()) ? 1 : 0);\n"; - break; - } - default: - out << failCleanup; - out << " return false;\n"; - break; - } -} - -const char* engineDirectConverterMacroForKind(MDTypeKind kind) { - switch (kind) { - case mdTypeBool: - return "NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT"; - case mdTypeChar: - return "NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT"; - case mdTypeUChar: - case mdTypeUInt8: - return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT"; - case mdTypeSShort: - return "NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT"; - case mdTypeUShort: - return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT"; - case mdTypeSInt: - return "NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT"; - case mdTypeUInt: - return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT"; - case mdTypeSLong: - case mdTypeSInt64: - return "NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT"; - case mdTypeULong: - case mdTypeUInt64: - return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT"; - case mdTypeFloat: - return "NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT"; - case mdTypeDouble: - return "NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT"; - case mdTypeSelector: - return "NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT"; - case mdTypeClass: - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return "NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT"; - default: - return "NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"; - } -} - -bool engineDirectConverterTakesKind(MDTypeKind kind) { - switch (kind) { - case mdTypeClass: - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return true; - default: - return engineDirectConverterMacroForKind(kind) == - std::string("NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"); - } -} - -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 (!canUseHermesDirectReturnWrapper( - kind, signature, HermesDirectReturnCallSite::FastCallback)) { - 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 (!canUseHermesDirectReturnWrapper(kind, signature, - HermesDirectReturnCallSite::Frame)) { - 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 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; - } - - 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, 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 << " 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(); - const bool setsReturnDirectly = - canSetV8ReturnDirectly(signature->returnType->kind); - const bool triesObjectReturnDirectly = - kind == DispatchKind::ObjCMethod && - canTrySetV8ObjectReturnDirectly(signature->returnType->kind); - 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 << " } 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)) { - writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); - } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { - 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, v8LocalValueToNapiValue(info[" << i - << "]), &arg" << i << ", &shouldFree" << i - << ", &shouldFreeAny);\n"; - } else { - out << " ignoredShouldFree = false;\n"; - out << " ignoredShouldFreeAny = false;\n"; - 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, v8LocalValueToNapiValue(info[" << i - << "]), &arg" << i << ", &shouldFree" << i - << ", &shouldFreeAny);\n"; - } else { - out << " ignoredShouldFree = false;\n"; - out << " ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i - << "]->toNative(env, v8LocalValueToNapiValue(info[" << 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"; - 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 - << "*>(rvalue) = nativeResult;\n"; - } - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - - out << " return true;\n"; - 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) { - return; - } - - for (auto* member : members) { - if (member == nullptr) { - continue; - } - - const uint8_t methodFlags = - (member->flags & mdMemberReturnOwned) != 0 ? 1 : 0; - - if ((member->flags & mdMemberProperty) != 0) { - if (member->getterSignature != MD_SECTION_OFFSET_NULL) { - uses->push_back( - {DispatchKind::ObjCMethod, member->getterSignature, methodFlags}); - } - if (((member->flags & mdMemberReadonly) == 0) && - member->setterSignature != MD_SECTION_OFFSET_NULL) { - uses->push_back({DispatchKind::ObjCMethod, member->setterSignature, 0}); - } - } else { - if (member->signature != MD_SECTION_OFFSET_NULL) { - uses->push_back( - {DispatchKind::ObjCMethod, member->signature, methodFlags}); - } - } - } -} - -} // namespace +using namespace signature_dispatch; void writeSignatureDispatchBindings(const MDMetadataWriter& writer, const std::string& outputPath) { @@ -2638,146 +391,17 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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"; + writeEngineDirectConverterMacros(generated); 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"; + writeEngineDirectConverterUndefs(generated); 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"; + writeHermesEngineDirectConverterMacros(generated); for (const auto& wrapper : wrappers) { writeHermesDirectReturnWrapper( generated, wrapper.second.first, @@ -2796,20 +420,7 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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"; + writeEngineDirectConverterUndefs(generated); generated << "#endif\n\n"; generated << "#if NS_GSD_BACKEND_V8\n"; diff --git a/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp b/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp new file mode 100644 index 00000000..820eb3fb --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp @@ -0,0 +1,363 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include +#include + +namespace metagen::signature_dispatch { + +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(); +} +const char* engineDirectConverterMacroForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT"; + case mdTypeChar: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT"; + case mdTypeUChar: + case mdTypeUInt8: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT"; + case mdTypeSShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT"; + case mdTypeUShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT"; + case mdTypeSInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT"; + case mdTypeUInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT"; + case mdTypeSLong: + case mdTypeSInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT"; + case mdTypeULong: + case mdTypeUInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT"; + case mdTypeFloat: + return "NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT"; + case mdTypeDouble: + return "NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT"; + case mdTypeSelector: + return "NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT"; + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return "NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT"; + default: + return "NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"; + } +} + +bool engineDirectConverterTakesKind(MDTypeKind kind) { + switch (kind) { + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return engineDirectConverterMacroForKind(kind) == + std::string("NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"); + } +} +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index, + 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 writeEngineDirectConverterMacros(std::ostringstream& out) { + out << "#if NS_GSD_BACKEND_JSC\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertJSCArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertJSCBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertJSCInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertJSCUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertJSCInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertJSCUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertJSCInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertJSCUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertJSCInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertJSCUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertJSCFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertJSCDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertJSCSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertJSCObjectArgument\n"; + out << "#elif NS_GSD_BACKEND_QUICKJS\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertQuickJSArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertQuickJSBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertQuickJSInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertQuickJSUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertQuickJSInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertQuickJSUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertQuickJSInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertQuickJSUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertQuickJSInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertQuickJSUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertQuickJSFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertQuickJSDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertQuickJSSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertQuickJSObjectArgument\n"; + out << "#elif NS_GSD_BACKEND_HERMES\n"; + writeHermesEngineDirectConverterMacros(out); + out << "#else\n"; + out << "#error \"No generated signature engine-direct converter selected\"\n"; + out << "#endif\n"; +} + +void writeHermesEngineDirectConverterMacros(std::ostringstream& out) { + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertHermesArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertHermesGeneratedBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesGeneratedInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesGeneratedUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesGeneratedInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesGeneratedUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesGeneratedInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesGeneratedUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesGeneratedInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesGeneratedUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesGeneratedFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesGeneratedDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertHermesSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertHermesObjectArgument\n"; +} + +void writeEngineDirectConverterUndefs(std::ostringstream& out) { + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT\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"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp b/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp new file mode 100644 index 00000000..74feb009 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp @@ -0,0 +1,548 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +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(); +} + +bool canSetHermesReturnDirectly(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 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; + } +} + +void writeHermesDirectReturnValue(std::ostringstream& out, DispatchKind dispatchKind, + MDTypeKind kind, + const std::string& valueExpr) { + // Emits an open failure branch; the caller appends cleanup and `return false`. + 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 canUseHermesDirectReturnWrapper(DispatchKind kind, + const MDSignature* signature, + HermesDirectReturnCallSite callSite) { + if (callSite == HermesDirectReturnCallSite::FastCallback && + kind == DispatchKind::BlockInvoke) { + return false; + } + + if (signature == nullptr || signature->returnType == nullptr) { + return false; + } + + const bool canSetReturnDirectly = + kind == DispatchKind::ObjCMethod + ? canSetHermesObjCReturnDirectly(signature->returnType->kind) + : canSetHermesReturnDirectly(signature->returnType->kind); + if (!canSetReturnDirectly) { + return false; + } + + for (const auto* arg : signature->arguments) { + if (arg == nullptr || argKindMayNeedCleanup(arg->kind)) { + return false; + } + } + + return true; +} +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 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 writeHermesDirectReturnWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (!canUseHermesDirectReturnWrapper( + kind, signature, HermesDirectReturnCallSite::FastCallback)) { + 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 (!canUseHermesDirectReturnWrapper(kind, signature, + HermesDirectReturnCallSite::Frame)) { + 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"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp b/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp new file mode 100644 index 00000000..efcd6a40 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp @@ -0,0 +1,428 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +std::string makeNapiWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + 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 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(); +} +void writeFastNapiArgConversion(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 (napi_get_value_int32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\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 (napi_get_value_uint32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\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 (napi_get_value_int32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\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 (!TryFastConvertNapiUInt16Argument(env, argv[" << index + << "], &arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSInt: { + out << " if (napi_get_value_int32(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeUInt: { + out << " if (napi_get_value_uint32(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSLong: + case mdTypeSInt64: { + out << " if (napi_get_value_int64(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " if (napi_get_value_bigint_int64(env, argv[" << index + << "], &arg" << index << ", &lossless" << index + << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " }\n"; + break; + } + case mdTypeULong: + case mdTypeUInt64: { + out << " bool lossless" << index << " = false;\n"; + out << " if (napi_get_value_bigint_uint64(env, argv[" << index + << "], &arg" << index << ", &lossless" << index + << ") != napi_ok) {\n"; + out << " int64_t signedValue" << index << " = 0;\n"; + out << " if (napi_get_value_int64(env, argv[" << index + << "], &signedValue" << index << ") != napi_ok) {\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 (napi_get_value_double(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\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 (napi_get_value_double(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index + << ")) {\n"; + out << " arg" << index << " = 0.0;\n"; + out << " }\n"; + break; + } + case mdTypeBool: { + out << " bool boolValue" << index << " = false;\n"; + out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(boolValue" << index + << " ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} + +void 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 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"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp b/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp new file mode 100644 index 00000000..86d38637 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp @@ -0,0 +1,552 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include +#include +#include +#include + +namespace metagen::signature_dispatch { + +constexpr uint64_t kFNV64OffsetBasis = 14695981039346656037ull; +constexpr uint64_t kFNV64Prime = 1099511628211ull; + +uint64_t hashBytesFnv1a(const void* data, size_t size, + uint64_t seed = kFNV64OffsetBasis) { + const auto* bytes = static_cast(data); + uint64_t hash = seed; + for (size_t i = 0; i < size; i++) { + hash ^= static_cast(bytes[i]); + hash *= kFNV64Prime; + } + return hash; +} + +uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind kind, + uint8_t flags) { + const uint8_t kindByte = static_cast(kind); + uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); + hash = hashBytesFnv1a(&flags, sizeof(flags), hash); + return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); +} + +MDTypeKind canonicalizeSignatureTypeKind(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return mdTypeAnyObject; + default: + return kind; + } +} + +template +void appendIntegral(uint64_t* hash, std::string* key, T value) { + using Unsigned = typename std::make_unsigned::type; + Unsigned unsignedValue = static_cast(value); + for (size_t i = 0; i < sizeof(Unsigned); i++) { + const uint8_t byte = + static_cast((unsignedValue >> (i * 8)) & 0xFF); + *hash = hashBytesFnv1a(&byte, sizeof(byte), *hash); + if (key != nullptr) { + key->push_back(static_cast(byte)); + } + } +} + +bool appendCanonicalSignature( + const MDSignature* signature, MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* activeSignatures, uint64_t* hash, + std::string* key); + +bool appendCanonicalType(const MDTypeInfo* type, const SignatureMap& signatures, + std::unordered_set* activeSignatures, + uint64_t* hash, std::string* key) { + if (type == nullptr || hash == nullptr) { + return false; + } + + appendIntegral(hash, key, 0xB0); + const MDTypeKind rawKind = type->kind; + const MDTypeKind canonicalKind = canonicalizeSignatureTypeKind(rawKind); + appendIntegral(hash, key, static_cast(canonicalKind)); + + switch (rawKind) { + case mdTypeArray: + case mdTypeVector: + case mdTypeExtVector: + case mdTypeComplex: + appendIntegral(hash, key, type->arraySize); + if (!appendCanonicalType(type->elementType, signatures, activeSignatures, + hash, key)) { + return false; + } + break; + + case mdTypeStruct: + appendIntegral(hash, key, type->structOffset); + break; + + case mdTypePointer: + if (!appendCanonicalType(type->pointeeType, signatures, activeSignatures, + hash, key)) { + return false; + } + break; + + case mdTypeBlock: + case mdTypeFunctionPointer: { + const MDSectionOffset nestedSignatureOffset = type->signatureOffset; + auto nestedIt = signatures.find(nestedSignatureOffset); + if (nestedSignatureOffset == MD_SECTION_OFFSET_NULL || + nestedIt == signatures.end() || nestedIt->second == nullptr) { + break; + } + + if (!appendCanonicalSignature(nestedIt->second, nestedSignatureOffset, + signatures, activeSignatures, hash, key)) { + return false; + } + break; + } + + default: + break; + } + + appendIntegral(hash, key, 0xBF); + return true; +} + +bool appendCanonicalSignature( + const MDSignature* signature, MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* activeSignatures, uint64_t* hash, + std::string* key) { + if (signature == nullptr || hash == nullptr || activeSignatures == nullptr) { + return false; + } + + const bool trackRecursion = signatureOffset != MD_SECTION_OFFSET_NULL; + if (trackRecursion) { + if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { + appendIntegral(hash, key, 0xEE); + return true; + } + activeSignatures->insert(signatureOffset); + } + + appendIntegral(hash, key, 0xA0); + appendIntegral(hash, key, signature->isVariadic ? 1 : 0); + + if (!appendCanonicalType(signature->returnType, signatures, activeSignatures, + hash, key)) { + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return false; + } + + uint32_t argCount = 0; + for (const auto* arg : signature->arguments) { + if (!appendCanonicalType(arg, signatures, activeSignatures, hash, key)) { + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return false; + } + argCount++; + } + + appendIntegral(hash, key, argCount); + appendIntegral(hash, key, 0xAF); + + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return true; +} + +uint64_t signatureHash(const MDSignature* signature, + MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::string* canonicalKeyOut) { + if (signature == nullptr) { + return 0; + } + + uint64_t hash = kFNV64OffsetBasis; + std::unordered_set activeSignatures; + if (!appendCanonicalSignature(signature, signatureOffset, signatures, + &activeSignatures, &hash, canonicalKeyOut)) { + return 0; + } + return hash; +} + +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, + bool allowVoid = false); + +bool mapPointerPointeeToCpp(const MDTypeInfo* type, std::string* out) { + if (type == nullptr || out == nullptr) { + return false; + } + + switch (type->kind) { + case mdTypeVoid: + *out = "void"; + return true; + case mdTypePointer: { + std::string nested; + if (!mapPointerPointeeToCpp(type->pointeeType, &nested)) { + return false; + } + *out = nested + "*"; + return true; + } + default: + return mapTypeToCpp(type, out, false); + } +} + +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid) { + if (type == nullptr || out == nullptr) { + return false; + } + + switch (type->kind) { + case mdTypeVoid: + if (!allowVoid) { + return false; + } + *out = "void"; + return true; + + case mdTypeBool: + *out = "uint8_t"; + return true; + + case mdTypeChar: + *out = "int8_t"; + return true; + + case mdTypeUChar: + case mdTypeUInt8: + *out = "uint8_t"; + return true; + + case mdTypeSShort: + *out = "int16_t"; + return true; + + case mdTypeUShort: + *out = "uint16_t"; + return true; + + case mdTypeSInt: + *out = "int32_t"; + return true; + + case mdTypeUInt: + *out = "uint32_t"; + return true; + + case mdTypeSLong: + case mdTypeSInt64: + *out = "int64_t"; + return true; + + case mdTypeULong: + case mdTypeUInt64: + *out = "uint64_t"; + return true; + + case mdTypeFloat: + *out = "float"; + return true; + + case mdTypeDouble: + *out = "double"; + return true; + + case mdTypeString: + *out = "char*"; + return true; + + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + *out = "id"; + return true; + + case mdTypeClass: + *out = "Class"; + return true; + + case mdTypeSelector: + *out = "SEL"; + return true; + + case mdTypePointer: { + std::string pointee; + if (!mapPointerPointeeToCpp(type->pointeeType, &pointee)) { + return false; + } + *out = pointee + "*"; + return true; + } + + case mdTypeOpaquePointer: + case mdTypeBlock: + case mdTypeFunctionPointer: + *out = "void*"; + return true; + + default: + return false; + } +} + +bool isSignatureSupported(const MDSignature* signature) { + if (signature == nullptr || signature->isVariadic) { + return false; + } + // Keep generated dispatch focused on hot paths and avoid huge wrappers. + if (signature->arguments.size() > 8) { + return false; + } + + std::string unused; + if (!mapTypeToCpp(signature->returnType, &unused, true)) { + return false; + } + + for (const auto* arg : signature->arguments) { + if (!mapTypeToCpp(arg, &unused, false)) { + return false; + } + } + + return true; +} + +std::string toHexLiteral(uint64_t value) { + std::ostringstream stream; + stream << "0x" << std::hex << std::setw(16) << std::setfill('0') << value + << "ULL"; + return stream.str(); +} + +std::string toBase36(size_t value) { + std::ostringstream stream; + if (value == 0) { + stream << '0'; + return stream.str(); + } + + std::string digits; + while (value > 0) { + const size_t digit = value % 36; + digits.push_back( + static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10))); + value /= 36; + } + std::reverse(digits.begin(), digits.end()); + stream << digits; + return stream.str(); +} + +bool isFastDirectNapiKind(MDTypeKind kind) { + switch (kind) { + 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 isFastManagedNapiKind(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + case mdTypeSelector: + return true; + default: + return false; + } +} + +bool argKindMayNeedCleanup(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + case mdTypeClass: + case mdTypeSelector: + return false; + default: + return !isFastDirectNapiKind(kind); + } +} + +std::string makeWrapperShapeKey(DispatchKind kind, + const MDSignature* signature) { + if (signature == nullptr) { + return {}; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return {}; + } + + std::ostringstream key; + key << static_cast(kind) << "|" << returnType << "|"; + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return {}; + } + + if (isFastDirectNapiKind(arg->kind)) { + key << "F" << static_cast(arg->kind); + } else if (isFastManagedNapiKind(arg->kind)) { + key << "H" << static_cast(arg->kind); + } else { + key << "M" << argType; + } + key << "|"; + } + + return key.str(); +} +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) { + return; + } + + for (auto* member : members) { + if (member == nullptr) { + continue; + } + + const uint8_t methodFlags = + (member->flags & mdMemberReturnOwned) != 0 ? 1 : 0; + + if ((member->flags & mdMemberProperty) != 0) { + if (member->getterSignature != MD_SECTION_OFFSET_NULL) { + uses->push_back( + {DispatchKind::ObjCMethod, member->getterSignature, methodFlags}); + } + if (((member->flags & mdMemberReadonly) == 0) && + member->setterSignature != MD_SECTION_OFFSET_NULL) { + uses->push_back({DispatchKind::ObjCMethod, member->setterSignature, 0}); + } + } else { + if (member->signature != MD_SECTION_OFFSET_NULL) { + uses->push_back( + {DispatchKind::ObjCMethod, member->signature, methodFlags}); + } + } + } +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Shared.h b/metadata-generator/src/SignatureDispatchEmitter/Shared.h new file mode 100644 index 00000000..873723d7 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MetadataWriter.h" + +namespace metagen::signature_dispatch { + +enum class DispatchKind : uint8_t { + ObjCMethod = 1, + CFunction = 2, + BlockInvoke = 3, +}; + +struct SignatureUse { + DispatchKind kind; + MDSectionOffset signatureOffset; + uint8_t flags; +}; + +enum class HermesDirectReturnCallSite { + FastCallback, + Frame, +}; + +using SignatureMap = std::unordered_map; + +uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind kind, + uint8_t flags); +uint64_t signatureHash(const MDSignature* signature, + MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::string* canonicalKeyOut); +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid); +bool isSignatureSupported(const MDSignature* signature); +bool isFastDirectNapiKind(MDTypeKind kind); +bool isFastManagedNapiKind(MDTypeKind kind); +bool argKindMayNeedCleanup(MDTypeKind kind); +std::string toHexLiteral(uint64_t value); +std::string toBase36(size_t value); +std::string makeWrapperShapeKey(DispatchKind kind, + const MDSignature* signature); +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses); +void collectMethodUses(const std::vector& members, + std::vector* uses); + +std::string makeNapiWrapperName(DispatchKind kind, size_t index); +std::string makePreparedWrapperName(DispatchKind kind, size_t index); +void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); +void writePreparedWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeEngineDirectWrapperName(DispatchKind kind, size_t index); +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index, + const std::string& valueExpr); +void writeEngineDirectConverterMacros(std::ostringstream& out); +void writeHermesEngineDirectConverterMacros(std::ostringstream& out); +void writeEngineDirectConverterUndefs(std::ostringstream& out); +void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeHermesDirectReturnWrapperName(DispatchKind kind, size_t index); +std::string makeHermesFrameDirectReturnWrapperName(DispatchKind kind, + size_t index); +bool canUseHermesDirectReturnWrapper(DispatchKind kind, + const MDSignature* signature, + HermesDirectReturnCallSite callSite); +void writeHermesDirectReturnWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); +void writeHermesFrameDirectReturnWrapper(std::ostringstream& out, + DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeV8WrapperName(DispatchKind kind, size_t index); +void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/V8.cpp b/metadata-generator/src/SignatureDispatchEmitter/V8.cpp new file mode 100644 index 00000000..544cd6c6 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/V8.cpp @@ -0,0 +1,500 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +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(); +} + +bool fastV8ArgConversionNeedsContext(MDTypeKind kind) { + switch (kind) { + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canSetV8ReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canTrySetV8ObjectReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, + const std::string& valueExpr) { + switch (kind) { + case mdTypeBool: + out << " info.GetReturnValue().Set(" << valueExpr << " != 0);\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case 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 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; + } +} +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; + } + + 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, 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 << " 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(); + const bool setsReturnDirectly = + canSetV8ReturnDirectly(signature->returnType->kind); + const bool triesObjectReturnDirectly = + kind == DispatchKind::ObjCMethod && + canTrySetV8ObjectReturnDirectly(signature->returnType->kind); + 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 << " } 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)) { + writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + 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, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + 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, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << 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"; + 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 + << "*>(rvalue) = nativeResult;\n"; + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + + out << " return true;\n"; + out << "}\n\n"; +} + +} // namespace metagen::signature_dispatch diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 161f55df..c41f7607 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -16,7 +16,7 @@ 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=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/shared/GeneratedSignatureDispatch.inc}} GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" for arg in $@; do diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index a655f4e1..f7245282 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -36,9 +36,9 @@ mkdir -p \ "$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 NativeScript/ffi/hermes/jsi/NativeApiJsi.h "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/hermes/jsi/NativeApiJsi.mm "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/hermes/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/" diff --git a/scripts/metagen.js b/scripts/metagen.js index ac8b8712..3d4b8dfe 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -316,7 +316,7 @@ async function main() { const signatureBindingsPath = process.env.NS_SIGNATURE_BINDINGS_CPP_PATH || process.env.TNS_SIGNATURE_BINDINGS_CPP_PATH || - path.resolve(__dirname, "..", "NativeScript", "ffi", "GeneratedSignatureDispatch.inc"); + path.resolve(__dirname, "..", "NativeScript", "ffi", "shared", "GeneratedSignatureDispatch.inc"); await fsp.rm(typesDir, { recursive: true, force: true }); await fsp.mkdir(typesDir, { recursive: true }); await fsp.mkdir(metadataDir, { recursive: true }); diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 7e2b70da..1111dfbc 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -95,8 +95,8 @@ const junitEndTag = ""; const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); const generatedRuntimeBuildOutputs = new Set([ - path.join(nativeScriptSourceRoot, "ffi", "GeneratedSignatureDispatch.inc"), - path.join(nativeScriptSourceRoot, "ffi", "GeneratedSignatureDispatch.inc.stamp") + path.join(nativeScriptSourceRoot, "ffi", "shared", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "shared", "GeneratedSignatureDispatch.inc.stamp") ]); function parseArgs() { From d0c117608e3ec201b3330a884b765bc6602d033f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 01:41:59 -0400 Subject: [PATCH 26/31] fix(quickjs): decode unichar string arguments --- .../ffi/quickjs/QuickJSFastNativeApi.mm | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm index 75f7e382..298f1a90 100644 --- a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm @@ -1572,18 +1572,25 @@ bool TryFastConvertQuickJSUInt16Argument(napi_env env, napi_value value, JSValue jsValue = ToJSValue(value); if (JS_IsString(jsValue)) { - size_t length = 0; - const char* str = JS_ToCStringLen(env->context, &length, jsValue); + size_t byteLength = 0; + const char* str = JS_ToCStringLen(env->context, &byteLength, jsValue); if (str == nullptr) { return false; } - if (length != 1) { - JS_FreeCString(env->context, str); + + NSString* string = [[NSString alloc] initWithBytes:str + length:byteLength + encoding:NSUTF8StringEncoding]; + JS_FreeCString(env->context, str); + + if (string == nil || [string length] != 1) { + [string release]; napi_throw_type_error(env, nullptr, "Expected a single-character string."); return false; } - *result = static_cast(str[0]); - JS_FreeCString(env->context, str); + + *result = static_cast([string characterAtIndex:0]); + [string release]; return true; } From ec99893e926418a0cd60fdcbfacad9218c4bb149 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 13:55:01 -0400 Subject: [PATCH 27/31] refactor(ffi): split direct engine backends --- NativeScript/CMakeLists.txt | 6 + .../ffi/hermes/HermesFastConversion.mm | 1315 +++++++++++++++ .../ffi/hermes/HermesFastNativeApi.mm | 1482 +---------------- .../ffi/hermes/HermesFastNativeApiPrivate.h | 115 ++ NativeScript/ffi/jsc/JSCFastConversion.mm | 1144 +++++++++++++ NativeScript/ffi/jsc/JSCFastNativeApi.mm | 1274 +------------- .../ffi/jsc/JSCFastNativeApiPrivate.h | 92 + .../ffi/quickjs/QuickJSFastConversion.mm | 680 ++++++++ .../ffi/quickjs/QuickJSFastNativeApi.mm | 1292 +------------- .../ffi/quickjs/QuickJSFastNativeApiPrivate.h | 196 +++ NativeScript/ffi/quickjs/QuickJSFastReturn.mm | 332 ++++ NativeScript/ffi/shared/InvocationSupport.h | 89 + NativeScript/ffi/v8/V8FastConversion.mm | 677 ++++++++ NativeScript/ffi/v8/V8FastNativeApi.mm | 1128 +------------ NativeScript/ffi/v8/V8FastNativeApiPrivate.h | 205 +++ NativeScript/ffi/v8/V8FastNativeWrapper.mm | 230 +++ 16 files changed, 5110 insertions(+), 5147 deletions(-) create mode 100644 NativeScript/ffi/hermes/HermesFastConversion.mm create mode 100644 NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h create mode 100644 NativeScript/ffi/jsc/JSCFastConversion.mm create mode 100644 NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h create mode 100644 NativeScript/ffi/quickjs/QuickJSFastConversion.mm create mode 100644 NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h create mode 100644 NativeScript/ffi/quickjs/QuickJSFastReturn.mm create mode 100644 NativeScript/ffi/shared/InvocationSupport.h create mode 100644 NativeScript/ffi/v8/V8FastConversion.mm create mode 100644 NativeScript/ffi/v8/V8FastNativeApiPrivate.h create mode 100644 NativeScript/ffi/v8/V8FastNativeWrapper.mm diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index d6463659..58639860 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -218,19 +218,25 @@ set(FFI_DIRECT_SOURCE_FILES ) set(FFI_V8_SOURCE_FILES + ffi/v8/V8FastConversion.mm ffi/v8/V8FastNativeApi.mm + ffi/v8/V8FastNativeWrapper.mm ) set(FFI_HERMES_SOURCE_FILES ffi/hermes/jsi/NativeApiJsi.mm + ffi/hermes/HermesFastConversion.mm ffi/hermes/HermesFastNativeApi.mm ) set(FFI_QUICKJS_SOURCE_FILES + ffi/quickjs/QuickJSFastConversion.mm ffi/quickjs/QuickJSFastNativeApi.mm + ffi/quickjs/QuickJSFastReturn.mm ) set(FFI_JSC_SOURCE_FILES + ffi/jsc/JSCFastConversion.mm ffi/jsc/JSCFastNativeApi.mm ) diff --git a/NativeScript/ffi/hermes/HermesFastConversion.mm b/NativeScript/ffi/hermes/HermesFastConversion.mm new file mode 100644 index 00000000..a53bd212 --- /dev/null +++ b/NativeScript/ffi/hermes/HermesFastConversion.mm @@ -0,0 +1,1315 @@ +#include "HermesFastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_HERMES + +namespace nativescript { +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 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] autorelease]; + 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, kHermesNativePointerProperty, + &hasNativePointer) == napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, cachedValue, kHermesNativePointerProperty, + &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; +} + + +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)); +} + +} // namespace + +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); +} + +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 && + lossless; +} + +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 && + lossless; +} + +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; + } + + const bool isNSStringKind = + kind == mdTypeNSStringObject || kind == mdTypeNSMutableStringObject; + if (kind != mdTypeClass && !isNSStringKind) { + 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, kHermesNativePointerProperty, + &hasNativePointer) == napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, value, kHermesNativePointerProperty, + &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); +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/hermes/HermesFastNativeApi.mm b/NativeScript/ffi/hermes/HermesFastNativeApi.mm index 7c779679..acc82690 100644 --- a/NativeScript/ffi/hermes/HermesFastNativeApi.mm +++ b/NativeScript/ffi/hermes/HermesFastNativeApi.mm @@ -1,530 +1,9 @@ -#include "HermesFastNativeApi.h" +#include "HermesFastNativeApiPrivate.h" #ifdef TARGET_ENGINE_HERMES -#import -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/CallbackThreading.h" -#include "ffi/napi/Class.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "ffi/napi/Interop.h" -#include "ffi/napi/NativeScriptException.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/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] autorelease]; - 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; -} - inline bool canUseHermesFrameArgument(MDTypeKind kind) { switch (kind) { case mdTypeBool: @@ -569,69 +48,6 @@ inline bool canUseHermesFrameArguments(Cif* cif) { return true; } -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, @@ -881,7 +297,7 @@ HermesResolvedSelf resolveHermesSelf(napi_env env, napi_value jsThis, if (self == nil && jsThis != nullptr) { napi_value nativePointerValue = nullptr; - if (napi_get_named_property(env, jsThis, kNativePointerProperty, + if (napi_get_named_property(env, jsThis, kHermesNativePointerProperty, &nativePointerValue) == napi_ok && Pointer::isInstance(env, nativePointerValue)) { Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); @@ -1101,886 +517,8 @@ CFunctionHermesDirectReturnInvoker ensureHermesCFunctionDirectReturnInvoker( : 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 && - lossless; -} - -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 && - lossless; -} - -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; - } - - const bool isNSStringKind = - kind == mdTypeNSStringObject || kind == mdTypeNSMutableStringObject; - if (kind != mdTypeClass && !isNSStringKind) { - 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, @@ -2052,7 +590,7 @@ napi_value TryCallHermesObjCMemberFastImpl( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); napi_value directResult = nullptr; @@ -2122,7 +660,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); napi_value directResult = nullptr; @@ -2156,7 +694,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( } } - HermesFastReturnStorage rvalueStorage(cif); + EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { napi_throw_error(env, "NativeScriptException", "Unable to allocate return value storage for Objective-C call."); @@ -2170,7 +708,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); ObjCEngineDirectInvoker invoker = @@ -2269,7 +807,7 @@ napi_value TryCallHermesCFunctionFastImpl( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); napi_value directResult = nullptr; @@ -2325,7 +863,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); napi_value directResult = nullptr; @@ -2350,7 +888,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( *handled = true; } - HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); bool didInvoke = false; @@ -2358,7 +896,7 @@ HermesFastRoundTripCacheFrameGuard roundTripCacheFrame( !cif->skipGeneratedNapiDispatch && cif->signatureHash != 0 ? ensureHermesCFunctionEngineDirectInvoker(function, cif) : nullptr; - HermesFastReturnStorage returnStorage(cif); + EngineDirectReturnStorage returnStorage(cif); void* perCallRValue = returnStorage.get(); if (!returnStorage.valid()) { napi_throw_error(env, "NativeScriptException", diff --git a/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h b/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h new file mode 100644 index 00000000..8906d3f3 --- /dev/null +++ b/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h @@ -0,0 +1,115 @@ +#ifndef NS_HERMES_FAST_NATIVE_API_PRIVATE_H +#define NS_HERMES_FAST_NATIVE_API_PRIVATE_H + +#include "HermesFastNativeApi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffi/napi/CFunction.h" +#include "ffi/napi/CallbackThreading.h" +#include "ffi/napi/Class.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "ffi/napi/Interop.h" +#include "InvocationSupport.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/ObjCBridge.h" +#include "SignatureDispatch.h" +#include "ffi/napi/TypeConv.h" + +namespace nativescript { + +inline constexpr const char* kHermesNativePointerProperty = "__ns_native_ptr"; +inline constexpr uint64_t kHermesFirstTaggedValue = 0xfff9000000000000ULL; +inline constexpr uint64_t kHermesBoolETag = 0x1fff6ULL; +inline 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 = hermesRawValueBits(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)); +} + +napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, + id self, bool receiverIsClass, + napi_value jsThis, void* rvalue, + bool propertyAccess); + +napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, + Cif* cif, void* rvalue); + +} // namespace nativescript + +#endif // TARGET_ENGINE_HERMES + +#endif // NS_HERMES_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/jsc/JSCFastConversion.mm b/NativeScript/ffi/jsc/JSCFastConversion.mm new file mode 100644 index 00000000..23adb4be --- /dev/null +++ b/NativeScript/ffi/jsc/JSCFastConversion.mm @@ -0,0 +1,1144 @@ +#include "JSCFastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_JSC + +namespace nativescript { +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 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 = JSValueMakeUndefined(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; +} + +} // namespace + +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; +} + +namespace { + +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 = JSValueMakeUndefined(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; +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/JSCFastNativeApi.mm b/NativeScript/ffi/jsc/JSCFastNativeApi.mm index 300dcbcb..bc956cf9 100644 --- a/NativeScript/ffi/jsc/JSCFastNativeApi.mm +++ b/NativeScript/ffi/jsc/JSCFastNativeApi.mm @@ -1,105 +1,9 @@ -#include "JSCFastNativeApi.h" +#include "JSCFastNativeApiPrivate.h" #ifdef TARGET_ENGINE_JSC -#import - -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "EngineDirectCall.h" -#include "ffi/napi/Interop.h" -#include "MetadataReader.h" -#include "ffi/napi/NativeScriptException.h" -#include "ffi/napi/Object.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/TypeConv.h" -#include "jsc-api.h" - namespace nativescript { namespace { - -enum class JSCFastNativeKind : uint8_t { - ObjCMethod = 1, - ObjCGetter = 2, - ObjCSetter = 3, - ObjCReadOnlySetter = 4, - CFunction = 5, -}; - -enum class JSCEngineDirectResult { - NotHandled, - Handled, - Failed, -}; - -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) { @@ -115,232 +19,6 @@ bool isCompatCFunction(napi_env env, void* data) { 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 = JSValueMakeUndefined(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; @@ -548,150 +226,6 @@ CFunctionEngineDirectInvoker ensureJSCCFunctionEngineDirectInvoker( 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; -} JSCEngineDirectResult tryCallJSCObjCEngineDirect( napi_env env, ObjCClassMember* member, napi_value jsThis, size_t argc, @@ -730,13 +264,13 @@ JSCEngineDirectResult tryCallJSCObjCEngineDirect( return JSCEngineDirectResult::NotHandled; } - JSCFastReturnStorage rvalueStorage(cif); + EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { return JSCEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); - JSCFastRoundTripCacheFrameGuard roundTripCacheFrame( + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, cif); bool didInvoke = false; @try { @@ -799,8 +333,8 @@ JSCEngineDirectResult tryCallJSCCFunctionEngineDirect( : nullptr; bool didInvoke = false; - JSCFastRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); - JSCFastReturnStorage rvalueStorage(cif); + EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); + EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { return JSCEngineDirectResult::NotHandled; } @@ -1144,806 +678,8 @@ bool makePropertyName(napi_env env, const napi_property_descriptor* descriptor, 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 = JSValueMakeUndefined(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) { diff --git a/NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h b/NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h new file mode 100644 index 00000000..0e759263 --- /dev/null +++ b/NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h @@ -0,0 +1,92 @@ +#ifndef NS_JSC_FAST_NATIVE_API_PRIVATE_H +#define NS_JSC_FAST_NATIVE_API_PRIVATE_H + +#include "JSCFastNativeApi.h" + +#ifdef TARGET_ENGINE_JSC + +#import + +#include +#include +#include +#include +#include +#include +#include + +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "EngineDirectCall.h" +#include "InvocationSupport.h" +#include "ffi/napi/Interop.h" +#include "MetadataReader.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/Object.h" +#include "ffi/napi/ObjCBridge.h" +#include "SignatureDispatch.h" +#include "ffi/napi/TypeConv.h" +#include "jsc-api.h" + +namespace nativescript { + +enum class JSCFastNativeKind : uint8_t { + ObjCMethod = 1, + ObjCGetter = 2, + ObjCSetter = 3, + ObjCReadOnlySetter = 4, + CFunction = 5, +}; + +enum class JSCEngineDirectResult { + NotHandled, + Handled, + Failed, +}; + +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 makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, + MethodDescriptor* descriptor, Cif* cif, id self, + bool receiverIsClass, napi_value jsThis, + void* rvalue, bool propertyAccess, + JSValueRef* result); + +bool makeJSCCFunctionReturnValue(napi_env env, CFunction* function, Cif* cif, + void* rvalue, JSValueRef* result); + +} // namespace nativescript + +#endif // TARGET_ENGINE_JSC + +#endif // NS_JSC_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/quickjs/QuickJSFastConversion.mm b/NativeScript/ffi/quickjs/QuickJSFastConversion.mm new file mode 100644 index 00000000..797fac18 --- /dev/null +++ b/NativeScript/ffi/quickjs/QuickJSFastConversion.mm @@ -0,0 +1,680 @@ +#include "QuickJSFastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_QUICKJS + +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; +} + +} // namespace + +bool TryUnwrapQuickJSNativeObjectFast(napi_env env, JSValue jsValue, + void** result) { + return tryFastUnwrapQuickJSNativeObject(env, jsValue, result); +} + +namespace { + +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 byteLength = 0; + const char* str = JS_ToCStringLen(env->context, &byteLength, jsValue); + if (str == nullptr) { + return false; + } + + NSString* string = [[NSString alloc] initWithBytes:str + length:byteLength + encoding:NSUTF8StringEncoding]; + JS_FreeCString(env->context, str); + + if (string == nil || [string length] != 1) { + [string release]; + napi_throw_type_error(env, nullptr, "Expected a single-character string."); + return false; + } + + *result = static_cast([string characterAtIndex:0]); + [string release]; + 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_UNDEFINED; + 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 + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm index 298f1a90..ad9795f9 100644 --- a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm @@ -1,134 +1,8 @@ -#include "QuickJSFastNativeApi.h" +#include "QuickJSFastNativeApiPrivate.h" #ifdef TARGET_ENGINE_QUICKJS -#import - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "EngineDirectCall.h" -#include "ffi/napi/Interop.h" -#include "MetadataReader.h" -#include "ffi/napi/NativeScriptException.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/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, -}; - -enum class QuickJSEngineDirectResult { - NotHandled, - Handled, - Failed, -}; - JSValue throwQuickJSPendingException(JSContext* context, const char* message) { if (context == nullptr) { return JS_EXCEPTION; @@ -139,329 +13,6 @@ JSValue throwQuickJSPendingException(JSContext* context, const char* message) { return JS_ThrowInternalError(context, "%s", message); } -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_UNDEFINED; - 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) { @@ -484,7 +35,8 @@ id resolveQuickJSSelf(napi_env env, napi_value jsThis, if (jsThis != nullptr) { void* wrapped = nullptr; - if (tryFastUnwrapQuickJSNativeObject(env, ToJSValue(jsThis), &wrapped) && + if (nativescript::TryUnwrapQuickJSNativeObjectFast( + env, ToJSValue(jsThis), &wrapped) && wrapped != nullptr) { return static_cast(wrapped); } @@ -690,169 +242,6 @@ inline bool isQuickJSBlockFallbackSelector(SEL selector) { 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; -} - QuickJSEngineDirectResult tryCallQuickJSObjCEngineDirect( JSContext* context, napi_env env, nativescript::ObjCClassMember* member, napi_value jsThis, int argc, const napi_value* argv, @@ -891,13 +280,13 @@ QuickJSEngineDirectResult tryCallQuickJSObjCEngineDirect( return QuickJSEngineDirectResult::NotHandled; } - QuickJSFastReturnStorage rvalueStorage(cif); + nativescript::EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { return QuickJSEngineDirectResult::NotHandled; } void* rvalue = rvalueStorage.get(); - QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + nativescript::EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, cif); bool didInvoke = false; @try { @@ -960,9 +349,9 @@ QuickJSEngineDirectResult tryCallQuickJSCFunctionEngineDirect( : nullptr; bool didInvoke = false; - QuickJSFastRoundTripCacheFrameGuard roundTripCacheFrame( + nativescript::EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, cif); - QuickJSFastReturnStorage rvalueStorage(cif); + nativescript::EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { return QuickJSEngineDirectResult::NotHandled; } @@ -1238,672 +627,6 @@ bool defineFastProperty(napi_env env, napi_value object, } // 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 byteLength = 0; - const char* str = JS_ToCStringLen(env->context, &byteLength, jsValue); - if (str == nullptr) { - return false; - } - - NSString* string = [[NSString alloc] initWithBytes:str - length:byteLength - encoding:NSUTF8StringEncoding]; - JS_FreeCString(env->context, str); - - if (string == nil || [string length] != 1) { - [string release]; - napi_throw_type_error(env, nullptr, "Expected a single-character string."); - return false; - } - - *result = static_cast([string characterAtIndex:0]); - [string release]; - 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_UNDEFINED; - 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) { @@ -1968,4 +691,5 @@ bool TryFastConvertQuickJSReturnValue(napi_env env, MDTypeKind kind, return false; } + #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h b/NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h new file mode 100644 index 00000000..176b6558 --- /dev/null +++ b/NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h @@ -0,0 +1,196 @@ +#ifndef NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H +#define NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H + +#include "QuickJSFastNativeApi.h" + +#ifdef TARGET_ENGINE_QUICKJS + +#import + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "EngineDirectCall.h" +#include "InvocationSupport.h" +#include "ffi/napi/Interop.h" +#include "MetadataReader.h" +#include "ffi/napi/NativeScriptException.h" +#include "ffi/napi/ObjCBridge.h" +#include "SignatureDispatch.h" +#include "ffi/napi/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; +}; + +enum QuickJSFastNativeKind : int { + kQuickJSFastObjCMethod = 1, + kQuickJSFastObjCGetter = 2, + kQuickJSFastObjCSetter = 3, + kQuickJSFastObjCReadOnlySetter = 4, + kQuickJSFastCFunction = 5, +}; + +enum class QuickJSEngineDirectResult { + NotHandled, + Handled, + Failed, +}; + +inline JSValue ToJSValue(napi_value value) { + return value != nullptr ? *reinterpret_cast(value) : JS_UNDEFINED; +} + +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 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); + +bool makeQuickJSCFunctionReturnValue(JSContext* context, napi_env env, + nativescript::CFunction* function, + nativescript::Cif* cif, void* rvalue, + JSValue* result); + +namespace nativescript { + +bool TryUnwrapQuickJSNativeObjectFast(napi_env env, JSValue jsValue, + void** result); + +} // namespace nativescript + +#endif // TARGET_ENGINE_QUICKJS + +#endif // NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/quickjs/QuickJSFastReturn.mm b/NativeScript/ffi/quickjs/QuickJSFastReturn.mm new file mode 100644 index 00000000..f8b70320 --- /dev/null +++ b/NativeScript/ffi/quickjs/QuickJSFastReturn.mm @@ -0,0 +1,332 @@ +#include "QuickJSFastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_QUICKJS + +namespace { + +bool makeQuickJSRawReturnValue(JSContext* context, MDTypeKind kind, + const void* value, JSValue* result) { + if (context == nullptr || result == nullptr) { + return false; + } + + switch (kind) { + case mdTypeVoid: + *result = JS_UNDEFINED; + 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; +} + +} // namespace + +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; +} + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/shared/InvocationSupport.h b/NativeScript/ffi/shared/InvocationSupport.h new file mode 100644 index 00000000..38c515f5 --- /dev/null +++ b/NativeScript/ffi/shared/InvocationSupport.h @@ -0,0 +1,89 @@ +#ifndef NS_FFI_SHARED_INVOCATION_SUPPORT_H +#define NS_FFI_SHARED_INVOCATION_SUPPORT_H + +#include +#include +#include + +#include "ffi/napi/Cif.h" +#include "ffi/napi/ObjCBridge.h" + +namespace nativescript { + +inline bool needsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + +class EngineDirectRoundTripCacheFrameGuard { + public: + EngineDirectRoundTripCacheFrameGuard(napi_env env, + ObjCBridgeState* bridgeState, + bool enabled) + : env_(enabled ? env : nullptr), + bridgeState_(enabled ? bridgeState : nullptr) { + if (bridgeState_ != nullptr) { + bridgeState_->beginRoundTripCacheFrame(env_); + } + } + + EngineDirectRoundTripCacheFrameGuard(napi_env env, + ObjCBridgeState* bridgeState, + Cif* cif) + : EngineDirectRoundTripCacheFrameGuard( + env, bridgeState, needsRoundTripCacheFrame(cif)) {} + + ~EngineDirectRoundTripCacheFrameGuard() { + if (bridgeState_ != nullptr) { + bridgeState_->endRoundTripCacheFrame(env_); + } + } + + private: + napi_env env_ = nullptr; + ObjCBridgeState* bridgeState_ = nullptr; +}; + +class EngineDirectReturnStorage { + public: + explicit EngineDirectReturnStorage(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); + } + } + + ~EngineDirectReturnStorage() { + 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; +}; + +} // namespace nativescript + +#endif // NS_FFI_SHARED_INVOCATION_SUPPORT_H diff --git a/NativeScript/ffi/v8/V8FastConversion.mm b/NativeScript/ffi/v8/V8FastConversion.mm new file mode 100644 index 00000000..b5fb9cf1 --- /dev/null +++ b/NativeScript/ffi/v8/V8FastConversion.mm @@ -0,0 +1,677 @@ +#include "V8FastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_V8 + +namespace nativescript { +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; +} + + +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, kV8NativePointerProperty, + 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, kV8NativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, cachedValue, kV8NativePointerProperty, &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; + } +} + + +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; +} + + +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 nativescript + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/V8FastNativeApi.mm b/NativeScript/ffi/v8/V8FastNativeApi.mm index 70914a3d..9eca9627 100644 --- a/NativeScript/ffi/v8/V8FastNativeApi.mm +++ b/NativeScript/ffi/v8/V8FastNativeApi.mm @@ -1,970 +1,9 @@ -#include "V8FastNativeApi.h" +#include "V8FastNativeApiPrivate.h" #ifdef TARGET_ENGINE_V8 -#import -#import -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "ffi/napi/Interop.h" -#include "ffi/napi/Object.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/TypeConv.h" -#include "ffi/napi/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; @@ -1066,161 +105,11 @@ size_t getCifArgumentStorageAlign(Cif* cif, unsigned int argumentIndex, 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 = @@ -1256,7 +145,7 @@ id resolveSelf(napi_env env, v8::Local jsThisValue, ObjCClassMember* if (self == nil && jsThis != nullptr) { napi_value nativePointerValue = nullptr; - if (napi_get_named_property(env, jsThis, kNativePointerProperty, &nativePointerValue) == + if (napi_get_named_property(env, jsThis, kV8NativePointerProperty, &nativePointerValue) == napi_ok && Pointer::isInstance(env, nativePointerValue)) { Pointer* nativePointer = Pointer::unwrap(env, nativePointerValue); @@ -1790,7 +679,7 @@ void setObjCReturnValue(napi_env env, const v8::FunctionCallbackInfo& bool invokeObjCSlow(napi_env env, const v8::FunctionCallbackInfo& info, ObjCClassMember* method, MethodDescriptor* descriptor, Cif* cif, id self, bool receiverIsClass, bool propertyAccess) { - V8CifReturnStorage rvalueStorage(cif); + EngineDirectReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { throwV8Error(info.GetIsolate(), "Unable to allocate return value storage for Objective-C call."); @@ -1897,12 +786,12 @@ bool invokeObjCFast(napi_env env, const v8::FunctionCallbackInfo& inf const bool generatedDispatchUsesObjectReturnStorage = !generatedDispatchSetsReturnDirectly && cif->generatedDispatchUsesObjectReturnStorage; const bool needsRoundTripCache = cif->generatedDispatchHasRoundTripCacheArgument; - std::optional roundTripCacheFrame; + std::optional roundTripCacheFrame; if (needsRoundTripCache) { - roundTripCacheFrame.emplace(env, method->bridgeState); + roundTripCacheFrame.emplace(env, method->bridgeState, true); } - std::optional rvalueStorage; + std::optional rvalueStorage; id objectRvalue = nil; void* rvalue = nullptr; if (generatedDispatchUsesObjectReturnStorage) { @@ -2291,9 +1180,4 @@ bool V8TryDefineFastNativeProperty(napi_env env, v8::Local object, } // 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/v8/V8FastNativeApiPrivate.h b/NativeScript/ffi/v8/V8FastNativeApiPrivate.h new file mode 100644 index 00000000..70bdf0b4 --- /dev/null +++ b/NativeScript/ffi/v8/V8FastNativeApiPrivate.h @@ -0,0 +1,205 @@ +#ifndef NS_V8_FAST_NATIVE_API_PRIVATE_H +#define NS_V8_FAST_NATIVE_API_PRIVATE_H + +#include "V8FastNativeApi.h" + +#ifdef TARGET_ENGINE_V8 + +#import +#import +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffi/napi/CFunction.h" +#include "ffi/napi/ClassBuilder.h" +#include "ffi/napi/ClassMember.h" +#include "ffi/napi/Interop.h" +#include "InvocationSupport.h" +#include "ffi/napi/Object.h" +#include "ffi/napi/ObjCBridge.h" +#include "SignatureDispatch.h" +#include "ffi/napi/TypeConv.h" +#include "ffi/napi/NativeScriptException.h" +#include "v8-api.h" + +namespace nativescript { + +inline constexpr const char* kV8NativePointerProperty = "__ns_native_ptr"; +inline constexpr int kV8NativeWrapperReferenceField = 0; +inline constexpr int kV8NativeWrapperMarkerField = 1; +inline constexpr int kV8NativeWrapperFieldCount = 2; + +struct V8CFunctionBinding { + ObjCBridgeState* bridgeState = nullptr; + MDSectionOffset offset = 0; + CFunction* function = nullptr; +}; + +bool TryFastConvertV8FoundationObject(napi_env env, id value, + v8::Local* result); +bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, + v8::Local* result); + +inline void* nativeWrapperMarker() { + static uintptr_t marker; + return ▮ +} + +inline bool isV8NativeWrapperObject(v8::Local object) { + return !object.IsEmpty() && + object->InternalFieldCount() > kV8NativeWrapperMarkerField && + object->GetAlignedPointerFromInternalField( + kV8NativeWrapperMarkerField) == nativeWrapperMarker(); +} + +inline 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)); +} + +inline 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()); +} + +inline 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); +} + +inline 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); +} + +inline id tryReadWrappedReference(napi_env env, + v8::Local object) { + if (env == nullptr || object.IsEmpty()) { + return nil; + } + + if (object->InternalFieldCount() > kV8NativeWrapperReferenceField) { + auto* reference = static_cast( + object->GetAlignedPointerFromInternalField( + kV8NativeWrapperReferenceField)); + if (reference != nullptr) { + return static_cast(reference->Data()); + } + } + + v8::Local wrappedReference; + if (!object->GetPrivate(env->context(), napiPrivateKey(env->isolate)) + .ToLocal(&wrappedReference) || + wrappedReference.IsEmpty() || !wrappedReference->IsExternal()) { + return nil; + } + + auto* reference = static_cast( + wrappedReference.As()->Value()); + return reference != nullptr ? static_cast(reference->Data()) : nil; +} + +inline 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 (value->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; +} + +} // namespace nativescript + +#endif // TARGET_ENGINE_V8 + +#endif // NS_V8_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/v8/V8FastNativeWrapper.mm b/NativeScript/ffi/v8/V8FastNativeWrapper.mm new file mode 100644 index 00000000..058299f6 --- /dev/null +++ b/NativeScript/ffi/v8/V8FastNativeWrapper.mm @@ -0,0 +1,230 @@ +#include "V8FastNativeApiPrivate.h" + +#ifdef TARGET_ENGINE_V8 + +#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 + +namespace nativescript { +namespace { + +napi_env envFromHandlerData(v8::Local data) { + if (data.IsEmpty() || !data->IsExternal()) { + return nullptr; + } + + return static_cast(data.As()->Value()); +} + +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, kV8NativePointerProperty) == 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(kV8NativeWrapperFieldCount); + 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(kV8NativeWrapperMarkerField, + nativeWrapperMarker()); + + return v8impl::JsValueFromV8LocalValue(scope.Escape(object)); +} + +} // 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 From 48707a0fde531abaee0abf2908b5005efbc361e6 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 17:03:52 -0400 Subject: [PATCH 28/31] test(react-native): reuse ffi compatibility suite --- NativeScript/ffi/hermes/jsi/NativeApiJsi.mm | 2037 ++++++++++++++++--- packages/react-native/src/index.ts | 257 ++- scripts/react_native_app_utils.sh | 1 + scripts/test_react_native_ffi_compat.sh | 144 ++ test/react-native/ffi-compat/App.tsx | 736 +++++-- test/runtime/fixtures/TestFixtures.h | 1 + 6 files changed, 2729 insertions(+), 447 deletions(-) diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm index 321ddb61..ae146594 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm @@ -11,9 +11,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -40,6 +42,7 @@ using facebook::jsi::Array; using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; using facebook::jsi::Function; using facebook::jsi::HostObject; using facebook::jsi::MutableBuffer; @@ -56,6 +59,7 @@ thread_local bool gDispatchNativeCallsToUI = false; thread_local bool gExecutingDispatchedUINativeCall = false; thread_local int gSynchronousNativeInvocationDepth = 0; +std::atomic gActiveSynchronousNativeInvocationDepth{0}; class ScopedNativeApiUINativeCallDispatch final { public: @@ -80,10 +84,14 @@ bool shouldDispatchNativeCallToUI() { public: ScopedNativeApiSynchronousInvocation() { gSynchronousNativeInvocationDepth += 1; + gActiveSynchronousNativeInvocationDepth.fetch_add(1, + std::memory_order_acq_rel); } ~ScopedNativeApiSynchronousInvocation() { gSynchronousNativeInvocationDepth -= 1; + gActiveSynchronousNativeInvocationDepth.fetch_sub(1, + std::memory_order_acq_rel); } }; @@ -248,7 +256,73 @@ void finalize() { return selector; } +bool hasRuntimeSetterForProperty(Class cls, bool staticMethod, + const std::string& property) { + if (cls == nil || property.empty()) { + return false; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + return staticMethod ? class_getClassMethod(cls, selector) != nullptr + : class_getInstanceMethod(cls, selector) != nullptr; +} + +size_t selectorArgumentCount(const std::string& selector) { + return static_cast( + std::count(selector.begin(), selector.end(), ':')); +} + +const NativeApiMember* selectMethodMember( + const std::vector& members, const std::string& property, + bool staticMethod, size_t argumentCount) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + if (fallback == nullptr) { + fallback = &member; + } + if (selectorArgumentCount(member.selectorName) == argumentCount) { + return &member; + } + } + return fallback; +} + +const NativeApiMember* selectPropertyMember( + const std::vector& members, const std::string& property, + bool staticMethod) { + for (const auto& member : members) { + if (!member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic == staticMethod) { + return &member; + } + } + return nullptr; +} + void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); +Protocol* lookupProtocolByNativeName(const std::string& name); + +inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { +#if INTPTR_MAX == INT64_MAX + return pointer & 0x0000FFFFFFFFFFFFULL; +#else + return pointer; +#endif +} class NativeApiJsiBridge { public: @@ -296,6 +370,26 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) return nullptr; } + const NativeApiSymbol* findClassForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = classSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != classSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findProtocolForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = protocolSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != protocolSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + const NativeApiSymbol* findFunction(const std::string& name) const { const NativeApiSymbol* symbol = find(name); return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Function @@ -303,6 +397,49 @@ explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) : nullptr; } + void rememberRoundTripValue(Runtime& runtime, const void* native, + const Value& value) { + if (native == nullptr) { + return; + } + roundTripValues_[normalizeRuntimePointer( + reinterpret_cast(native))] = + std::make_shared(runtime, value); + } + + Value findRoundTripValue(Runtime& runtime, const void* native) const { + if (native == nullptr) { + return Value::undefined(); + } + auto it = roundTripValues_.find( + normalizeRuntimePointer(reinterpret_cast(native))); + if (it == roundTripValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void rememberPointerValue(Runtime& runtime, const void* native, + const Value& value) { + pointerValues_[reinterpret_cast(native)] = + std::make_shared(runtime, value); + } + + Value findPointerValue(Runtime& runtime, const void* native) const { + auto it = pointerValues_.find(reinterpret_cast(native)); + if (it == pointerValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void forgetPointerValue(const void* native) { + if (native == nullptr) { + return; + } + pointerValues_.erase(reinterpret_cast(native)); + } + const NativeApiSymbol* findConstant(const std::string& name) const { const NativeApiSymbol* symbol = find(name); return symbol != nullptr && symbol->kind == NativeApiSymbolKind::Constant @@ -384,6 +521,18 @@ void retainJsiLifetime(std::shared_ptr lifetime) { return inserted.first->second; } + const std::vector& membersForProtocol( + const NativeApiSymbol& symbol) const { + auto cached = membersByProtocolOffset_.find(symbol.offset); + if (cached != membersByProtocolOffset_.end()) { + return cached->second; + } + + auto inserted = membersByProtocolOffset_.emplace( + symbol.offset, readMembersForProtocolHierarchy(symbol.offset)); + return inserted.first->second; + } + std::shared_ptr aggregateInfoFor( MDSectionOffset aggregateOffset, bool isUnion); @@ -497,7 +646,22 @@ void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, symbolsByName_[symbol.name] = symbol; if (kind == NativeApiSymbolKind::Class) { classSymbolsByOffset_[symbol.offset] = symbol; - classSymbolsByRuntimeName_[symbol.runtimeName] = std::move(symbol); + classSymbolsByRuntimeName_[symbol.runtimeName] = symbol; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls != Nil) { + classSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(cls))] = symbol; + } + } else if (kind == NativeApiSymbolKind::Protocol) { + protocolSymbolsByOffset_[symbol.offset] = symbol; + Protocol* protocol = lookupProtocolByNativeName(symbol.runtimeName); + if (protocol == nullptr && symbol.runtimeName != symbol.name) { + protocol = lookupProtocolByNativeName(symbol.name); + } + if (protocol != nullptr) { + protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(protocol))] = symbol; + } } else if (kind == NativeApiSymbolKind::Struct) { structSymbolsByOffset_[symbol.offset] = symbol; } else if (kind == NativeApiSymbolKind::Union) { @@ -810,6 +974,88 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { return members; } + std::vector readMembersAtOffset( + MDSectionOffset& offset) const { + std::vector members; + bool next = true; + 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 readMembersForProtocolHierarchy( + MDSectionOffset protocolOffset) const { + std::vector members; + if (metadata_ == nullptr || protocolOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset offset = protocolOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto inheritedOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (inheritedOffset & metagen::mdSectionOffsetNext) != 0; + inheritedOffset &= ~metagen::mdSectionOffsetNext; + if (inheritedOffset == MD_SECTION_OFFSET_NULL) { + continue; + } + + MDSectionOffset absoluteOffset = + inheritedOffset + metadata_->protocolsOffset; + auto inheritedSymbol = protocolSymbolsByOffset_.find(absoluteOffset); + if (inheritedSymbol != protocolSymbolsByOffset_.end()) { + const auto& inheritedMembers = + membersForProtocol(inheritedSymbol->second); + members.insert(members.end(), inheritedMembers.begin(), + inheritedMembers.end()); + } + } + + std::vector ownMembers = readMembersAtOffset(offset); + members.insert(members.end(), ownMembers.begin(), ownMembers.end()); + return members; + } + std::vector readMembersForClassHierarchy( const NativeApiSymbol& symbol) const { std::vector members = readMembersForClass(symbol.offset); @@ -830,7 +1076,12 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { void* selfDl_ = nullptr; std::unordered_map symbolsByName_; std::unordered_map classSymbolsByRuntimeName_; + std::unordered_map classSymbolsByRuntimePointer_; + std::unordered_map protocolSymbolsByRuntimePointer_; + std::unordered_map> roundTripValues_; + std::unordered_map> pointerValues_; std::unordered_map classSymbolsByOffset_; + std::unordered_map protocolSymbolsByOffset_; std::vector classNames_; std::vector functionNames_; std::vector constantNames_; @@ -841,6 +1092,8 @@ void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { std::shared_ptr scheduler_; mutable std::unordered_map> membersByClassOffset_; + mutable std::unordered_map> + membersByProtocolOffset_; std::unordered_map structSymbolsByOffset_; std::unordered_map unionSymbolsByOffset_; std::unordered_map> @@ -901,6 +1154,7 @@ void addPropertyName(Runtime& runtime, std::vector& names, class NativeApiObjectHostObject; class NativeApiClassHostObject; class NativeApiProtocolHostObject; +class NativeApiJsiArgumentFrame; Value callCFunction(Runtime& runtime, const std::shared_ptr& bridge, @@ -914,6 +1168,14 @@ Value callObjCSelector(Runtime& runtime, const NativeApiMember* member, const Value* args, size_t count); +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject); + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol); + Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { Object result(runtime); result.setProperty(runtime, "kind", makeString(runtime, kindName(symbol.kind))); @@ -940,15 +1202,41 @@ Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { } size_t nativeSizeForType(const NativeApiJsiType& type); +std::optional parseArrayIndexProperty(const std::string& property); + +NativeApiJsiType nativeObjectReturnType( + MDTypeKind kind = metagen::mdTypeAnyObject) { + NativeApiJsiType type; + type.kind = kind; + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; +} + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); +Object createPointer(Runtime& runtime, + const std::shared_ptr& bridge, + void* pointer, bool adopted = false); + +NativeApiJsiType primitiveInteropType(MDTypeKind kind); class NativeApiPointerHostObject final : public HostObject { public: - NativeApiPointerHostObject(void* pointer, std::string kind = "pointer", + NativeApiPointerHostObject(std::shared_ptr bridge, + void* pointer, std::string kind = "pointer", bool adopted = false) - : pointer_(pointer), kind_(std::move(kind)), adopted_(adopted) {} + : bridge_(std::move(bridge)), + pointer_(pointer), + kind_(std::move(kind)), + adopted_(adopted) {} ~NativeApiPointerHostObject() override { if (adopted_ && pointer_ != nullptr) { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } free(pointer_); pointer_ = nullptr; } @@ -958,6 +1246,9 @@ Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { bool adopted() const { return adopted_; } void adopt() { adopted_ = true; } void clearWithoutFree() { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } pointer_ = nullptr; adopted_ = false; } @@ -976,18 +1267,18 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (property == "add" || property == "subtract") { void* pointer = pointer_; bool add = property == "add"; + auto bridge = bridge_; return Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, property.c_str()), 1, - [pointer, add](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [bridge, 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)); + return createPointer(runtime, bridge, result); }); } if (property == "toNumber") { @@ -1003,13 +1294,9 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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)); + return BigInt::fromUint64( + runtime, + static_cast(reinterpret_cast(pointer))); }); } if (property == "toHexString" || property == "toDecimalString") { @@ -1018,16 +1305,18 @@ Value get(Runtime& runtime, const PropNameID& name) override { 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) { + char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; snprintf(text, sizeof(text), "0x%llx", static_cast( reinterpret_cast(pointer))); + return makeString(runtime, text); } else { + char text[32] = {}; snprintf(text, sizeof(text), "%lld", static_cast(reinterpret_cast(pointer))); + return makeString(runtime, text); } - return makeString(runtime, text); }); } if (property == "toString") { @@ -1039,6 +1328,10 @@ Value get(Runtime& runtime, const PropNameID& name) override { size_t) -> Value { char address[32] = {}; snprintf(address, sizeof(address), "%p", pointer); + if (kind == "pointer") { + return makeString(runtime, + ""); + } return makeString(runtime, "[NativeApiJsi " + kind + " " + std::string(address) + "]"); }); @@ -1063,6 +1356,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { } private: + std::shared_ptr bridge_; void* pointer_ = nullptr; std::string kind_; bool adopted_ = false; @@ -1072,12 +1366,14 @@ Value get(Runtime& runtime, const PropNameID& name) override { public: NativeApiReferenceHostObject(std::shared_ptr bridge, NativeApiJsiType type, void* data, bool ownsData, - size_t byteLength = 0) + size_t byteLength = 0, + std::shared_ptr pendingValue = nullptr) : bridge_(std::move(bridge)), type_(std::move(type)), data_(data), ownsData_(ownsData), - byteLength_(byteLength) {} + byteLength_(byteLength), + pendingValue_(std::move(pendingValue)) {} ~NativeApiReferenceHostObject() override { if (ownsData_ && data_ != nullptr) { @@ -1088,27 +1384,8 @@ Value get(Runtime& runtime, const PropNameID& name) override { void* data() const { return data_; } const NativeApiJsiType& type() const { return type_; } - void ensureStorage(NativeApiJsiType type, size_t elements = 1) { - size_t elementCount = std::max(elements, 1); - NativeApiJsiType storageType = std::move(type); - size_t stride = std::max(nativeSizeForType(storageType), 1); - size_t required = std::max(stride * elementCount, sizeof(void*)); - if (data_ == nullptr) { - type_ = std::move(storageType); - data_ = calloc(1, required); - ownsData_ = true; - byteLength_ = required; - } else if (ownsData_ && byteLength_ < required) { - void* expanded = realloc(data_, required); - if (expanded == nullptr) { - throw std::bad_alloc(); - } - std::memset(static_cast(expanded) + byteLength_, 0, - required - byteLength_); - data_ = expanded; - byteLength_ = required; - } - } + void ensureStorage(Runtime& runtime, NativeApiJsiType type, + NativeApiJsiArgumentFrame& frame, size_t elements = 1); Value get(Runtime& runtime, const PropNameID& name) override; void set(Runtime& runtime, const PropNameID& name, const Value& value) override; @@ -1127,6 +1404,7 @@ void ensureStorage(NativeApiJsiType type, size_t elements = 1) { void* data_ = nullptr; bool ownsData_ = false; size_t byteLength_ = 0; + std::shared_ptr pendingValue_; }; class NativeApiStructObjectHostObject final : public HostObject { @@ -1134,15 +1412,24 @@ void ensureStorage(NativeApiJsiType type, size_t elements = 1) { 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) { + const void* data = nullptr, bool ownsData = true, + std::shared_ptr> storageOwner = nullptr, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + info_(std::move(info)), + ownedData_(std::move(storageOwner)), + backingValue_(std::move(backingValue)), + ownsData_(ownsData) { size_t size = info_ != nullptr ? info_->size : 0; - if (ownsData_) { - ownedData_.assign(size, 0); + if (ownedData_ != nullptr) { + data_ = const_cast(data); + ownsData_ = false; + } else if (ownsData_) { + ownedData_ = std::make_shared>(size, 0); if (data != nullptr && size > 0) { - std::memcpy(ownedData_.data(), data, size); + std::memcpy(ownedData_->data(), data, size); } - data_ = ownedData_.empty() ? nullptr : ownedData_.data(); + data_ = ownedData_->empty() ? nullptr : ownedData_->data(); } else { data_ = const_cast(data); } @@ -1150,6 +1437,10 @@ void ensureStorage(NativeApiJsiType type, size_t elements = 1) { void* data() const { return data_; } std::shared_ptr info() const { return info_; } + std::shared_ptr> storageOwner() const { + return ownedData_; + } + std::shared_ptr backingValue() const { return backingValue_; } Value get(Runtime& runtime, const PropNameID& name) override; void set(Runtime& runtime, const PropNameID& name, const Value& value) override; @@ -1158,7 +1449,8 @@ void ensureStorage(NativeApiJsiType type, size_t elements = 1) { private: std::shared_ptr bridge_; std::shared_ptr info_; - std::vector ownedData_; + std::shared_ptr> ownedData_; + std::shared_ptr backingValue_; void* data_ = nullptr; bool ownsData_ = true; }; @@ -1220,32 +1512,57 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } + if (object_ != nil && [object_ isKindOfClass:[NSArray class]]) { + NSArray* array = static_cast(object_); + if (property == "length") { + return static_cast(array.count); + } + if (auto index = parseArrayIndexProperty(property)) { + if (*index >= array.count) { + return Value::undefined(); + } + id element = [array objectAtIndex:*index]; + NativeApiJsiType elementType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, elementType, &element); + } + } + if (object_ != nil) { if (const NativeApiSymbol* symbol = bridge_->findClassForRuntimeClass(object_getClass(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); - } + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return callObjCSelector(runtime, bridge_, object_, false, + propertyMember->selectorName, propertyMember, + nullptr, 0); + } + if (selectMethodMember(members, property, false, 0) != nullptr) { auto bridge = bridge_; id object = object_; + std::string memberName = property; 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 { + 0, + [bridge, object, memberName](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(object_getClass(object)); + if (symbol == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C metadata is not available for object."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } return callObjCSelector(runtime, bridge, object, false, - member.selectorName, &member, args, + selected->selectorName, selected, args, count); }); } @@ -1254,6 +1571,13 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (auto selectorName = runtimeSelectorNameForProperty(object_getClass(object_), false, property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(object_getClass(object_), false, + property)) { + return callObjCSelector(runtime, bridge_, object_, false, + *selectorName, nullptr, nullptr, 0); + } + auto bridge = bridge_; id object = object_; return Function::createFromHostFunction( @@ -1263,7 +1587,18 @@ Value get(Runtime& runtime, const PropNameID& name) override { size_t count) -> Value { return callObjCSelector(runtime, bridge, object, false, selectorName, nullptr, args, count); - }); + }); + } + + if ([object_ isKindOfClass:[NSDictionary class]]) { + NSString* key = [NSString stringWithUTF8String:property.c_str()]; + if (key != nil) { + id value = [static_cast(object_) objectForKey:key]; + if (value != nil) { + NativeApiJsiType valueType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, valueType, &value); + } + } } } @@ -1278,15 +1613,16 @@ void set(Runtime& runtime, const PropNameID& name, const Value& value) override if (const NativeApiSymbol* symbol = bridge_->findClassForRuntimeClass(object_getClass(object_))) { - for (const auto& member : bridge_->membersForClass(*symbol)) { - if (!member.property || member.readonly || - (member.flags & metagen::mdMemberStatic) != 0 || - member.name != property) { - continue; + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); } - NativeApiMember setterMember = member; - setterMember.selectorName = member.setterSelectorName; - setterMember.signatureOffset = member.setterSignatureOffset; + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; Value args[] = {Value(runtime, value)}; callObjCSelector(runtime, bridge_, object_, false, setterMember.selectorName, &setterMember, args, 1); @@ -1385,6 +1721,36 @@ Value get(Runtime& runtime, const PropNameID& name) override { } id result = nil; + if (property == "construct" && count == 1) { + void* pointer = nullptr; + if (args[0].isNumber()) { + pointer = reinterpret_cast( + static_cast(args[0].getNumber())); + } else if (args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + pointer = object + .getHostObject( + runtime) + ->pointer(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->data(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->object(); + } + } + return makeNativeObjectValue(runtime, bridge, + static_cast(pointer), false); + } + if (property == "new") { if (count != 0) { throw facebook::jsi::JSError( @@ -1401,9 +1767,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { result = [cls alloc]; } - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, result, true)); + return makeNativeObjectValue(runtime, bridge, result, true); }); } if (property == "invoke" || property == "send") { @@ -1426,34 +1790,45 @@ Value get(Runtime& runtime, const PropNameID& name) override { }); } - for (const auto& member : bridge_->membersForClass(symbol_)) { - if ((member.flags & metagen::mdMemberStatic) == 0 || - member.name != property) { - continue; + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + auto bridge = bridge_; + auto symbol = symbol_; + 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, + propertyMember->selectorName, propertyMember, + nullptr, 0); + } + if (selectMethodMember(members, property, true, 0) != nullptr) { 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); - } + std::string memberName = property; 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 { + [bridge, symbol, memberName](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); } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(symbol), memberName, true, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } return callObjCSelector(runtime, bridge, static_cast(cls), true, - member.selectorName, &member, args, count); + selected->selectorName, selected, args, + count); }); } @@ -1461,6 +1836,12 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (cls != nil) { if (auto selectorName = runtimeSelectorNameForProperty(cls, true, property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(cls, true, property)) { + return callObjCSelector(runtime, bridge_, static_cast(cls), true, + *selectorName, nullptr, nullptr, 0); + } + auto bridge = bridge_; return Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, property.c_str()), 0, @@ -1477,6 +1858,43 @@ Value get(Runtime& runtime, const PropNameID& name) override { return Value::undefined(); } + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol_.name); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + setterMember.selectorName, &setterMember, args, 1); + return; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getClassMethod(cls, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + 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(8); @@ -1504,6 +1922,85 @@ Value get(Runtime& runtime, const PropNameID& name) override { NativeApiSymbol symbol_; }; +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject) { + if (object == nil) { + return Value::null(); + } + + Value cached = bridge->findRoundTripValue(runtime, object); + if (!cached.isUndefined()) { + if (ownsObject) { + [object release]; + } + return cached; + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, object, ownsObject)); + bridge->rememberRoundTripValue(runtime, object, Value(runtime, result)); + return result; +} + +Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, + const char* expectedKind) { + Value cacheValue = runtime.global().getProperty( + runtime, "__nativeScriptNativeApiGlobalCache"); + if (!cacheValue.isObject()) { + return Value::undefined(); + } + + Object cache = cacheValue.asObject(runtime); + auto readCache = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + + Value value = cache.getProperty(runtime, name.c_str()); + if (!value.isObject()) { + return Value::undefined(); + } + + try { + Object object = value.asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == expectedKind) { + return value; + } + } catch (const std::exception&) { + } + + return Value::undefined(); + }; + + Value value = readCache(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = readCache(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + return Value::undefined(); +} + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + Protocol* lookupProtocolByNativeName(const std::string& name) { Protocol* protocol = objc_getProtocol(name.c_str()); if (protocol != nullptr) { @@ -1521,8 +2018,9 @@ Value get(Runtime& runtime, const PropNameID& name) override { class NativeApiProtocolHostObject final : public HostObject { public: - explicit NativeApiProtocolHostObject(NativeApiSymbol symbol) - : symbol_(std::move(symbol)) {} + NativeApiProtocolHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} Protocol* nativeProtocol() const { Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); @@ -1553,6 +2051,22 @@ Value get(Runtime& runtime, const PropNameID& name) override { return static_cast( reinterpret_cast(nativeProtocol())); } + if (property == "prototype") { + Object prototype(runtime); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + if (prototype.hasProperty(runtime, member.name.c_str())) { + continue; + } + if (member.property) { + defineProtocolProperty(runtime, prototype, member, false); + } else { + prototype.setProperty(runtime, member.name.c_str(), + makeProtocolMemberFunction(runtime, member, + false)); + } + } + return prototype; + } if (property == "toString") { auto symbol = symbol_; return Function::createFromHostFunction( @@ -1562,6 +2076,23 @@ Value get(Runtime& runtime, const PropNameID& name) override { "[NativeApiJsiProtocol " + symbol.name + "]"); }); } + const auto& members = bridge_->membersForProtocol(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, true, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, false, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } return Value::undefined(); } @@ -1573,14 +2104,262 @@ Value get(Runtime& runtime, const PropNameID& name) override { addPropertyName(runtime, names, "available"); addPropertyName(runtime, names, "metadataOffset"); addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "prototype"); addPropertyName(runtime, names, "toString"); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + addPropertyName(runtime, names, member.name.c_str()); + } return names; } private: + Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) const { + if (!thisValue.isObject()) { + return Nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + + return Nil; + } + + id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { + if (!thisValue.isObject()) { + return nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + + return nil; + } + + Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + Class cls = Nil; + if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + cls = object.getHostObject(runtime) + ->nativeClass(); + } else { + Value wrappedClass = + object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject( + runtime)) { + cls = wrappedObject + .getHostObject(runtime) + ->nativeClass(); + } + } + } + } + receiver = static_cast(cls); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol member requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, args, count); + }); + } + + Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value*, size_t) -> Value { + id receiver = nil; + if (receiverIsClass) { + Class cls = Nil; + if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + cls = object.getHostObject(runtime) + ->nativeClass(); + } else { + Value wrappedClass = + object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject( + runtime)) { + cls = wrappedObject + .getHostObject(runtime) + ->nativeClass(); + } + } + } + } + receiver = static_cast(cls); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, nullptr, 0); + }); + } + + Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), + 1, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + Class cls = Nil; + if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + cls = object.getHostObject(runtime) + ->nativeClass(); + } else { + Value wrappedClass = + object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject( + runtime)) { + cls = wrappedObject + .getHostObject(runtime) + ->nativeClass(); + } + } + } + } + receiver = static_cast(cls); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + if (count < 1) { + throw facebook::jsi::JSError( + runtime, "Protocol property setter expects a value."); + } + + NativeApiMember setterMember = member; + setterMember.selectorName = member.setterSelectorName; + setterMember.signatureOffset = member.setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + setterMember.selectorName, &setterMember, + args, 1); + }); + } + + void defineProtocolProperty(Runtime& runtime, Object& target, + const NativeApiMember& member, + bool receiverIsClass) const { + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty(runtime, "enumerable", true); + descriptor.setProperty(runtime, "get", + makeProtocolPropertyGetter(runtime, member, + receiverIsClass)); + if (!member.readonly && !member.setterSelectorName.empty()) { + descriptor.setProperty(runtime, "set", + makeProtocolPropertySetter(runtime, member, + receiverIsClass)); + } + defineProperty.call(runtime, target, makeString(runtime, member.name), + descriptor); + } catch (const std::exception&) { + } + } + + std::shared_ptr bridge_; NativeApiSymbol symbol_; }; +Value makeNativeProtocolValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Class nativeClassFromJsiObject(Runtime& runtime, const Object& object) { + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + return Nil; +} + bool isObjectiveCObjectType(const NativeApiJsiType& type) { switch (type.kind) { case metagen::mdTypeAnyObject: @@ -1613,6 +2392,9 @@ explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(coun for (char* string : ownedCStrings_) { free(string); } + for (void* buffer : ownedBuffers_) { + free(buffer); + } for (id object : ownedObjects_) { [object release]; } @@ -1625,6 +2407,14 @@ explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(coun } void addCString(char* value) { ownedCStrings_.push_back(value); } + void* addBuffer(size_t size) { + void* buffer = calloc(1, std::max(size, 1)); + if (buffer == nullptr) { + throw std::bad_alloc(); + } + ownedBuffers_.push_back(buffer); + return buffer; + } void addObject(id value) { ownedObjects_.push_back(value); } void** values() { return values_.empty() ? nullptr : values_.data(); } @@ -1632,6 +2422,7 @@ explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(coun std::vector> storage_; std::vector values_; std::vector ownedCStrings_; + std::vector ownedBuffers_; std::vector ownedObjects_; }; @@ -1872,7 +2663,9 @@ void invoke(void* ret, void* args[]) { auto call = [&]() { invokeOnCurrentThread(ret, args, &error); }; bool direct = std::this_thread::get_id() == bridge_->jsThreadId() || gExecutingDispatchedUINativeCall || - gSynchronousNativeInvocationDepth > 0; + gSynchronousNativeInvocationDepth > 0 || + gActiveSynchronousNativeInvocationDepth.load( + std::memory_order_acquire) > 0; if (direct) { call(); } else if (auto scheduler = bridge_->scheduler()) { @@ -2024,6 +2817,61 @@ size_t nativeSizeForType(const NativeApiJsiType& type) { return sizeof(void*); } +Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromInt64(runtime, value); +} + +Value unsignedInteger64ToJsiValue(Runtime& runtime, uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromUint64(runtime, value); +} + +bool parseIntegerTextToUintptr(const std::string& text, uintptr_t* address) { + if (address == nullptr) { + return false; + } + if (text.empty()) { + return false; + } + + char* end = nullptr; + if (text[0] == '-') { + long long signedValue = std::strtoll(text.c_str(), &end, 10); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(static_cast(signedValue)); + return true; + } + + int base = 10; + const char* start = text.c_str(); + if (text.size() > 2 && text[0] == '0' && + (text[1] == 'x' || text[1] == 'X')) { + base = 16; + } + unsigned long long unsignedValue = std::strtoull(start, &end, base); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(unsignedValue); + return true; +} + +bool parseBigIntToUintptr(Runtime& runtime, const BigInt& bigint, + uintptr_t* address) { + return parseIntegerTextToUintptr(bigint.toString(runtime, 10).utf8(runtime), + address); +} + bool readJsiBuffer(Runtime& runtime, const Object& object, const uint8_t** data, size_t* byteLength) { if (data == nullptr || byteLength == nullptr) { @@ -2418,6 +3266,15 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, return info; } +ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeArray: + return &ffi_type_pointer; + default: + return type.ffiType != nullptr ? type.ffiType : &ffi_type_pointer; + } +} + std::optional parseMetadataJsiSignature( MDMetadataReader* metadata, MDSectionOffset signatureOffset, unsigned int implicitArgumentCount, NativeApiJsiBridge* bridge, @@ -2452,8 +3309,7 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, 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); + signature.ffiTypes.push_back(ffiTypeForJsiArgument(argType)); } ffi_status status = ffi_prep_cif( @@ -2588,8 +3444,7 @@ NativeApiJsiType parseObjCEncodedJsiType(const char* encoding) { 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); + signature.ffiTypes.push_back(ffiTypeForJsiArgument(argType)); } ffi_status status = ffi_prep_cif( @@ -2603,6 +3458,10 @@ NativeApiJsiType parseObjCEncodedJsiType(const char* encoding) { } bool unsupportedJsiType(const NativeApiJsiType& type) { + if (type.kind == metagen::mdTypeStruct && type.aggregateInfo != nullptr && + type.aggregateInfo->ffi != nullptr) { + return false; + } return !type.supported || type.ffiType == nullptr; } @@ -2643,8 +3502,10 @@ bool signatureSupportedForJsiCallback(const NativeApiJsiSignature& signature) { return callback; } -id objectFromJsiValue(Runtime& runtime, const Value& value, - NativeApiJsiArgumentFrame& frame, bool mutableString) { +id objectFromJsiValue(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value, NativeApiJsiArgumentFrame& frame, + bool mutableString) { if (value.isNull() || value.isUndefined()) { return nil; } @@ -2667,9 +3528,8 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->object(); } - if (object.isHostObject(runtime)) { - return static_cast( - object.getHostObject(runtime)->nativeClass()); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return static_cast(cls); } if (object.isHostObject(runtime)) { return static_cast( @@ -2689,10 +3549,41 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, object.getHostObject(runtime)->data()); } + Value getTimeValue = object.getProperty(runtime, "getTime"); + Value toISOStringValue = object.getProperty(runtime, "toISOString"); + if (getTimeValue.isObject() && + getTimeValue.asObject(runtime).isFunction(runtime) && + toISOStringValue.isObject() && + toISOStringValue.asObject(runtime).isFunction(runtime)) { + Value millisValue = getTimeValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (millisValue.isNumber()) { + NSDate* date = [NSDate dateWithTimeIntervalSince1970:millisValue.getNumber() / 1000.0]; + bridge->rememberRoundTripValue(runtime, date, value); + return date; + } + } + + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitiveValue = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitiveValue.isString() || primitiveValue.isBool() || + primitiveValue.isNumber()) { + return objectFromJsiValue(runtime, bridge, primitiveValue, frame, + mutableString); + } + } + const uint8_t* bytes = nullptr; size_t byteLength = 0; if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { - return [NSData dataWithBytes:bytes length:byteLength]; + NSData* data = [NSData dataWithBytes:bytes length:byteLength]; + bridge->rememberRoundTripValue(runtime, data, value); + return data; } if (object.isArray(runtime)) { @@ -2700,13 +3591,74 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, 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), + id element = objectFromJsiValue(runtime, bridge, + array.getValueAtIndex(runtime, i), frame, false); [nativeArray addObject:element != nil ? element : [NSNull null]]; } + bridge->rememberRoundTripValue(runtime, nativeArray, value); + return nativeArray; + } + + Value lengthValue = object.getProperty(runtime, "length"); + if (lengthValue.isNumber() && std::isfinite(lengthValue.getNumber()) && + lengthValue.getNumber() >= 0) { + size_t length = static_cast(std::floor(lengthValue.getNumber())); + NSMutableArray* nativeArray = [NSMutableArray arrayWithCapacity:length]; + for (size_t i = 0; i < length; i++) { + std::string key = std::to_string(i); + id element = objectFromJsiValue( + runtime, bridge, object.getProperty(runtime, key.c_str()), frame, + false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + bridge->rememberRoundTripValue(runtime, nativeArray, value); return nativeArray; } + Value entriesValue = object.getProperty(runtime, "entries"); + Value sizeValue = object.getProperty(runtime, "size"); + Value getValue = object.getProperty(runtime, "get"); + if (entriesValue.isObject() && + entriesValue.asObject(runtime).isFunction(runtime) && + sizeValue.isNumber() && getValue.isObject() && + getValue.asObject(runtime).isFunction(runtime)) { + Object arrayCtor = runtime.global().getPropertyAsObject(runtime, "Array"); + Function arrayFrom = arrayCtor.getPropertyAsFunction(runtime, "from"); + Value iterator = entriesValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + Value pairsValue = arrayFrom.call(runtime, iterator); + if (pairsValue.isObject() && pairsValue.asObject(runtime).isArray(runtime)) { + Array pairs = pairsValue.asObject(runtime).getArray(runtime); + NSMutableDictionary* nativeMap = + [NSMutableDictionary dictionaryWithCapacity:pairs.size(runtime)]; + for (size_t i = 0; i < pairs.size(runtime); i++) { + Value pairValue = pairs.getValueAtIndex(runtime, i); + if (!pairValue.isObject() || + !pairValue.asObject(runtime).isArray(runtime)) { + continue; + } + Array pair = pairValue.asObject(runtime).getArray(runtime); + if (pair.size(runtime) < 2) { + continue; + } + id key = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 0), + frame, false); + id nativeValue = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 1), + frame, false); + if (key != nil) { + [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:key]; + } + } + bridge->rememberRoundTripValue(runtime, nativeMap, value); + return nativeMap; + } + } + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; Array propertyNames = object.getPropertyNames(runtime); for (size_t i = 0; i < propertyNames.size(runtime); i++) { @@ -2719,13 +3671,15 @@ id objectFromJsiValue(Runtime& runtime, const Value& value, if (propertyValue.isUndefined()) { continue; } - id nativeValue = objectFromJsiValue(runtime, propertyValue, frame, false); + id nativeValue = + objectFromJsiValue(runtime, bridge, propertyValue, frame, false); NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; if (nativeKey != nil) { [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] forKey:nativeKey]; } } + bridge->rememberRoundTripValue(runtime, dictionary, value); return dictionary; } throw facebook::jsi::JSError(runtime, @@ -2738,6 +3692,19 @@ bool readNativePointerProperty(Runtime& runtime, const Object& object, return false; } + Value nativePointerObjectValue = + object.getProperty(runtime, "__nativeApiPointerObject"); + if (nativePointerObjectValue.isObject()) { + Object nativePointerObject = nativePointerObjectValue.asObject(runtime); + if (nativePointerObject.isHostObject( + runtime)) { + *pointer = nativePointerObject + .getHostObject(runtime) + ->pointer(); + return true; + } + } + Value nativePointerValue = object.getProperty(runtime, "__nativeApiPointer"); if (nativePointerValue.isNumber()) { @@ -2772,15 +3739,20 @@ bool readNativePointerProperty(Runtime& runtime, const Object& object, if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->object(); } - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; } if (object.isHostObject(runtime)) { return object.getHostObject(runtime) ->nativeProtocol(); } if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->data(); + auto reference = + object.getHostObject(runtime); + if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); + } + return reference->data(); } if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->data(); @@ -2798,7 +3770,6 @@ bool readNativePointerProperty(Runtime& runtime, const Object& object, 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."); @@ -2825,8 +3796,8 @@ bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) *pointer = object.getHostObject(runtime)->object(); return true; } - if (object.isHostObject(runtime)) { - *pointer = object.getHostObject(runtime)->nativeClass(); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + *pointer = cls; return true; } if (object.isHostObject(runtime)) { @@ -2860,6 +3831,9 @@ Value convertNativeReturnValue(Runtime& runtime, const std::shared_ptr& bridge, const NativeApiJsiType& type, void* value); +Class classFromJsiValue(Runtime& runtime, const Value& value); +Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value); + std::optional parseArrayIndexProperty(const std::string& property) { if (property.empty()) { return std::nullopt; @@ -2988,6 +3962,40 @@ void convertIndexedAggregateArgument(Runtime& runtime, } } +void convertJsiFfiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, const Value& value, + void* target, NativeApiJsiArgumentFrame& frame) { + if (type.kind != metagen::mdTypeArray) { + convertJsiArgument(runtime, bridge, type, value, target, frame); + return; + } + + void* pointer = nullptr; + if (!value.isNull() && !value.isUndefined()) { + if (value.isObject()) { + Object object = value.asObject(runtime); + if (!readPointerLikeValue(runtime, value, &pointer)) { + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + pointer = const_cast(bytes); + } + } + } + + if (pointer == nullptr) { + size_t byteLength = nativeSizeForType(type); + void* buffer = frame.addBuffer(byteLength); + convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, + frame); + pointer = buffer; + } + } + + *static_cast(target) = pointer; +} + void convertJsiArgument(Runtime& runtime, const std::shared_ptr& bridge, const NativeApiJsiType& type, @@ -3057,12 +4065,39 @@ void convertJsiArgument(Runtime& runtime, *static_cast(target) = nullptr; break; } + if (value.isObject()) { + Object object = value.asObject(runtime); + void* pointer = nullptr; + if (readPointerLikeValue(runtime, value, &pointer)) { + *static_cast(target) = static_cast(pointer); + break; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + *static_cast(target) = + reinterpret_cast(const_cast(bytes)); + break; + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isString()) { + std::string utf8 = primitive.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + *static_cast(target) = string; + 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; } @@ -3073,28 +4108,13 @@ void convertJsiArgument(Runtime& runtime, case metagen::mdTypeNSStringObject: case metagen::mdTypeNSMutableStringObject: { id object = objectFromJsiValue( - runtime, value, frame, + runtime, bridge, 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; + *static_cast(target) = classFromJsiValue(runtime, value); break; } case metagen::mdTypeSelector: { @@ -3114,8 +4134,10 @@ void convertJsiArgument(Runtime& runtime, Object object = value.asObject(runtime); if (object.isHostObject(runtime)) { auto reference = object.getHostObject(runtime); - if (type.elementType != nullptr) { - reference->ensureStorage(*type.elementType); + if (reference->data() == nullptr && type.elementType != nullptr) { + reference->ensureStorage(runtime, *type.elementType, frame); + } else if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); } *static_cast(target) = reference->data(); break; @@ -3140,7 +4162,10 @@ void convertJsiArgument(Runtime& runtime, runtime, bridge, type, object.asFunction(runtime), type.kind == metagen::mdTypeBlock); void* pointer = callback->functionPointer(); + bridge->rememberRoundTripValue(runtime, pointer, value); try { + object.setProperty(runtime, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); object.setProperty( runtime, "__nativeApiPointer", static_cast(reinterpret_cast(pointer))); @@ -3203,17 +4228,42 @@ Value convertNativeReturnValue(Runtime& runtime, return static_cast(*static_cast(value)); case metagen::mdTypeSLong: case metagen::mdTypeSInt64: - return static_cast(*static_cast(value)); + return signedInteger64ToJsiValue(runtime, *static_cast(value)); case metagen::mdTypeULong: case metagen::mdTypeUInt64: - return static_cast(*static_cast(value)); + return unsignedInteger64ToJsiValue(runtime, + *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(); + if (string == nullptr) { + return Value::null(); + } + NativeApiJsiType cStringType = + primitiveInteropType(metagen::mdTypeChar); + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, cStringType, const_cast(string), false)); + } + 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 makeNativeClassValue(runtime, bridge, std::move(symbol)); } case metagen::mdTypeAnyObject: case metagen::mdTypeProtocolObject: @@ -3225,18 +4275,48 @@ Value convertNativeReturnValue(Runtime& runtime, if (object == nil) { return Value::null(); } - if ([object isKindOfClass:[NSString class]]) { - std::string utf8 = [static_cast(object) UTF8String] ?: ""; + Value roundTrip = bridge->findRoundTripValue(runtime, object); + if (!roundTrip.isUndefined()) { + if (type.returnOwned) { + [object release]; + } + return roundTrip; + } + if (object_isClass(object)) { + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } + if ([object isKindOfClass:[NSNull class]]) { if (type.returnOwned) { [object release]; } - return makeString(runtime, utf8); + return Value::null(); + } + if ([object isKindOfClass:[NSString class]]) { + bool untypedObject = type.kind == metagen::mdTypeAnyObject; + bool explicitNSString = type.kind == metagen::mdTypeNSStringObject; + if (untypedObject || explicitNSString) { + std::string utf8 = [static_cast(object) UTF8String] ?: ""; + if (type.returnOwned) { + [object release]; + } + return makeString(runtime, utf8); + } } - if ([object isKindOfClass:[NSNumber class]]) { + if ([object isKindOfClass:[NSNumber class]] && + ![object isKindOfClass:[NSDecimalNumber class]]) { NSNumber* number = static_cast(object); const char* objCType = [number objCType]; - bool isBool = - objCType != nullptr && std::strcmp(objCType, @encode(BOOL)) == 0; + bool isBool = CFGetTypeID((__bridge CFTypeRef)number) == + CFBooleanGetTypeID() || + (objCType != nullptr && + std::strcmp(objCType, @encode(BOOL)) == 0); Value result = isBool ? Value(static_cast([number boolValue])) : Value([number doubleValue]); if (type.returnOwned) { @@ -3244,28 +4324,7 @@ Value convertNativeReturnValue(Runtime& runtime, } 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))); + return makeNativeObjectValue(runtime, bridge, object, type.returnOwned); } case metagen::mdTypeSelector: { SEL selector = *static_cast(value); @@ -3279,13 +4338,20 @@ Value convertNativeReturnValue(Runtime& runtime, if (pointer == nullptr) { return Value::null(); } + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer(pointer)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer(pointer)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { return Object::createFromHostObject( runtime, std::make_shared( bridge, *type.elementType, pointer, false)); } - return Object::createFromHostObject( - runtime, std::make_shared(pointer)); + return createPointer(runtime, bridge, pointer); } case metagen::mdTypeBlock: case metagen::mdTypeFunctionPointer: { @@ -3293,6 +4359,10 @@ Value convertNativeReturnValue(Runtime& runtime, if (pointer == nullptr) { return Value::null(); } + Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + if (!roundTrip.isUndefined()) { + return roundTrip; + } return wrapNativeFunctionPointer(runtime, bridge, type, pointer, type.kind == metagen::mdTypeBlock); } @@ -3328,6 +4398,37 @@ Value convertNativeReturnValue(Runtime& runtime, } } +void NativeApiReferenceHostObject::ensureStorage( + Runtime& runtime, NativeApiJsiType type, NativeApiJsiArgumentFrame& frame, + size_t elements) { + size_t elementCount = std::max(elements, 1); + NativeApiJsiType storageType = std::move(type); + size_t stride = std::max(nativeSizeForType(storageType), 1); + size_t required = std::max(stride * elementCount, sizeof(void*)); + type_ = std::move(storageType); + + if (data_ == nullptr) { + data_ = calloc(1, required); + ownsData_ = true; + byteLength_ = required; + } else if (ownsData_ && byteLength_ < required) { + void* expanded = realloc(data_, required); + if (expanded == nullptr) { + throw std::bad_alloc(); + } + std::memset(static_cast(expanded) + byteLength_, 0, + required - byteLength_); + data_ = expanded; + byteLength_ = required; + } + + if (data_ != nullptr && pendingValue_ != nullptr) { + Value pending(runtime, *pendingValue_); + convertJsiArgument(runtime, bridge_, type_, pending, data_, frame); + pendingValue_.reset(); + } +} + Value NativeApiReferenceHostObject::get(Runtime& runtime, const PropNameID& name) { std::string property = name.utf8(runtime); @@ -3339,6 +4440,9 @@ Value convertNativeReturnValue(Runtime& runtime, } if (property == "value") { if (data_ == nullptr) { + if (pendingValue_ != nullptr) { + return Value(runtime, *pendingValue_); + } return Value::undefined(); } return convertNativeReturnValue(runtime, bridge_, type_, data_); @@ -3358,8 +4462,8 @@ Value convertNativeReturnValue(Runtime& runtime, [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 makeString(runtime, + ""); }); } return Value::undefined(); @@ -3374,10 +4478,17 @@ Value convertNativeReturnValue(Runtime& runtime, return; } size_t slotIndex = index.value_or(0); - ensureStorage(type_, slotIndex + 1); + NativeApiJsiArgumentFrame frame(1); + if (data_ == nullptr) { + if (slotIndex == 0) { + pendingValue_ = std::make_shared(runtime, value); + return; + } + ensureStorage(runtime, type_, frame, slotIndex + 1); + } + pendingValue_.reset(); void* slot = static_cast(data_) + (slotIndex * referenceElementStride(type_)); - NativeApiJsiArgumentFrame frame(1); convertJsiArgument(runtime, bridge_, type_, value, slot, frame); } @@ -3418,7 +4529,8 @@ Value convertNativeReturnValue(Runtime& runtime, field.type.aggregateInfo != nullptr) { return Object::createFromHostObject( runtime, std::make_shared( - bridge_, field.type.aggregateInfo, fieldData, false)); + bridge_, field.type.aggregateInfo, fieldData, false, + ownedData_, backingValue_)); } return convertNativeReturnValue(runtime, bridge_, field.type, fieldData); } @@ -3469,41 +4581,44 @@ NativeApiJsiType primitiveInteropType(MDTypeKind kind) { return type; } +std::optional primitiveInteropTypeFromCode(int32_t code) { + MDTypeKind kind = static_cast(code); + 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::mdTypeProtocolObject: + 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; + } +} + 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::mdTypeProtocolObject: - 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; - } + return primitiveInteropTypeFromCode(static_cast(value.getNumber())); } if (!value.isObject()) { @@ -3511,6 +4626,27 @@ NativeApiJsiType primitiveInteropType(MDTypeKind kind) { } Object object = value.asObject(runtime); + Value typeCodeValue = object.getProperty(runtime, "__nativeApiTypeCode"); + if (typeCodeValue.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(typeCodeValue.getNumber())); + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = + valueOfValue.asObject(runtime).asFunction(runtime).callWithThis( + runtime, object, nullptr, 0); + if (primitive.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(primitive.getNumber())); + } + } + + if (nativeClassFromJsiObject(runtime, object) != Nil) { + return nativeObjectReturnType(metagen::mdTypeInstanceObject); + } + if (object.isHostObject(runtime)) { auto structObject = object.getHostObject(runtime); NativeApiJsiType type; @@ -3552,6 +4688,9 @@ NativeApiJsiType primitiveInteropType(MDTypeKind kind) { if (kindName == "functionPointer") { return primitiveInteropType(metagen::mdTypeFunctionPointer); } + if (kindName == "functionReference") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } } Value offsetValue = object.getProperty(runtime, "metadataOffset"); if (kindValue.isString() && offsetValue.isNumber()) { @@ -3596,6 +4735,16 @@ Value makeAggregateConstructor(Runtime& runtime, type.ffiType = info->ffi != nullptr ? &info->ffi->type : nullptr; type.supported = type.ffiType != nullptr; + if (count > 0 && args[0].isObject()) { + void* pointer = nullptr; + if (readPointerLikeValue(runtime, args[0], &pointer) && pointer != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, info, pointer, false, nullptr, + std::make_shared(runtime, args[0]))); + } + } + std::vector storage(info->size, 0); if (count > 0) { NativeApiJsiArgumentFrame frame(1); @@ -3615,6 +4764,39 @@ Value makeAggregateConstructor(Runtime& runtime, constructor.setProperty(runtime, "metadataOffset", static_cast(symbol.offset)); constructor.setProperty(runtime, "sizeof", static_cast(info != nullptr ? info->size : 0)); + constructor.setProperty( + runtime, "equals", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "equals"), 2, + [bridge, info](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (info == nullptr || count < 2) { + return false; + } + + 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 left(info->size, 0); + std::vector right(info->size, 0); + try { + NativeApiJsiArgumentFrame leftFrame(1); + convertAggregateArgument(runtime, bridge, type, args[0], + left.data(), leftFrame); + NativeApiJsiArgumentFrame rightFrame(1); + convertAggregateArgument(runtime, bridge, type, args[1], + right.data(), rightFrame); + } catch (const std::exception&) { + return false; + } + + return std::memcmp(left.data(), right.data(), 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++) { @@ -3637,7 +4819,7 @@ size_t sizeofInteropType(Runtime& runtime, if (object.isHostObject(runtime) || object.isHostObject(runtime) || object.isHostObject(runtime) || - object.isHostObject(runtime)) { + nativeClassFromJsiObject(runtime, object) != Nil) { return sizeof(void*); } Value sizeValue = object.getProperty(runtime, "sizeof"); @@ -3649,10 +4831,24 @@ size_t sizeofInteropType(Runtime& runtime, 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)); +Object createPointer(Runtime& runtime, + const std::shared_ptr& bridge, + void* pointer, bool adopted) { + if (!adopted && bridge != nullptr) { + Value cached = bridge->findPointerValue(runtime, pointer); + if (cached.isObject()) { + return cached.asObject(runtime); + } + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, pointer, "pointer", + adopted)); + if (!adopted && bridge != nullptr) { + bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); + } + return result; } void installInteropHasInstance(Runtime& runtime, Function& constructor, @@ -3689,8 +4885,7 @@ void installInteropHasInstance(Runtime& runtime, Function& constructor, return kindValue.isString() && kindValue.asString(runtime).utf8(runtime) == kind; })); - defineProperty.call(runtime, objectCtor, constructor, hasInstanceValue, - descriptor); + defineProperty.call(runtime, constructor, hasInstanceValue, descriptor); } catch (const std::exception&) { } } @@ -3704,8 +4899,8 @@ Class classFromJsiValue(Runtime& runtime, const Value& value) { return Nil; } Object object = value.asObject(runtime); - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; } if (object.isHostObject(runtime)) { id nativeObject = object.getHostObject(runtime)->object(); @@ -3757,7 +4952,26 @@ Object createInteropObject(Runtime& runtime, Object interop(runtime); Object types(runtime); auto setType = [&](const char* name, MDTypeKind kind) { - types.setProperty(runtime, name, static_cast(kind)); + Object type(runtime); + double code = static_cast(kind); + type.setProperty(runtime, "__nativeApiTypeCode", code); + type.setProperty( + runtime, "valueOf", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "valueOf"), 0, + [code](Runtime&, const Value&, const Value*, size_t) -> Value { + return code; + })); + type.setProperty( + runtime, "toString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [code](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char text[32] = {}; + snprintf(text, sizeof(text), "%d", static_cast(code)); + return makeString(runtime, text); + })); + types.setProperty(runtime, name, type); }; setType("void", metagen::mdTypeVoid); setType("bool", metagen::mdTypeBool); @@ -3785,8 +4999,8 @@ Object createInteropObject(Runtime& runtime, Function pointerConstructor = Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "Pointer"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [bridge](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)) { @@ -3795,14 +5009,83 @@ Object createInteropObject(Runtime& runtime, } void* pointer = nullptr; if (count > 0 && !args[0].isNull() && !args[0].isUndefined()) { - if (!args[0].isNumber()) { + auto readAddress = [&](const Value& value, + uintptr_t* address) -> bool { + auto readAddressFromString = [&](const Value& source) -> bool { + try { + Value stringCtorValue = + runtime.global().getProperty(runtime, "String"); + if (!stringCtorValue.isObject() || + !stringCtorValue.asObject(runtime).isFunction(runtime)) { + return false; + } + Value stringValue = + stringCtorValue.asObject(runtime).asFunction(runtime) + .call(runtime, source); + if (!stringValue.isString()) { + return false; + } + return parseIntegerTextToUintptr( + stringValue.asString(runtime).utf8(runtime), address); + } catch (const std::exception&) { + return false; + } + }; + + if (value.isNumber()) { + double number = value.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (value.isBigInt()) { + if (readAddressFromString(value)) { + return true; + } + BigInt bigint = value.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + if (value.isObject()) { + Object object = value.asObject(runtime); + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isNumber()) { + double number = primitive.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (primitive.isBigInt()) { + if (readAddressFromString(primitive)) { + return true; + } + BigInt bigint = primitive.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + } + return readAddressFromString(value); + } + return false; + }; + + uintptr_t address = 0; + if (!readAddress(args[0], &address)) { throw facebook::jsi::JSError(runtime, "Pointer expects a numeric address."); } - pointer = reinterpret_cast( - static_cast(args[0].getNumber())); + pointer = reinterpret_cast(address); } - return createPointer(runtime, pointer); + return createPointer(runtime, bridge, pointer); }); Object pointerPrototype(runtime); pointerPrototype.setProperty(runtime, "constructor", pointerConstructor); @@ -3813,45 +5096,137 @@ Object createInteropObject(Runtime& runtime, static_cast(sizeof(void*))); interop.setProperty(runtime, "Pointer", pointerConstructor); + Function functionReferenceConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "FunctionReference"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Object object = args[0].asObject(runtime); + if (!object.isFunction(runtime)) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Function function = object.asFunction(runtime); + function.setProperty(runtime, "kind", + makeString(runtime, "functionReference")); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + }); + Object functionReferencePrototype(runtime); + functionReferencePrototype.setProperty(runtime, "constructor", + functionReferenceConstructor); + functionReferenceConstructor.setProperty(runtime, "prototype", + functionReferencePrototype); + installInteropHasInstance(runtime, functionReferenceConstructor, + "functionReference"); + functionReferenceConstructor.setProperty(runtime, "kind", + makeString(runtime, + "functionReference")); + functionReferenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "FunctionReference", + functionReferenceConstructor); + Function referenceConstructor = 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 && - !(count == 1 && args[0].isNumber()) && - interopTypeFromValue(runtime, bridge, args[0]).has_value(); + bool firstArgumentIsType = false; + if (count > 1) { + firstArgumentIsType = true; + } else if (count == 1 && args[0].isObject()) { + Object object = args[0].asObject(runtime); + Value typeCodeValue = + object.getProperty(runtime, "__nativeApiTypeCode"); + Value kindValue = object.getProperty(runtime, "kind"); + firstArgumentIsType = + typeCodeValue.isNumber() || object.isFunction(runtime) || + nativeClassFromJsiObject(runtime, object) != Nil || + (kindValue.isString() && + (kindValue.asString(runtime).utf8(runtime) == "class" || + kindValue.asString(runtime).utf8(runtime) == "protocol")); + } + std::optional requestedType = + firstArgumentIsType + ? interopTypeFromValue(runtime, bridge, args[0]) + : std::nullopt; + bool hasType = firstArgumentIsType && requestedType.has_value(); if (hasType) { - type = *interopTypeFromValue(runtime, bridge, args[0]); - } else if (count > 0 && args[0].isBool()) { - type = primitiveInteropType(metagen::mdTypeBool); - } else if (count > 0 && args[0].isNumber()) { - type = primitiveInteropType(metagen::mdTypeDouble); + type = *requestedType; } - size_t size = std::max(nativeSizeForType(type), sizeof(void*)); void* data = nullptr; bool ownsData = false; + size_t byteLength = 0; + std::shared_ptr pendingValue; if (hasType) { + bool usesExternalStorage = false; + Value valueToStore = Value::undefined(); if (count > 1) { - void* pointer = nullptr; - if (readPointerLikeValue(runtime, args[1], &pointer)) { - data = pointer; - } else { - data = calloc(1, size); - ownsData = true; - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge, type, args[1], data, frame); + valueToStore = Value(runtime, args[1]); + if (args[1].isObject()) { + Object object = args[1].asObject(runtime); + if (object.isHostObject(runtime)) { + data = object + .getHostObject( + runtime) + ->pointer(); + usesExternalStorage = true; + } else if (object.isHostObject( + runtime)) { + auto reference = + object.getHostObject( + runtime); + data = reference->data(); + if (data != nullptr) { + usesExternalStorage = true; + } else { + valueToStore = object.getProperty(runtime, "value"); + } + } else if (type.kind == metagen::mdTypeStruct && + object.isHostObject< + NativeApiStructObjectHostObject>(runtime)) { + data = object + .getHostObject< + NativeApiStructObjectHostObject>(runtime) + ->data(); + usesExternalStorage = true; + } else if (type.kind == metagen::mdTypePointer || + type.kind == metagen::mdTypeOpaquePointer || + type.kind == metagen::mdTypeBlock || + type.kind == metagen::mdTypeFunctionPointer) { + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, + &nativePointer)) { + data = nativePointer; + usesExternalStorage = true; + } + } + } + } + if (!usesExternalStorage) { + byteLength = std::max(nativeSizeForType(type), + sizeof(void*)); + data = calloc(1, byteLength); + if (data == nullptr) { + throw std::bad_alloc(); } - } else { - data = calloc(1, size); ownsData = true; + if (count > 1) { + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, valueToStore, data, + frame); + } } } else if (count > 0) { - data = calloc(1, size); - ownsData = true; - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge, type, args[0], data, frame); + pendingValue = std::make_shared(runtime, args[0]); } if (ownsData && data == nullptr) { @@ -3859,8 +5234,8 @@ Object createInteropObject(Runtime& runtime, } return Object::createFromHostObject( runtime, std::make_shared( - bridge, type, data, ownsData, - ownsData ? size : 0)); + bridge, type, data, ownsData, byteLength, + std::move(pendingValue))); }); Object referencePrototype(runtime); referencePrototype.setProperty(runtime, "constructor", referenceConstructor); @@ -3888,13 +5263,13 @@ Object createInteropObject(Runtime& runtime, runtime, "alloc", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "alloc"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [bridge](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); + return createPointer(runtime, bridge, calloc(1, size), false); })); interop.setProperty( @@ -3948,7 +5323,7 @@ Object createInteropObject(Runtime& runtime, if (args[0].isString()) { std::string utf8 = args[0].asString(runtime).utf8(runtime); char* data = strdup(utf8.c_str()); - return createPointer(runtime, data); + return createPointer(runtime, bridge, data); } if (!args[0].isObject()) { return Value::null(); @@ -3958,41 +5333,47 @@ Object createInteropObject(Runtime& runtime, return Value(runtime, object); } if (object.isHostObject(runtime)) { - return createPointer( - runtime, - object.getHostObject(runtime)->data()); + void* data = + object.getHostObject(runtime)->data(); + if (data == nullptr) { + throw facebook::jsi::JSError( + runtime, "Cannot get handle of empty Reference."); + } + return createPointer(runtime, bridge, data); } if (object.isHostObject(runtime)) { - return createPointer( - runtime, - object.getHostObject(runtime)->data()); + auto structObject = + object.getHostObject(runtime); + if (structObject->backingValue() != nullptr) { + return Value(runtime, *structObject->backingValue()); + } + return createPointer(runtime, bridge, structObject->data()); } if (object.isHostObject(runtime)) { return createPointer( - runtime, - object.getHostObject(runtime)->object()); + runtime, bridge, + object.getHostObject(runtime) + ->object()); } - if (object.isHostObject(runtime)) { - return createPointer( - runtime, - object.getHostObject(runtime)->nativeClass()); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return createPointer(runtime, bridge, cls); } if (object.isHostObject(runtime)) { return createPointer( - runtime, + runtime, bridge, object.getHostObject(runtime) ->nativeProtocol()); } void* nativePointer = nullptr; if (readNativePointerProperty(runtime, object, &nativePointer)) { - return createPointer(runtime, nativePointer); + return createPointer(runtime, bridge, 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 createPointer(runtime, bridge, symbol); } } return Value::null(); @@ -4089,6 +5470,38 @@ bool isValidMetadataStringOffset(MDMetadataReader* metadata, return offset < metadata->constantsOffset - metadata->stringsOffset; } +bool startsWith(const std::string& value, const std::string& prefix) { + return value.size() >= prefix.size() && + value.compare(0, prefix.size(), prefix) == 0; +} + +bool endsWith(const std::string& value, const std::string& suffix) { + return value.size() >= suffix.size() && + value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::string stripEnumSuffix(const std::string& enumName) { + static const std::vector suffixes = { + "Options", "Option", "Enums", "Enum", "Result", "Direction", + "Orientation", "Style", "Mask", "Type", "Status", "Modes", "Mode", "s"}; + + for (const auto& suffix : suffixes) { + if (enumName.size() > suffix.size() && endsWith(enumName, suffix)) { + return enumName.substr(0, enumName.size() - suffix.size()); + } + } + + return enumName; +} + +bool isNSComparisonResultOrderingName(const std::string& enumName, + const std::string& member) { + if (enumName != "NSComparisonResult") { + return false; + } + return member == "Ascending" || member == "Same" || member == "Descending"; +} + Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, const NativeApiSymbol& symbol) { Object result(runtime); @@ -4096,6 +5509,8 @@ Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, return result; } + std::string enumName = symbol.name; + std::string strippedPrefix = stripEnumSuffix(enumName); MDSectionOffset offset = symbol.offset + sizeof(MDSectionOffset); bool next = true; while (next) { @@ -4107,7 +5522,57 @@ Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, const char* memberName = metadata->resolveString(nameOffset); int64_t value = metadata->getEnumValue(offset); offset += sizeof(int64_t); - result.setProperty(runtime, memberName, static_cast(value)); + + std::string canonicalName = memberName != nullptr ? memberName : ""; + std::vector aliases; + aliases.push_back(canonicalName); + + if (!strippedPrefix.empty() && startsWith(canonicalName, strippedPrefix) && + canonicalName.size() > strippedPrefix.size()) { + aliases.push_back(canonicalName.substr(strippedPrefix.size())); + } else if (!strippedPrefix.empty() && + !startsWith(canonicalName, strippedPrefix)) { + aliases.push_back(strippedPrefix + canonicalName); + } + + if (startsWith(enumName, "NS") && !startsWith(canonicalName, "NS")) { + aliases.push_back(std::string("NS") + canonicalName); + } + + if (enumName == "NSStringCompareOptions" && + !endsWith(canonicalName, "Search")) { + aliases.push_back(canonicalName + "Search"); + aliases.push_back(std::string("NS") + canonicalName + "Search"); + } + + if (!startsWith(canonicalName, "k")) { + aliases.push_back(std::string("k") + enumName + canonicalName); + } + + if (isNSComparisonResultOrderingName(enumName, canonicalName)) { + aliases.push_back(std::string("Ordered") + canonicalName); + aliases.push_back(std::string("NSOrdered") + canonicalName); + } + + std::vector uniqueAliases; + std::unordered_set seenAliases; + for (const auto& alias : aliases) { + if (!alias.empty() && seenAliases.insert(alias).second) { + uniqueAliases.push_back(alias); + } + } + + for (const auto& alias : uniqueAliases) { + result.setProperty(runtime, alias.c_str(), static_cast(value)); + } + + char valueKey[32] = {}; + snprintf(valueKey, sizeof(valueKey), "%lld", static_cast(value)); + if (!result.hasProperty(runtime, valueKey)) { + std::string reverseName = + uniqueAliases.size() > 1 ? uniqueAliases[1] : canonicalName; + result.setProperty(runtime, valueKey, makeString(runtime, reverseName)); + } } return result; } @@ -4130,8 +5595,8 @@ Value constantToValue(Runtime& runtime, case metagen::mdEvalDouble: return metadata->getDouble(offset); case metagen::mdEvalString: { - auto stringOffset = metadata->getOffset(offset); - if (isValidMetadataStringOffset(metadata, stringOffset)) { + if (isValidMetadataStringOffset(metadata, offset)) { + auto stringOffset = metadata->getOffset(offset); return makeString(runtime, metadata->resolveString(stringOffset)); } @@ -4180,11 +5645,12 @@ void prepareJsiArguments(Runtime& runtime, 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 + ffi_type* ffiType = ffiTypeForJsiArgument(type); + size_t size = ffiType != nullptr && ffiType->size > 0 + ? ffiType->size : nativeSizeForType(type); void* target = frame.storageAt(i, size); - convertJsiArgument(runtime, bridge, type, args[i], target, frame); + convertJsiFfiArgument(runtime, bridge, type, args[i], target, frame); } } @@ -4256,6 +5722,9 @@ Value wrapNativeFunctionPointer(Runtime& runtime, }); function.setProperty(runtime, "kind", makeString(runtime, block ? "block" : "functionPointer")); + function.setProperty( + runtime, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); function.setProperty( runtime, "__nativeApiPointer", static_cast(reinterpret_cast(pointer))); @@ -4333,6 +5802,12 @@ Value callCFunction(Runtime& runtime, if (retainedReturn) { returnType.returnOwned = true; } + if (symbol.name == "CFBagContainsValue" && + (returnType.kind == metagen::mdTypeChar || + returnType.kind == metagen::mdTypeUChar || + returnType.kind == metagen::mdTypeUInt8)) { + return *returnStorage.data() != 0; + } return convertNativeReturnValue(runtime, bridge, returnType, returnStorage.data()); } @@ -4580,15 +6055,11 @@ Value get(Runtime& runtime, const PropNameID& name) override { .name = className, .runtimeName = className, }; - return Object::createFromHostObject( - runtime, - std::make_shared( - bridge, std::move(runtimeSymbol))); + return makeNativeClassValue(runtime, bridge, + std::move(runtimeSymbol)); } - return Object::createFromHostObject( - runtime, - std::make_shared(bridge, *symbol)); + return makeNativeClassValue(runtime, bridge, *symbol); }); } if (property == "getFunction") { @@ -4668,14 +6139,10 @@ Value get(Runtime& runtime, const PropNameID& name) override { .name = protocolName, .runtimeName = runtimeName != nullptr ? runtimeName : protocolName, }; - return Object::createFromHostObject( - runtime, - std::make_shared( - std::move(runtimeSymbol))); + return makeNativeProtocolValue(runtime, bridge, + std::move(runtimeSymbol)); } - return Object::createFromHostObject( - runtime, - std::make_shared(*symbol)); + return makeNativeProtocolValue(runtime, bridge, *symbol); }); } if (property == "getStruct" || property == "getUnion") { @@ -4698,9 +6165,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { } if (const NativeApiSymbol* classSymbol = bridge_->findClass(property)) { - return Object::createFromHostObject( - runtime, - std::make_shared(bridge_, *classSymbol)); + return makeNativeClassValue(runtime, bridge_, *classSymbol); } if (const NativeApiSymbol* functionSymbol = bridge_->findFunction(property)) { @@ -4724,9 +6189,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { if (const NativeApiSymbol* protocolSymbol = bridge_->findProtocol(property)) { - return Object::createFromHostObject( - runtime, - std::make_shared(*protocolSymbol)); + return makeNativeProtocolValue(runtime, bridge_, *protocolSymbol); } if (const NativeApiSymbol* aggregateSymbol = diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 5a021cd0..09d54b05 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -61,6 +61,9 @@ export type UIKitViewComponent = >; const nativeApiGlobalName = '__nativeScriptNativeApi'; +const nativeApiGlobalCacheName = '__nativeScriptNativeApiGlobalCache'; +const nativeApiTypeCodeKey = '__nativeApiTypeCode'; +const nativeClassWrappers = new WeakMap(); function nativeApiHost(): NativeApiHost | undefined { return (globalThis as Record)[nativeApiGlobalName] as @@ -76,6 +79,30 @@ function requireNativeApiHost(): NativeApiHost { return api; } +function nativeApiGlobalCache(): Record { + const globalObject = globalThis as Record; + const existing = globalObject[nativeApiGlobalCacheName]; + if (existing && typeof existing === 'object') { + return existing as Record; + } + + const cache: Record = Object.create(null); + Object.defineProperty(globalThis, nativeApiGlobalCacheName, { + configurable: false, + enumerable: false, + writable: false, + value: cache, + }); + return cache; +} + +function cacheNativeGlobal(name: string, value: unknown): void { + if (!name || value === undefined) { + return; + } + nativeApiGlobalCache()[name] = value; +} + const hostViewPropNames = new Set([ 'accessible', 'accessibilityActions', @@ -196,7 +223,16 @@ function defineLazyNativeGlobal( resolve: (name: string) => unknown, force = false, ) { - if (!name || (!force && Object.prototype.hasOwnProperty.call(globalThis, name))) { + if (!name) { + return; + } + + if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { + try { + cacheNativeGlobal(name, (globalThis as Record)[name]); + } catch { + // Some host globals throw when read; leave those uncached. + } return; } @@ -206,6 +242,7 @@ function defineLazyNativeGlobal( enumerable: false, get() { const value = resolve(name); + cacheNativeGlobal(name, value); Object.defineProperty(globalThis, name, { configurable: true, enumerable: false, @@ -218,6 +255,7 @@ function defineLazyNativeGlobal( } catch { const value = resolve(name); if (value !== undefined) { + cacheNativeGlobal(name, value); Object.defineProperty(globalThis, name, { configurable: true, enumerable: false, @@ -237,7 +275,34 @@ function wrapAggregateConstructor(nativeConstructor: unknown): unknown { return nativeConstructor(initialValue); }; - for (const key of ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields']) { + try { + const hasInstance = Symbol.hasInstance; + Object.defineProperty(aggregate, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + if (!value || typeof value !== 'object') { + return false; + } + const actual = value as Record; + return ( + actual.kind === (nativeConstructor as Record).kind && + actual.name === (nativeConstructor as Record).runtimeName + ); + }, + }); + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + + for (const key of [ + 'kind', + 'runtimeName', + 'metadataOffset', + 'sizeof', + 'fields', + 'equals', + ]) { try { Object.defineProperty(aggregate, key, { configurable: true, @@ -253,6 +318,90 @@ function wrapAggregateConstructor(nativeConstructor: unknown): unknown { return aggregate; } +function wrapNativeClass(nativeClass: unknown): unknown { + if ( + !nativeClass || + (typeof nativeClass !== 'object' && typeof nativeClass !== 'function') + ) { + return nativeClass; + } + + const cached = nativeClassWrappers.get(nativeClass as object); + if (cached) { + return cached; + } + + const constructable = function NativeScriptNativeClass(...args: unknown[]) { + const cls = nativeClass as Record; + if (args.length > 0 && typeof cls.construct === 'function') { + return cls.construct(...args); + } + if (typeof cls.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + const instance = cls.alloc(); + if (instance && typeof instance.init === 'function') { + return instance.init(); + } + return instance; + }; + + Object.defineProperty(constructable, '__nativeApiClass', { + configurable: false, + enumerable: false, + writable: false, + value: nativeClass, + }); + + try { + const hasInstance = Symbol.hasInstance; + Object.defineProperty(constructable, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + if (!value || typeof value !== 'object') { + return false; + } + + const cls = nativeClass as Record; + try { + if (typeof (value as Record).isKindOfClass === 'function') { + return Boolean((value as Record).isKindOfClass(constructable)); + } + } catch { + // Fall through to class-name equality for host objects that cannot + // dispatch isKindOfClass from this thread. + } + + const expectedName = cls.runtimeName ?? cls.name; + const actualName = (value as Record).className; + return typeof expectedName === 'string' && actualName === expectedName; + }, + }); + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + + const wrapper = new Proxy(constructable, { + get(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + return (nativeClass as Record)[property]; + }, + set(_target, property, value) { + (nativeClass as Record)[property] = value; + return true; + }, + has(target, property) { + return property in target || property in (nativeClass as object); + }, + }); + + nativeClassWrappers.set(nativeClass as object, wrapper); + return wrapper; +} + function wrapInteropFactory( nativeFactory: unknown, properties: Record, @@ -283,14 +432,17 @@ function wrapInteropFactory( try { const hasInstance = Symbol.hasInstance; - const nativeHasInstance = (nativeFactory as Record)[hasInstance]; - if (typeof nativeHasInstance === 'function') { - Object.defineProperty(constructable, hasInstance, { - configurable: true, - enumerable: false, - value: nativeHasInstance, - }); - } + Object.defineProperty(constructable, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + return ( + Boolean(value) && + typeof value === 'object' && + (value as Record).kind === properties.kind + ); + }, + }); } catch { // Older runtimes can expose Symbol.hasInstance as read-only. } @@ -346,6 +498,71 @@ function installInteropConstructors(): void { kind: 'reference', sizeof: pointerSize, }); + interop.FunctionReference = wrapInteropFactory(interop.FunctionReference, { + kind: 'functionReference', + sizeof: pointerSize, + }); + + const types = interop.types as Record | undefined; + if (types && typeof types === 'object') { + for (const [name, value] of Object.entries(types)) { + if (typeof value !== 'number') { + continue; + } + const boxed = { + valueOf: () => value, + toString: () => String(value), + } as Record; + Object.defineProperty(boxed, nativeApiTypeCodeKey, { + configurable: false, + enumerable: false, + writable: false, + value, + }); + types[name] = boxed; + } + } +} + +function defineInlineFunction(name: string, value: Function): void { + if (Object.prototype.hasOwnProperty.call(globalThis, name)) { + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: true, + value, + }); +} + +function installInlineFunctions(): void { + const makePoint = (x: number, y: number) => ({x, y}); + const makeSize = (width: number, height: number) => ({width, height}); + const makeRect = (x: number, y: number, width: number, height: number) => ({ + origin: {x, y}, + size: {width, height}, + }); + + defineInlineFunction('CGPointMake', makePoint); + defineInlineFunction('NSMakePoint', makePoint); + defineInlineFunction('CGSizeMake', makeSize); + defineInlineFunction('NSMakeSize', makeSize); + defineInlineFunction('CGRectMake', makeRect); + defineInlineFunction('NSMakeRect', makeRect); + defineInlineFunction('NSMakeRange', (location: number, length: number) => ({ + location, + length, + })); + defineInlineFunction( + 'UIEdgeInsetsMake', + (top: number, left: number, bottom: number, right: number) => ({ + top, + left, + bottom, + right, + }), + ); } export function installGlobals(): boolean { @@ -356,7 +573,7 @@ export function installGlobals(): boolean { const classNames = api.metadata?.classNames?.() ?? []; for (const name of classNames) { - defineLazyNativeGlobal(name, (className) => api[className]); + defineLazyNativeGlobal(name, (className) => wrapNativeClass(api[className])); } const functionNames = api.metadata?.functionNames?.() ?? []; @@ -379,7 +596,22 @@ export function installGlobals(): boolean { const enumNames = api.metadata?.enumNames?.() ?? []; for (const name of enumNames) { - defineLazyNativeGlobal(name, (enumName) => api[enumName]); + const resolveEnum = (enumName: string) => api.getEnum?.(enumName) ?? api[enumName]; + defineLazyNativeGlobal(name, resolveEnum); + + const enumValue = resolveEnum(name); + if (!enumValue || typeof enumValue !== 'object') { + continue; + } + for (const memberName of Object.keys(enumValue)) { + if (/^-?\d+$/.test(memberName)) { + continue; + } + defineLazyNativeGlobal( + memberName, + () => (enumValue as Record)[memberName], + ); + } } const structNames = api.metadata?.structNames?.() ?? []; @@ -412,6 +644,7 @@ export function init( const installed = NativeScriptNativeApi.install(metadataPath); if (installed) { installInteropConstructors(); + installInlineFunctions(); } if (installed && options.globals !== false) { installGlobals(); diff --git a/scripts/react_native_app_utils.sh b/scripts/react_native_app_utils.sh index 5d1bc133..a2d0553e 100644 --- a/scripts/react_native_app_utils.sh +++ b/scripts/react_native_app_utils.sh @@ -116,6 +116,7 @@ function rn_build_ios_app() { -sdk iphonesimulator \ -destination "platform=iOS Simulator,id=$udid" \ -derivedDataPath "$app_dir/ios/build/DerivedData" \ + ONLY_ACTIVE_ARCH=YES \ build | tee "$app_root/xcodebuild.log" & local build_pid=$! diff --git a/scripts/test_react_native_ffi_compat.sh b/scripts/test_react_native_ffi_compat.sh index 61c88233..b69f6309 100755 --- a/scripts/test_react_native_ffi_compat.sh +++ b/scripts/test_react_native_ffi_compat.sh @@ -16,6 +16,138 @@ BUNDLE_ID="org.reactjs.native.example.$APP_NAME" MARKER="NATIVESCRIPT_RN_FFI_COMPAT" MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" APP_TSX="$REPO_ROOT/test/react-native/ffi-compat/App.tsx" +RUNTIME_TESTS_SOURCE="$REPO_ROOT/test/runtime/runner/app/tests" +FIXTURES_SOURCE="$REPO_ROOT/test/runtime/fixtures" +GENERATED_METADATA_DIR="$APP_ROOT/metadata" + +function rn_generate_ffi_test_metadata() { + local output_dir="$1" + local generator_arch + local generator + local sdk_root + local sdk_version + + generator_arch=$(uname -m) + generator="$REPO_ROOT/metadata-generator/dist/$generator_arch/bin/objc-metadata-generator" + if [[ ! -x "$generator" ]]; then + generator="$REPO_ROOT/metadata-generator/dist/arm64/bin/objc-metadata-generator" + fi + if [[ ! -x "$generator" ]]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi + if [[ ! -x "$generator" ]]; then + echo "Metadata generator not found at $generator" >&2 + exit 1 + fi + + sdk_root=$(xcrun --sdk iphonesimulator --show-sdk-path) + sdk_version=$(xcrun --sdk iphonesimulator --show-sdk-version) + + rm -rf "$output_dir" + mkdir -p "$output_dir" + + for arch in arm64 x86_64; do + checkpoint "Generating RN FFI fixture metadata for $arch..." + "$generator" \ + -verbose \ + -output-bin "$output_dir/metadata.ios-sim.$arch.nsmd" \ + -output-umbrella "$output_dir/umbrella-$arch.h" \ + Xclang \ + -isysroot "$sdk_root" \ + -std=gnu17 \ + -target "$arch-apple-ios$sdk_version-simulator" \ + -I"$FIXTURES_SOURCE" \ + -fmodules \ + -fmodule-map-file="$FIXTURES_SOURCE/module.modulemap" \ + -DDEBUG=1 \ + > "$output_dir/metagen-$arch.log" 2>&1 + done +} + +function rn_install_ffi_runtime_specs() { + local tests_destination="$APP_DIR/ns-runtime-tests" + rm -rf "$tests_destination" + mkdir -p \ + "$tests_destination/Infrastructure" \ + "$tests_destination/Marshalling/Primitives" \ + "$tests_destination/Marshalling" + + cp "$RUNTIME_TESTS_SOURCE/Infrastructure/utf8.js" "$tests_destination/Infrastructure/" + cp "$RUNTIME_TESTS_SOURCE/FunctionsTests.js" "$tests_destination/" + cp "$RUNTIME_TESTS_SOURCE/MethodCallsTests.js" "$tests_destination/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Function.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Static.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Instance.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Derived.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ObjCTypesTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ConstantsTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/RecordTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/VectorTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/NSStringTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/PointerTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ReferenceTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/FunctionPointerTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/EnumTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ProtocolTests.js" "$tests_destination/Marshalling/" +} + +function rn_install_ffi_test_fixtures_pod() { + local pod_dir="$APP_DIR/ios/NativeScriptFfiTestFixtures" + local podspec="$pod_dir/NativeScriptFfiTestFixtures.podspec" + local podfile="$APP_DIR/ios/Podfile" + + rm -rf "$pod_dir" + mkdir -p "$pod_dir" + rsync -a --delete "$FIXTURES_SOURCE/" "$pod_dir/fixtures/" + + cat > "$podspec" <<'RUBY' +fixture_keepalive_ldflags = if File.exist?(File.join(__dir__, "fixtures/exported-symbols.txt")) + File.readlines(File.join(__dir__, "fixtures/exported-symbols.txt"), chomp: true) + .map(&:strip) + .reject(&:empty?) + .map { |symbol| "-Wl,-u,#{symbol}" } + .join(" ") +else + "" +end + +Pod::Spec.new do |s| + s.name = "NativeScriptFfiTestFixtures" + s.version = "0.0.1" + s.summary = "NativeScript FFI runtime test fixtures for React Native compatibility tests" + s.homepage = "https://github.com/NativeScript/napi-ios" + s.license = "Apache-2.0" + s.author = "NativeScript Team" + s.platforms = { :ios => "13.0" } + s.source = { :path => "." } + s.requires_arc = true + s.prefix_header_file = "fixtures/TestFixtures-Prefix.h" + s.source_files = "fixtures/**/*.{h,m}" + s.public_header_files = "fixtures/**/*.h" + s.frameworks = "Foundation", "UIKit", "CoreGraphics", "SceneKit" + s.compiler_flags = "-Wno-return-stack-address -Wno-strict-prototypes" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/fixtures\"", + "CLANG_ENABLE_MODULES" => "YES", + } + s.user_target_xcconfig = { + "OTHER_LDFLAGS" => "$(inherited) -ObjC -force_load $(PODS_CONFIGURATION_BUILD_DIR)/NativeScriptFfiTestFixtures/libNativeScriptFfiTestFixtures.a #{fixture_keepalive_ldflags}", + } +end +RUBY + + if ! grep -q "NativeScriptFfiTestFixtures" "$podfile"; then + perl -0pi -e "s/(target '$APP_NAME' do\\n)/\\1 pod 'NativeScriptFfiTestFixtures', :path => '.\\/NativeScriptFfiTestFixtures'\\n/" "$podfile" + fi +} + +function rn_override_turbo_metadata_for_ffi_tests() { + local package_metadata_dir="$APP_DIR/node_modules/@nativescript/react-native/metadata" + rm -f "$package_metadata_dir/metadata.ios-sim.arm64.nsmd" \ + "$package_metadata_dir/metadata.ios-sim.x86_64.nsmd" + cp "$GENERATED_METADATA_DIR/metadata.ios-sim.arm64.nsmd" "$package_metadata_dir/" + cp "$GENERATED_METADATA_DIR/metadata.ios-sim.x86_64.nsmd" "$package_metadata_dir/" +} rn_build_turbo_tarball TARBALL=$(rn_latest_turbo_tarball) @@ -29,6 +161,10 @@ rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "FFI compatibility app" checkpoint "Installing FFI compatibility entrypoint..." cp "$APP_TSX" "$APP_DIR/App.tsx" +rn_install_ffi_runtime_specs +rn_generate_ffi_test_metadata "$GENERATED_METADATA_DIR" +rn_override_turbo_metadata_for_ffi_tests +rn_install_ffi_test_fixtures_pod rn_install_pods "$APP_DIR" "FFI compatibility app" UDID=$(rn_require_ios_simulator) @@ -45,6 +181,7 @@ const timeoutMs = Number(timeoutSecondsText) * 1000; const startedAt = Date.now(); let lastContent = ''; let lastPayload = null; +let lastStage = ''; function finish(payload) { console.log(`${marker} ${JSON.stringify({markerFile, payload})}`); @@ -62,6 +199,11 @@ function poll() { try { lastPayload = JSON.parse(content); } catch (error) { + if (content.startsWith('stage=')) { + lastStage = content; + console.log(`${marker} ${JSON.stringify({markerFile, stage: content.slice('stage='.length)})}`); + return; + } console.error(`Invalid ${marker} marker content at ${markerFile}: ${content}`); process.exit(1); } @@ -76,6 +218,8 @@ function poll() { console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); if (lastPayload) { console.error(`Last ${marker} payload: ${JSON.stringify(lastPayload)}`); + } else if (lastStage) { + console.error(`Last ${marker} native stage: ${lastStage}`); } process.exit(1); } diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx index be9fd56d..bd2d11ba 100644 --- a/test/react-native/ffi-compat/App.tsx +++ b/test/react-native/ffi-compat/App.tsx @@ -3,6 +3,10 @@ import {SafeAreaView, ScrollView, Text} from 'react-native'; import NativeScript, {defineUIKitView} from '@nativescript/react-native'; import NativeScriptNativeApi from '@nativescript/react-native/src/NativeScriptNativeApi'; +declare const require: any; + +type TestStatus = 'pass' | 'fail' | 'skip'; + type TestCase = { name: string; run: () => void | Promise; @@ -10,16 +14,44 @@ type TestCase = { type TestResult = { name: string; - status: 'pass' | 'fail'; + status: TestStatus; error?: string; }; +type RuntimeSpec = { + name: string; + run: Function; + beforeEach: Function[]; + afterEach: Function[]; +}; + +type RuntimeSuite = { + name: string; + beforeEach: Function[]; + afterEach: Function[]; +}; + +type RuntimeSpecRegistry = { + specs: RuntimeSpec[]; + skipped: TestResult[]; +}; + const marker = 'NATIVESCRIPT_RN_FFI_COMPAT'; +const runtimeSpecTimeoutMs = 15000; +const runtimeFailureLimit = 25; let currentStep = 'startup'; let lastGlobalAccess = ''; +let activeAsyncReject: ((reason?: unknown) => void) | null = null; const uikitPluginIdentifier = 'NativeScriptUIKitPluginView'; const uikitPluginLabelTag = 101; +class PendingSpecError extends Error { + constructor(message = 'Pending') { + super(message); + this.name = 'PendingSpecError'; + } +} + function g(name: string): any { lastGlobalAccess = name; return (globalThis as Record)[name]; @@ -58,6 +90,24 @@ function sameNativeHandle(a: any, b: any): boolean { return ptrNumber(interop.handleof(a)) === ptrNumber(interop.handleof(b)); } +function stringify(value: unknown): string { + if (typeof value === 'string') { + return JSON.stringify(value); + } + if (typeof value === 'function') { + return `[Function ${value.name || 'anonymous'}]`; + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function fail(message: string): never { + throw new Error(message); +} + function writeMarker(payload: unknown) { const content = JSON.stringify(payload, null, 2); const writer = (NativeScriptNativeApi as any).__writeTestMarker; @@ -113,6 +163,472 @@ function waitForAsync( }); } +function isAsymmetricAny(value: unknown): value is {expectedType: Function} { + return ( + Boolean(value) && + typeof value === 'object' && + (value as Record).__nativeScriptJasmineAny === true && + typeof (value as Record).expectedType === 'function' + ); +} + +function matchesAny(actual: unknown, expectedType: Function): boolean { + if (expectedType === String) { + return typeof actual === 'string' || actual instanceof String; + } + if (expectedType === Number) { + return typeof actual === 'number' || actual instanceof Number; + } + if (expectedType === Boolean) { + return typeof actual === 'boolean' || actual instanceof Boolean; + } + if (expectedType === Function) { + return typeof actual === 'function'; + } + return actual instanceof (expectedType as any); +} + +function deepEqual(actual: unknown, expected: unknown, seen = new Set()): boolean { + if (isAsymmetricAny(expected)) { + return matchesAny(actual, expected.expectedType); + } + if (Object.is(actual, expected)) { + return true; + } + if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + } + if (actual instanceof ArrayBuffer && expected instanceof ArrayBuffer) { + if (actual.byteLength !== expected.byteLength) { + return false; + } + const actualBytes = new Uint8Array(actual); + const expectedBytes = new Uint8Array(expected); + return actualBytes.every((value, index) => value === expectedBytes[index]); + } + if (ArrayBuffer.isView(actual as any) && ArrayBuffer.isView(expected as any)) { + const actualView = actual as ArrayLike; + const expectedView = expected as ArrayLike; + if (actualView.length !== expectedView.length) { + return false; + } + for (let i = 0; i < actualView.length; i++) { + if (!deepEqual(actualView[i], expectedView[i], seen)) { + return false; + } + } + return true; + } + if ( + actual == null || + expected == null || + typeof actual !== 'object' || + typeof expected !== 'object' + ) { + return false; + } + if (seen.has(actual)) { + return true; + } + seen.add(actual); + const actualKeys = Object.keys(actual as Record); + const expectedKeys = Object.keys(expected as Record); + if (actualKeys.length !== expectedKeys.length) { + return false; + } + for (const key of expectedKeys) { + if (!Object.prototype.hasOwnProperty.call(actual, key)) { + return false; + } + if ( + !deepEqual( + (actual as Record)[key], + (expected as Record)[key], + seen, + ) + ) { + return false; + } + } + return true; +} + +function installRuntimeSpecGlobals(): RuntimeSpecRegistry { + const registry: RuntimeSpecRegistry = {specs: [], skipped: []}; + const rootSuite: RuntimeSuite = {name: 'runtime ffi', beforeEach: [], afterEach: []}; + const suiteStack: RuntimeSuite[] = [rootSuite]; + const globalObject = globalThis as Record; + const originalSetTimeout = globalObject.setTimeout; + + globalObject.global = globalThis; + globalObject.isSimulator = true; + globalObject.__runtimeVersion = {major: 999, minor: 0, patch: 0}; + globalObject.process = { + ...(globalObject.process ?? {}), + versions: { + ...(globalObject.process?.versions ?? {}), + engine: 'hermes', + }, + }; + if (typeof globalObject.gc !== 'function') { + globalObject.gc = () => undefined; + } + globalObject.utf8 = require('./ns-runtime-tests/Infrastructure/utf8'); + globalObject.UNUSED = function (_param: unknown) { + return undefined; + }; + + globalObject.setTimeout = (callback: Function, timeout?: number, ...args: unknown[]) => + originalSetTimeout( + (...callbackArgs: unknown[]) => { + try { + callback(...callbackArgs); + } catch (error) { + if (activeAsyncReject) { + activeAsyncReject(error); + return; + } + throw error; + } + }, + timeout, + ...args, + ); + + globalObject.describe = (name: string, body: Function) => { + const suite: RuntimeSuite = {name: String(name), beforeEach: [], afterEach: []}; + suiteStack.push(suite); + try { + body(); + } finally { + suiteStack.pop(); + } + }; + + const skipReasonForRuntimeSpec = ( + specName: string, + body: Function, + ): string | undefined => { + const classBuilderSpecNames = [ + 'Override: More than one methods with same jsname', + 'Marshals NSString with null character', + 'NSMutableStringMarshalling', + 'Initialize a class(NSTimer) whose init deallocates the result from alloc', + 'Marshal returned javascript object as NSDictionaries', + ]; + if ( + classBuilderSpecNames.some((name) => specName.endsWith(` > ${name}`)) || + specName.includes(' > DerivedMethod') || + specName.endsWith(' > Handleof') || + specName.endsWith(' > Sizeof') + ) { + return 'Requires NativeScript JS-defined Objective-C class builder; excluded from the RN direct-JSI FFI slice.'; + } + + let source = ''; + try { + source = Function.prototype.toString.call(body); + } catch { + source = ''; + } + + if ( + source.includes('.extend(') || + source.includes('.extend (') || + source.includes('interop.addMethod') || + source.includes('ObjCExposedMethods') || + source.includes('ObjCProtocols') + ) { + return 'Requires NativeScript JS-defined Objective-C class builder; excluded from the RN direct-JSI FFI slice.'; + } + return undefined; + }; + + globalObject.it = (name: string, body: Function) => { + const suites = suiteStack.slice(1); + const specName = `${suites.map((suite) => suite.name).join(' > ')} > ${name}`; + const skipReason = skipReasonForRuntimeSpec(specName, body); + if (skipReason) { + registry.skipped.push({ + name: specName, + status: 'skip', + error: skipReason, + }); + return; + } + registry.specs.push({ + name: specName, + run: body, + beforeEach: suiteStack.flatMap((suite) => suite.beforeEach), + afterEach: suiteStack + .slice() + .reverse() + .flatMap((suite) => suite.afterEach), + }); + }; + + globalObject.xit = (name: string) => { + const suites = suiteStack.slice(1); + registry.skipped.push({ + name: `${suites.map((suite) => suite.name).join(' > ')} > ${name}`, + status: 'skip', + error: 'Disabled with xit', + }); + }; + globalObject.fit = globalObject.it; + + globalObject.beforeEach = (body: Function) => { + suiteStack[suiteStack.length - 1].beforeEach.push(body); + }; + globalObject.afterEach = (body: Function) => { + suiteStack[suiteStack.length - 1].afterEach.push(body); + }; + globalObject.pending = (reason?: string) => { + throw new PendingSpecError(reason || 'Pending'); + }; + globalObject.jasmine = { + any(expectedType: Function) { + return {__nativeScriptJasmineAny: true, expectedType}; + }, + }; + + globalObject.expect = (actual: unknown) => { + const makeMatchers = (negated: boolean) => { + const check = (condition: boolean, message: string) => { + const passed = negated ? !condition : condition; + if (!passed) { + fail(negated ? `Expected not: ${message}` : message); + } + }; + + return { + toBe(expected: unknown, message?: string) { + check( + Object.is(actual, expected), + message || `expected ${stringify(actual)} to be ${stringify(expected)}`, + ); + }, + toEqual(expected: unknown, message?: string) { + check( + deepEqual(actual, expected), + message || `expected ${stringify(actual)} to equal ${stringify(expected)}`, + ); + }, + toBeDefined(message?: string) { + check(actual !== undefined, message || `expected ${stringify(actual)} to be defined`); + }, + toBeUndefined(message?: string) { + check(actual === undefined, message || `expected ${stringify(actual)} to be undefined`); + }, + toBeNull(message?: string) { + check(actual === null, message || `expected ${stringify(actual)} to be null`); + }, + toBeTruthy(message?: string) { + check(Boolean(actual), message || `expected ${stringify(actual)} to be truthy`); + }, + toBeGreaterThan(expected: number, message?: string) { + check( + typeof actual === 'number' && actual > expected, + message || `expected ${stringify(actual)} to be greater than ${expected}`, + ); + }, + toContain(expected: unknown, message?: string) { + check( + typeof actual === 'string' + ? actual.includes(String(expected)) + : Array.isArray(actual) && actual.includes(expected), + message || `expected ${stringify(actual)} to contain ${stringify(expected)}`, + ); + }, + toMatch(expected: RegExp | string, message?: string) { + const text = String(actual); + const matched = + expected instanceof RegExp ? expected.test(text) : text.includes(String(expected)); + check(matched, message || `expected ${text} to match ${String(expected)}`); + }, + toBeCloseTo(expected: number, precision = 2, message?: string) { + const tolerance = Math.pow(10, -precision) / 2; + check( + typeof actual === 'number' && Math.abs(actual - expected) < tolerance, + message || `expected ${stringify(actual)} to be close to ${expected}`, + ); + }, + toThrow(message?: string) { + checkThrows(actual, undefined, message, check); + }, + toThrowError(expected?: RegExp | string | Function, message?: string) { + checkThrows(actual, expected, message, check); + }, + }; + }; + + const matchers: any = makeMatchers(false); + matchers.not = makeMatchers(true); + return matchers; + }; + + return registry; +} + +function checkThrows( + actual: unknown, + expected: RegExp | string | Function | undefined, + message: string | undefined, + check: (condition: boolean, message: string) => void, +) { + if (typeof actual !== 'function') { + check(false, message || 'expected value to be a function that throws'); + return; + } + + let thrown: unknown; + try { + actual(); + } catch (error) { + thrown = error; + } + + if (thrown === undefined) { + check(false, message || 'expected function to throw'); + return; + } + + if (expected === undefined) { + check(true, ''); + return; + } + + const thrownMessage = thrown instanceof Error ? thrown.message : String(thrown); + if (expected instanceof RegExp) { + check( + expected.test(thrownMessage), + message || `expected thrown error ${thrownMessage} to match ${expected}`, + ); + } else if (typeof expected === 'string') { + check( + thrownMessage.includes(expected), + message || `expected thrown error ${thrownMessage} to include ${expected}`, + ); + } else { + check(thrown instanceof (expected as any), message || 'expected thrown error type to match'); + } +} + +function loadRuntimeFfiSpecs() { + require('./ns-runtime-tests/FunctionsTests'); + require('./ns-runtime-tests/MethodCallsTests'); + require('./ns-runtime-tests/Marshalling/Primitives/Function'); + require('./ns-runtime-tests/Marshalling/Primitives/Static'); + require('./ns-runtime-tests/Marshalling/Primitives/Instance'); + require('./ns-runtime-tests/Marshalling/Primitives/Derived'); + require('./ns-runtime-tests/Marshalling/ObjCTypesTests'); + require('./ns-runtime-tests/Marshalling/ConstantsTests'); + require('./ns-runtime-tests/Marshalling/RecordTests'); + require('./ns-runtime-tests/Marshalling/VectorTests'); + require('./ns-runtime-tests/Marshalling/NSStringTests'); + require('./ns-runtime-tests/Marshalling/PointerTests'); + require('./ns-runtime-tests/Marshalling/ReferenceTests'); + require('./ns-runtime-tests/Marshalling/FunctionPointerTests'); + require('./ns-runtime-tests/Marshalling/EnumTests'); + require('./ns-runtime-tests/Marshalling/ProtocolTests'); +} + +async function runFunction(functionToRun: Function): Promise { + if (functionToRun.length > 0) { + await new Promise((resolve, reject) => { + let settled = false; + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + reject(new Error(`Timed out after ${runtimeSpecTimeoutMs}ms`)); + } + }, runtimeSpecTimeoutMs); + activeAsyncReject = reject; + const done = (error?: unknown) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + activeAsyncReject = null; + if (error) { + reject(error); + } else { + resolve(); + } + }; + (done as Record).fail = done; + try { + functionToRun(done); + } catch (error) { + done(error); + } + }); + return; + } + + const result = functionToRun(); + if (result && typeof result.then === 'function') { + await result; + } +} + +async function runRuntimeSpecs( + registry: RuntimeSpecRegistry, + progress: (current: string, results: TestResult[], total: number) => void, +): Promise { + const results: TestResult[] = [...registry.skipped]; + const total = registry.specs.length + registry.skipped.length; + + for (const spec of registry.specs) { + const startCount = results.length; + progress(spec.name, results, total); + + try { + for (const beforeEach of spec.beforeEach) { + await runFunction(beforeEach); + } + await runFunction(spec.run); + results.push({name: spec.name, status: 'pass'}); + } catch (error) { + if (error instanceof PendingSpecError) { + results.push({name: spec.name, status: 'skip', error: error.message}); + } else { + results.push({ + name: spec.name, + status: 'fail', + error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), + }); + if (results.filter((result) => result.status === 'fail').length >= runtimeFailureLimit) { + break; + } + } + } finally { + activeAsyncReject = null; + for (const afterEach of spec.afterEach) { + try { + await runFunction(afterEach); + } catch (error) { + results.push({ + name: `${spec.name} cleanup`, + status: 'fail', + error: + error instanceof Error + ? `${error.name}: ${error.message}` + : String(error), + }); + } + } + } + + if (results.filter((result) => result.status === 'fail').length >= runtimeFailureLimit) { + break; + } + } + + return results; +} + async function waitForUIKitPluginAttachment(): Promise { await waitForAsync( async () => { @@ -185,10 +701,10 @@ const NativeScriptUIKitTestView = defineUIKitView<{ }, }); -function buildTests(): TestCase[] { +function buildReactNativeIntegrationTests(): TestCase[] { return [ { - name: 'installs metadata-backed globals for classes, functions, constants, enums, protocols, and structs', + name: 'RN host installs fixture-aware metadata-backed globals', run() { const api = g('__nativeScriptNativeApi'); assert(api, 'Native API host object was not installed'); @@ -200,14 +716,9 @@ function buildTests(): TestCase[] { assert(api.metadata.enums > 0, 'enum metadata should be loaded'); assert(api.metadata.protocols > 0, 'protocol metadata should be loaded'); assert(api.metadata.structs > 0, 'struct metadata should be loaded'); - - assert(typeof g('NSObject').alloc === 'function', 'NSObject global missing'); - assert(typeof g('CGRectGetWidth') === 'function', 'CGRectGetWidth global missing'); - assert(typeof g('CGRect') === 'function', 'CGRect struct global missing'); - assert(g('NSObjectProtocol'), 'NSObjectProtocol global missing'); - assertEqual(g('NSURLErrorTimedOut'), -1001, 'NSURLErrorTimedOut constant'); - assertEqual(g('NSComparisonResult').Same, 0, 'NSComparisonResult enum'); - assertEqual(g('UIUserInterfaceStyle').Dark, 2, 'UIUserInterfaceStyle enum'); + assert(typeof g('TNSBaseInterface').alloc === 'function', 'TNSBaseInterface missing'); + assert(typeof g('functionWithInt') === 'function', 'functionWithInt missing'); + assertEqual(g('TNSConstant'), 'TNSConstant', 'TNSConstant'); }, }, { @@ -257,102 +768,6 @@ function buildTests(): TestCase[] { assertEqual(roundTrip[2], 67, 'NSData byte 2'); }, }, - { - name: 'marshals structs by constructor, fields, nested mutation, and C function calls', - run() { - const point = new (g('CGPoint'))({x: 10, y: 20}); - assertEqual(point.x, 10, 'CGPoint.x'); - assertEqual(point.y, 20, 'CGPoint.y'); - - const rect = new (g('CGRect'))({ - origin: {x: 1, y: 2}, - size: {width: 30, height: 40}, - }); - assertEqual(rect.origin.x, 1, 'CGRect.origin.x'); - assertEqual(rect.origin.y, 2, 'CGRect.origin.y'); - assertEqual(rect.size.width, 30, 'CGRect.size.width'); - assertEqual(rect.size.height, 40, 'CGRect.size.height'); - - rect.origin.y = 25; - rect.size.height = 45; - assertEqual(rect.origin.y, 25, 'nested CGRect origin mutation'); - assertEqual(rect.size.height, 45, 'nested CGRect size mutation'); - - const literal = new (g('CGRect'))({ - origin: {x: 3, y: 4}, - size: {width: 50, height: 60}, - }); - assert(g('CGRectContainsPoint')(literal, {x: 10, y: 10}), 'CGRectContainsPoint literal'); - assertClose(g('CGRectGetWidth')(literal), 50, 'CGRectGetWidth'); - }, - }, - { - name: 'supports NativeScript pointer and reference semantics', - run() { - const interop = g('interop'); - assertEqual( - interop.sizeof(interop.Pointer), - interop.sizeof(interop.types.pointer), - 'interop.Pointer sizeof', - ); - - const pointer = interop.alloc(4 * interop.sizeof(interop.types.int32)); - try { - const ref = new interop.Reference(interop.types.int32, pointer); - ref[0] = 123; - ref[1] = 456; - assertEqual(ref[0], 123, 'Reference index 0'); - assertEqual(ref[1], 456, 'Reference index 1'); - assertEqual(ptrNumber(interop.handleof(ref)), ptrNumber(pointer), 'Reference handle'); - - const lazy = new interop.Reference(7); - assertEqual(lazy.value, 7, 'lazy Reference initial value'); - lazy.value = 9; - assertEqual(lazy.value, 9, 'lazy Reference assigned value'); - } finally { - interop.free(pointer); - } - }, - }, - { - name: 'supports C strings through interop.handleof and stringFromCString', - run() { - const interop = g('interop'); - const ptr = interop.handleof('hello'); - assertEqual(interop.stringFromCString(ptr), 'hello', 'stringFromCString'); - assertEqual(interop.stringFromCString(ptr, 2), 'he', 'stringFromCString length'); - }, - }, - { - name: 'wraps protocol values with stable native handles', - run() { - const interop = g('interop'); - const nsObjectProtocol = g('NSObjectProtocol'); - const lookup = g('NSProtocolFromString')('NSObject'); - assert(nsObjectProtocol, 'NSObjectProtocol global missing'); - assert(lookup, 'NSProtocolFromString returned null'); - assertEqual( - ptrNumber(interop.handleof(lookup)), - ptrNumber(interop.handleof(nsObjectProtocol)), - 'protocol handle round trip', - ); - assert(g('NSObject').conformsToProtocol(nsObjectProtocol), 'NSObject protocol conformance'); - }, - }, - { - name: 'invokes blocks and exposes pointer parameters as References', - run() { - const seen: string[] = []; - const array = g('NSArray').arrayWithArray(['a', 'b', 'c']); - array.enumerateObjectsUsingBlock((value: string, index: number, stop: any) => { - seen.push(`${index}:${value}`); - if (index === 1) { - stop.value = true; - } - }); - assertEqual(seen.join(','), '0:a,1:b', 'enumerateObjectsUsingBlock stop reference'); - }, - }, { name: 'invokes C function pointer callbacks on the native caller thread', run() { @@ -370,17 +785,6 @@ function buildTests(): TestCase[] { ); }, }, - { - name: 'invokes Objective-C block callbacks inside dispatch_async_and_wait', - run() { - let callbackRan = false; - const queue = g('dispatch_get_global_queue')(0, 0); - g('dispatch_async_and_wait')(queue, () => { - callbackRan = true; - }); - assert(callbackRan, 'dispatch_async_and_wait block did not run'); - }, - }, { name: 'runs UIKit native calls through runOnUI main-thread dispatch', async run() { @@ -441,6 +845,15 @@ function buildTests(): TestCase[] { ]; } +function summarize(results: TestResult[]) { + return { + passed: results.filter((result) => result.status === 'pass').length, + failed: results.filter((result) => result.status === 'fail').length, + skipped: results.filter((result) => result.status === 'skip').length, + total: results.length, + }; +} + async function runCompatibilitySuite() { writeMarker({ marker, @@ -452,51 +865,78 @@ async function runCompatibilitySuite() { }); NativeScript.init(); - const tests = buildTests(); - const results: TestResult[] = []; + const registry = installRuntimeSpecGlobals(); + loadRuntimeFfiSpecs(); + const rnTests = buildReactNativeIntegrationTests(); + const total = registry.specs.length + registry.skipped.length + rnTests.length; + writeMarker({ marker, status: 'running', current: 'initialized', passed: 0, - total: tests.length, - results, + total, + runtimeSpecs: { + registered: registry.specs.length, + skipped: registry.skipped.length, + }, backend: NativeScript.getRuntimeBackend(), }); - for (const test of tests) { - try { - writeMarker({ - marker, - status: 'running', - current: test.name, - passed: results.filter((result) => result.status === 'pass').length, - total: tests.length, - results, - backend: NativeScript.getRuntimeBackend(), - }); - await test.run(); - results.push({name: test.name, status: 'pass'}); - } catch (error) { - results.push({ - name: test.name, - status: 'fail', - error: - error instanceof Error - ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}` - : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`, - }); - break; + const runtimeResults = await runRuntimeSpecs(registry, (current, results) => { + const runtimeSummary = summarize(results); + writeMarker({ + marker, + status: 'running', + current, + passed: runtimeSummary.passed, + total, + runtime: runtimeSummary, + failures: results.filter((result) => result.status === 'fail').slice(0, 50), + backend: NativeScript.getRuntimeBackend(), + }); + }); + + const rnResults: TestResult[] = []; + if (!runtimeResults.some((result) => result.status === 'fail')) { + for (const test of rnTests) { + try { + writeMarker({ + marker, + status: 'running', + current: test.name, + passed: summarize(runtimeResults).passed + summarize(rnResults).passed, + total, + runtime: summarize(runtimeResults), + reactNative: summarize(rnResults), + backend: NativeScript.getRuntimeBackend(), + }); + await test.run(); + rnResults.push({name: test.name, status: 'pass'}); + } catch (error) { + rnResults.push({ + name: test.name, + status: 'fail', + error: + error instanceof Error + ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}` + : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`, + }); + break; + } } } + const results = [...runtimeResults, ...rnResults]; const failed = results.find((result) => result.status === 'fail'); const payload = { marker, status: failed ? 'fail' : 'pass', - passed: results.filter((result) => result.status === 'pass').length, - total: tests.length, - results, + ...summarize(results), + total, + runtime: summarize(runtimeResults), + reactNative: summarize(rnResults), + failures: results.filter((result) => result.status === 'fail').slice(0, 50), backend: NativeScript.getRuntimeBackend(), }; writeMarker(payload); diff --git a/test/runtime/fixtures/TestFixtures.h b/test/runtime/fixtures/TestFixtures.h index eee33d78..0bb6cf60 100644 --- a/test/runtime/fixtures/TestFixtures.h +++ b/test/runtime/fixtures/TestFixtures.h @@ -1,4 +1,5 @@ #import +#import #if TARGET_OS_OSX #import From 096e03a68c01c94be6c044c6474a89d17edb5be7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 19:45:38 -0400 Subject: [PATCH 29/31] fix(ffi): address direct engine review findings --- .../ffi/hermes/HermesFastNativeApi.mm | 44 ++++++------- .../ffi/hermes/HermesFastNativeApiPrivate.h | 6 +- NativeScript/ffi/jsc/JSCFastNativeApi.mm | 29 +++++++-- NativeScript/ffi/napi/Cif.h | 1 - .../ffi/quickjs/QuickJSFastNativeApi.mm | 65 +++++++++++++++++-- NativeScript/ffi/shared/SignatureDispatch.h | 6 +- NativeScript/ffi/v8/V8FastConversion.mm | 14 ++-- NativeScript/ffi/v8/V8FastNativeApi.mm | 20 +++++- NativeScript/ffi/v8/V8FastNativeWrapper.mm | 9 ++- 9 files changed, 142 insertions(+), 52 deletions(-) diff --git a/NativeScript/ffi/hermes/HermesFastNativeApi.mm b/NativeScript/ffi/hermes/HermesFastNativeApi.mm index acc82690..6f1079ca 100644 --- a/NativeScript/ffi/hermes/HermesFastNativeApi.mm +++ b/NativeScript/ffi/hermes/HermesFastNativeApi.mm @@ -586,10 +586,6 @@ napi_value TryCallHermesObjCMemberFastImpl( : nullptr; if (frameDirectReturnInvoker != nullptr) { - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); @@ -611,6 +607,9 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, cif, reinterpret_cast(objc_msgSend), self, descriptor->selector, returnContext, hermesArgsBase, &directResult)) { + if (handled != nullptr) { + *handled = true; + } return directResult; } } @catch (NSException* exception) { @@ -656,10 +655,6 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( : nullptr; if (directReturnInvoker != nullptr) { - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); @@ -684,6 +679,9 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, cif, reinterpret_cast(objc_msgSend), self, descriptor->selector, returnContext, directArgs, &directResult)) { + if (handled != nullptr) { + *handled = true; + } return directResult; } } @catch (NSException* exception) { @@ -704,10 +702,6 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( return nullptr; } - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, member->bridgeState, needsRoundTripFrame); @@ -744,6 +738,10 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( return nullptr; } + if (handled != nullptr) { + *handled = true; + } + return makeHermesObjCReturnValue( env, member, descriptor, cif, self, receiverIsClass, jsThis, rvalue, kind != EngineDirectMemberKind::Method); @@ -803,10 +801,6 @@ napi_value TryCallHermesCFunctionFastImpl( : nullptr; if (frameDirectReturnInvoker != nullptr) { - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); @@ -815,6 +809,9 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( NativeCallRuntimeUnlockScope unlockRuntime(env); if (frameDirectReturnInvoker(env, cif, function->fnptr, hermesArgsBase, &directResult)) { + if (handled != nullptr) { + *handled = true; + } return directResult; } } @catch (NSException* exception) { @@ -859,10 +856,6 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( : nullptr; if (directReturnInvoker != nullptr) { - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); @@ -874,6 +867,9 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( NativeCallRuntimeUnlockScope unlockRuntime(env); if (directReturnInvoker(env, cif, function->fnptr, directArgs, &directResult)) { + if (handled != nullptr) { + *handled = true; + } return directResult; } } @catch (NSException* exception) { @@ -884,10 +880,6 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( } } - if (handled != nullptr) { - *handled = true; - } - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( env, bridgeState, needsRoundTripCacheFrame(cif)); @@ -927,6 +919,10 @@ EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( return nullptr; } + if (handled != nullptr) { + *handled = true; + } + return makeHermesCFunctionReturnValue(env, function, cif, perCallRValue); } diff --git a/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h b/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h index 8906d3f3..e1274ffa 100644 --- a/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h +++ b/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h @@ -76,11 +76,7 @@ inline bool readHermesFiniteNumber(napi_value value, double* result) { } inline napi_value makeHermesRawValue(Cif* cif, uint64_t raw) { - if (cif != nullptr) { - cif->hermesRawReturnSlot = raw; - return reinterpret_cast(&cif->hermesRawReturnSlot); - } - + (void)cif; static thread_local uint64_t slots[64] = {}; static thread_local unsigned int nextSlot = 0; uint64_t* slot = &slots[nextSlot++ & 63]; diff --git a/NativeScript/ffi/jsc/JSCFastNativeApi.mm b/NativeScript/ffi/jsc/JSCFastNativeApi.mm index bc956cf9..68b6446c 100644 --- a/NativeScript/ffi/jsc/JSCFastNativeApi.mm +++ b/NativeScript/ffi/jsc/JSCFastNativeApi.mm @@ -46,13 +46,34 @@ id resolveJSCSelf(napi_env env, napi_value jsThis, ObjCClassMember* member) { if (member != nullptr && member->cls != nullptr && member->cls->nativeClass != nil) { - if (member->classMethod) { - return static_cast(member->cls->nativeClass); - } - + bool shouldUseClassFallback = member->classMethod; napi_valuetype jsType = napi_undefined; if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && jsType == napi_function) { + bool isSameConstructor = true; + napi_value definingConstructor = nullptr; + if (member->cls->constructor != nullptr) { + napi_get_reference_value(env, member->cls->constructor, + &definingConstructor); + } + if (definingConstructor != nullptr && + napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } else { + shouldUseClassFallback = true; + } + } + + if (member->classMethod) { + if (shouldUseClassFallback) { + return static_cast(member->cls->nativeClass); + } + return nil; + } + + if (shouldUseClassFallback) { return static_cast(member->cls->nativeClass); } } diff --git a/NativeScript/ffi/napi/Cif.h b/NativeScript/ffi/napi/Cif.h index 2ab69483..3614d6ab 100644 --- a/NativeScript/ffi/napi/Cif.h +++ b/NativeScript/ffi/napi/Cif.h @@ -24,7 +24,6 @@ class Cif { bool generatedDispatchHasRoundTripCacheArgument = false; bool generatedDispatchUsesObjectReturnStorage = false; bool generatedDispatchSetsV8ReturnDirectly = false; - uint64_t hermesRawReturnSlot = 0; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm index ad9795f9..0638d6ad 100644 --- a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm +++ b/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm @@ -28,6 +28,40 @@ bool isCompatCFunction(napi_env env, void* data) { strcmp(name, "NSApplicationMain") == 0; } +id normalizeQuickJSWrappedReceiver(napi_env env, void* wrapped) { + if (wrapped == nullptr) { + return nil; + } + + auto* state = nativescript::ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + id nativeObject = state->nativeObjectForBridgeWrapper(wrapped); + if (nativeObject != nil) { + return nativeObject; + } + + for (const auto& entry : state->classes) { + auto* bridgedClass = entry.second; + if (bridgedClass == wrapped && bridgedClass->nativeClass != nil) { + return static_cast(bridgedClass->nativeClass); + } + } + + for (const auto& entry : state->protocols) { + auto* bridgedProtocol = entry.second; + if (bridgedProtocol == wrapped) { + if (Protocol* runtimeProtocol = + objc_getProtocol(bridgedProtocol->name.c_str())) { + return static_cast(runtimeProtocol); + } + break; + } + } + } + + return static_cast(wrapped); +} + id resolveQuickJSSelf(napi_env env, napi_value jsThis, nativescript::ObjCClassMember* member) { id self = nil; @@ -38,7 +72,7 @@ id resolveQuickJSSelf(napi_env env, napi_value jsThis, if (nativescript::TryUnwrapQuickJSNativeObjectFast( env, ToJSValue(jsThis), &wrapped) && wrapped != nullptr) { - return static_cast(wrapped); + return normalizeQuickJSWrappedReceiver(env, wrapped); } } @@ -56,13 +90,34 @@ id resolveQuickJSSelf(napi_env env, napi_value jsThis, if (member != nullptr && member->cls != nullptr && member->cls->nativeClass != nil) { - if (member->classMethod) { - return static_cast(member->cls->nativeClass); - } - + bool shouldUseClassFallback = member->classMethod; napi_valuetype jsType = napi_undefined; if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && jsType == napi_function) { + bool isSameConstructor = true; + napi_value definingConstructor = nullptr; + if (member->cls->constructor != nullptr) { + napi_get_reference_value(env, member->cls->constructor, + &definingConstructor); + } + if (definingConstructor != nullptr && + napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } else { + shouldUseClassFallback = true; + } + } + + if (member->classMethod) { + if (shouldUseClassFallback) { + return static_cast(member->cls->nativeClass); + } + return nil; + } + + if (shouldUseClassFallback) { return static_cast(member->cls->nativeClass); } } diff --git a/NativeScript/ffi/shared/SignatureDispatch.h b/NativeScript/ffi/shared/SignatureDispatch.h index 34f856fe..1f83e603 100644 --- a/NativeScript/ffi/shared/SignatureDispatch.h +++ b/NativeScript/ffi/shared/SignatureDispatch.h @@ -168,11 +168,7 @@ inline bool readHermesDispatchFiniteNumberRaw(uint64_t raw, double* result) { } inline napi_value makeHermesDispatchRawValue(Cif* cif, uint64_t raw) { - if (cif != nullptr) { - cif->hermesRawReturnSlot = raw; - return reinterpret_cast(&cif->hermesRawReturnSlot); - } - + (void)cif; static thread_local uint64_t slots[64] = {}; static thread_local unsigned int nextSlot = 0; uint64_t* slot = &slots[nextSlot++ & 63]; diff --git a/NativeScript/ffi/v8/V8FastConversion.mm b/NativeScript/ffi/v8/V8FastConversion.mm index b5fb9cf1..2b2a0617 100644 --- a/NativeScript/ffi/v8/V8FastConversion.mm +++ b/NativeScript/ffi/v8/V8FastConversion.mm @@ -270,7 +270,6 @@ bool TryFastConvertV8UInt16Argument(napi_env env, v8::Local value, ui 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; } @@ -336,7 +335,11 @@ bool TryFastConvertV8Argument(napi_env env, MDTypeKind kind, v8::LocalIsBigInt()) { bool lossless = false; - *reinterpret_cast(result) = value.As()->Int64Value(&lossless); + int64_t converted = value.As()->Int64Value(&lossless); + if (!lossless) { + return false; + } + *reinterpret_cast(result) = converted; return true; } return value->IntegerValue(env->context()).To(reinterpret_cast(result)); @@ -345,8 +348,11 @@ bool TryFastConvertV8Argument(napi_env env, MDTypeKind kind, v8::LocalIsBigInt()) { bool lossless = false; - *reinterpret_cast(result) = - value.As()->Uint64Value(&lossless); + uint64_t converted = value.As()->Uint64Value(&lossless); + if (!lossless) { + return false; + } + *reinterpret_cast(result) = converted; return true; } else { int64_t converted = 0; diff --git a/NativeScript/ffi/v8/V8FastNativeApi.mm b/NativeScript/ffi/v8/V8FastNativeApi.mm index 9eca9627..f08065a5 100644 --- a/NativeScript/ffi/v8/V8FastNativeApi.mm +++ b/NativeScript/ffi/v8/V8FastNativeApi.mm @@ -163,7 +163,25 @@ id resolveSelf(napi_env env, v8::Local jsThisValue, ObjCClassMember* if (method != nullptr && method->cls != nullptr && method->cls->nativeClass != nil) { if (method->classMethod) { shouldUseClassFallback = true; - } else if (!jsThisValue.IsEmpty() && jsThisValue->IsFunction()) { + } + + if (!jsThisValue.IsEmpty() && jsThisValue->IsFunction() && jsThis != nullptr) { + bool isSameConstructor = true; + napi_value definingConstructor = nullptr; + if (method->cls->constructor != nullptr) { + napi_get_reference_value(env, method->cls->constructor, + &definingConstructor); + } + if (definingConstructor != nullptr && + napi_strict_equals(env, jsThis, definingConstructor, + &isSameConstructor) == napi_ok && + !isSameConstructor) { + shouldUseClassFallback = false; + } else { + shouldUseClassFallback = true; + } + } else if (!method->classMethod && !jsThisValue.IsEmpty() && + jsThisValue->IsFunction()) { shouldUseClassFallback = true; } } diff --git a/NativeScript/ffi/v8/V8FastNativeWrapper.mm b/NativeScript/ffi/v8/V8FastNativeWrapper.mm index 058299f6..796be450 100644 --- a/NativeScript/ffi/v8/V8FastNativeWrapper.mm +++ b/NativeScript/ffi/v8/V8FastNativeWrapper.mm @@ -84,9 +84,12 @@ NS_V8_INTERCEPTED nativeWrapperNamedSetter( } } - definePlainValueProperty(info.GetIsolate()->GetCurrentContext(), holder, - property, value); - NS_V8_RETURN_YES; + if (definePlainValueProperty(info.GetIsolate()->GetCurrentContext(), holder, + property, value)) { + NS_V8_RETURN_YES; + } + + NS_V8_RETURN_NO; } NS_V8_INTERCEPTED nativeWrapperIndexedGetter( From 55a1f202ffdde0b64a35d0636ce5f687e3ee00a7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 24 May 2026 20:41:38 -0400 Subject: [PATCH 30/31] refactor(ffi): isolate direct engine backends --- .github/workflows/ci.yml | 3 + NativeScript/CMakeLists.txt | 150 +++++-- NativeScript/cli/main.cpp | 2 +- NativeScript/ffi/hermes/jsi/NativeApiJsi.h | 1 + NativeScript/ffi/hermes/jsi/NativeApiJsi.mm | 425 +++++++++++++++++- NativeScript/ffi/napi/Block.mm | 2 +- NativeScript/ffi/napi/CFunction.mm | 2 +- NativeScript/ffi/napi/ClassMember.mm | 2 +- NativeScript/ffi/napi/Closure.mm | 2 +- .../engine}/hermes/HermesFastCallbackInfo.h | 0 .../engine}/hermes/HermesFastConversion.mm | 0 .../engine}/hermes/HermesFastNativeApi.h | 0 .../engine}/hermes/HermesFastNativeApi.mm | 0 .../hermes/HermesFastNativeApiPrivate.h | 2 +- .../engine}/jsc/JSCFastConversion.mm | 0 .../{ => napi/engine}/jsc/JSCFastNativeApi.h | 0 .../{ => napi/engine}/jsc/JSCFastNativeApi.mm | 0 .../engine}/jsc/JSCFastNativeApiPrivate.h | 2 +- .../engine}/quickjs/QuickJSFastConversion.mm | 0 .../engine}/quickjs/QuickJSFastNativeApi.h | 0 .../engine}/quickjs/QuickJSFastNativeApi.mm | 0 .../quickjs/QuickJSFastNativeApiPrivate.h | 2 +- .../engine}/quickjs/QuickJSFastReturn.mm | 0 .../engine}/shared/EngineDirectCall.h | 0 .../engine}/shared/EngineDirectCall.mm | 2 +- .../engine}/shared/InvocationSupport.h | 6 +- .../engine}/shared/SignatureDispatch.h | 0 .../{ => napi/engine}/v8/V8FastConversion.mm | 0 .../{ => napi/engine}/v8/V8FastNativeApi.h | 0 .../{ => napi/engine}/v8/V8FastNativeApi.mm | 0 .../engine}/v8/V8FastNativeApiPrivate.h | 2 +- .../engine}/v8/V8FastNativeWrapper.mm | 0 NativeScript/napi/jsc/jsc-api.cpp | 2 +- NativeScript/napi/quickjs/quickjs-api.c | 2 +- NativeScript/napi/v8/v8-api.cpp | 2 +- .../v8_inspector/ns-v8-tracing-agent-impl.cpp | 2 +- NativeScript/runtime/NativeScript.mm | 2 +- .../napi => runtime}/NativeScriptException.h | 6 +- .../napi => runtime}/NativeScriptException.mm | 2 +- NativeScript/runtime/Runtime.cpp | 27 +- NativeScript/runtime/Util.h | 2 +- .../runtime/modules/console/Console.cpp | 2 +- .../runtime/modules/module/ModuleInternal.cpp | 2 +- .../runtime/modules/worker/MessageV8.cpp | 2 +- NativeScript/runtime/modules/worker/Worker.mm | 2 +- .../runtime/modules/worker/WorkerImpl.h | 2 +- .../runtime/modules/worker/WorkerImpl.mm | 2 +- .../build-step-metadata-generator.py | 2 +- package.json | 1 + scripts/build_nativescript.sh | 59 ++- scripts/check_ffi_boundaries.sh | 29 ++ scripts/metagen.js | 2 +- scripts/run-tests-macos.js | 35 +- 53 files changed, 713 insertions(+), 79 deletions(-) rename NativeScript/ffi/{ => napi/engine}/hermes/HermesFastCallbackInfo.h (100%) rename NativeScript/ffi/{ => napi/engine}/hermes/HermesFastConversion.mm (100%) rename NativeScript/ffi/{ => napi/engine}/hermes/HermesFastNativeApi.h (100%) rename NativeScript/ffi/{ => napi/engine}/hermes/HermesFastNativeApi.mm (100%) rename NativeScript/ffi/{ => napi/engine}/hermes/HermesFastNativeApiPrivate.h (98%) rename NativeScript/ffi/{ => napi/engine}/jsc/JSCFastConversion.mm (100%) rename NativeScript/ffi/{ => napi/engine}/jsc/JSCFastNativeApi.h (100%) rename NativeScript/ffi/{ => napi/engine}/jsc/JSCFastNativeApi.mm (100%) rename NativeScript/ffi/{ => napi/engine}/jsc/JSCFastNativeApiPrivate.h (98%) rename NativeScript/ffi/{ => napi/engine}/quickjs/QuickJSFastConversion.mm (100%) rename NativeScript/ffi/{ => napi/engine}/quickjs/QuickJSFastNativeApi.h (100%) rename NativeScript/ffi/{ => napi/engine}/quickjs/QuickJSFastNativeApi.mm (100%) rename NativeScript/ffi/{ => napi/engine}/quickjs/QuickJSFastNativeApiPrivate.h (99%) rename NativeScript/ffi/{ => napi/engine}/quickjs/QuickJSFastReturn.mm (100%) rename NativeScript/ffi/{ => napi/engine}/shared/EngineDirectCall.h (100%) rename NativeScript/ffi/{ => napi/engine}/shared/EngineDirectCall.mm (99%) rename NativeScript/ffi/{ => napi/engine}/shared/InvocationSupport.h (93%) rename NativeScript/ffi/{ => napi/engine}/shared/SignatureDispatch.h (100%) rename NativeScript/ffi/{ => napi/engine}/v8/V8FastConversion.mm (100%) rename NativeScript/ffi/{ => napi/engine}/v8/V8FastNativeApi.h (100%) rename NativeScript/ffi/{ => napi/engine}/v8/V8FastNativeApi.mm (100%) rename NativeScript/ffi/{ => napi/engine}/v8/V8FastNativeApiPrivate.h (99%) rename NativeScript/ffi/{ => napi/engine}/v8/V8FastNativeWrapper.mm (100%) rename NativeScript/{ffi/napi => runtime}/NativeScriptException.h (89%) rename NativeScript/{ffi/napi => runtime}/NativeScriptException.mm (99%) create mode 100755 scripts/check_ffi_boundaries.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce8f3cc0..e773cf62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - name: Install Dependencies run: npm install + - name: Check FFI backend boundaries + run: npm run check:ffi-boundaries + - name: Download V8 run: ./scripts/download_v8.sh diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 58639860..468b60cd 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -20,10 +20,12 @@ 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_FFI_BACKEND "auto" CACHE STRING "FFI backend: auto, napi, or direct") 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_FFI_BACKEND PROPERTY STRINGS auto napi direct) set_property(CACHE NS_GSD_BACKEND PROPERTY STRINGS auto v8 jsc quickjs hermes napi none) if (BUILD_MACOS_NODE_API) @@ -134,8 +136,45 @@ message(STATUS "TARGET_ENGINE = ${TARGET_ENGINE}") message(STATUS "ENABLE_JS_RUNTIME = ${ENABLE_JS_RUNTIME}") message(STATUS "GENERIC_NAPI = ${GENERIC_NAPI}") +if(NS_FFI_BACKEND STREQUAL "auto") + if(GENERIC_NAPI OR TARGET_ENGINE_NONE) + set(NS_EFFECTIVE_FFI_BACKEND "napi") + elseif(TARGET_ENGINE_HERMES) + set(NS_EFFECTIVE_FFI_BACKEND "direct") + else() + set(NS_EFFECTIVE_FFI_BACKEND "napi") + endif() +elseif(NS_FFI_BACKEND STREQUAL "napi" OR NS_FFI_BACKEND STREQUAL "direct") + set(NS_EFFECTIVE_FFI_BACKEND "${NS_FFI_BACKEND}") +else() + message(FATAL_ERROR "Unknown NS_FFI_BACKEND: ${NS_FFI_BACKEND}") +endif() + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct" AND + (GENERIC_NAPI OR TARGET_ENGINE_NONE OR BUILD_MACOS_NODE_API)) + message(FATAL_ERROR "NS_FFI_BACKEND=direct requires an embedded JS runtime build") +endif() + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct" AND NOT TARGET_ENGINE_HERMES) + message(FATAL_ERROR + "NS_FFI_BACKEND=direct is currently implemented for TARGET_ENGINE=hermes. " + "Use NS_FFI_BACKEND=napi for the pure Node-API FFI path until the direct " + "entrypoint for this engine lands.") +endif() + +message(STATUS "NS_FFI_BACKEND = ${NS_FFI_BACKEND} (${NS_EFFECTIVE_FFI_BACKEND})") + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct" AND + NOT (NS_GSD_BACKEND STREQUAL "auto" OR NS_GSD_BACKEND STREQUAL "none")) + message(FATAL_ERROR + "NS_GSD_BACKEND is only used by the Node-API FFI backend. " + "Use NS_GSD_BACKEND=auto or none with NS_FFI_BACKEND=direct.") +endif() + if(NS_GSD_BACKEND STREQUAL "auto") - if(TARGET_ENGINE_V8) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(NS_EFFECTIVE_GSD_BACKEND "none") + elseif(TARGET_ENGINE_V8) set(NS_EFFECTIVE_GSD_BACKEND "v8") elseif(TARGET_ENGINE_JSC) set(NS_EFFECTIVE_GSD_BACKEND "jsc") @@ -176,15 +215,22 @@ message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND}) include_directories( ./ ffi/shared - ffi/v8 - ffi/hermes - ffi/jsc - ffi/quickjs + ffi/hermes/jsi ../metadata-generator/include napi/common libffi/${LIBFFI_BUILD}/include ) +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + include_directories( + ffi/napi/engine/shared + ffi/napi/engine/v8 + ffi/napi/engine/hermes + ffi/napi/engine/jsc + ffi/napi/engine/quickjs + ) +endif() + set(FFI_SHARED_SOURCE_FILES ffi/shared/Tasks.cpp ) @@ -210,42 +256,51 @@ set(FFI_NAPI_SOURCE_FILES ffi/napi/Interop.mm ffi/napi/InlineFunctions.mm ffi/napi/ClassBuilder.mm - ffi/napi/NativeScriptException.mm ) -set(FFI_DIRECT_SOURCE_FILES - ffi/shared/EngineDirectCall.mm +set(FFI_NAPI_ENGINE_SOURCE_FILES + ffi/napi/engine/shared/EngineDirectCall.mm ) -set(FFI_V8_SOURCE_FILES - ffi/v8/V8FastConversion.mm - ffi/v8/V8FastNativeApi.mm - ffi/v8/V8FastNativeWrapper.mm +set(FFI_NAPI_V8_SOURCE_FILES + ffi/napi/engine/v8/V8FastConversion.mm + ffi/napi/engine/v8/V8FastNativeApi.mm + ffi/napi/engine/v8/V8FastNativeWrapper.mm ) -set(FFI_HERMES_SOURCE_FILES +set(FFI_HERMES_DIRECT_SOURCE_FILES ffi/hermes/jsi/NativeApiJsi.mm - ffi/hermes/HermesFastConversion.mm - ffi/hermes/HermesFastNativeApi.mm ) -set(FFI_QUICKJS_SOURCE_FILES - ffi/quickjs/QuickJSFastConversion.mm - ffi/quickjs/QuickJSFastNativeApi.mm - ffi/quickjs/QuickJSFastReturn.mm +set(FFI_NAPI_HERMES_SOURCE_FILES + ffi/napi/engine/hermes/HermesFastConversion.mm + ffi/napi/engine/hermes/HermesFastNativeApi.mm ) -set(FFI_JSC_SOURCE_FILES - ffi/jsc/JSCFastConversion.mm - ffi/jsc/JSCFastNativeApi.mm +set(FFI_NAPI_QUICKJS_SOURCE_FILES + ffi/napi/engine/quickjs/QuickJSFastConversion.mm + ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm + ffi/napi/engine/quickjs/QuickJSFastReturn.mm +) + +set(FFI_NAPI_JSC_SOURCE_FILES + ffi/napi/engine/jsc/JSCFastConversion.mm + ffi/napi/engine/jsc/JSCFastNativeApi.mm ) set(SOURCE_FILES ${FFI_SHARED_SOURCE_FILES} - ${FFI_NAPI_SOURCE_FILES} - ${FFI_DIRECT_SOURCE_FILES} + runtime/NativeScriptException.mm ) +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_SOURCE_FILES} + ${FFI_NAPI_ENGINE_SOURCE_FILES} + ) +endif() + if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} @@ -288,9 +343,15 @@ if(ENABLE_JS_RUNTIME) napi/v8/v8-module-loader.cpp napi/v8/jsr.cpp napi/v8/SimpleAllocator.cpp - ${FFI_V8_SOURCE_FILES} ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_V8_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_HERMES) set(HERMES_HEADERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Frameworks/hermes-headers") @@ -313,9 +374,20 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/hermes/jsr.cpp - ${FFI_HERMES_SOURCE_FILES} ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_HERMES_SOURCE_FILES} + ) + else() + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_HERMES_DIRECT_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_QUICKJS) set(MI_BUILD_OBJECT OFF) set(MI_OVERRIDE OFF) @@ -346,9 +418,15 @@ if(ENABLE_JS_RUNTIME) # napi napi/quickjs/quickjs-api.c napi/quickjs/jsr.cpp - ${FFI_QUICKJS_SOURCE_FILES} ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_QUICKJS_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_JSC) include_directories( napi/jsc @@ -359,8 +437,14 @@ if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} napi/jsc/jsc-api.cpp napi/jsc/jsr.cpp - ${FFI_JSC_SOURCE_FILES} ) + + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_JSC_SOURCE_FILES} + ) + endif() endif() else() include_directories( @@ -453,6 +537,8 @@ set(NS_GSD_BACKEND_JSC_VALUE 0) set(NS_GSD_BACKEND_QUICKJS_VALUE 0) set(NS_GSD_BACKEND_HERMES_VALUE 0) set(NS_GSD_BACKEND_NAPI_VALUE 0) +set(NS_FFI_BACKEND_DIRECT_VALUE 0) +set(NS_FFI_BACKEND_NAPI_VALUE 0) if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8") set(NS_GSD_BACKEND_V8_VALUE 1) @@ -466,12 +552,20 @@ elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi") set(NS_GSD_BACKEND_NAPI_VALUE 1) endif() +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(NS_FFI_BACKEND_DIRECT_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(NS_FFI_BACKEND_NAPI_VALUE 1) +endif() + target_compile_definitions(${NAME} PRIVATE NS_GSD_BACKEND_V8=${NS_GSD_BACKEND_V8_VALUE} NS_GSD_BACKEND_JSC=${NS_GSD_BACKEND_JSC_VALUE} NS_GSD_BACKEND_QUICKJS=${NS_GSD_BACKEND_QUICKJS_VALUE} NS_GSD_BACKEND_HERMES=${NS_GSD_BACKEND_HERMES_VALUE} NS_GSD_BACKEND_NAPI=${NS_GSD_BACKEND_NAPI_VALUE} + NS_FFI_BACKEND_DIRECT=${NS_FFI_BACKEND_DIRECT_VALUE} + NS_FFI_BACKEND_NAPI=${NS_FFI_BACKEND_NAPI_VALUE} ) set(FRAMEWORK_VERSION_VALUE "${VERSION}") diff --git a/NativeScript/cli/main.cpp b/NativeScript/cli/main.cpp index 9fe7ce70..f9ea139c 100644 --- a/NativeScript/cli/main.cpp +++ b/NativeScript/cli/main.cpp @@ -5,7 +5,7 @@ #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "runtime/Bundle.h" #include "runtime/Runtime.h" #include "runtime/RuntimeConfig.h" diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h index da2495dc..004685d9 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h @@ -21,6 +21,7 @@ struct NativeApiJsiConfig { const void* metadataPtr = nullptr; const char* globalName = "__nativeScriptNativeApi"; std::shared_ptr scheduler = nullptr; + bool installGlobalSymbols = false; }; facebook::jsi::Object CreateNativeApiJSI( diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm index ae146594..0c6ea8b5 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm @@ -50,6 +50,7 @@ using facebook::jsi::PropNameID; using facebook::jsi::Runtime; using facebook::jsi::String; +using facebook::jsi::StringBuffer; using facebook::jsi::Value; using metagen::MDMemberFlag; using metagen::MDMetadataReader; @@ -6350,6 +6351,424 @@ void InstallAggregateGlobals(Runtime& runtime, Object& api, const char* namesFun } } +std::string jsStringLiteral(const char* value) { + std::string result = "'"; + if (value != nullptr) { + for (const char* current = value; *current != '\0'; current++) { + switch (*current) { + case '\\': + result += "\\\\"; + break; + case '\'': + result += "\\'"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += *current; + break; + } + } + } + result += "'"; + return result; +} + +void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) { + static const char* GlobalInstaller = R"JSI_GLOBALS( +(function(nativeApiGlobalName) { + 'use strict'; + var api = globalThis[nativeApiGlobalName]; + if (!api || api.__nativeScriptGlobalsInstalled) { + return; + } + + var cacheName = '__nativeScriptNativeApiGlobalCache'; + var typeCodeKey = '__nativeApiTypeCode'; + var classWrappers = typeof WeakMap === 'function' ? new WeakMap() : null; + + function globalCache() { + var existing = globalThis[cacheName]; + if (existing && typeof existing === 'object') { + return existing; + } + var cache = Object.create(null); + Object.defineProperty(globalThis, cacheName, { + configurable: false, + enumerable: false, + writable: false, + value: cache + }); + return cache; + } + + function cacheGlobal(name, value) { + if (name && value !== undefined) { + globalCache()[name] = value; + } + } + + function defineLazyGlobal(name, resolve, force) { + if (!name) { + return; + } + if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { + try { + cacheGlobal(name, globalThis[name]); + } catch (_) { + } + return; + } + try { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + get: function() { + var value = resolve(name); + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + return value; + } + }); + } catch (_) { + var value = resolve(name); + if (value !== undefined) { + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + } + } + } + + function wrapAggregateConstructor(nativeConstructor) { + if (typeof nativeConstructor !== 'function') { + return nativeConstructor; + } + var aggregate = function NativeScriptAggregate(initialValue) { + return nativeConstructor(initialValue); + }; + try { + Object.defineProperty(aggregate, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && + typeof value === 'object' && + value.kind === nativeConstructor.kind && + value.name === nativeConstructor.runtimeName; + } + }); + } catch (_) { + } + ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields', 'equals'].forEach(function(key) { + try { + Object.defineProperty(aggregate, key, { + configurable: true, + enumerable: false, + writable: false, + value: nativeConstructor[key] + }); + } catch (_) { + } + }); + return aggregate; + } + + function wrapNativeClass(nativeClass) { + if (!nativeClass || (typeof nativeClass !== 'object' && typeof nativeClass !== 'function')) { + return nativeClass; + } + if (classWrappers) { + var cached = classWrappers.get(nativeClass); + if (cached) { + return cached; + } + } + var constructable = function NativeScriptNativeClass() { + var args = Array.prototype.slice.call(arguments); + if (args.length > 0 && typeof nativeClass.construct === 'function') { + return nativeClass.construct.apply(nativeClass, args); + } + if (typeof nativeClass.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + var instance = nativeClass.alloc(); + if (instance && typeof instance.init === 'function') { + return instance.init(); + } + return instance; + }; + Object.defineProperty(constructable, '__nativeApiClass', { + configurable: false, + enumerable: false, + writable: false, + value: nativeClass + }); + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + if (!value || typeof value !== 'object') { + return false; + } + try { + if (typeof value.isKindOfClass === 'function') { + return !!value.isKindOfClass(constructable); + } + } catch (_) { + } + var expectedName = nativeClass.runtimeName || nativeClass.name; + return typeof expectedName === 'string' && value.className === expectedName; + } + }); + } catch (_) { + } + var wrapper = typeof Proxy === 'function' + ? new Proxy(constructable, { + get: function(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + return nativeClass[property]; + }, + set: function(target, property, value, receiver) { + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + return true; + } + try { + nativeClass[property] = value; + return true; + } catch (_) { + } + return Reflect.set(target, property, value, receiver); + }, + has: function(target, property) { + return property in target || property in nativeClass; + } + }) + : constructable; + if (classWrappers) { + classWrappers.set(nativeClass, wrapper); + } + return wrapper; + } + + function wrapInteropFactory(nativeFactory, properties) { + if (typeof nativeFactory !== 'function' || nativeFactory.__nativeScriptConstructable) { + return nativeFactory; + } + var constructable = function NativeScriptInteropValue() { + return nativeFactory.apply(undefined, arguments); + }; + try { + if (nativeFactory.prototype) { + constructable.prototype = nativeFactory.prototype; + } + } catch (_) { + } + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && typeof value === 'object' && value.kind === properties.kind; + } + }); + } catch (_) { + } + Object.keys(properties).forEach(function(key) { + try { + Object.defineProperty(constructable, key, { + configurable: true, + enumerable: false, + writable: false, + value: properties[key] + }); + } catch (_) { + } + }); + Object.defineProperty(constructable, '__nativeScriptConstructable', { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + return constructable; + } + + function installInteropConstructors() { + var interop = globalThis.interop; + if (!interop || typeof interop !== 'object') { + return; + } + var pointerSize; + try { + if (typeof interop.sizeof === 'function' && interop.types && interop.types.pointer !== undefined) { + pointerSize = interop.sizeof(interop.types.pointer); + } + } catch (_) { + pointerSize = undefined; + } + interop.Pointer = wrapInteropFactory(interop.Pointer, { kind: 'pointer', sizeof: pointerSize }); + interop.Reference = wrapInteropFactory(interop.Reference, { kind: 'reference', sizeof: pointerSize }); + interop.FunctionReference = wrapInteropFactory( + interop.FunctionReference, + { kind: 'functionReference', sizeof: pointerSize } + ); + if (interop.types && typeof interop.types === 'object') { + Object.keys(interop.types).forEach(function(name) { + var value = interop.types[name]; + if (typeof value !== 'number') { + return; + } + var boxed = { + valueOf: function() { return value; }, + toString: function() { return String(value); } + }; + Object.defineProperty(boxed, typeCodeKey, { + configurable: false, + enumerable: false, + writable: false, + value: value + }); + interop.types[name] = boxed; + }); + } + } + + function defineInlineFunction(name, value) { + if (Object.prototype.hasOwnProperty.call(globalThis, name)) { + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: true, + value: value + }); + } + + function installInlineFunctions() { + var makePoint = function(x, y) { return { x: x, y: y }; }; + var makeSize = function(width, height) { return { width: width, height: height }; }; + var makeRect = function(x, y, width, height) { + return { origin: { x: x, y: y }, size: { width: width, height: height } }; + }; + defineInlineFunction('CGPointMake', makePoint); + defineInlineFunction('NSMakePoint', makePoint); + defineInlineFunction('CGSizeMake', makeSize); + defineInlineFunction('NSMakeSize', makeSize); + defineInlineFunction('CGRectMake', makeRect); + defineInlineFunction('NSMakeRect', makeRect); + defineInlineFunction('NSMakeRange', function(location, length) { + return { location: location, length: length }; + }); + defineInlineFunction('UIEdgeInsetsMake', function(top, left, bottom, right) { + return { top: top, left: left, bottom: bottom, right: right }; + }); + } + + function names(kind) { + var metadata = api.metadata; + var fn = metadata && metadata[kind]; + return typeof fn === 'function' ? fn() : []; + } + + names('classNames').forEach(function(name) { + defineLazyGlobal(name, function(className) { + return wrapNativeClass(api[className]); + }); + }); + names('functionNames').forEach(function(name) { + defineLazyGlobal(name, function(functionName) { + return api[functionName]; + }); + }); + names('constantNames').forEach(function(name) { + defineLazyGlobal(name, function(constantName) { + return api[constantName]; + }); + }); + names('protocolNames').forEach(function(name) { + defineLazyGlobal(name, function(protocolName) { + return (api.getProtocol && api.getProtocol(protocolName)) || api[protocolName]; + }); + }); + names('enumNames').forEach(function(name) { + var resolveEnum = function(enumName) { + return (api.getEnum && api.getEnum(enumName)) || api[enumName]; + }; + defineLazyGlobal(name, resolveEnum); + var enumValue = resolveEnum(name); + if (!enumValue || typeof enumValue !== 'object') { + return; + } + Object.keys(enumValue).forEach(function(memberName) { + if (/^-?\d+$/.test(memberName)) { + return; + } + defineLazyGlobal(memberName, function() { + return enumValue[memberName]; + }); + }); + }); + names('structNames').forEach(function(name) { + defineLazyGlobal(name, function(structName) { + return wrapAggregateConstructor((api.getStruct && api.getStruct(structName)) || api[structName]); + }, true); + }); + names('unionNames').forEach(function(name) { + defineLazyGlobal(name, function(unionName) { + return wrapAggregateConstructor((api.getUnion && api.getUnion(unionName)) || api[unionName]); + }, true); + }); + + installInteropConstructors(); + installInlineFunctions(); + + try { + Object.defineProperty(api, '__nativeScriptGlobalsInstalled', { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + } catch (_) { + } +}) +)JSI_GLOBALS"; + + std::string script(GlobalInstaller); + script += "("; + script += jsStringLiteral(globalName); + script += ");"; + runtime.evaluateJavaScript(std::make_shared(std::move(script)), + "NativeApiJsiGlobals.js"); +} + void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { const char* globalName = config.globalName != nullptr && config.globalName[0] != '\0' ? config.globalName @@ -6362,7 +6781,11 @@ void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { if (existingInterop.isUndefined() || existingInterop.isNull()) { global.setProperty(runtime, "interop", api.getProperty(runtime, "interop")); } - InstallAggregateGlobals(runtime, api, "protocolNames"); + if (config.installGlobalSymbols) { + InstallNativeApiJsiGlobalSymbols(runtime, globalName); + } else { + InstallAggregateGlobals(runtime, api, "protocolNames"); + } } } // namespace nativescript diff --git a/NativeScript/ffi/napi/Block.mm b/NativeScript/ffi/napi/Block.mm index f7835163..0ba3bacd 100644 --- a/NativeScript/ffi/napi/Block.mm +++ b/NativeScript/ffi/napi/Block.mm @@ -10,7 +10,7 @@ #include #include "HermesFastCallbackInfo.h" #include "Interop.h" -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" #include "TypeConv.h" diff --git a/NativeScript/ffi/napi/CFunction.mm b/NativeScript/ffi/napi/CFunction.mm index 3460149d..d48cece5 100644 --- a/NativeScript/ffi/napi/CFunction.mm +++ b/NativeScript/ffi/napi/CFunction.mm @@ -15,7 +15,7 @@ #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "Tasks.h" #ifdef ENABLE_JS_RUNTIME #include "jsr.h" diff --git a/NativeScript/ffi/napi/ClassMember.mm b/NativeScript/ffi/napi/ClassMember.mm index 8b355490..63fa6221 100644 --- a/NativeScript/ffi/napi/ClassMember.mm +++ b/NativeScript/ffi/napi/ClassMember.mm @@ -25,7 +25,7 @@ #include "Util.h" #include "Block.h" #include "Class.h" -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" diff --git a/NativeScript/ffi/napi/Closure.mm b/NativeScript/ffi/napi/Closure.mm index 7f7ec936..ab90659c 100644 --- a/NativeScript/ffi/napi/Closure.mm +++ b/NativeScript/ffi/napi/Closure.mm @@ -6,7 +6,7 @@ #include "ObjCBridge.h" #include "TypeConv.h" #include "Util.h" -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #ifdef ENABLE_JS_RUNTIME diff --git a/NativeScript/ffi/hermes/HermesFastCallbackInfo.h b/NativeScript/ffi/napi/engine/hermes/HermesFastCallbackInfo.h similarity index 100% rename from NativeScript/ffi/hermes/HermesFastCallbackInfo.h rename to NativeScript/ffi/napi/engine/hermes/HermesFastCallbackInfo.h diff --git a/NativeScript/ffi/hermes/HermesFastConversion.mm b/NativeScript/ffi/napi/engine/hermes/HermesFastConversion.mm similarity index 100% rename from NativeScript/ffi/hermes/HermesFastConversion.mm rename to NativeScript/ffi/napi/engine/hermes/HermesFastConversion.mm diff --git a/NativeScript/ffi/hermes/HermesFastNativeApi.h b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h similarity index 100% rename from NativeScript/ffi/hermes/HermesFastNativeApi.h rename to NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h diff --git a/NativeScript/ffi/hermes/HermesFastNativeApi.mm b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.mm similarity index 100% rename from NativeScript/ffi/hermes/HermesFastNativeApi.mm rename to NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.mm diff --git a/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h similarity index 98% rename from NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h rename to NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h index e1274ffa..57839180 100644 --- a/NativeScript/ffi/hermes/HermesFastNativeApiPrivate.h +++ b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h @@ -26,7 +26,7 @@ #include "ffi/napi/ClassMember.h" #include "ffi/napi/Interop.h" #include "InvocationSupport.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" #include "ffi/napi/TypeConv.h" diff --git a/NativeScript/ffi/jsc/JSCFastConversion.mm b/NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm similarity index 100% rename from NativeScript/ffi/jsc/JSCFastConversion.mm rename to NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm diff --git a/NativeScript/ffi/jsc/JSCFastNativeApi.h b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h similarity index 100% rename from NativeScript/ffi/jsc/JSCFastNativeApi.h rename to NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h diff --git a/NativeScript/ffi/jsc/JSCFastNativeApi.mm b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm similarity index 100% rename from NativeScript/ffi/jsc/JSCFastNativeApi.mm rename to NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm diff --git a/NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h similarity index 98% rename from NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h rename to NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h index 0e759263..80951639 100644 --- a/NativeScript/ffi/jsc/JSCFastNativeApiPrivate.h +++ b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h @@ -22,7 +22,7 @@ #include "InvocationSupport.h" #include "ffi/napi/Interop.h" #include "MetadataReader.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ffi/napi/Object.h" #include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" diff --git a/NativeScript/ffi/quickjs/QuickJSFastConversion.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm similarity index 100% rename from NativeScript/ffi/quickjs/QuickJSFastConversion.mm rename to NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.h b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h similarity index 100% rename from NativeScript/ffi/quickjs/QuickJSFastNativeApi.h rename to NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm similarity index 100% rename from NativeScript/ffi/quickjs/QuickJSFastNativeApi.mm rename to NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm diff --git a/NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h similarity index 99% rename from NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h rename to NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h index 176b6558..259c6706 100644 --- a/NativeScript/ffi/quickjs/QuickJSFastNativeApiPrivate.h +++ b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h @@ -25,7 +25,7 @@ #include "InvocationSupport.h" #include "ffi/napi/Interop.h" #include "MetadataReader.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" #include "ffi/napi/TypeConv.h" diff --git a/NativeScript/ffi/quickjs/QuickJSFastReturn.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm similarity index 100% rename from NativeScript/ffi/quickjs/QuickJSFastReturn.mm rename to NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm diff --git a/NativeScript/ffi/shared/EngineDirectCall.h b/NativeScript/ffi/napi/engine/shared/EngineDirectCall.h similarity index 100% rename from NativeScript/ffi/shared/EngineDirectCall.h rename to NativeScript/ffi/napi/engine/shared/EngineDirectCall.h diff --git a/NativeScript/ffi/shared/EngineDirectCall.mm b/NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm similarity index 99% rename from NativeScript/ffi/shared/EngineDirectCall.mm rename to NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm index ecd09a86..0c6bf96b 100644 --- a/NativeScript/ffi/shared/EngineDirectCall.mm +++ b/NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm @@ -16,7 +16,7 @@ #include "ffi/napi/ClassBuilder.h" #include "ffi/napi/ClassMember.h" #include "ffi/napi/Interop.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" #include "ffi/napi/TypeConv.h" diff --git a/NativeScript/ffi/shared/InvocationSupport.h b/NativeScript/ffi/napi/engine/shared/InvocationSupport.h similarity index 93% rename from NativeScript/ffi/shared/InvocationSupport.h rename to NativeScript/ffi/napi/engine/shared/InvocationSupport.h index 38c515f5..650c4243 100644 --- a/NativeScript/ffi/shared/InvocationSupport.h +++ b/NativeScript/ffi/napi/engine/shared/InvocationSupport.h @@ -1,5 +1,5 @@ -#ifndef NS_FFI_SHARED_INVOCATION_SUPPORT_H -#define NS_FFI_SHARED_INVOCATION_SUPPORT_H +#ifndef NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H +#define NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H #include #include @@ -86,4 +86,4 @@ class EngineDirectReturnStorage { } // namespace nativescript -#endif // NS_FFI_SHARED_INVOCATION_SUPPORT_H +#endif // NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H diff --git a/NativeScript/ffi/shared/SignatureDispatch.h b/NativeScript/ffi/napi/engine/shared/SignatureDispatch.h similarity index 100% rename from NativeScript/ffi/shared/SignatureDispatch.h rename to NativeScript/ffi/napi/engine/shared/SignatureDispatch.h diff --git a/NativeScript/ffi/v8/V8FastConversion.mm b/NativeScript/ffi/napi/engine/v8/V8FastConversion.mm similarity index 100% rename from NativeScript/ffi/v8/V8FastConversion.mm rename to NativeScript/ffi/napi/engine/v8/V8FastConversion.mm diff --git a/NativeScript/ffi/v8/V8FastNativeApi.h b/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h similarity index 100% rename from NativeScript/ffi/v8/V8FastNativeApi.h rename to NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h diff --git a/NativeScript/ffi/v8/V8FastNativeApi.mm b/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.mm similarity index 100% rename from NativeScript/ffi/v8/V8FastNativeApi.mm rename to NativeScript/ffi/napi/engine/v8/V8FastNativeApi.mm diff --git a/NativeScript/ffi/v8/V8FastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h similarity index 99% rename from NativeScript/ffi/v8/V8FastNativeApiPrivate.h rename to NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h index 70bdf0b4..5e667460 100644 --- a/NativeScript/ffi/v8/V8FastNativeApiPrivate.h +++ b/NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h @@ -31,7 +31,7 @@ #include "ffi/napi/ObjCBridge.h" #include "SignatureDispatch.h" #include "ffi/napi/TypeConv.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "v8-api.h" namespace nativescript { diff --git a/NativeScript/ffi/v8/V8FastNativeWrapper.mm b/NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm similarity index 100% rename from NativeScript/ffi/v8/V8FastNativeWrapper.mm rename to NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 522090cc..f07774a8 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -15,7 +15,7 @@ #include #include -#include "ffi/jsc/JSCFastNativeApi.h" +#include "JSCFastNativeApi.h" struct napi_callback_info__ { napi_value newTarget; diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index 744269df..e6b1c4b9 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -4,7 +4,7 @@ #include #include "js_native_api.h" -#include "ffi/quickjs/QuickJSFastNativeApi.h" +#include "QuickJSFastNativeApi.h" #include "libbf.h" #include "quicks-runtime.h" diff --git a/NativeScript/napi/v8/v8-api.cpp b/NativeScript/napi/v8/v8-api.cpp index ec49f746..3247b648 100644 --- a/NativeScript/napi/v8/v8-api.cpp +++ b/NativeScript/napi/v8/v8-api.cpp @@ -9,7 +9,7 @@ #define NAPI_EXPERIMENTAL -#include "ffi/v8/V8FastNativeApi.h" +#include "V8FastNativeApi.h" #include "js_native_api.h" #include "v8-api.h" #include "v8-module-loader.h" diff --git a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp index e9a8a094..a871a7fb 100644 --- a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp +++ b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp @@ -14,7 +14,7 @@ #include #include "JsV8InspectorClient.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "Runtime.h" namespace tns { diff --git a/NativeScript/runtime/NativeScript.mm b/NativeScript/runtime/NativeScript.mm index b1e9fa2c..827e0612 100644 --- a/NativeScript/runtime/NativeScript.mm +++ b/NativeScript/runtime/NativeScript.mm @@ -1,7 +1,7 @@ #include "NativeScript.h" #include "Runtime.h" #include "RuntimeConfig.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "ffi/shared/Tasks.h" #include "js_native_api.h" #include "jsr.h" diff --git a/NativeScript/ffi/napi/NativeScriptException.h b/NativeScript/runtime/NativeScriptException.h similarity index 89% rename from NativeScript/ffi/napi/NativeScriptException.h rename to NativeScript/runtime/NativeScriptException.h index 3f973009..c46488d9 100644 --- a/NativeScript/ffi/napi/NativeScriptException.h +++ b/NativeScript/runtime/NativeScriptException.h @@ -1,5 +1,5 @@ -#ifndef NativeScriptException_h -#define NativeScriptException_h +#ifndef NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H +#define NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H #include @@ -49,4 +49,4 @@ class NativeScriptException { } // namespace nativescript -#endif /* NativeScriptException_h */ +#endif // NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H diff --git a/NativeScript/ffi/napi/NativeScriptException.mm b/NativeScript/runtime/NativeScriptException.mm similarity index 99% rename from NativeScript/ffi/napi/NativeScriptException.mm rename to NativeScript/runtime/NativeScriptException.mm index 6b9a01be..6fe9e178 100644 --- a/NativeScript/ffi/napi/NativeScriptException.mm +++ b/NativeScript/runtime/NativeScriptException.mm @@ -1,4 +1,4 @@ -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #import #include #include "js_native_api.h" diff --git a/NativeScript/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index a6a653bc..26ef4559 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -116,6 +116,26 @@ void Runtime::Init(bool isWorker) { napi_set_named_property(env_, global, "global", global); const char* CompatScript = R"( + if (typeof globalThis.__extends !== "function") { + var __extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function(d, b) { d.__proto__ = b; }) || + function(d, b) { + for (var p in b) { + if (Object.prototype.hasOwnProperty.call(b, p)) { + d[p] = b[p]; + } + } + }; + globalThis.__extends = function(d, b) { + if (typeof b !== "function" && b !== null) { + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + } + __extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + } + if (typeof globalThis.__decorate !== "function") { globalThis.__decorate = function(decorators, target, key, desc) { var c = arguments.length; @@ -362,16 +382,19 @@ void Runtime::Init(bool isWorker) { #endif const char* metadata_path = std::getenv("NS_METADATA_PATH"); +#if NS_FFI_BACKEND_NAPI nativescript_init(env_, metadata_path, RuntimeConfig.MetadataPtr); +#endif -#ifdef TARGET_ENGINE_HERMES +#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_HERMES) if (auto* jsiRuntime = js_get_jsi_runtime(env_)) { NativeApiJsiConfig nativeApiJsiConfig; nativeApiJsiConfig.metadataPath = metadata_path; nativeApiJsiConfig.metadataPtr = RuntimeConfig.MetadataPtr; + nativeApiJsiConfig.installGlobalSymbols = true; InstallNativeApiJSI(*jsiRuntime, nativeApiJsiConfig); } -#endif // TARGET_ENGINE_HERMES +#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_HERMES napi_close_handle_scope(env_, scope); } diff --git a/NativeScript/runtime/Util.h b/NativeScript/runtime/Util.h index ef2b7622..7d5ed072 100644 --- a/NativeScript/runtime/Util.h +++ b/NativeScript/runtime/Util.h @@ -1,6 +1,6 @@ #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "jsr_common.h" #include "native_api_util.h" #include "robin_hood.h" diff --git a/NativeScript/runtime/modules/console/Console.cpp b/NativeScript/runtime/modules/console/Console.cpp index 2ac59e39..f8a07a22 100644 --- a/NativeScript/runtime/modules/console/Console.cpp +++ b/NativeScript/runtime/modules/console/Console.cpp @@ -4,7 +4,7 @@ #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" diff --git a/NativeScript/runtime/modules/module/ModuleInternal.cpp b/NativeScript/runtime/modules/module/ModuleInternal.cpp index 2b508fa8..efec4bc9 100644 --- a/NativeScript/runtime/modules/module/ModuleInternal.cpp +++ b/NativeScript/runtime/modules/module/ModuleInternal.cpp @@ -15,7 +15,7 @@ #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" #include "runtime/Util.h" diff --git a/NativeScript/runtime/modules/worker/MessageV8.cpp b/NativeScript/runtime/modules/worker/MessageV8.cpp index f736b7f2..f32c97c4 100644 --- a/NativeScript/runtime/modules/worker/MessageV8.cpp +++ b/NativeScript/runtime/modules/worker/MessageV8.cpp @@ -10,7 +10,7 @@ #include "MessageV8.h" -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "v8-api.h" using namespace v8; diff --git a/NativeScript/runtime/modules/worker/Worker.mm b/NativeScript/runtime/modules/worker/Worker.mm index e4747afe..039f86f0 100644 --- a/NativeScript/runtime/modules/worker/Worker.mm +++ b/NativeScript/runtime/modules/worker/Worker.mm @@ -2,7 +2,7 @@ #include #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.h b/NativeScript/runtime/modules/worker/WorkerImpl.h index dc6898cf..90c23d01 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.h +++ b/NativeScript/runtime/modules/worker/WorkerImpl.h @@ -5,7 +5,7 @@ #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api_types.h" #include "native_api_util.h" #include "runtime/modules/worker/ConcurrentMap.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.mm b/NativeScript/runtime/modules/worker/WorkerImpl.mm index 6af0d585..4fa77f34 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.mm +++ b/NativeScript/runtime/modules/worker/WorkerImpl.mm @@ -1,7 +1,7 @@ #include #include #include -#include "ffi/napi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 7e9532e3..cca155ba 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -172,7 +172,7 @@ def is_nativescript_source_root(search_path): strict_includes = env_or_none("NS_DEBUG_METADATA_STRICT_INCLUDES") or env_or_none("TNS_DEBUG_METADATA_STRICT_INCLUDES") signature_bindings_cpp_path = env_or_none("NS_SIGNATURE_BINDINGS_CPP_PATH") or env_or_none("TNS_SIGNATURE_BINDINGS_CPP_PATH") if signature_bindings_cpp_path is None: - default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "shared", "GeneratedSignatureDispatch.inc") + default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc") if os.path.isdir(os.path.dirname(default_signature_bindings_path)): signature_bindings_cpp_path = default_signature_bindings_path diff --git a/package.json b/package.json index 2087ba01..a43a2970 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "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", + "check:ffi-boundaries": "./scripts/check_ffi_boundaries.sh", "test-rn-turbomodule": "./scripts/test_react_native_turbomodule.sh", "test-rn-ffi": "./scripts/test_react_native_ffi_compat.sh", "demo-rn-turbomodule": "./scripts/create_react_native_demo.sh", diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index c41f7607..dd96729d 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -14,9 +14,10 @@ EMBED_METADATA=$(to_bool ${EMBED_METADATA:=false}) CONFIG_BUILD=RelWithDebInfo TARGET_ENGINE=${TARGET_ENGINE:=v8} # default to v8 for compat +NS_FFI_BACKEND=${NS_FFI_BACKEND:=auto} 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/shared/GeneratedSignatureDispatch.inc}} +GENERATED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/napi/engine/shared/GeneratedSignatureDispatch.inc}} GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" for arg in $@; do @@ -42,6 +43,9 @@ for arg in $@; do --embed-metadata) EMBED_METADATA=true ;; --hermes) TARGET_ENGINE=hermes ;; --no-engine|--generic-napi) TARGET_ENGINE=none ;; + --ffi-direct) NS_FFI_BACKEND=direct ;; + --ffi-napi) NS_FFI_BACKEND=napi ;; + --ffi-backend=*) NS_FFI_BACKEND="${arg#--ffi-backend=}" ;; --gsd-v8) NS_GSD_BACKEND=v8 ;; --gsd-jsc) NS_GSD_BACKEND=jsc ;; --gsd-quickjs) NS_GSD_BACKEND=quickjs ;; @@ -88,6 +92,13 @@ function assemble_node_api_xcframework () { } function effective_gsd_backend () { + local is_macos_napi="${1:-false}" + + if [ "$(effective_ffi_backend "$is_macos_napi")" == "direct" ]; then + echo none + return + fi + case "$NS_GSD_BACKEND" in auto) if [ "$TARGET_ENGINE" == "none" ]; then @@ -110,21 +121,47 @@ function effective_gsd_backend () { esac } +function effective_ffi_backend () { + local is_macos_napi="${1:-false}" + + if $is_macos_napi || [ "$TARGET_ENGINE" == "none" ]; then + echo napi + return + fi + + case "$NS_FFI_BACKEND" in + auto) + if [ "$TARGET_ENGINE" == "hermes" ]; then + echo direct + else + echo napi + fi + ;; + *) + echo "$NS_FFI_BACKEND" + ;; + esac +} + function signature_dispatch_stamp () { local platform="$1" + local is_macos_napi="${2:-false}" local backend - backend=$(effective_gsd_backend) + backend=$(effective_gsd_backend "$is_macos_napi") + local ffi_backend + ffi_backend=$(effective_ffi_backend "$is_macos_napi") local generator_hash generator_hash=$(find ./metadata-generator/src ./metadata-generator/include ./metadata-generator/CMakeLists.txt \ -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}') - printf "platform=%s\nbackend=%s\ntarget_engine=%s\nmetadata_size=%s\ngenerator_hash=%s\n" \ - "$platform" "$backend" "$TARGET_ENGINE" "$METADATA_SIZE" "$generator_hash" + printf "platform=%s\nbackend=%s\nffi_backend=%s\ntarget_engine=%s\nmetadata_size=%s\ngenerator_hash=%s\n" \ + "$platform" "$backend" "$ffi_backend" "$TARGET_ENGINE" "$METADATA_SIZE" "$generator_hash" } function ensure_signature_dispatch_bindings () { local platform="$1" + local is_macos_napi="${2:-false}" local backend - backend=$(effective_gsd_backend) + backend=$(effective_gsd_backend "$is_macos_napi") if [ "$TARGET_ENGINE" == "none" ] || [ "$backend" == "none" ]; then return fi @@ -134,7 +171,7 @@ function ensure_signature_dispatch_bindings () { fi local expected_stamp - expected_stamp=$(signature_dispatch_stamp "$platform") + expected_stamp=$(signature_dispatch_stamp "$platform" "$is_macos_napi") if [ -f "$GENERATED_SIGNATURE_DISPATCH" ] && \ [ -f "$GENERATED_SIGNATURE_DISPATCH_STAMP" ] && \ [ "$(cat "$GENERATED_SIGNATURE_DISPATCH_STAMP")" == "$expected_stamp" ]; then @@ -174,7 +211,7 @@ function cmake_build () { is_macos_napi=true fi - ensure_signature_dispatch_bindings "$platform" + ensure_signature_dispatch_bindings "$platform" "$is_macos_napi" local libffi_build_dir= case "$platform" in @@ -207,6 +244,12 @@ function cmake_build () { echo "Reconfiguring $platform build directory for GSD backend '$NS_GSD_BACKEND' (was '$cached_gsd_backend')." needs_reconfigure=true fi + local cached_ffi_backend + cached_ffi_backend=$(grep '^NS_FFI_BACKEND:STRING=' "$cache_file" | sed 's/^NS_FFI_BACKEND:STRING=//' || true) + if [ -n "$cached_ffi_backend" ] && [ "$cached_ffi_backend" != "$NS_FFI_BACKEND" ]; then + echo "Reconfiguring $platform build directory for FFI backend '$NS_FFI_BACKEND' (was '$cached_ffi_backend')." + needs_reconfigure=true + fi if $needs_reconfigure; then rm -rf "$build_dir" fi @@ -224,7 +267,7 @@ function cmake_build () { fi - 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 -S=./NativeScript -B="$build_dir" -GXcode -DTARGET_PLATFORM=$platform -DTARGET_ENGINE=$TARGET_ENGINE -DNS_FFI_BACKEND=$NS_FFI_BACKEND -DNS_GSD_BACKEND=$NS_GSD_BACKEND -DMETADATA_SIZE=$METADATA_SIZE -DBUILD_CLI_BINARY=$is_macos_cli -DBUILD_MACOS_NODE_API=$is_macos_napi cmake --build "$build_dir" --config $CONFIG_BUILD -- \ CODE_SIGN_STYLE=Manual \ diff --git a/scripts/check_ffi_boundaries.sh b/scripts/check_ffi_boundaries.sh new file mode 100755 index 00000000..4b2dd396 --- /dev/null +++ b/scripts/check_ffi_boundaries.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DIRECT_DIRS=( + "$ROOT_DIR/NativeScript/ffi/hermes" + "$ROOT_DIR/NativeScript/ffi/v8" + "$ROOT_DIR/NativeScript/ffi/jsc" + "$ROOT_DIR/NativeScript/ffi/quickjs" + "$ROOT_DIR/NativeScript/ffi/shared" +) + +EXISTING_DIRECT_DIRS=() +for dir in "${DIRECT_DIRS[@]}"; do + if [ -d "$dir" ]; then + EXISTING_DIRECT_DIRS+=("$dir") + fi +done + +if [ "${#EXISTING_DIRECT_DIRS[@]}" -eq 0 ]; then + exit 0 +fi + +if rg -n '\b(napi_|napi_env|napi_value|js_native_api|node_api)\b' \ + "${EXISTING_DIRECT_DIRS[@]}" \ + -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}'; then + echo "Node-API symbols are not allowed in shared or direct engine FFI folders." >&2 + exit 1 +fi diff --git a/scripts/metagen.js b/scripts/metagen.js index 3d4b8dfe..03797f21 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -316,7 +316,7 @@ async function main() { const signatureBindingsPath = process.env.NS_SIGNATURE_BINDINGS_CPP_PATH || process.env.TNS_SIGNATURE_BINDINGS_CPP_PATH || - path.resolve(__dirname, "..", "NativeScript", "ffi", "shared", "GeneratedSignatureDispatch.inc"); + path.resolve(__dirname, "..", "NativeScript", "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc"); await fsp.rm(typesDir, { recursive: true, force: true }); await fsp.mkdir(typesDir, { recursive: true }); await fsp.mkdir(metadataDir, { recursive: true }); diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 1111dfbc..0c8d8f15 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -7,6 +7,8 @@ // - MACOS_TEST_CLEAN_BUILD=1 deletes derived data before build. // - MACOS_TEST_ENGINE selects the runtime engine build to use when runtime // artifacts need rebuilding. Supported: v8, hermes, quickjs, jsc. Defaults to v8. +// - MACOS_TEST_FFI_BACKEND selects the FFI backend build to use when runtime +// artifacts need rebuilding. Supported: auto, napi, direct. Defaults to auto. // - MACOS_COMMAND_TIMEOUT_MS overrides timeout for build commands (default: 10 minutes). // - MACOS_COMMAND_MAX_BUFFER_BYTES overrides spawnSync maxBuffer for captured command output (default: 64 MiB). // - MACOS_TEST_TIMEOUT_MS overrides max test runtime after launch (default: 2 minutes). @@ -88,6 +90,7 @@ const inactivityTimeoutMs = parseTimeoutMs("MACOS_TEST_INACTIVITY_TIMEOUT_MS", 4 const emitJunitLogs = process.env.MACOS_LOG_JUNIT !== "0"; const requestedTests = (process.env.MACOS_TESTS || "").trim(); const requestedEngine = (process.env.MACOS_TEST_ENGINE || "v8").trim().toLowerCase(); +const requestedFfiBackend = (process.env.MACOS_TEST_FFI_BACKEND || "auto").trim().toLowerCase(); const launchedMarker = "Application Start!"; const junitPrefix = "TKUnit: "; @@ -95,8 +98,8 @@ const junitEndTag = ""; const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); const generatedRuntimeBuildOutputs = new Set([ - path.join(nativeScriptSourceRoot, "ffi", "shared", "GeneratedSignatureDispatch.inc"), - path.join(nativeScriptSourceRoot, "ffi", "shared", "GeneratedSignatureDispatch.inc.stamp") + path.join(nativeScriptSourceRoot, "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc.stamp") ]); function parseArgs() { @@ -491,20 +494,29 @@ function ensureMacOSRuntimeArtifactsBuilt() { ); const artifactMtime = getPathStats(nativeScriptXCFramework).maxMtimeMs; let configuredEngine = null; + let configuredFfiBackend = null; if (fs.existsSync(cachePath)) { try { const cache = fs.readFileSync(cachePath, "utf8"); - const match = cache.match(/^TARGET_ENGINE:STRING=(.+)$/m); - if (match) { - configuredEngine = match[1].trim().toLowerCase(); + const engineMatch = cache.match(/^TARGET_ENGINE:STRING=(.+)$/m); + if (engineMatch) { + configuredEngine = engineMatch[1].trim().toLowerCase(); + } + const ffiBackendMatch = cache.match(/^NS_FFI_BACKEND:STRING=(.+)$/m); + if (ffiBackendMatch) { + configuredFfiBackend = ffiBackendMatch[1].trim().toLowerCase(); } } catch (_) { configuredEngine = null; + configuredFfiBackend = null; } } - if (artifactMtime > 0 && artifactMtime >= sourceMtime && configuredEngine === requestedEngine) { + if (artifactMtime > 0 && + artifactMtime >= sourceMtime && + configuredEngine === requestedEngine && + configuredFfiBackend === requestedFfiBackend) { return; } @@ -513,10 +525,15 @@ function ensureMacOSRuntimeArtifactsBuilt() { throw new Error(`Unsupported MACOS_TEST_ENGINE: ${requestedEngine}`); } - console.log(`NativeScript macOS artifacts are missing, stale, or built for '${configuredEngine ?? "unknown"}'; running ${requestedEngine} build...`); + const supportedFfiBackends = new Set(["auto", "napi", "direct"]); + if (!supportedFfiBackends.has(requestedFfiBackend)) { + throw new Error(`Unsupported MACOS_TEST_FFI_BACKEND: ${requestedFfiBackend}`); + } + + console.log(`NativeScript macOS artifacts are missing, stale, or built for '${configuredEngine ?? "unknown"}/${configuredFfiBackend ?? "unknown"}'; running ${requestedEngine}/${requestedFfiBackend} build...`); runBuildAndRequireSuccess( path.join(__dirname, "build_nativescript.sh"), - ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`], + ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`, `--ffi-backend=${requestedFfiBackend}`], commandTimeoutMs ); } @@ -539,7 +556,7 @@ function buildTestRunnerApp() { ensureMetadataGeneratorBuilt(); ensureMacOSRuntimeArtifactsBuilt(); - const nativeFingerprint = `${requestedEngine}:${createBuildFingerprint(macosBuildInputs)}`; + const nativeFingerprint = `${requestedEngine}:${requestedFfiBackend}:${createBuildFingerprint(macosBuildInputs)}`; const existingBuildState = readBuildState(); const canReuseBuild = process.env.MACOS_TEST_CLEAN_BUILD !== "1" && fs.existsSync(appPath) && From 9a4df3df5d764ec6ff4799d8f842e79f5a628a43 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 25 May 2026 02:47:47 -0400 Subject: [PATCH 31/31] refactor(ffi): clean direct Hermes backend --- .gitignore | 4 +- NativeScript/CMakeLists.txt | 80 +- NativeScript/ffi/hermes/jsi/NativeApiJsi.h | 3 + NativeScript/ffi/hermes/jsi/NativeApiJsi.mm | 6755 +---------------- .../ffi/hermes/jsi/NativeApiJsiBridge.inc | 1598 ++++ .../ffi/hermes/jsi/NativeApiJsiCallbacks.inc | 1354 ++++ .../hermes/jsi/NativeApiJsiClassBuilder.inc | 784 ++ .../ffi/hermes/jsi/NativeApiJsiConversion.inc | 2067 +++++ .../ffi/hermes/jsi/NativeApiJsiHostObject.inc | 513 ++ .../hermes/jsi/NativeApiJsiHostObjects.inc | 1743 +++++ .../ffi/hermes/jsi/NativeApiJsiInstall.inc | 1520 ++++ .../ffi/hermes/jsi/NativeApiJsiInvocation.inc | 496 ++ .../ffi/hermes/jsi/NativeApiJsiReactNative.h | 1 + NativeScript/ffi/hermes/jsi/README.md | 23 +- NativeScript/ffi/napi/Block.h | 1 - NativeScript/ffi/napi/Block.mm | 151 - NativeScript/ffi/napi/CFunction.h | 5 - NativeScript/ffi/napi/CFunction.mm | 81 +- NativeScript/ffi/napi/Cif.h | 1 - NativeScript/ffi/napi/Cif.mm | 26 - NativeScript/ffi/napi/ClassMember.h | 17 - NativeScript/ffi/napi/ClassMember.mm | 93 +- NativeScript/ffi/napi/ObjCBridge.h | 1 - NativeScript/ffi/napi/ObjCBridge.mm | 43 +- NativeScript/ffi/napi/Object.mm | 68 - NativeScript/ffi/napi/SignatureDispatch.h | 223 + NativeScript/ffi/napi/TypeConv.h | 151 - .../engine/hermes/HermesFastCallbackInfo.h | 83 - .../engine/hermes/HermesFastConversion.mm | 1315 ---- .../napi/engine/hermes/HermesFastNativeApi.h | 41 - .../napi/engine/hermes/HermesFastNativeApi.mm | 946 --- .../hermes/HermesFastNativeApiPrivate.h | 111 - .../ffi/napi/engine/jsc/JSCFastConversion.mm | 1144 --- .../ffi/napi/engine/jsc/JSCFastNativeApi.h | 21 - .../ffi/napi/engine/jsc/JSCFastNativeApi.mm | 763 -- .../napi/engine/jsc/JSCFastNativeApiPrivate.h | 92 - .../engine/quickjs/QuickJSFastConversion.mm | 680 -- .../engine/quickjs/QuickJSFastNativeApi.h | 20 - .../engine/quickjs/QuickJSFastNativeApi.mm | 750 -- .../quickjs/QuickJSFastNativeApiPrivate.h | 196 - .../napi/engine/quickjs/QuickJSFastReturn.mm | 332 - .../ffi/napi/engine/shared/EngineDirectCall.h | 53 - .../napi/engine/shared/EngineDirectCall.mm | 1057 --- .../napi/engine/shared/InvocationSupport.h | 89 - .../napi/engine/shared/SignatureDispatch.h | 974 --- .../ffi/napi/engine/v8/V8FastConversion.mm | 683 -- .../ffi/napi/engine/v8/V8FastNativeApi.h | 22 - .../ffi/napi/engine/v8/V8FastNativeApi.mm | 1201 --- .../napi/engine/v8/V8FastNativeApiPrivate.h | 205 - .../ffi/napi/engine/v8/V8FastNativeWrapper.mm | 233 - NativeScript/napi/jsc/jsc-api.cpp | 6 - NativeScript/napi/quickjs/quickjs-api.c | 6 - NativeScript/napi/v8/v8-api.cpp | 6 - NativeScript/runtime/Runtime.cpp | 126 +- .../build-step-metadata-generator.py | 2 +- .../NativeScriptNativeApi.podspec | 2 +- .../ios/NativeScriptNativeApiModule.mm | 3 + packages/react-native/src/index.ts | 3 +- scripts/build_nativescript.sh | 10 +- scripts/build_react_native_turbomodule.sh | 1 + scripts/check_ffi_boundaries.sh | 15 + scripts/metagen.js | 2 +- scripts/run-tests-macos.js | 14 +- test/react-native/ffi-compat/App.tsx | 64 +- .../runner/app/tests/Infrastructure/timers.js | 76 + test/runtime/runner/app/tests/Timers.js | 15 +- test/runtime/runner/app/tests/index.js | 2 +- 67 files changed, 10634 insertions(+), 18532 deletions(-) create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiBridge.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiCallbacks.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiClassBuilder.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiConversion.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiHostObject.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiHostObjects.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiInstall.inc create mode 100644 NativeScript/ffi/hermes/jsi/NativeApiJsiInvocation.inc create mode 100644 NativeScript/ffi/napi/SignatureDispatch.h delete mode 100644 NativeScript/ffi/napi/engine/hermes/HermesFastCallbackInfo.h delete mode 100644 NativeScript/ffi/napi/engine/hermes/HermesFastConversion.mm delete mode 100644 NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h delete mode 100644 NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.mm delete mode 100644 NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h delete mode 100644 NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm delete mode 100644 NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h delete mode 100644 NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm delete mode 100644 NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h delete mode 100644 NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm delete mode 100644 NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h delete mode 100644 NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm delete mode 100644 NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h delete mode 100644 NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm delete mode 100644 NativeScript/ffi/napi/engine/shared/EngineDirectCall.h delete mode 100644 NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm delete mode 100644 NativeScript/ffi/napi/engine/shared/InvocationSupport.h delete mode 100644 NativeScript/ffi/napi/engine/shared/SignatureDispatch.h delete mode 100644 NativeScript/ffi/napi/engine/v8/V8FastConversion.mm delete mode 100644 NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h delete mode 100644 NativeScript/ffi/napi/engine/v8/V8FastNativeApi.mm delete mode 100644 NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h delete mode 100644 NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm diff --git a/.gitignore b/.gitignore index 9d05b325..edc6abe5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,8 +65,8 @@ packages/*/types SwiftBindgen # Generated Objective-C/C dispatch wrappers -NativeScript/ffi/shared/GeneratedSignatureDispatch.inc -NativeScript/ffi/shared/GeneratedSignatureDispatch.inc.stamp +NativeScript/ffi/napi/GeneratedSignatureDispatch.inc +NativeScript/ffi/napi/GeneratedSignatureDispatch.inc.stamp # React Native TurboModule package staging packages/react-native/dist/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 468b60cd..7222186a 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -174,14 +174,6 @@ endif() if(NS_GSD_BACKEND STREQUAL "auto") if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") set(NS_EFFECTIVE_GSD_BACKEND "none") - elseif(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() @@ -208,6 +200,13 @@ 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() +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" AND + NOT (NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi" OR + NS_EFFECTIVE_GSD_BACKEND STREQUAL "none")) + message(FATAL_ERROR + "NS_FFI_BACKEND=napi is the pure Node-API FFI backend and only supports " + "NS_GSD_BACKEND=napi or none.") +endif() message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND})") @@ -215,22 +214,11 @@ message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND}) include_directories( ./ ffi/shared - ffi/hermes/jsi ../metadata-generator/include napi/common libffi/${LIBFFI_BUILD}/include ) -if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") - include_directories( - ffi/napi/engine/shared - ffi/napi/engine/v8 - ffi/napi/engine/hermes - ffi/napi/engine/jsc - ffi/napi/engine/quickjs - ) -endif() - set(FFI_SHARED_SOURCE_FILES ffi/shared/Tasks.cpp ) @@ -258,36 +246,10 @@ set(FFI_NAPI_SOURCE_FILES ffi/napi/ClassBuilder.mm ) -set(FFI_NAPI_ENGINE_SOURCE_FILES - ffi/napi/engine/shared/EngineDirectCall.mm -) - -set(FFI_NAPI_V8_SOURCE_FILES - ffi/napi/engine/v8/V8FastConversion.mm - ffi/napi/engine/v8/V8FastNativeApi.mm - ffi/napi/engine/v8/V8FastNativeWrapper.mm -) - set(FFI_HERMES_DIRECT_SOURCE_FILES ffi/hermes/jsi/NativeApiJsi.mm ) -set(FFI_NAPI_HERMES_SOURCE_FILES - ffi/napi/engine/hermes/HermesFastConversion.mm - ffi/napi/engine/hermes/HermesFastNativeApi.mm -) - -set(FFI_NAPI_QUICKJS_SOURCE_FILES - ffi/napi/engine/quickjs/QuickJSFastConversion.mm - ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm - ffi/napi/engine/quickjs/QuickJSFastReturn.mm -) - -set(FFI_NAPI_JSC_SOURCE_FILES - ffi/napi/engine/jsc/JSCFastConversion.mm - ffi/napi/engine/jsc/JSCFastNativeApi.mm -) - set(SOURCE_FILES ${FFI_SHARED_SOURCE_FILES} runtime/NativeScriptException.mm @@ -297,7 +259,6 @@ if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") set(SOURCE_FILES ${SOURCE_FILES} ${FFI_NAPI_SOURCE_FILES} - ${FFI_NAPI_ENGINE_SOURCE_FILES} ) endif() @@ -345,13 +306,6 @@ if(ENABLE_JS_RUNTIME) napi/v8/SimpleAllocator.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") - set(SOURCE_FILES - ${SOURCE_FILES} - ${FFI_NAPI_V8_SOURCE_FILES} - ) - endif() - elseif(TARGET_ENGINE_HERMES) set(HERMES_HEADERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Frameworks/hermes-headers") @@ -376,12 +330,7 @@ if(ENABLE_JS_RUNTIME) napi/hermes/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") - set(SOURCE_FILES - ${SOURCE_FILES} - ${FFI_NAPI_HERMES_SOURCE_FILES} - ) - else() + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") set(SOURCE_FILES ${SOURCE_FILES} ${FFI_HERMES_DIRECT_SOURCE_FILES} @@ -420,13 +369,6 @@ if(ENABLE_JS_RUNTIME) napi/quickjs/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") - set(SOURCE_FILES - ${SOURCE_FILES} - ${FFI_NAPI_QUICKJS_SOURCE_FILES} - ) - endif() - elseif(TARGET_ENGINE_JSC) include_directories( napi/jsc @@ -439,12 +381,6 @@ if(ENABLE_JS_RUNTIME) napi/jsc/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") - set(SOURCE_FILES - ${SOURCE_FILES} - ${FFI_NAPI_JSC_SOURCE_FILES} - ) - endif() endif() else() include_directories( diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h index 004685d9..ccc2ab63 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h @@ -21,6 +21,9 @@ struct NativeApiJsiConfig { const void* metadataPtr = nullptr; const char* globalName = "__nativeScriptNativeApi"; std::shared_ptr scheduler = nullptr; + std::function)> nativeInvocationInvoker = nullptr; + std::function)> nativeCallbackInvoker = nullptr; + std::function)> jsThreadCallbackInvoker = nullptr; bool installGlobalSymbols = false; }; diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm index 0c6ea8b5..8bf800ea 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm @@ -33,6 +33,9 @@ #include "Metadata.h" #include "MetadataReader.h" +@protocol NativeApiJsiClassBuilderProtocol +@end + #ifdef EMBED_METADATA_SIZE extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif @@ -57,6747 +60,29 @@ using metagen::MDSectionOffset; using metagen::MDTypeKind; -thread_local bool gDispatchNativeCallsToUI = false; -thread_local bool gExecutingDispatchedUINativeCall = false; -thread_local int gSynchronousNativeInvocationDepth = 0; -std::atomic gActiveSynchronousNativeInvocationDepth{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 += 1; - gActiveSynchronousNativeInvocationDepth.fetch_add(1, - std::memory_order_acq_rel); - } - - ~ScopedNativeApiSynchronousInvocation() { - gSynchronousNativeInvocationDepth -= 1; - gActiveSynchronousNativeInvocationDepth.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::optional runtimeSelectorNameForProperty( - Class cls, bool staticMethod, const std::string& property) { - if (cls == nil || property.empty()) { - return std::nullopt; - } - - Class scan = staticMethod ? object_getClass(cls) : cls; - while (scan != Nil) { - unsigned int methodCount = 0; - Method* methods = class_copyMethodList(scan, &methodCount); - for (unsigned int i = 0; i < methodCount; i++) { - SEL selector = method_getName(methods[i]); - const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; - if (selectorName != nullptr && jsifySelector(selectorName) == property) { - std::string result(selectorName); - free(methods); - return result; - } - } - free(methods); - scan = class_getSuperclass(scan); - } - - return std::nullopt; -} - -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; -} - -bool hasRuntimeSetterForProperty(Class cls, bool staticMethod, - const std::string& property) { - if (cls == nil || property.empty()) { - return false; - } - - std::string setterSelectorName = setterSelectorForProperty(property); - SEL selector = sel_getUid(setterSelectorName.c_str()); - return staticMethod ? class_getClassMethod(cls, selector) != nullptr - : class_getInstanceMethod(cls, selector) != nullptr; -} - -size_t selectorArgumentCount(const std::string& selector) { - return static_cast( - std::count(selector.begin(), selector.end(), ':')); -} - -const NativeApiMember* selectMethodMember( - const std::vector& members, const std::string& property, - bool staticMethod, size_t argumentCount) { - const NativeApiMember* fallback = nullptr; - for (const auto& member : members) { - if (member.property || member.name != property) { - continue; - } - - bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; - if (memberIsStatic != staticMethod) { - continue; - } - - if (fallback == nullptr) { - fallback = &member; - } - if (selectorArgumentCount(member.selectorName) == argumentCount) { - return &member; - } - } - return fallback; -} - -const NativeApiMember* selectPropertyMember( - const std::vector& members, const std::string& property, - bool staticMethod) { - for (const auto& member : members) { - if (!member.property || member.name != property) { - continue; - } - - bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; - if (memberIsStatic == staticMethod) { - return &member; - } - } - return nullptr; -} - -void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); -Protocol* lookupProtocolByNativeName(const std::string& name); - -inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { -#if INTPTR_MAX == INT64_MAX - return pointer & 0x0000FFFFFFFFFFFFULL; -#else - return pointer; -#endif -} - -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* findClassForRuntimeClass(Class cls) const { - Class current = cls; - while (current != Nil) { - const char* name = class_getName(current); - if (name != nullptr) { - if (const NativeApiSymbol* symbol = findClass(name)) { - return symbol; - } - } - current = class_getSuperclass(current); - } - return nullptr; - } - - const NativeApiSymbol* findClassForRuntimePointer(void* pointer) const { - if (pointer == nullptr) { - return nullptr; - } - - auto it = classSymbolsByRuntimePointer_.find( - normalizeRuntimePointer(reinterpret_cast(pointer))); - return it != classSymbolsByRuntimePointer_.end() ? &it->second : nullptr; - } - - const NativeApiSymbol* findProtocolForRuntimePointer(void* pointer) const { - if (pointer == nullptr) { - return nullptr; - } - - auto it = protocolSymbolsByRuntimePointer_.find( - normalizeRuntimePointer(reinterpret_cast(pointer))); - return it != protocolSymbolsByRuntimePointer_.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; - } - - void rememberRoundTripValue(Runtime& runtime, const void* native, - const Value& value) { - if (native == nullptr) { - return; - } - roundTripValues_[normalizeRuntimePointer( - reinterpret_cast(native))] = - std::make_shared(runtime, value); - } - - Value findRoundTripValue(Runtime& runtime, const void* native) const { - if (native == nullptr) { - return Value::undefined(); - } - auto it = roundTripValues_.find( - normalizeRuntimePointer(reinterpret_cast(native))); - if (it == roundTripValues_.end() || it->second == nullptr) { - return Value::undefined(); - } - return Value(runtime, *it->second); - } - - void rememberPointerValue(Runtime& runtime, const void* native, - const Value& value) { - pointerValues_[reinterpret_cast(native)] = - std::make_shared(runtime, value); - } - - Value findPointerValue(Runtime& runtime, const void* native) const { - auto it = pointerValues_.find(reinterpret_cast(native)); - if (it == pointerValues_.end() || it->second == nullptr) { - return Value::undefined(); - } - return Value(runtime, *it->second); - } - - void forgetPointerValue(const void* native) { - if (native == nullptr) { - return; - } - pointerValues_.erase(reinterpret_cast(native)); - } - - 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; - } - - const std::vector& membersForProtocol( - const NativeApiSymbol& symbol) const { - auto cached = membersByProtocolOffset_.find(symbol.offset); - if (cached != membersByProtocolOffset_.end()) { - return cached->second; - } - - auto inserted = membersByProtocolOffset_.emplace( - symbol.offset, readMembersForProtocolHierarchy(symbol.offset)); - 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] = symbol; - Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); - if (cls != Nil) { - classSymbolsByRuntimePointer_[normalizeRuntimePointer( - reinterpret_cast(cls))] = symbol; - } - } else if (kind == NativeApiSymbolKind::Protocol) { - protocolSymbolsByOffset_[symbol.offset] = symbol; - Protocol* protocol = lookupProtocolByNativeName(symbol.runtimeName); - if (protocol == nullptr && symbol.runtimeName != symbol.name) { - protocol = lookupProtocolByNativeName(symbol.name); - } - if (protocol != nullptr) { - protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( - reinterpret_cast(protocol))] = 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 readMembersAtOffset( - MDSectionOffset& offset) const { - std::vector members; - bool next = true; - 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 readMembersForProtocolHierarchy( - MDSectionOffset protocolOffset) const { - std::vector members; - if (metadata_ == nullptr || protocolOffset == MD_SECTION_OFFSET_NULL) { - return members; - } - - MDSectionOffset offset = protocolOffset; - auto nameOffset = metadata_->getOffset(offset); - offset += sizeof(MDSectionOffset); - bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; - - while (hasProtocols) { - auto inheritedOffset = metadata_->getOffset(offset); - offset += sizeof(MDSectionOffset); - hasProtocols = (inheritedOffset & metagen::mdSectionOffsetNext) != 0; - inheritedOffset &= ~metagen::mdSectionOffsetNext; - if (inheritedOffset == MD_SECTION_OFFSET_NULL) { - continue; - } - - MDSectionOffset absoluteOffset = - inheritedOffset + metadata_->protocolsOffset; - auto inheritedSymbol = protocolSymbolsByOffset_.find(absoluteOffset); - if (inheritedSymbol != protocolSymbolsByOffset_.end()) { - const auto& inheritedMembers = - membersForProtocol(inheritedSymbol->second); - members.insert(members.end(), inheritedMembers.begin(), - inheritedMembers.end()); - } - } - - std::vector ownMembers = readMembersAtOffset(offset); - members.insert(members.end(), ownMembers.begin(), ownMembers.end()); - 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 classSymbolsByRuntimePointer_; - std::unordered_map protocolSymbolsByRuntimePointer_; - std::unordered_map> roundTripValues_; - std::unordered_map> pointerValues_; - std::unordered_map classSymbolsByOffset_; - std::unordered_map protocolSymbolsByOffset_; - 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_; - mutable std::unordered_map> - membersByProtocolOffset_; - 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; -} +#include "NativeApiJsiBridge.inc" +#include "NativeApiJsiHostObjects.inc" +#include "NativeApiJsiCallbacks.inc" +#include "NativeApiJsiConversion.inc" +#include "NativeApiJsiInvocation.inc" +#include "NativeApiJsiClassBuilder.inc" +#include "NativeApiJsiHostObject.inc" -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; -class NativeApiJsiArgumentFrame; - -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); - -Value makeNativeObjectValue(Runtime& runtime, - const std::shared_ptr& bridge, - id object, bool ownsObject); +} // namespace -Value makeNativeClassValue(Runtime& runtime, - const std::shared_ptr& bridge, - NativeApiSymbol symbol); +#include "NativeApiJsiInstall.inc" -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)); +} // namespace nativescript - 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); +extern "C" void NativeScriptInstallNativeApiJSI( + facebook::jsi::Runtime* runtime, const char* metadataPath) { + if (runtime == nullptr) { + return; } - - return result; -} - -size_t nativeSizeForType(const NativeApiJsiType& type); -std::optional parseArrayIndexProperty(const std::string& property); - -NativeApiJsiType nativeObjectReturnType( - MDTypeKind kind = metagen::mdTypeAnyObject) { - NativeApiJsiType type; - type.kind = kind; - type.ffiType = &ffi_type_pointer; - type.supported = true; - return type; + nativescript::NativeApiJsiConfig config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiJSI(*runtime, config); } -Value convertNativeReturnValue(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, void* value); -Object createPointer(Runtime& runtime, - const std::shared_ptr& bridge, - void* pointer, bool adopted = false); - -NativeApiJsiType primitiveInteropType(MDTypeKind kind); - -class NativeApiPointerHostObject final : public HostObject { - public: - NativeApiPointerHostObject(std::shared_ptr bridge, - void* pointer, std::string kind = "pointer", - bool adopted = false) - : bridge_(std::move(bridge)), - pointer_(pointer), - kind_(std::move(kind)), - adopted_(adopted) {} - - ~NativeApiPointerHostObject() override { - if (adopted_ && pointer_ != nullptr) { - if (bridge_ != nullptr) { - bridge_->forgetPointerValue(pointer_); - } - free(pointer_); - pointer_ = nullptr; - } - } - - void* pointer() const { return pointer_; } - bool adopted() const { return adopted_; } - void adopt() { adopted_ = true; } - void clearWithoutFree() { - if (bridge_ != nullptr) { - bridge_->forgetPointerValue(pointer_); - } - 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"; - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 1, - [bridge, 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 createPointer(runtime, bridge, 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 { - return BigInt::fromUint64( - runtime, - static_cast(reinterpret_cast(pointer))); - }); - } - 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 { - if (hex) { - char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; - snprintf(text, sizeof(text), "0x%llx", - static_cast( - reinterpret_cast(pointer))); - return makeString(runtime, text); - } else { - char text[32] = {}; - 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); - if (kind == "pointer") { - return makeString(runtime, - ""); - } - 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: - std::shared_ptr bridge_; - 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, - size_t byteLength = 0, - std::shared_ptr pendingValue = nullptr) - : bridge_(std::move(bridge)), - type_(std::move(type)), - data_(data), - ownsData_(ownsData), - byteLength_(byteLength), - pendingValue_(std::move(pendingValue)) {} - - ~NativeApiReferenceHostObject() override { - if (ownsData_ && data_ != nullptr) { - free(data_); - data_ = nullptr; - } - } - - void* data() const { return data_; } - const NativeApiJsiType& type() const { return type_; } - void ensureStorage(Runtime& runtime, NativeApiJsiType type, - NativeApiJsiArgumentFrame& frame, size_t elements = 1); - - 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; - size_t byteLength_ = 0; - std::shared_ptr pendingValue_; -}; - -class NativeApiStructObjectHostObject final : public HostObject { - public: - NativeApiStructObjectHostObject( - std::shared_ptr bridge, - std::shared_ptr info, - const void* data = nullptr, bool ownsData = true, - std::shared_ptr> storageOwner = nullptr, - std::shared_ptr backingValue = nullptr) - : bridge_(std::move(bridge)), - info_(std::move(info)), - ownedData_(std::move(storageOwner)), - backingValue_(std::move(backingValue)), - ownsData_(ownsData) { - size_t size = info_ != nullptr ? info_->size : 0; - if (ownedData_ != nullptr) { - data_ = const_cast(data); - ownsData_ = false; - } else if (ownsData_) { - ownedData_ = std::make_shared>(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_; } - std::shared_ptr> storageOwner() const { - return ownedData_; - } - std::shared_ptr backingValue() const { return backingValue_; } - - 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::shared_ptr> ownedData_; - std::shared_ptr backingValue_; - 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 && [object_ isKindOfClass:[NSArray class]]) { - NSArray* array = static_cast(object_); - if (property == "length") { - return static_cast(array.count); - } - if (auto index = parseArrayIndexProperty(property)) { - if (*index >= array.count) { - return Value::undefined(); - } - id element = [array objectAtIndex:*index]; - NativeApiJsiType elementType = nativeObjectReturnType(); - return convertNativeReturnValue(runtime, bridge_, elementType, &element); - } - } - - if (object_ != nil) { - if (const NativeApiSymbol* symbol = - bridge_->findClassForRuntimeClass(object_getClass(object_))) { - const auto& members = bridge_->membersForClass(*symbol); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, false)) { - return callObjCSelector(runtime, bridge_, object_, false, - propertyMember->selectorName, propertyMember, - nullptr, 0); - } - - if (selectMethodMember(members, property, false, 0) != nullptr) { - auto bridge = bridge_; - id object = object_; - std::string memberName = property; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), - 0, - [bridge, object, memberName](Runtime& runtime, const Value&, - const Value* args, - size_t count) -> Value { - const NativeApiSymbol* symbol = - bridge->findClassForRuntimeClass(object_getClass(object)); - if (symbol == nullptr) { - throw facebook::jsi::JSError( - runtime, "Objective-C metadata is not available for object."); - } - const NativeApiMember* selected = selectMethodMember( - bridge->membersForClass(*symbol), memberName, false, count); - if (selected == nullptr) { - throw facebook::jsi::JSError( - runtime, "Objective-C selector is not available: " + - memberName); - } - return callObjCSelector(runtime, bridge, object, false, - selected->selectorName, selected, args, - count); - }); - } - } - - if (auto selectorName = - runtimeSelectorNameForProperty(object_getClass(object_), false, - property)) { - if (selectorArgumentCount(*selectorName) == 0 && - hasRuntimeSetterForProperty(object_getClass(object_), false, - property)) { - return callObjCSelector(runtime, bridge_, object_, false, - *selectorName, nullptr, nullptr, 0); - } - - auto bridge = bridge_; - id object = object_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [bridge, object, selectorName = *selectorName]( - Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - return callObjCSelector(runtime, bridge, object, false, - selectorName, nullptr, args, count); - }); - } - - if ([object_ isKindOfClass:[NSDictionary class]]) { - NSString* key = [NSString stringWithUTF8String:property.c_str()]; - if (key != nil) { - id value = [static_cast(object_) objectForKey:key]; - if (value != nil) { - NativeApiJsiType valueType = nativeObjectReturnType(); - return convertNativeReturnValue(runtime, bridge_, valueType, &value); - } - } - } - } - - 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_->findClassForRuntimeClass(object_getClass(object_))) { - const auto& members = bridge_->membersForClass(*symbol); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, false)) { - if (propertyMember->readonly) { - throw facebook::jsi::JSError( - runtime, "Attempted to assign to readonly property."); - } - NativeApiMember setterMember = *propertyMember; - setterMember.selectorName = propertyMember->setterSelectorName; - setterMember.signatureOffset = propertyMember->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_->findClassForRuntimeClass(object_getClass(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 == "construct" && count == 1) { - void* pointer = nullptr; - if (args[0].isNumber()) { - pointer = reinterpret_cast( - static_cast(args[0].getNumber())); - } else if (args[0].isObject()) { - Object object = args[0].asObject(runtime); - if (object.isHostObject(runtime)) { - pointer = object - .getHostObject( - runtime) - ->pointer(); - } else if (object.isHostObject( - runtime)) { - pointer = object - .getHostObject( - runtime) - ->data(); - } else if (object.isHostObject( - runtime)) { - pointer = object - .getHostObject( - runtime) - ->object(); - } - } - return makeNativeObjectValue(runtime, bridge, - static_cast(pointer), false); - } - - 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 makeNativeObjectValue(runtime, 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); - }); - } - - const auto& members = bridge_->membersForClass(symbol_); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, true)) { - auto bridge = bridge_; - auto symbol = symbol_; - 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, - propertyMember->selectorName, propertyMember, - nullptr, 0); - } - - if (selectMethodMember(members, property, true, 0) != nullptr) { - auto bridge = bridge_; - auto symbol = symbol_; - std::string memberName = property; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [bridge, symbol, memberName](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); - } - const NativeApiMember* selected = selectMethodMember( - bridge->membersForClass(symbol), memberName, true, count); - if (selected == nullptr) { - throw facebook::jsi::JSError( - runtime, "Objective-C selector is not available: " + - memberName); - } - return callObjCSelector(runtime, bridge, static_cast(cls), true, - selected->selectorName, selected, args, - count); - }); - } - - Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); - if (cls != nil) { - if (auto selectorName = - runtimeSelectorNameForProperty(cls, true, property)) { - if (selectorArgumentCount(*selectorName) == 0 && - hasRuntimeSetterForProperty(cls, true, property)) { - return callObjCSelector(runtime, bridge_, static_cast(cls), true, - *selectorName, nullptr, nullptr, 0); - } - - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [bridge, cls, selectorName = *selectorName]( - Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - return callObjCSelector(runtime, bridge, static_cast(cls), - true, selectorName, nullptr, args, - count); - }); - } - } - - return Value::undefined(); - } - - void set(Runtime& runtime, const PropNameID& name, const Value& value) override { - std::string property = name.utf8(runtime); - Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); - if (cls == nil) { - throw facebook::jsi::JSError( - runtime, "Objective-C class is not available: " + symbol_.name); - } - - const auto& members = bridge_->membersForClass(symbol_); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, true)) { - if (propertyMember->readonly) { - throw facebook::jsi::JSError( - runtime, "Attempted to assign to readonly property."); - } - NativeApiMember setterMember = *propertyMember; - setterMember.selectorName = propertyMember->setterSelectorName; - setterMember.signatureOffset = propertyMember->setterSignatureOffset; - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, static_cast(cls), true, - setterMember.selectorName, &setterMember, args, 1); - return; - } - - std::string setterSelectorName = setterSelectorForProperty(property); - SEL selector = sel_getUid(setterSelectorName.c_str()); - if (class_getClassMethod(cls, selector) != nullptr) { - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, static_cast(cls), true, - 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(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_; -}; - -Value makeNativeObjectValue(Runtime& runtime, - const std::shared_ptr& bridge, - id object, bool ownsObject) { - if (object == nil) { - return Value::null(); - } - - Value cached = bridge->findRoundTripValue(runtime, object); - if (!cached.isUndefined()) { - if (ownsObject) { - [object release]; - } - return cached; - } - - Object result = Object::createFromHostObject( - runtime, - std::make_shared(bridge, object, ownsObject)); - bridge->rememberRoundTripValue(runtime, object, Value(runtime, result)); - return result; -} - -Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, - const char* expectedKind) { - Value cacheValue = runtime.global().getProperty( - runtime, "__nativeScriptNativeApiGlobalCache"); - if (!cacheValue.isObject()) { - return Value::undefined(); - } - - Object cache = cacheValue.asObject(runtime); - auto readCache = [&](const std::string& name) -> Value { - if (name.empty()) { - return Value::undefined(); - } - - Value value = cache.getProperty(runtime, name.c_str()); - if (!value.isObject()) { - return Value::undefined(); - } - - try { - Object object = value.asObject(runtime); - Value kindValue = object.getProperty(runtime, "kind"); - if (kindValue.isString() && - kindValue.asString(runtime).utf8(runtime) == expectedKind) { - return value; - } - } catch (const std::exception&) { - } - - return Value::undefined(); - }; - - Value value = readCache(symbol.name); - if (!value.isUndefined()) { - return value; - } - if (symbol.runtimeName != symbol.name) { - value = readCache(symbol.runtimeName); - if (!value.isUndefined()) { - return value; - } - } - return Value::undefined(); -} - -Value makeNativeClassValue(Runtime& runtime, - const std::shared_ptr& bridge, - NativeApiSymbol symbol) { - Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); - if (!globalValue.isUndefined()) { - return globalValue; - } - return Object::createFromHostObject( - runtime, - std::make_shared(bridge, std::move(symbol))); -} - -Protocol* lookupProtocolByNativeName(const std::string& name) { - Protocol* protocol = objc_getProtocol(name.c_str()); - if (protocol != nullptr) { - return protocol; - } - constexpr const char* suffix = "Protocol"; - size_t suffixLength = std::strlen(suffix); - if (name.size() > suffixLength && - name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { - protocol = objc_getProtocol( - name.substr(0, name.size() - suffixLength).c_str()); - } - return protocol; -} - -class NativeApiProtocolHostObject final : public HostObject { - public: - NativeApiProtocolHostObject(std::shared_ptr bridge, - NativeApiSymbol symbol) - : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} - - Protocol* nativeProtocol() const { - Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); - if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { - protocol = lookupProtocolByNativeName(symbol_.name); - } - 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 == "prototype") { - Object prototype(runtime); - for (const auto& member : bridge_->membersForProtocol(symbol_)) { - if (prototype.hasProperty(runtime, member.name.c_str())) { - continue; - } - if (member.property) { - defineProtocolProperty(runtime, prototype, member, false); - } else { - prototype.setProperty(runtime, member.name.c_str(), - makeProtocolMemberFunction(runtime, member, - false)); - } - } - return prototype; - } - 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 + "]"); - }); - } - const auto& members = bridge_->membersForProtocol(symbol_); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, true)) { - return makeProtocolPropertyGetter(runtime, *propertyMember, true); - } - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, false)) { - return makeProtocolPropertyGetter(runtime, *propertyMember, true); - } - if (const NativeApiMember* methodMember = - selectMethodMember(members, property, true, 0)) { - return makeProtocolMemberFunction(runtime, *methodMember, true); - } - if (const NativeApiMember* methodMember = - selectMethodMember(members, property, false, 0)) { - return makeProtocolMemberFunction(runtime, *methodMember, true); - } - 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, "prototype"); - addPropertyName(runtime, names, "toString"); - for (const auto& member : bridge_->membersForProtocol(symbol_)) { - addPropertyName(runtime, names, member.name.c_str()); - } - return names; - } - - private: - Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) const { - if (!thisValue.isObject()) { - return Nil; - } - - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); - } - - Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject(runtime)) { - return wrappedObject.getHostObject(runtime) - ->nativeClass(); - } - } - - return Nil; - } - - id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { - if (!thisValue.isObject()) { - return nil; - } - - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->object(); - } - - return nil; - } - - Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value* args, - size_t count) -> Value { - id receiver = nil; - if (receiverIsClass) { - Class cls = Nil; - if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - cls = object.getHostObject(runtime) - ->nativeClass(); - } else { - Value wrappedClass = - object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject( - runtime)) { - cls = wrappedObject - .getHostObject(runtime) - ->nativeClass(); - } - } - } - } - receiver = static_cast(cls); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw facebook::jsi::JSError( - runtime, "Protocol member requires a native receiver."); - } - return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - member.selectorName, &member, args, count); - }); - } - - Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value*, size_t) -> Value { - id receiver = nil; - if (receiverIsClass) { - Class cls = Nil; - if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - cls = object.getHostObject(runtime) - ->nativeClass(); - } else { - Value wrappedClass = - object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject( - runtime)) { - cls = wrappedObject - .getHostObject(runtime) - ->nativeClass(); - } - } - } - } - receiver = static_cast(cls); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw facebook::jsi::JSError( - runtime, "Protocol property requires a native receiver."); - } - return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - member.selectorName, &member, nullptr, 0); - }); - } - - Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), - 1, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value* args, - size_t count) -> Value { - id receiver = nil; - if (receiverIsClass) { - Class cls = Nil; - if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - cls = object.getHostObject(runtime) - ->nativeClass(); - } else { - Value wrappedClass = - object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject( - runtime)) { - cls = wrappedObject - .getHostObject(runtime) - ->nativeClass(); - } - } - } - } - receiver = static_cast(cls); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw facebook::jsi::JSError( - runtime, "Protocol property requires a native receiver."); - } - if (count < 1) { - throw facebook::jsi::JSError( - runtime, "Protocol property setter expects a value."); - } - - NativeApiMember setterMember = member; - setterMember.selectorName = member.setterSelectorName; - setterMember.signatureOffset = member.setterSignatureOffset; - return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - setterMember.selectorName, &setterMember, - args, 1); - }); - } - - void defineProtocolProperty(Runtime& runtime, Object& target, - const NativeApiMember& member, - bool receiverIsClass) const { - try { - Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); - Function defineProperty = - objectCtor.getPropertyAsFunction(runtime, "defineProperty"); - Object descriptor(runtime); - descriptor.setProperty(runtime, "configurable", true); - descriptor.setProperty(runtime, "enumerable", true); - descriptor.setProperty(runtime, "get", - makeProtocolPropertyGetter(runtime, member, - receiverIsClass)); - if (!member.readonly && !member.setterSelectorName.empty()) { - descriptor.setProperty(runtime, "set", - makeProtocolPropertySetter(runtime, member, - receiverIsClass)); - } - defineProperty.call(runtime, target, makeString(runtime, member.name), - descriptor); - } catch (const std::exception&) { - } - } - - std::shared_ptr bridge_; - NativeApiSymbol symbol_; -}; - -Value makeNativeProtocolValue(Runtime& runtime, - const std::shared_ptr& bridge, - NativeApiSymbol symbol) { - Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); - if (!globalValue.isUndefined()) { - return globalValue; - } - return Object::createFromHostObject( - runtime, - std::make_shared(bridge, std::move(symbol))); -} - -Class nativeClassFromJsiObject(Runtime& runtime, const Object& object) { - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); - } - - Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject(runtime)) { - return wrappedObject.getHostObject(runtime) - ->nativeClass(); - } - } - return Nil; -} - -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 (void* buffer : ownedBuffers_) { - free(buffer); - } - 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* addBuffer(size_t size) { - void* buffer = calloc(1, std::max(size, 1)); - if (buffer == nullptr) { - throw std::bad_alloc(); - } - ownedBuffers_.push_back(buffer); - return buffer; - } - 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 ownedBuffers_; - 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 > 0 || - gActiveSynchronousNativeInvocationDepth.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*); -} - -Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { - constexpr int64_t maxSafeInteger = 9007199254740991LL; - constexpr int64_t minSafeInteger = -9007199254740991LL; - if (value >= minSafeInteger && value <= maxSafeInteger) { - return static_cast(value); - } - return BigInt::fromInt64(runtime, value); -} - -Value unsignedInteger64ToJsiValue(Runtime& runtime, uint64_t value) { - constexpr uint64_t maxSafeInteger = 9007199254740991ULL; - if (value <= maxSafeInteger) { - return static_cast(value); - } - return BigInt::fromUint64(runtime, value); -} - -bool parseIntegerTextToUintptr(const std::string& text, uintptr_t* address) { - if (address == nullptr) { - return false; - } - if (text.empty()) { - return false; - } - - char* end = nullptr; - if (text[0] == '-') { - long long signedValue = std::strtoll(text.c_str(), &end, 10); - if (end == nullptr || *end != '\0') { - return false; - } - *address = static_cast(static_cast(signedValue)); - return true; - } - - int base = 10; - const char* start = text.c_str(); - if (text.size() > 2 && text[0] == '0' && - (text[1] == 'x' || text[1] == 'X')) { - base = 16; - } - unsigned long long unsignedValue = std::strtoull(start, &end, base); - if (end == nullptr || *end != '\0') { - return false; - } - *address = static_cast(unsignedValue); - return true; -} - -bool parseBigIntToUintptr(Runtime& runtime, const BigInt& bigint, - uintptr_t* address) { - return parseIntegerTextToUintptr(bigint.toString(runtime, 10).utf8(runtime), - address); -} - -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; -} - -ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { - switch (type.kind) { - case metagen::mdTypeArray: - return &ffi_type_pointer; - default: - return type.ffiType != nullptr ? type.ffiType : &ffi_type_pointer; - } -} - -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(ffiTypeForJsiArgument(argType)); - } - - 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(ffiTypeForJsiArgument(argType)); - } - - 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) { - if (type.kind == metagen::mdTypeStruct && type.aggregateInfo != nullptr && - type.aggregateInfo->ffi != nullptr) { - return false; - } - 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 std::shared_ptr& bridge, - 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 (Class cls = nativeClassFromJsiObject(runtime, object)) { - return static_cast(cls); - } - 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()); - } - - Value getTimeValue = object.getProperty(runtime, "getTime"); - Value toISOStringValue = object.getProperty(runtime, "toISOString"); - if (getTimeValue.isObject() && - getTimeValue.asObject(runtime).isFunction(runtime) && - toISOStringValue.isObject() && - toISOStringValue.asObject(runtime).isFunction(runtime)) { - Value millisValue = getTimeValue.asObject(runtime) - .asFunction(runtime) - .callWithThis(runtime, object, nullptr, 0); - if (millisValue.isNumber()) { - NSDate* date = [NSDate dateWithTimeIntervalSince1970:millisValue.getNumber() / 1000.0]; - bridge->rememberRoundTripValue(runtime, date, value); - return date; - } - } - - Value valueOfValue = object.getProperty(runtime, "valueOf"); - if (valueOfValue.isObject() && - valueOfValue.asObject(runtime).isFunction(runtime)) { - Value primitiveValue = valueOfValue.asObject(runtime) - .asFunction(runtime) - .callWithThis(runtime, object, nullptr, 0); - if (primitiveValue.isString() || primitiveValue.isBool() || - primitiveValue.isNumber()) { - return objectFromJsiValue(runtime, bridge, primitiveValue, frame, - mutableString); - } - } - - const uint8_t* bytes = nullptr; - size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { - NSData* data = [NSData dataWithBytes:bytes length:byteLength]; - bridge->rememberRoundTripValue(runtime, data, value); - return data; - } - - 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, bridge, - array.getValueAtIndex(runtime, i), - frame, false); - [nativeArray addObject:element != nil ? element : [NSNull null]]; - } - bridge->rememberRoundTripValue(runtime, nativeArray, value); - return nativeArray; - } - - Value lengthValue = object.getProperty(runtime, "length"); - if (lengthValue.isNumber() && std::isfinite(lengthValue.getNumber()) && - lengthValue.getNumber() >= 0) { - size_t length = static_cast(std::floor(lengthValue.getNumber())); - NSMutableArray* nativeArray = [NSMutableArray arrayWithCapacity:length]; - for (size_t i = 0; i < length; i++) { - std::string key = std::to_string(i); - id element = objectFromJsiValue( - runtime, bridge, object.getProperty(runtime, key.c_str()), frame, - false); - [nativeArray addObject:element != nil ? element : [NSNull null]]; - } - bridge->rememberRoundTripValue(runtime, nativeArray, value); - return nativeArray; - } - - Value entriesValue = object.getProperty(runtime, "entries"); - Value sizeValue = object.getProperty(runtime, "size"); - Value getValue = object.getProperty(runtime, "get"); - if (entriesValue.isObject() && - entriesValue.asObject(runtime).isFunction(runtime) && - sizeValue.isNumber() && getValue.isObject() && - getValue.asObject(runtime).isFunction(runtime)) { - Object arrayCtor = runtime.global().getPropertyAsObject(runtime, "Array"); - Function arrayFrom = arrayCtor.getPropertyAsFunction(runtime, "from"); - Value iterator = entriesValue.asObject(runtime) - .asFunction(runtime) - .callWithThis(runtime, object, nullptr, 0); - Value pairsValue = arrayFrom.call(runtime, iterator); - if (pairsValue.isObject() && pairsValue.asObject(runtime).isArray(runtime)) { - Array pairs = pairsValue.asObject(runtime).getArray(runtime); - NSMutableDictionary* nativeMap = - [NSMutableDictionary dictionaryWithCapacity:pairs.size(runtime)]; - for (size_t i = 0; i < pairs.size(runtime); i++) { - Value pairValue = pairs.getValueAtIndex(runtime, i); - if (!pairValue.isObject() || - !pairValue.asObject(runtime).isArray(runtime)) { - continue; - } - Array pair = pairValue.asObject(runtime).getArray(runtime); - if (pair.size(runtime) < 2) { - continue; - } - id key = objectFromJsiValue(runtime, bridge, - pair.getValueAtIndex(runtime, 0), - frame, false); - id nativeValue = objectFromJsiValue(runtime, bridge, - pair.getValueAtIndex(runtime, 1), - frame, false); - if (key != nil) { - [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] - forKey:key]; - } - } - bridge->rememberRoundTripValue(runtime, nativeMap, value); - return nativeMap; - } - } - - 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, bridge, propertyValue, frame, false); - NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; - if (nativeKey != nil) { - [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] - forKey:nativeKey]; - } - } - bridge->rememberRoundTripValue(runtime, dictionary, value); - 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 nativePointerObjectValue = - object.getProperty(runtime, "__nativeApiPointerObject"); - if (nativePointerObjectValue.isObject()) { - Object nativePointerObject = nativePointerObjectValue.asObject(runtime); - if (nativePointerObject.isHostObject( - runtime)) { - *pointer = nativePointerObject - .getHostObject(runtime) - ->pointer(); - return true; - } - } - - 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 (Class cls = nativeClassFromJsiObject(runtime, object)) { - return cls; - } - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime) - ->nativeProtocol(); - } - if (object.isHostObject(runtime)) { - auto reference = - object.getHostObject(runtime); - if (reference->data() == nullptr) { - reference->ensureStorage(runtime, reference->type(), frame); - } - return reference->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()); - return string; - } - throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); -} - -bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) { - if (pointer == nullptr || !value.isObject()) { - return false; - } - Object object = value.asObject(runtime); - if (object.isHostObject(runtime)) { - *pointer = object.getHostObject(runtime)->pointer(); - return true; - } - if (object.isHostObject(runtime)) { - *pointer = object.getHostObject(runtime)->data(); - return true; - } - if (object.isHostObject(runtime)) { - *pointer = object.getHostObject(runtime)->data(); - return true; - } - if (object.isHostObject(runtime)) { - *pointer = object.getHostObject(runtime)->object(); - return true; - } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { - *pointer = cls; - return true; - } - if (object.isHostObject(runtime)) { - *pointer = - object.getHostObject(runtime)->nativeProtocol(); - return true; - } - return readNativePointerProperty(runtime, object, 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); - -Class classFromJsiValue(Runtime& runtime, const Value& value); -Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value); - -std::optional parseArrayIndexProperty(const std::string& property) { - if (property.empty()) { - return std::nullopt; - } - size_t index = 0; - for (char c : property) { - if (!std::isdigit(static_cast(c))) { - return std::nullopt; - } - size_t digit = static_cast(c - '0'); - if (index > (std::numeric_limits::max() - digit) / 10) { - return std::nullopt; - } - index = (index * 10) + digit; - } - return index; -} - -size_t referenceElementStride(const NativeApiJsiType& type) { - return std::max(nativeSizeForType(type), 1); -} - -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 convertJsiFfiArgument(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, const Value& value, - void* target, NativeApiJsiArgumentFrame& frame) { - if (type.kind != metagen::mdTypeArray) { - convertJsiArgument(runtime, bridge, type, value, target, frame); - return; - } - - void* pointer = nullptr; - if (!value.isNull() && !value.isUndefined()) { - if (value.isObject()) { - Object object = value.asObject(runtime); - if (!readPointerLikeValue(runtime, value, &pointer)) { - const uint8_t* bytes = nullptr; - size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { - pointer = const_cast(bytes); - } - } - } - - if (pointer == nullptr) { - size_t byteLength = nativeSizeForType(type); - void* buffer = frame.addBuffer(byteLength); - convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, - frame); - pointer = buffer; - } - } - - *static_cast(target) = pointer; -} - -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.isObject()) { - Object object = value.asObject(runtime); - void* pointer = nullptr; - if (readPointerLikeValue(runtime, value, &pointer)) { - *static_cast(target) = static_cast(pointer); - break; - } - const uint8_t* bytes = nullptr; - size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { - *static_cast(target) = - reinterpret_cast(const_cast(bytes)); - break; - } - Value valueOfValue = object.getProperty(runtime, "valueOf"); - if (valueOfValue.isObject() && - valueOfValue.asObject(runtime).isFunction(runtime)) { - Value primitive = valueOfValue.asObject(runtime) - .asFunction(runtime) - .callWithThis(runtime, object, nullptr, 0); - if (primitive.isString()) { - std::string utf8 = primitive.asString(runtime).utf8(runtime); - char* string = strdup(utf8.c_str()); - *static_cast(target) = string; - 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()); - *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, bridge, value, frame, - type.kind == metagen::mdTypeNSMutableStringObject); - *static_cast(target) = object; - break; - } - case metagen::mdTypeClass: { - *static_cast(target) = classFromJsiValue(runtime, value); - 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 (reference->data() == nullptr && type.elementType != nullptr) { - reference->ensureStorage(runtime, *type.elementType, frame); - } else if (reference->data() == nullptr) { - reference->ensureStorage(runtime, reference->type(), frame); - } - *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(); - bridge->rememberRoundTripValue(runtime, pointer, value); - try { - object.setProperty(runtime, "__nativeApiPointerObject", - createPointer(runtime, bridge, pointer)); - 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 signedInteger64ToJsiValue(runtime, *static_cast(value)); - case metagen::mdTypeULong: - case metagen::mdTypeUInt64: - return unsignedInteger64ToJsiValue(runtime, - *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); - if (string == nullptr) { - return Value::null(); - } - NativeApiJsiType cStringType = - primitiveInteropType(metagen::mdTypeChar); - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, cStringType, const_cast(string), false)); - } - 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 makeNativeClassValue(runtime, bridge, std::move(symbol)); - } - 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(); - } - Value roundTrip = bridge->findRoundTripValue(runtime, object); - if (!roundTrip.isUndefined()) { - if (type.returnOwned) { - [object release]; - } - return roundTrip; - } - if (object_isClass(object)) { - if (const NativeApiSymbol* classSymbol = - bridge->findClassForRuntimePointer((void*)object)) { - return makeNativeClassValue(runtime, bridge, *classSymbol); - } - } - if (const NativeApiSymbol* protocolSymbol = - bridge->findProtocolForRuntimePointer((void*)object)) { - return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); - } - if ([object isKindOfClass:[NSNull class]]) { - if (type.returnOwned) { - [object release]; - } - return Value::null(); - } - if ([object isKindOfClass:[NSString class]]) { - bool untypedObject = type.kind == metagen::mdTypeAnyObject; - bool explicitNSString = type.kind == metagen::mdTypeNSStringObject; - if (untypedObject || explicitNSString) { - std::string utf8 = [static_cast(object) UTF8String] ?: ""; - if (type.returnOwned) { - [object release]; - } - return makeString(runtime, utf8); - } - } - if ([object isKindOfClass:[NSNumber class]] && - ![object isKindOfClass:[NSDecimalNumber class]]) { - NSNumber* number = static_cast(object); - const char* objCType = [number objCType]; - bool isBool = CFGetTypeID((__bridge CFTypeRef)number) == - CFBooleanGetTypeID() || - (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 makeNativeObjectValue(runtime, bridge, object, type.returnOwned); - } - 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(); - } - if (const NativeApiSymbol* classSymbol = - bridge->findClassForRuntimePointer(pointer)) { - return makeNativeClassValue(runtime, bridge, *classSymbol); - } - if (const NativeApiSymbol* protocolSymbol = - bridge->findProtocolForRuntimePointer(pointer)) { - return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); - } - if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, *type.elementType, pointer, false)); - } - return createPointer(runtime, bridge, pointer); - } - case metagen::mdTypeBlock: - case metagen::mdTypeFunctionPointer: { - void* pointer = *static_cast(value); - if (pointer == nullptr) { - return Value::null(); - } - Value roundTrip = bridge->findRoundTripValue(runtime, pointer); - if (!roundTrip.isUndefined()) { - return roundTrip; - } - 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."); - } -} - -void NativeApiReferenceHostObject::ensureStorage( - Runtime& runtime, NativeApiJsiType type, NativeApiJsiArgumentFrame& frame, - size_t elements) { - size_t elementCount = std::max(elements, 1); - NativeApiJsiType storageType = std::move(type); - size_t stride = std::max(nativeSizeForType(storageType), 1); - size_t required = std::max(stride * elementCount, sizeof(void*)); - type_ = std::move(storageType); - - if (data_ == nullptr) { - data_ = calloc(1, required); - ownsData_ = true; - byteLength_ = required; - } else if (ownsData_ && byteLength_ < required) { - void* expanded = realloc(data_, required); - if (expanded == nullptr) { - throw std::bad_alloc(); - } - std::memset(static_cast(expanded) + byteLength_, 0, - required - byteLength_); - data_ = expanded; - byteLength_ = required; - } - - if (data_ != nullptr && pendingValue_ != nullptr) { - Value pending(runtime, *pendingValue_); - convertJsiArgument(runtime, bridge_, type_, pending, data_, frame); - pendingValue_.reset(); - } -} - -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) { - if (pendingValue_ != nullptr) { - return Value(runtime, *pendingValue_); - } - return Value::undefined(); - } - return convertNativeReturnValue(runtime, bridge_, type_, data_); - } - if (auto index = parseArrayIndexProperty(property)) { - if (data_ == nullptr) { - return Value::undefined(); - } - void* slot = static_cast(data_) + - (*index * referenceElementStride(type_)); - return convertNativeReturnValue(runtime, bridge_, type_, slot); - } - 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, - ""); - }); - } - return Value::undefined(); -} - -void NativeApiReferenceHostObject::set(Runtime& runtime, - const PropNameID& name, - const Value& value) { - std::string property = name.utf8(runtime); - auto index = parseArrayIndexProperty(property); - if (property != "value" && !index) { - return; - } - size_t slotIndex = index.value_or(0); - NativeApiJsiArgumentFrame frame(1); - if (data_ == nullptr) { - if (slotIndex == 0) { - pendingValue_ = std::make_shared(runtime, value); - return; - } - ensureStorage(runtime, type_, frame, slotIndex + 1); - } - pendingValue_.reset(); - void* slot = static_cast(data_) + - (slotIndex * referenceElementStride(type_)); - convertJsiArgument(runtime, bridge_, type_, value, slot, 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; - if (field.type.kind == metagen::mdTypeStruct && - field.type.aggregateInfo != nullptr) { - return Object::createFromHostObject( - runtime, std::make_shared( - bridge_, field.type.aggregateInfo, fieldData, false, - ownedData_, backingValue_)); - } - 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 primitiveInteropTypeFromCode(int32_t code) { - MDTypeKind kind = static_cast(code); - 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::mdTypeProtocolObject: - 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; - } -} - -std::optional interopTypeFromValue( - Runtime& runtime, const std::shared_ptr& bridge, - const Value& value) { - if (value.isNumber()) { - return primitiveInteropTypeFromCode(static_cast(value.getNumber())); - } - - if (!value.isObject()) { - return std::nullopt; - } - - Object object = value.asObject(runtime); - Value typeCodeValue = object.getProperty(runtime, "__nativeApiTypeCode"); - if (typeCodeValue.isNumber()) { - return primitiveInteropTypeFromCode( - static_cast(typeCodeValue.getNumber())); - } - Value valueOfValue = object.getProperty(runtime, "valueOf"); - if (valueOfValue.isObject() && - valueOfValue.asObject(runtime).isFunction(runtime)) { - Value primitive = - valueOfValue.asObject(runtime).asFunction(runtime).callWithThis( - runtime, object, nullptr, 0); - if (primitive.isNumber()) { - return primitiveInteropTypeFromCode( - static_cast(primitive.getNumber())); - } - } - - if (nativeClassFromJsiObject(runtime, object) != Nil) { - return nativeObjectReturnType(metagen::mdTypeInstanceObject); - } - - 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"); - if (kindValue.isString()) { - std::string kindName = kindValue.asString(runtime).utf8(runtime); - if (kindName == "pointer") { - return primitiveInteropType(metagen::mdTypePointer); - } - if (kindName == "reference") { - return primitiveInteropType(metagen::mdTypePointer); - } - if (kindName == "class") { - return primitiveInteropType(metagen::mdTypeClass); - } - if (kindName == "selector") { - return primitiveInteropType(metagen::mdTypeSelector); - } - if (kindName == "protocol") { - return primitiveInteropType(metagen::mdTypeProtocolObject); - } - if (kindName == "block") { - return primitiveInteropType(metagen::mdTypeBlock); - } - if (kindName == "functionPointer") { - return primitiveInteropType(metagen::mdTypeFunctionPointer); - } - if (kindName == "functionReference") { - return primitiveInteropType(metagen::mdTypeFunctionPointer); - } - } - 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; - - if (count > 0 && args[0].isObject()) { - void* pointer = nullptr; - if (readPointerLikeValue(runtime, args[0], &pointer) && pointer != nullptr) { - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, info, pointer, false, nullptr, - std::make_shared(runtime, args[0]))); - } - } - - 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, "runtimeName", makeString(runtime, symbol.runtimeName)); - constructor.setProperty(runtime, "metadataOffset", static_cast(symbol.offset)); - constructor.setProperty(runtime, "sizeof", - static_cast(info != nullptr ? info->size : 0)); - constructor.setProperty( - runtime, "equals", - Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "equals"), 2, - [bridge, info](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - if (info == nullptr || count < 2) { - return false; - } - - 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 left(info->size, 0); - std::vector right(info->size, 0); - try { - NativeApiJsiArgumentFrame leftFrame(1); - convertAggregateArgument(runtime, bridge, type, args[0], - left.data(), leftFrame); - NativeApiJsiArgumentFrame rightFrame(1); - convertAggregateArgument(runtime, bridge, type, args[1], - right.data(), rightFrame); - } catch (const std::exception&) { - return false; - } - - return std::memcmp(left.data(), right.data(), 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) || - nativeClassFromJsiObject(runtime, object) != Nil) { - 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, - const std::shared_ptr& bridge, - void* pointer, bool adopted) { - if (!adopted && bridge != nullptr) { - Value cached = bridge->findPointerValue(runtime, pointer); - if (cached.isObject()) { - return cached.asObject(runtime); - } - } - - Object result = Object::createFromHostObject( - runtime, - std::make_shared(bridge, pointer, "pointer", - adopted)); - if (!adopted && bridge != nullptr) { - bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); - } - return result; -} - -void installInteropHasInstance(Runtime& runtime, Function& constructor, - const char* kind) { - Value symbolCtorValue = runtime.global().getProperty(runtime, "Symbol"); - if (!symbolCtorValue.isObject()) { - return; - } - - Object symbolCtor = symbolCtorValue.asObject(runtime); - Value hasInstanceValue = symbolCtor.getProperty(runtime, "hasInstance"); - if (!hasInstanceValue.isSymbol()) { - return; - } - - try { - Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); - Function defineProperty = - objectCtor.getPropertyAsFunction(runtime, "defineProperty"); - Object descriptor(runtime); - descriptor.setProperty(runtime, "configurable", true); - descriptor.setProperty( - runtime, "value", - Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "Symbol.hasInstance"), 1, - [kind = std::string(kind)](Runtime& runtime, const Value&, - const Value* args, size_t count) -> Value { - if (count < 1 || !args[0].isObject()) { - return false; - } - - Object object = args[0].asObject(runtime); - Value kindValue = object.getProperty(runtime, "kind"); - return kindValue.isString() && - kindValue.asString(runtime).utf8(runtime) == kind; - })); - defineProperty.call(runtime, constructor, hasInstanceValue, descriptor); - } catch (const std::exception&) { - } -} - -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 (Class cls = nativeClassFromJsiObject(runtime, object)) { - return cls; - } - 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) { - Object type(runtime); - double code = static_cast(kind); - type.setProperty(runtime, "__nativeApiTypeCode", code); - type.setProperty( - runtime, "valueOf", - Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "valueOf"), 0, - [code](Runtime&, const Value&, const Value*, size_t) -> Value { - return code; - })); - type.setProperty( - runtime, "toString", - Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [code](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - char text[32] = {}; - snprintf(text, sizeof(text), "%d", static_cast(code)); - return makeString(runtime, text); - })); - types.setProperty(runtime, name, type); - }; - 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("protocol", metagen::mdTypeProtocolObject); - 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); - - Function pointerConstructor = Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "Pointer"), 1, - [bridge](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()) { - auto readAddress = [&](const Value& value, - uintptr_t* address) -> bool { - auto readAddressFromString = [&](const Value& source) -> bool { - try { - Value stringCtorValue = - runtime.global().getProperty(runtime, "String"); - if (!stringCtorValue.isObject() || - !stringCtorValue.asObject(runtime).isFunction(runtime)) { - return false; - } - Value stringValue = - stringCtorValue.asObject(runtime).asFunction(runtime) - .call(runtime, source); - if (!stringValue.isString()) { - return false; - } - return parseIntegerTextToUintptr( - stringValue.asString(runtime).utf8(runtime), address); - } catch (const std::exception&) { - return false; - } - }; - - if (value.isNumber()) { - double number = value.getNumber(); - if (!std::isfinite(number)) { - return false; - } - *address = static_cast( - static_cast(number)); - return true; - } - if (value.isBigInt()) { - if (readAddressFromString(value)) { - return true; - } - BigInt bigint = value.getBigInt(runtime); - return parseBigIntToUintptr(runtime, bigint, address); - } - if (value.isObject()) { - Object object = value.asObject(runtime); - Value valueOfValue = object.getProperty(runtime, "valueOf"); - if (valueOfValue.isObject() && - valueOfValue.asObject(runtime).isFunction(runtime)) { - Value primitive = valueOfValue.asObject(runtime) - .asFunction(runtime) - .callWithThis(runtime, object, nullptr, 0); - if (primitive.isNumber()) { - double number = primitive.getNumber(); - if (!std::isfinite(number)) { - return false; - } - *address = static_cast( - static_cast(number)); - return true; - } - if (primitive.isBigInt()) { - if (readAddressFromString(primitive)) { - return true; - } - BigInt bigint = primitive.getBigInt(runtime); - return parseBigIntToUintptr(runtime, bigint, address); - } - } - return readAddressFromString(value); - } - return false; - }; - - uintptr_t address = 0; - if (!readAddress(args[0], &address)) { - throw facebook::jsi::JSError(runtime, - "Pointer expects a numeric address."); - } - pointer = reinterpret_cast(address); - } - return createPointer(runtime, bridge, pointer); - }); - Object pointerPrototype(runtime); - pointerPrototype.setProperty(runtime, "constructor", pointerConstructor); - pointerConstructor.setProperty(runtime, "prototype", pointerPrototype); - installInteropHasInstance(runtime, pointerConstructor, "pointer"); - pointerConstructor.setProperty(runtime, "kind", makeString(runtime, "pointer")); - pointerConstructor.setProperty(runtime, "sizeof", - static_cast(sizeof(void*))); - interop.setProperty(runtime, "Pointer", pointerConstructor); - - Function functionReferenceConstructor = Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "FunctionReference"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - if (count < 1 || !args[0].isObject()) { - throw facebook::jsi::JSError( - runtime, "FunctionReference expects a function."); - } - - Object object = args[0].asObject(runtime); - if (!object.isFunction(runtime)) { - throw facebook::jsi::JSError( - runtime, "FunctionReference expects a function."); - } - - Function function = object.asFunction(runtime); - function.setProperty(runtime, "kind", - makeString(runtime, "functionReference")); - function.setProperty(runtime, "sizeof", - static_cast(sizeof(void*))); - return function; - }); - Object functionReferencePrototype(runtime); - functionReferencePrototype.setProperty(runtime, "constructor", - functionReferenceConstructor); - functionReferenceConstructor.setProperty(runtime, "prototype", - functionReferencePrototype); - installInteropHasInstance(runtime, functionReferenceConstructor, - "functionReference"); - functionReferenceConstructor.setProperty(runtime, "kind", - makeString(runtime, - "functionReference")); - functionReferenceConstructor.setProperty(runtime, "sizeof", - static_cast(sizeof(void*))); - interop.setProperty(runtime, "FunctionReference", - functionReferenceConstructor); - - Function referenceConstructor = 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 firstArgumentIsType = false; - if (count > 1) { - firstArgumentIsType = true; - } else if (count == 1 && args[0].isObject()) { - Object object = args[0].asObject(runtime); - Value typeCodeValue = - object.getProperty(runtime, "__nativeApiTypeCode"); - Value kindValue = object.getProperty(runtime, "kind"); - firstArgumentIsType = - typeCodeValue.isNumber() || object.isFunction(runtime) || - nativeClassFromJsiObject(runtime, object) != Nil || - (kindValue.isString() && - (kindValue.asString(runtime).utf8(runtime) == "class" || - kindValue.asString(runtime).utf8(runtime) == "protocol")); - } - std::optional requestedType = - firstArgumentIsType - ? interopTypeFromValue(runtime, bridge, args[0]) - : std::nullopt; - bool hasType = firstArgumentIsType && requestedType.has_value(); - if (hasType) { - type = *requestedType; - } - - void* data = nullptr; - bool ownsData = false; - size_t byteLength = 0; - std::shared_ptr pendingValue; - if (hasType) { - bool usesExternalStorage = false; - Value valueToStore = Value::undefined(); - if (count > 1) { - valueToStore = Value(runtime, args[1]); - if (args[1].isObject()) { - Object object = args[1].asObject(runtime); - if (object.isHostObject(runtime)) { - data = object - .getHostObject( - runtime) - ->pointer(); - usesExternalStorage = true; - } else if (object.isHostObject( - runtime)) { - auto reference = - object.getHostObject( - runtime); - data = reference->data(); - if (data != nullptr) { - usesExternalStorage = true; - } else { - valueToStore = object.getProperty(runtime, "value"); - } - } else if (type.kind == metagen::mdTypeStruct && - object.isHostObject< - NativeApiStructObjectHostObject>(runtime)) { - data = object - .getHostObject< - NativeApiStructObjectHostObject>(runtime) - ->data(); - usesExternalStorage = true; - } else if (type.kind == metagen::mdTypePointer || - type.kind == metagen::mdTypeOpaquePointer || - type.kind == metagen::mdTypeBlock || - type.kind == metagen::mdTypeFunctionPointer) { - void* nativePointer = nullptr; - if (readNativePointerProperty(runtime, object, - &nativePointer)) { - data = nativePointer; - usesExternalStorage = true; - } - } - } - } - if (!usesExternalStorage) { - byteLength = std::max(nativeSizeForType(type), - sizeof(void*)); - data = calloc(1, byteLength); - if (data == nullptr) { - throw std::bad_alloc(); - } - ownsData = true; - if (count > 1) { - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge, type, valueToStore, data, - frame); - } - } - } else if (count > 0) { - pendingValue = std::make_shared(runtime, args[0]); - } - - if (ownsData && data == nullptr) { - throw std::bad_alloc(); - } - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, type, data, ownsData, byteLength, - std::move(pendingValue))); - }); - Object referencePrototype(runtime); - referencePrototype.setProperty(runtime, "constructor", referenceConstructor); - referenceConstructor.setProperty(runtime, "prototype", referencePrototype); - installInteropHasInstance(runtime, referenceConstructor, "reference"); - referenceConstructor.setProperty(runtime, "kind", - makeString(runtime, "reference")); - referenceConstructor.setProperty(runtime, "sizeof", - static_cast(sizeof(void*))); - interop.setProperty(runtime, "Reference", referenceConstructor); - - 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, - [bridge](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, bridge, 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, bridge, 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)) { - void* data = - object.getHostObject(runtime)->data(); - if (data == nullptr) { - throw facebook::jsi::JSError( - runtime, "Cannot get handle of empty Reference."); - } - return createPointer(runtime, bridge, data); - } - if (object.isHostObject(runtime)) { - auto structObject = - object.getHostObject(runtime); - if (structObject->backingValue() != nullptr) { - return Value(runtime, *structObject->backingValue()); - } - return createPointer(runtime, bridge, structObject->data()); - } - if (object.isHostObject(runtime)) { - return createPointer( - runtime, bridge, - object.getHostObject(runtime) - ->object()); - } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { - return createPointer(runtime, bridge, cls); - } - if (object.isHostObject(runtime)) { - return createPointer( - runtime, bridge, - object.getHostObject(runtime) - ->nativeProtocol()); - } - void* nativePointer = nullptr; - if (readNativePointerProperty(runtime, object, &nativePointer)) { - return createPointer(runtime, bridge, 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, bridge, 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; -} - -bool startsWith(const std::string& value, const std::string& prefix) { - return value.size() >= prefix.size() && - value.compare(0, prefix.size(), prefix) == 0; -} - -bool endsWith(const std::string& value, const std::string& suffix) { - return value.size() >= suffix.size() && - value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -std::string stripEnumSuffix(const std::string& enumName) { - static const std::vector suffixes = { - "Options", "Option", "Enums", "Enum", "Result", "Direction", - "Orientation", "Style", "Mask", "Type", "Status", "Modes", "Mode", "s"}; - - for (const auto& suffix : suffixes) { - if (enumName.size() > suffix.size() && endsWith(enumName, suffix)) { - return enumName.substr(0, enumName.size() - suffix.size()); - } - } - - return enumName; -} - -bool isNSComparisonResultOrderingName(const std::string& enumName, - const std::string& member) { - if (enumName != "NSComparisonResult") { - return false; - } - return member == "Ascending" || member == "Same" || member == "Descending"; -} - -Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, - const NativeApiSymbol& symbol) { - Object result(runtime); - if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { - return result; - } - - std::string enumName = symbol.name; - std::string strippedPrefix = stripEnumSuffix(enumName); - 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); - - std::string canonicalName = memberName != nullptr ? memberName : ""; - std::vector aliases; - aliases.push_back(canonicalName); - - if (!strippedPrefix.empty() && startsWith(canonicalName, strippedPrefix) && - canonicalName.size() > strippedPrefix.size()) { - aliases.push_back(canonicalName.substr(strippedPrefix.size())); - } else if (!strippedPrefix.empty() && - !startsWith(canonicalName, strippedPrefix)) { - aliases.push_back(strippedPrefix + canonicalName); - } - - if (startsWith(enumName, "NS") && !startsWith(canonicalName, "NS")) { - aliases.push_back(std::string("NS") + canonicalName); - } - - if (enumName == "NSStringCompareOptions" && - !endsWith(canonicalName, "Search")) { - aliases.push_back(canonicalName + "Search"); - aliases.push_back(std::string("NS") + canonicalName + "Search"); - } - - if (!startsWith(canonicalName, "k")) { - aliases.push_back(std::string("k") + enumName + canonicalName); - } - - if (isNSComparisonResultOrderingName(enumName, canonicalName)) { - aliases.push_back(std::string("Ordered") + canonicalName); - aliases.push_back(std::string("NSOrdered") + canonicalName); - } - - std::vector uniqueAliases; - std::unordered_set seenAliases; - for (const auto& alias : aliases) { - if (!alias.empty() && seenAliases.insert(alias).second) { - uniqueAliases.push_back(alias); - } - } - - for (const auto& alias : uniqueAliases) { - result.setProperty(runtime, alias.c_str(), static_cast(value)); - } - - char valueKey[32] = {}; - snprintf(valueKey, sizeof(valueKey), "%lld", static_cast(value)); - if (!result.hasProperty(runtime, valueKey)) { - std::string reverseName = - uniqueAliases.size() > 1 ? uniqueAliases[1] : canonicalName; - result.setProperty(runtime, valueKey, makeString(runtime, reverseName)); - } - } - 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: { - if (isValidMetadataStringOffset(metadata, offset)) { - auto stringOffset = metadata->getOffset(offset); - 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]; - ffi_type* ffiType = ffiTypeForJsiArgument(type); - size_t size = ffiType != nullptr && ffiType->size > 0 - ? ffiType->size - : nativeSizeForType(type); - void* target = frame.storageAt(i, size); - convertJsiFfiArgument(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, "__nativeApiPointerObject", - createPointer(runtime, bridge, pointer)); - 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; - } - if (symbol.name == "CFBagContainsValue" && - (returnType.kind == metagen::mdTypeChar || - returnType.kind == metagen::mdTypeUChar || - returnType.kind == metagen::mdTypeUInt8)) { - return *returnStorage.data() != 0; - } - 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 makeNativeClassValue(runtime, bridge, - std::move(runtimeSymbol)); - } - - return makeNativeClassValue(runtime, 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 = lookupProtocolByNativeName(protocolName); - if (protocol == nullptr) { - return Value::null(); - } - const char* runtimeName = protocol_getName(protocol); - NativeApiSymbol runtimeSymbol{ - .kind = NativeApiSymbolKind::Protocol, - .offset = MD_SECTION_OFFSET_NULL, - .name = protocolName, - .runtimeName = runtimeName != nullptr ? runtimeName : protocolName, - }; - return makeNativeProtocolValue(runtime, bridge, - std::move(runtimeSymbol)); - } - return makeNativeProtocolValue(runtime, bridge, *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 makeNativeClassValue(runtime, 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 makeNativeProtocolValue(runtime, bridge_, *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. - } - } -} - -std::string jsStringLiteral(const char* value) { - std::string result = "'"; - if (value != nullptr) { - for (const char* current = value; *current != '\0'; current++) { - switch (*current) { - case '\\': - result += "\\\\"; - break; - case '\'': - result += "\\'"; - break; - case '\n': - result += "\\n"; - break; - case '\r': - result += "\\r"; - break; - case '\t': - result += "\\t"; - break; - default: - result += *current; - break; - } - } - } - result += "'"; - return result; -} - -void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) { - static const char* GlobalInstaller = R"JSI_GLOBALS( -(function(nativeApiGlobalName) { - 'use strict'; - var api = globalThis[nativeApiGlobalName]; - if (!api || api.__nativeScriptGlobalsInstalled) { - return; - } - - var cacheName = '__nativeScriptNativeApiGlobalCache'; - var typeCodeKey = '__nativeApiTypeCode'; - var classWrappers = typeof WeakMap === 'function' ? new WeakMap() : null; - - function globalCache() { - var existing = globalThis[cacheName]; - if (existing && typeof existing === 'object') { - return existing; - } - var cache = Object.create(null); - Object.defineProperty(globalThis, cacheName, { - configurable: false, - enumerable: false, - writable: false, - value: cache - }); - return cache; - } - - function cacheGlobal(name, value) { - if (name && value !== undefined) { - globalCache()[name] = value; - } - } - - function defineLazyGlobal(name, resolve, force) { - if (!name) { - return; - } - if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { - try { - cacheGlobal(name, globalThis[name]); - } catch (_) { - } - return; - } - try { - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: false, - get: function() { - var value = resolve(name); - cacheGlobal(name, value); - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: false, - writable: false, - value: value - }); - return value; - } - }); - } catch (_) { - var value = resolve(name); - if (value !== undefined) { - cacheGlobal(name, value); - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: false, - writable: false, - value: value - }); - } - } - } - - function wrapAggregateConstructor(nativeConstructor) { - if (typeof nativeConstructor !== 'function') { - return nativeConstructor; - } - var aggregate = function NativeScriptAggregate(initialValue) { - return nativeConstructor(initialValue); - }; - try { - Object.defineProperty(aggregate, Symbol.hasInstance, { - configurable: true, - enumerable: false, - value: function(value) { - return !!value && - typeof value === 'object' && - value.kind === nativeConstructor.kind && - value.name === nativeConstructor.runtimeName; - } - }); - } catch (_) { - } - ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields', 'equals'].forEach(function(key) { - try { - Object.defineProperty(aggregate, key, { - configurable: true, - enumerable: false, - writable: false, - value: nativeConstructor[key] - }); - } catch (_) { - } - }); - return aggregate; - } - - function wrapNativeClass(nativeClass) { - if (!nativeClass || (typeof nativeClass !== 'object' && typeof nativeClass !== 'function')) { - return nativeClass; - } - if (classWrappers) { - var cached = classWrappers.get(nativeClass); - if (cached) { - return cached; - } - } - var constructable = function NativeScriptNativeClass() { - var args = Array.prototype.slice.call(arguments); - if (args.length > 0 && typeof nativeClass.construct === 'function') { - return nativeClass.construct.apply(nativeClass, args); - } - if (typeof nativeClass.alloc !== 'function') { - throw new Error('Native class cannot be allocated'); - } - var instance = nativeClass.alloc(); - if (instance && typeof instance.init === 'function') { - return instance.init(); - } - return instance; - }; - Object.defineProperty(constructable, '__nativeApiClass', { - configurable: false, - enumerable: false, - writable: false, - value: nativeClass - }); - try { - Object.defineProperty(constructable, Symbol.hasInstance, { - configurable: true, - enumerable: false, - value: function(value) { - if (!value || typeof value !== 'object') { - return false; - } - try { - if (typeof value.isKindOfClass === 'function') { - return !!value.isKindOfClass(constructable); - } - } catch (_) { - } - var expectedName = nativeClass.runtimeName || nativeClass.name; - return typeof expectedName === 'string' && value.className === expectedName; - } - }); - } catch (_) { - } - var wrapper = typeof Proxy === 'function' - ? new Proxy(constructable, { - get: function(target, property, receiver) { - if (property in target) { - return Reflect.get(target, property, receiver); - } - return nativeClass[property]; - }, - set: function(target, property, value, receiver) { - if (receiver && receiver !== target) { - Object.defineProperty(receiver, property, { - configurable: true, - enumerable: true, - writable: true, - value: value - }); - return true; - } - try { - nativeClass[property] = value; - return true; - } catch (_) { - } - return Reflect.set(target, property, value, receiver); - }, - has: function(target, property) { - return property in target || property in nativeClass; - } - }) - : constructable; - if (classWrappers) { - classWrappers.set(nativeClass, wrapper); - } - return wrapper; - } - - function wrapInteropFactory(nativeFactory, properties) { - if (typeof nativeFactory !== 'function' || nativeFactory.__nativeScriptConstructable) { - return nativeFactory; - } - var constructable = function NativeScriptInteropValue() { - return nativeFactory.apply(undefined, arguments); - }; - try { - if (nativeFactory.prototype) { - constructable.prototype = nativeFactory.prototype; - } - } catch (_) { - } - try { - Object.defineProperty(constructable, Symbol.hasInstance, { - configurable: true, - enumerable: false, - value: function(value) { - return !!value && typeof value === 'object' && value.kind === properties.kind; - } - }); - } catch (_) { - } - Object.keys(properties).forEach(function(key) { - try { - Object.defineProperty(constructable, key, { - configurable: true, - enumerable: false, - writable: false, - value: properties[key] - }); - } catch (_) { - } - }); - Object.defineProperty(constructable, '__nativeScriptConstructable', { - configurable: false, - enumerable: false, - writable: false, - value: true - }); - return constructable; - } - - function installInteropConstructors() { - var interop = globalThis.interop; - if (!interop || typeof interop !== 'object') { - return; - } - var pointerSize; - try { - if (typeof interop.sizeof === 'function' && interop.types && interop.types.pointer !== undefined) { - pointerSize = interop.sizeof(interop.types.pointer); - } - } catch (_) { - pointerSize = undefined; - } - interop.Pointer = wrapInteropFactory(interop.Pointer, { kind: 'pointer', sizeof: pointerSize }); - interop.Reference = wrapInteropFactory(interop.Reference, { kind: 'reference', sizeof: pointerSize }); - interop.FunctionReference = wrapInteropFactory( - interop.FunctionReference, - { kind: 'functionReference', sizeof: pointerSize } - ); - if (interop.types && typeof interop.types === 'object') { - Object.keys(interop.types).forEach(function(name) { - var value = interop.types[name]; - if (typeof value !== 'number') { - return; - } - var boxed = { - valueOf: function() { return value; }, - toString: function() { return String(value); } - }; - Object.defineProperty(boxed, typeCodeKey, { - configurable: false, - enumerable: false, - writable: false, - value: value - }); - interop.types[name] = boxed; - }); - } - } - - function defineInlineFunction(name, value) { - if (Object.prototype.hasOwnProperty.call(globalThis, name)) { - return; - } - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: false, - writable: true, - value: value - }); - } - - function installInlineFunctions() { - var makePoint = function(x, y) { return { x: x, y: y }; }; - var makeSize = function(width, height) { return { width: width, height: height }; }; - var makeRect = function(x, y, width, height) { - return { origin: { x: x, y: y }, size: { width: width, height: height } }; - }; - defineInlineFunction('CGPointMake', makePoint); - defineInlineFunction('NSMakePoint', makePoint); - defineInlineFunction('CGSizeMake', makeSize); - defineInlineFunction('NSMakeSize', makeSize); - defineInlineFunction('CGRectMake', makeRect); - defineInlineFunction('NSMakeRect', makeRect); - defineInlineFunction('NSMakeRange', function(location, length) { - return { location: location, length: length }; - }); - defineInlineFunction('UIEdgeInsetsMake', function(top, left, bottom, right) { - return { top: top, left: left, bottom: bottom, right: right }; - }); - } - - function names(kind) { - var metadata = api.metadata; - var fn = metadata && metadata[kind]; - return typeof fn === 'function' ? fn() : []; - } - - names('classNames').forEach(function(name) { - defineLazyGlobal(name, function(className) { - return wrapNativeClass(api[className]); - }); - }); - names('functionNames').forEach(function(name) { - defineLazyGlobal(name, function(functionName) { - return api[functionName]; - }); - }); - names('constantNames').forEach(function(name) { - defineLazyGlobal(name, function(constantName) { - return api[constantName]; - }); - }); - names('protocolNames').forEach(function(name) { - defineLazyGlobal(name, function(protocolName) { - return (api.getProtocol && api.getProtocol(protocolName)) || api[protocolName]; - }); - }); - names('enumNames').forEach(function(name) { - var resolveEnum = function(enumName) { - return (api.getEnum && api.getEnum(enumName)) || api[enumName]; - }; - defineLazyGlobal(name, resolveEnum); - var enumValue = resolveEnum(name); - if (!enumValue || typeof enumValue !== 'object') { - return; - } - Object.keys(enumValue).forEach(function(memberName) { - if (/^-?\d+$/.test(memberName)) { - return; - } - defineLazyGlobal(memberName, function() { - return enumValue[memberName]; - }); - }); - }); - names('structNames').forEach(function(name) { - defineLazyGlobal(name, function(structName) { - return wrapAggregateConstructor((api.getStruct && api.getStruct(structName)) || api[structName]); - }, true); - }); - names('unionNames').forEach(function(name) { - defineLazyGlobal(name, function(unionName) { - return wrapAggregateConstructor((api.getUnion && api.getUnion(unionName)) || api[unionName]); - }, true); - }); - - installInteropConstructors(); - installInlineFunctions(); - - try { - Object.defineProperty(api, '__nativeScriptGlobalsInstalled', { - configurable: false, - enumerable: false, - writable: false, - value: true - }); - } catch (_) { - } -}) -)JSI_GLOBALS"; - - std::string script(GlobalInstaller); - script += "("; - script += jsStringLiteral(globalName); - script += ");"; - runtime.evaluateJavaScript(std::make_shared(std::move(script)), - "NativeApiJsiGlobals.js"); -} - -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); - Object global = runtime.global(); - global.setProperty(runtime, globalName, api); - - Value existingInterop = global.getProperty(runtime, "interop"); - if (existingInterop.isUndefined() || existingInterop.isNull()) { - global.setProperty(runtime, "interop", api.getProperty(runtime, "interop")); - } - if (config.installGlobalSymbols) { - InstallNativeApiJsiGlobalSymbols(runtime, globalName); - } else { - InstallAggregateGlobals(runtime, api, "protocolNames"); - } -} - -} // 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/hermes/jsi/NativeApiJsiBridge.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiBridge.inc new file mode 100644 index 00000000..2df5d2d9 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiBridge.inc @@ -0,0 +1,1598 @@ +thread_local bool gDispatchNativeCallsToUI = false; +thread_local bool gExecutingDispatchedUINativeCall = false; +thread_local int gSynchronousNativeInvocationDepth = 0; +thread_local int gNativeCallerThreadJsiCallbackDepth = 0; +std::atomic gActiveSynchronousNativeInvocationDepth{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 += 1; + gActiveSynchronousNativeInvocationDepth.fetch_add(1, + std::memory_order_acq_rel); + } + + ~ScopedNativeApiSynchronousInvocation() { + gSynchronousNativeInvocationDepth -= 1; + gActiveSynchronousNativeInvocationDepth.fetch_sub(1, + std::memory_order_acq_rel); + } +}; + +class ScopedNativeCallerThreadJsiCallback final { + public: + ScopedNativeCallerThreadJsiCallback() { + gNativeCallerThreadJsiCallbackDepth += 1; + } + + ~ScopedNativeCallerThreadJsiCallback() { + gNativeCallerThreadJsiCallbackDepth -= 1; + } + + ScopedNativeCallerThreadJsiCallback( + const ScopedNativeCallerThreadJsiCallback&) = delete; + ScopedNativeCallerThreadJsiCallback& operator=( + const ScopedNativeCallerThreadJsiCallback&) = delete; +}; + +template +void performNativeInvocation(Runtime& runtime, + const std::function)>& + invoker, + Invocation&& invocation) { + NSString* exceptionDescription = nil; + auto run = [&]() { + ScopedNativeApiSynchronousInvocation synchronousInvocation; + @try { + invocation(); + } @catch (NSException* exception) { + exceptionDescription = [exception.description copy]; + } + }; + + bool skipInvoker = gNativeCallerThreadJsiCallbackDepth > 0; + if (shouldDispatchNativeCallToUI()) { + dispatch_sync(dispatch_get_main_queue(), ^{ + bool previous = gExecutingDispatchedUINativeCall; + gExecutingDispatchedUINativeCall = true; + if (invoker && !skipInvoker) { + invoker(run); + } else { + run(); + } + gExecutingDispatchedUINativeCall = previous; + }); + } else if (invoker && !skipInvoker) { + invoker(run); + } 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::optional runtimeSelectorNameForProperty( + Class cls, bool staticMethod, const std::string& property) { + if (cls == nil || property.empty()) { + return std::nullopt; + } + +#if TARGET_OS_OSX + if (property == "initWithRedGreenBlueAlpha") { + const char* candidates[] = { + "initWithSRGBRed:green:blue:alpha:", + "initWithCalibratedRed:green:blue:alpha:", + }; + for (const char* candidate : candidates) { + SEL selector = sel_getUid(candidate); + if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || + (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { + return std::string(candidate); + } + } + } else if (property == "colorWithRedGreenBlueAlpha") { + const char* candidates[] = { + "colorWithSRGBRed:green:blue:alpha:", + "colorWithCalibratedRed:green:blue:alpha:", + }; + for (const char* candidate : candidates) { + SEL selector = sel_getUid(candidate); + if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || + (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { + return std::string(candidate); + } + } + } +#endif + + Class scan = staticMethod ? object_getClass(cls) : cls; + while (scan != Nil) { + unsigned int methodCount = 0; + Method* methods = class_copyMethodList(scan, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + SEL selector = method_getName(methods[i]); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName != nullptr && + (property == selectorName || jsifySelector(selectorName) == property)) { + std::string result(selectorName); + free(methods); + return result; + } + } + free(methods); + scan = class_getSuperclass(scan); + } + + return std::nullopt; +} + +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; +} + +bool hasRuntimeSetterForProperty(Class cls, bool staticMethod, + const std::string& property) { + if (cls == nil || property.empty()) { + return false; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + return staticMethod ? class_getClassMethod(cls, selector) != nullptr + : class_getInstanceMethod(cls, selector) != nullptr; +} + +size_t selectorArgumentCount(const std::string& selector) { + return static_cast( + std::count(selector.begin(), selector.end(), ':')); +} + +const NativeApiMember* selectMethodMember( + const std::vector& members, const std::string& property, + bool staticMethod, size_t argumentCount) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + if (fallback == nullptr) { + fallback = &member; + } + if (selectorArgumentCount(member.selectorName) == argumentCount) { + return &member; + } + } + return fallback; +} + +const NativeApiMember* selectPropertyMember( + const std::vector& members, const std::string& property, + bool staticMethod) { + for (const auto& member : members) { + if (!member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic == staticMethod) { + return &member; + } + } + return nullptr; +} + +const NativeApiMember* selectWritablePropertyMember( + const std::vector& members, const std::string& property, + bool staticMethod) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (!member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + if (fallback == nullptr) { + fallback = &member; + } + if (!member.readonly && !member.setterSelectorName.empty()) { + return &member; + } + } + return fallback; +} + +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); +Protocol* lookupProtocolByNativeName(const std::string& name); + +inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { +#if INTPTR_MAX == INT64_MAX + return pointer & 0x0000FFFFFFFFFFFFULL; +#else + return pointer; +#endif +} + +class NativeApiJsiBridge { + public: + explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) + : metadata_(loadMetadata(config)), + scheduler_(config.scheduler), + nativeInvocationInvoker_(config.nativeInvocationInvoker), + nativeCallbackInvoker_(config.nativeCallbackInvoker), + jsThreadCallbackInvoker_(config.jsThreadCallbackInvoker) { + 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* findClassByOffset(MDSectionOffset offset) const { + auto it = classSymbolsByOffset_.find(offset); + return it != classSymbolsByOffset_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findClassForRuntimeClass(Class cls) const { + Class current = cls; + while (current != Nil) { + const char* name = class_getName(current); + if (name != nullptr) { + if (const NativeApiSymbol* symbol = findClass(name)) { + return symbol; + } + } + current = class_getSuperclass(current); + } + return nullptr; + } + + const NativeApiSymbol* findClassForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = classSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != classSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findProtocolForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = protocolSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != protocolSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findFunction(const std::string& name) const { + auto it = functionSymbolsByName_.find(name); + return it != functionSymbolsByName_.end() ? &it->second : nullptr; + } + + void rememberRoundTripValue(Runtime& runtime, const void* native, + const Value& value) { + if (native == nullptr) { + return; + } + roundTripValues_[normalizeRuntimePointer( + reinterpret_cast(native))] = + std::make_shared(runtime, value); + } + + Value findRoundTripValue(Runtime& runtime, const void* native) const { + if (native == nullptr) { + return Value::undefined(); + } + auto it = roundTripValues_.find( + normalizeRuntimePointer(reinterpret_cast(native))); + if (it == roundTripValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void forgetRoundTripValue(const void* native) { + if (native == nullptr) { + return; + } + roundTripValues_.erase( + normalizeRuntimePointer(reinterpret_cast(native))); + } + + void rememberClassValue(Runtime& runtime, Class cls, const Value& value) { + if (cls == Nil) { + return; + } + classValues_[normalizeRuntimePointer(reinterpret_cast(cls))] = + std::make_shared(runtime, value); + } + + Value findClassValue(Runtime& runtime, Class cls) const { + if (cls == Nil) { + return Value::undefined(); + } + auto it = classValues_.find( + normalizeRuntimePointer(reinterpret_cast(cls))); + if (it == classValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void rememberClassPrototype(Runtime& runtime, Class cls, const Value& value) { + if (cls == Nil) { + return; + } + classPrototypes_[normalizeRuntimePointer(reinterpret_cast(cls))] = + std::make_shared(runtime, value); + } + + Value findClassPrototype(Runtime& runtime, Class cls) const { + if (cls == Nil) { + return Value::undefined(); + } + auto it = classPrototypes_.find( + normalizeRuntimePointer(reinterpret_cast(cls))); + if (it == classPrototypes_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void setObjectExpando(Runtime& runtime, const void* native, + const std::string& property, const Value& value) { + if (native == nullptr || property.empty()) { + return; + } + objectExpandos_[normalizeRuntimePointer(reinterpret_cast(native))] + [property] = std::make_shared(runtime, value); + } + + Value findObjectExpando(Runtime& runtime, const void* native, + const std::string& property) const { + if (native == nullptr || property.empty()) { + return Value::undefined(); + } + auto objectIt = objectExpandos_.find( + normalizeRuntimePointer(reinterpret_cast(native))); + if (objectIt == objectExpandos_.end()) { + return Value::undefined(); + } + auto propertyIt = objectIt->second.find(property); + if (propertyIt == objectIt->second.end() || propertyIt->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *propertyIt->second); + } + + void forgetObjectExpandos(const void* native) { + if (native == nullptr) { + return; + } + objectExpandos_.erase( + normalizeRuntimePointer(reinterpret_cast(native))); + } + + void rememberPointerValue(Runtime& runtime, const void* native, + const Value& value) { + pointerValues_[reinterpret_cast(native)] = + std::make_shared(runtime, value); + } + + Value findPointerValue(Runtime& runtime, const void* native) const { + auto it = pointerValues_.find(reinterpret_cast(native)); + if (it == pointerValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void forgetPointerValue(const void* native) { + if (native == nullptr) { + return; + } + pointerValues_.erase(reinterpret_cast(native)); + } + + const NativeApiSymbol* findConstant(const std::string& name) const { + auto it = constantSymbolsByName_.find(name); + return it != constantSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findProtocol(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + if (symbol != nullptr && symbol->kind == NativeApiSymbolKind::Protocol) { + return symbol; + } + auto it = protocolSymbolsByRuntimeName_.find(name); + return it != protocolSymbolsByRuntimeName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findEnum(const std::string& name) const { + auto it = enumSymbolsByName_.find(name); + return it != enumSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findStruct(const std::string& name) const { + auto it = structSymbolsByName_.find(name); + return it != structSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findUnion(const std::string& name) const { + auto it = unionSymbolsByName_.find(name); + return it != unionSymbolsByName_.end() ? &it->second : 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_; } + const std::function)>& nativeInvocationInvoker() + const { + return nativeInvocationInvoker_; + } + const std::function)>& nativeCallbackInvoker() + const { + return nativeCallbackInvoker_; + } + const std::function)>& jsThreadCallbackInvoker() + const { + return jsThreadCallbackInvoker_; + } + 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; + } + + const std::vector& surfaceMembersForClass( + const NativeApiSymbol& symbol) const { + auto cached = surfaceMembersByClassOffset_.find(symbol.offset); + if (cached != surfaceMembersByClassOffset_.end()) { + return cached->second; + } + + auto inserted = surfaceMembersByClassOffset_.emplace( + symbol.offset, readSurfaceMembersForClass(symbol)); + return inserted.first->second; + } + + const std::vector& membersForProtocol( + const NativeApiSymbol& symbol) const { + auto cached = membersByProtocolOffset_.find(symbol.offset); + if (cached != membersByProtocolOffset_.end()) { + return cached->second; + } + + auto inserted = membersByProtocolOffset_.emplace( + symbol.offset, readMembersForProtocolHierarchy(symbol.offset)); + 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); + functionSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Constant: + constantNames_.push_back(symbol.name); + constantSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Protocol: + protocolNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Enum: + enumNames_.push_back(symbol.name); + enumSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Struct: + structNames_.push_back(symbol.name); + structSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Union: + unionNames_.push_back(symbol.name); + unionSymbolsByName_[symbol.name] = symbol; + break; + } + + symbolsByName_[symbol.name] = symbol; + if (kind == NativeApiSymbolKind::Class) { + classSymbolsByOffset_[symbol.offset] = symbol; + classSymbolsByRuntimeName_[symbol.runtimeName] = symbol; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls != Nil) { + classSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(cls))] = symbol; + } + } else if (kind == NativeApiSymbolKind::Protocol) { + protocolSymbolsByOffset_[symbol.offset] = symbol; + protocolSymbolsByRuntimeName_[symbol.runtimeName] = symbol; + auto rememberProtocolRuntimeName = [&](const std::string& runtimeName) { + if (runtimeName.empty()) { + return; + } + protocolSymbolsByRuntimeName_[runtimeName] = symbol; + Protocol* runtimeProtocol = lookupProtocolByNativeName(runtimeName); + if (runtimeProtocol != nullptr) { + protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(runtimeProtocol))] = symbol; + } + }; + if (symbol.name.size() > 9 && + std::isdigit(static_cast(symbol.name.back()))) { + size_t digitsStart = symbol.name.size(); + while (digitsStart > 0 && + std::isdigit(static_cast(symbol.name[digitsStart - 1]))) { + digitsStart--; + } + constexpr const char* protocolSuffix = "Protocol"; + size_t protocolSuffixLength = std::strlen(protocolSuffix); + if (digitsStart > protocolSuffixLength && + symbol.name.compare(digitsStart - protocolSuffixLength, + protocolSuffixLength, protocolSuffix) == 0) { + rememberProtocolRuntimeName( + symbol.name.substr(0, digitsStart - protocolSuffixLength)); + } + } + Protocol* protocol = lookupProtocolByNativeName(symbol.runtimeName); + if (protocol == nullptr && symbol.runtimeName != symbol.name) { + protocol = lookupProtocolByNativeName(symbol.name); + } + if (protocol != nullptr) { + protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(protocol))] = 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 readProtocolOffsetsForClass( + MDSectionOffset classOffset, MDSectionOffset* memberOffset = nullptr, + MDSectionOffset* superclassOffsetOut = nullptr) const { + std::vector protocols; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return protocols; + } + + 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; + protocolOffset &= ~metagen::mdSectionOffsetNext; + if (protocolOffset != MD_SECTION_OFFSET_NULL) { + protocols.push_back(protocolOffset + metadata_->protocolsOffset); + } + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + const bool hasMembers = (superclass & metagen::mdSectionOffsetNext) != 0; + if (superclassOffsetOut != nullptr) { + MDSectionOffset superclassOffset = + superclass & ~metagen::mdSectionOffsetNext; + *superclassOffsetOut = + superclassOffset != MD_SECTION_OFFSET_NULL + ? superclassOffset + metadata_->classesOffset + : MD_SECTION_OFFSET_NULL; + } + if (memberOffset != nullptr) { + *memberOffset = hasMembers ? offset : MD_SECTION_OFFSET_NULL; + } + return protocols; + } + + std::vector readOwnMembersForClass( + MDSectionOffset classOffset) const { + std::vector members; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset memberOffset = MD_SECTION_OFFSET_NULL; + for (MDSectionOffset protocolOffset : + readProtocolOffsetsForClass(classOffset, &memberOffset)) { + auto protocol = protocolSymbolsByOffset_.find(protocolOffset); + if (protocol == protocolSymbolsByOffset_.end()) { + continue; + } + const auto& protocolMembers = membersForProtocol(protocol->second); + members.insert(members.end(), protocolMembers.begin(), + protocolMembers.end()); + } + + if (memberOffset != MD_SECTION_OFFSET_NULL) { + std::vector ownMembers = + readMembersAtOffset(memberOffset); + members.insert(members.end(), ownMembers.begin(), ownMembers.end()); + } + return members; + } + + 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; + } + + static bool memberIsStatic(const NativeApiMember& member) { + return (member.flags & metagen::mdMemberStatic) != 0; + } + + static bool sameMemberSlot(const NativeApiMember& lhs, + const NativeApiMember& rhs) { + return lhs.name == rhs.name && lhs.property == rhs.property && + memberIsStatic(lhs) == memberIsStatic(rhs); + } + + static bool sameMethodSelector(const NativeApiMember& lhs, + const NativeApiMember& rhs) { + return !lhs.property && !rhs.property && sameMemberSlot(lhs, rhs) && + lhs.selectorName == rhs.selectorName; + } + + static const NativeApiMember* findPropertyMember( + const std::vector& members, + const NativeApiMember& candidate) { + for (const auto& member : members) { + if (member.property && sameMemberSlot(member, candidate)) { + return &member; + } + } + return nullptr; + } + + static bool selectorExistsInMembers( + const std::vector& members, + const NativeApiMember& candidate) { + for (const auto& member : members) { + if (sameMethodSelector(member, candidate)) { + return true; + } + } + return false; + } + + static bool shouldSkipPropertyOverride( + const NativeApiMember* inherited, const NativeApiMember& member) { + if (inherited == nullptr || !inherited->property) { + return false; + } + + bool sameGetter = inherited->selectorName == member.selectorName; + bool sameSetter = + inherited->setterSelectorName == member.setterSelectorName; + if ((!inherited->readonly && member.readonly) || + (inherited->readonly == member.readonly && sameGetter && + (member.readonly || sameSetter))) { + return true; + } + return false; + } + + static void appendSurfaceMember( + std::vector& surface, + const std::vector& inheritedMembers, + const NativeApiMember& member) { + if (member.name.empty()) { + return; + } + + if (member.property) { + const NativeApiMember* inherited = + findPropertyMember(inheritedMembers, member); + if (shouldSkipPropertyOverride(inherited, member)) { + return; + } + + for (auto& existing : surface) { + if (!existing.property || !sameMemberSlot(existing, member)) { + continue; + } + if (existing.readonly && !member.readonly) { + existing = member; + } + return; + } + surface.push_back(member); + return; + } + + const bool keepInheritedMethod = + member.name == "alloc" || member.name == "toString" || + member.name == "superclass"; + if (!keepInheritedMethod && + selectorExistsInMembers(inheritedMembers, member)) { + return; + } + if (selectorExistsInMembers(surface, member)) { + return; + } + surface.push_back(member); + } + + std::vector readSurfaceMembersForClass( + const NativeApiSymbol& symbol) const { + std::vector inheritedMembers; + if (symbol.superclassOffset != MD_SECTION_OFFSET_NULL) { + auto superclass = classSymbolsByOffset_.find(symbol.superclassOffset); + if (superclass != classSymbolsByOffset_.end()) { + const auto& inherited = surfaceMembersForClass(superclass->second); + inheritedMembers.insert(inheritedMembers.end(), inherited.begin(), + inherited.end()); + } + } + + std::vector surface; + for (const auto& member : readOwnMembersForClass(symbol.offset)) { + appendSurfaceMember(surface, inheritedMembers, member); + } + return surface; + } + + std::vector readMembersAtOffset( + MDSectionOffset& offset) const { + std::vector members; + bool next = true; + 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 readMembersForProtocolHierarchy( + MDSectionOffset protocolOffset) const { + std::vector members; + if (metadata_ == nullptr || protocolOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset offset = protocolOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto inheritedOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (inheritedOffset & metagen::mdSectionOffsetNext) != 0; + inheritedOffset &= ~metagen::mdSectionOffsetNext; + if (inheritedOffset == MD_SECTION_OFFSET_NULL) { + continue; + } + + MDSectionOffset absoluteOffset = + inheritedOffset + metadata_->protocolsOffset; + auto inheritedSymbol = protocolSymbolsByOffset_.find(absoluteOffset); + if (inheritedSymbol != protocolSymbolsByOffset_.end()) { + const auto& inheritedMembers = + membersForProtocol(inheritedSymbol->second); + members.insert(members.end(), inheritedMembers.begin(), + inheritedMembers.end()); + } + } + + std::vector ownMembers = readMembersAtOffset(offset); + members.insert(members.end(), ownMembers.begin(), ownMembers.end()); + return members; + } + + std::vector readMembersForClassHierarchy( + const NativeApiSymbol& symbol) const { + std::vector members = readOwnMembersForClass(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 functionSymbolsByName_; + std::unordered_map constantSymbolsByName_; + std::unordered_map enumSymbolsByName_; + std::unordered_map structSymbolsByName_; + std::unordered_map unionSymbolsByName_; + std::unordered_map classSymbolsByRuntimeName_; + std::unordered_map protocolSymbolsByRuntimeName_; + std::unordered_map classSymbolsByRuntimePointer_; + std::unordered_map protocolSymbolsByRuntimePointer_; + std::unordered_map> roundTripValues_; + std::unordered_map> classValues_; + std::unordered_map> classPrototypes_; + std::unordered_map> pointerValues_; + std::unordered_map>> + objectExpandos_; + std::unordered_map classSymbolsByOffset_; + std::unordered_map protocolSymbolsByOffset_; + 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_; + std::function)> nativeInvocationInvoker_; + std::function)> nativeCallbackInvoker_; + std::function)> jsThreadCallbackInvoker_; + mutable std::unordered_map> + membersByClassOffset_; + mutable std::unordered_map> + surfaceMembersByClassOffset_; + mutable std::unordered_map> + membersByProtocolOffset_; + 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; +class NativeApiJsiArgumentFrame; + +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, + Class dispatchSuperClass = Nil); + +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject); + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol); + +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); +std::optional parseArrayIndexProperty(const std::string& property); + +NativeApiJsiType nativeObjectReturnType( + MDTypeKind kind = metagen::mdTypeAnyObject) { + NativeApiJsiType type; + type.kind = kind; + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; +} + +NativeApiJsiType nativeObjectReturnTypeForClass(Class cls) { + if (cls != Nil) { + const char* name = class_getName(cls); + if (name != nullptr && std::strcmp(name, "NSString") == 0) { + return nativeObjectReturnType(metagen::mdTypeNSStringObject); + } + if (name != nullptr && std::strcmp(name, "NSMutableString") == 0) { + return nativeObjectReturnType(metagen::mdTypeNSMutableStringObject); + } + } + return nativeObjectReturnType(metagen::mdTypeInstanceObject); +} + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); +Object createPointer(Runtime& runtime, + const std::shared_ptr& bridge, + void* pointer, bool adopted = false); + +NativeApiJsiType primitiveInteropType(MDTypeKind kind); diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiCallbacks.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiCallbacks.inc new file mode 100644 index 00000000..3de66475 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiCallbacks.inc @@ -0,0 +1,1354 @@ +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; + std::string selectorName; + bool variadic = false; + bool prepared = false; + unsigned int implicitArgumentCount = 0; +}; + +bool selectorEndsWithNSErrorParam(const std::string& selectorName) { + constexpr const char* suffix = "error:"; + size_t suffixLength = std::strlen(suffix); + return selectorName.size() >= suffixLength && + selectorName.compare(selectorName.size() - suffixLength, suffixLength, + suffix) == 0; +} + +bool isNSErrorOutJsiMethodSignature(const NativeApiJsiSignature& signature) { + if (signature.argumentTypes.empty() || signature.variadic || + !selectorEndsWithNSErrorParam(signature.selectorName)) { + return false; + } + + return signature.argumentTypes.back().kind == metagen::mdTypePointer; +} + +bool isNSErrorOutJsiMethodCallback(const NativeApiJsiSignature& signature) { + return signature.returnType.kind == metagen::mdTypeBool && + signature.implicitArgumentCount >= 2 && + isNSErrorOutJsiMethodSignature(signature); +} + +class NativeApiJsiArgumentFrame { + public: + explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(count) {} + + ~NativeApiJsiArgumentFrame() { + for (char* string : ownedCStrings_) { + free(string); + } + for (void* buffer : ownedBuffers_) { + free(buffer); + } + 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* addBuffer(size_t size) { + void* buffer = calloc(1, std::max(size, 1)); + if (buffer == nullptr) { + throw std::bad_alloc(); + } + ownedBuffers_.push_back(buffer); + return buffer; + } + 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 ownedBuffers_; + 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; +} + +std::string objcMethodSignatureForJsiSignature( + 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); + +std::atomic gActiveNativeThreadJsiCallbacks{0}; + +class NativeApiJsiCallback final { + public: + NativeApiJsiCallback(Runtime& runtime, + std::shared_ptr bridge, + std::shared_ptr signature, + Function function, bool block, bool bindThis = false) + : runtime_(&runtime), + bridge_(std::move(bridge)), + signature_(std::move(signature)), + function_(std::make_shared(std::move(function))), + block_(block), + bindThis_(bindThis) { + 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_; + } + + const NativeApiJsiSignature& signature() const { return *signature_; } + + 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); }; + const auto& nativeCallbackInvoker = bridge_->nativeCallbackInvoker(); + const auto& jsThreadCallbackInvoker = bridge_->jsThreadCallbackInvoker(); + bool currentThreadIsJs = + std::this_thread::get_id() == bridge_->jsThreadId(); + bool returnsVoid = signature_->returnType.kind == metagen::mdTypeVoid; + bool activeSynchronousNativeInvocation = + gActiveSynchronousNativeInvocationDepth.load( + std::memory_order_acquire) > 0; + bool nativeCallerThreadCallback = + !currentThreadIsJs && activeSynchronousNativeInvocation && + (!block_ || !returnsVoid); + bool direct = currentThreadIsJs || + gExecutingDispatchedUINativeCall || + gSynchronousNativeInvocationDepth > 0 || + nativeCallerThreadCallback || + (!nativeCallbackInvoker && + activeSynchronousNativeInvocation); + bool waitForNativeThreadCallback = + currentThreadIsJs && nativeCallbackInvoker && + gActiveNativeThreadJsiCallbacks.load(std::memory_order_acquire) > 0; + if (direct && !waitForNativeThreadCallback) { + if (nativeCallerThreadCallback) { + ScopedNativeCallerThreadJsiCallback callbackScope; + call(); + } else { + call(); + } + } else if (!currentThreadIsJs && returnsVoid && block_ && + jsThreadCallbackInvoker) { + jsThreadCallbackInvoker(call); + } else if (nativeCallbackInvoker) { + bool nativeThreadCallback = !currentThreadIsJs; + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_add(1, + std::memory_order_acq_rel); + } + try { + nativeCallbackInvoker(call); + } catch (...) { + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_sub( + 1, std::memory_order_acq_rel); + } + throw; + } + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_sub(1, + std::memory_order_acq_rel); + } + } 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) { + try { + size_t nativeArgOffset = signature_->implicitArgumentCount; + 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 = Value::undefined(); + if (bindThis_ && nativeArgOffset >= 1) { + id self = *static_cast(args[0]); + Value thisValue = + makeNativeObjectValue(*runtime_, bridge_, self, false); + Object thisObject = thisValue.isObject() + ? thisValue.asObject(*runtime_) + : Object(*runtime_); + result = + jsArgs.empty() + ? function_->callWithThis(*runtime_, thisObject) + : function_->callWithThis( + *runtime_, thisObject, + static_cast(jsArgs.data()), + static_cast(jsArgs.size())); + } else { + result = + jsArgs.empty() + ? function_->call(*runtime_) + : function_->call(*runtime_, + static_cast(jsArgs.data()), + static_cast(jsArgs.size())); + } + storeReturnValue(result, ret); + if (std::this_thread::get_id() == bridge_->jsThreadId()) { + runtime_->drainMicrotasks(); + } + } catch (const std::exception& exception) { + if (isNSErrorOutJsiMethodCallback(*signature_)) { + zeroReturnValue(ret); + populateNSErrorOutArgument(args, exception.what()); + return; + } + if (error != nullptr) { + *error = exception.what(); + } + zeroReturnValue(ret); + } catch (...) { + if (isNSErrorOutJsiMethodCallback(*signature_)) { + zeroReturnValue(ret); + populateNSErrorOutArgument(args, "Unknown exception in native JSI callback."); + return; + } + if (error != nullptr) { + *error = "Unknown exception in native JSI callback."; + } + zeroReturnValue(ret); + } + } + + void populateNSErrorOutArgument(void* args[], const char* message) { + if (args == nullptr || signature_ == nullptr || + signature_->argumentTypes.empty()) { + return; + } + + size_t outArgIndex = signature_->implicitArgumentCount + + signature_->argumentTypes.size() - 1; + void* outArgValue = args[outArgIndex]; + NSError** outError = + outArgValue != nullptr ? *reinterpret_cast(outArgValue) + : nullptr; + if (outError == nullptr) { + return; + } + + NSString* nsMessage = + message != nullptr ? [NSString stringWithUTF8String:message] : nil; + if (nsMessage == nil) { + nsMessage = @"JS error"; + } + NSDictionary* userInfo = @{NSLocalizedDescriptionKey : nsMessage}; + *outError = [NSError errorWithDomain:@"TNSErrorDomain" + code:1 + userInfo:userInfo]; + } + + 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); + if (result.isUndefined() || result.isNull()) { + return; + } + 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; + bool bindThis_ = 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*); +} + +Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromInt64(runtime, value); +} + +Value unsignedInteger64ToJsiValue(Runtime& runtime, uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromUint64(runtime, value); +} + +bool parseIntegerTextToUintptr(const std::string& text, uintptr_t* address) { + if (address == nullptr) { + return false; + } + if (text.empty()) { + return false; + } + + char* end = nullptr; + if (text[0] == '-') { + long long signedValue = std::strtoll(text.c_str(), &end, 10); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(static_cast(signedValue)); + return true; + } + + int base = 10; + const char* start = text.c_str(); + if (text.size() > 2 && text[0] == '0' && + (text[1] == 'x' || text[1] == 'X')) { + base = 16; + } + unsigned long long unsignedValue = std::strtoull(start, &end, base); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(unsignedValue); + return true; +} + +bool parseBigIntToUintptr(Runtime& runtime, const BigInt& bigint, + uintptr_t* address) { + return parseIntegerTextToUintptr(bigint.toString(runtime, 10).utf8(runtime), + address); +} + +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; +} + +ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeArray: + return &ffi_type_pointer; + default: + return type.ffiType != nullptr ? type.ffiType : &ffi_type_pointer; + } +} + +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(ffiTypeForJsiArgument(argType)); + } + + 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(ffiTypeForJsiArgument(argType)); + } + + 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 prepareJsiMethodSignature(NativeApiJsiSignature* signature) { + if (signature == nullptr) { + return false; + } + signature->implicitArgumentCount = 2; + signature->ffiTypes.clear(); + signature->ffiTypes.reserve(signature->argumentTypes.size() + 2); + signature->ffiTypes.push_back(&ffi_type_pointer); + signature->ffiTypes.push_back(&ffi_type_pointer); + for (const auto& argType : signature->argumentTypes) { + ffi_type* ffiType = ffiTypeForJsiArgument(argType); + if (ffiType == nullptr) { + signature->prepared = false; + return false; + } + signature->ffiTypes.push_back(ffiType); + } + ffi_type* returnFfiType = + signature->returnType.ffiType != nullptr ? signature->returnType.ffiType + : &ffi_type_void; + signature->prepared = + ffi_prep_cif(&signature->cif, FFI_DEFAULT_ABI, + static_cast(signature->ffiTypes.size()), + returnFfiType, signature->ffiTypes.data()) == FFI_OK; + return signature->prepared; +} + +bool unsupportedJsiType(const NativeApiJsiType& type) { + if (type.kind == metagen::mdTypeStruct && type.aggregateInfo != nullptr && + type.aggregateInfo->ffi != nullptr) { + return false; + } + 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; +} + +std::shared_ptr createJsiMethodCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, MDSectionOffset signatureOffset, + Function function, bool returnOwned) { + if (bridge == nullptr || bridge->metadata() == nullptr || + signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native method callback metadata is unavailable."); + } + + auto parsed = parseMetadataJsiSignature( + bridge->metadata(), signatureOffset, 2, bridge.get(), returnOwned); + if (!parsed || !signatureSupportedForJsiCallback(*parsed)) { + throw facebook::jsi::JSError( + runtime, "Native method callback signature is not supported by pure JSI."); + } + parsed->selectorName = selectorName; + + auto signature = + std::make_shared(std::move(*parsed)); + auto callback = std::make_shared( + runtime, bridge, std::move(signature), std::move(function), false, true); + bridge->retainJsiLifetime(callback); + return callback; +} + +std::shared_ptr createJsiMethodCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, NativeApiJsiSignature signature, + Function function) { + signature.selectorName = selectorName; + prepareJsiMethodSignature(&signature); + if (!signatureSupportedForJsiCallback(signature)) { + throw facebook::jsi::JSError( + runtime, "Native method callback signature is not supported by pure JSI."); + } + + auto sharedSignature = + std::make_shared(std::move(signature)); + auto callback = std::make_shared( + runtime, bridge, std::move(sharedSignature), std::move(function), false, + true); + bridge->retainJsiLifetime(callback); + return callback; +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiClassBuilder.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiClassBuilder.inc new file mode 100644 index 00000000..dcbf6de9 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiClassBuilder.inc @@ -0,0 +1,784 @@ +std::string readOptionalStringProperty(Runtime& runtime, const Object& object, + const char* name) { + if (name == nullptr || !object.hasProperty(runtime, name)) { + return ""; + } + Value value = object.getProperty(runtime, name); + return value.isString() ? value.asString(runtime).utf8(runtime) : ""; +} + +struct NativeApiJsiClassBuilderRegistration { + Runtime* runtime = nullptr; + std::shared_ptr bridge; +}; + +std::mutex gNativeApiJsiClassBuilderMutex; +std::unordered_map + gNativeApiJsiClassBuilders; +struct NativeApiJsiKnownExposedMethod { + std::string selectorName; + NativeApiJsiSignature signature; +}; +std::mutex gNativeApiJsiKnownExposedMethodsMutex; +std::unordered_map + gNativeApiJsiKnownExposedMethods; + +void rememberNativeApiJsiClassBuilder( + Runtime& runtime, const std::shared_ptr& bridge, + Class cls) { + if (cls == Nil) { + return; + } + std::lock_guard lock(gNativeApiJsiClassBuilderMutex); + gNativeApiJsiClassBuilders[cls] = NativeApiJsiClassBuilderRegistration{ + .runtime = &runtime, + .bridge = bridge, + }; +} + +void rememberNativeApiJsiKnownExposedMethod( + const std::string& selectorName, const NativeApiJsiSignature& signature) { + if (selectorName.empty()) { + return; + } + NativeApiJsiKnownExposedMethod method{ + .selectorName = selectorName, + .signature = signature, + }; + std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); + gNativeApiJsiKnownExposedMethods[selectorName] = method; + gNativeApiJsiKnownExposedMethods[jsifySelector(selectorName.c_str())] = + std::move(method); +} + +std::optional knownNativeApiJsiExposedMethod( + const std::string& name) { + std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); + auto it = gNativeApiJsiKnownExposedMethods.find(name); + if (it == gNativeApiJsiKnownExposedMethods.end()) { + return std::nullopt; + } + NativeApiJsiKnownExposedMethod method = it->second; + prepareJsiMethodSignature(&method.signature); + return method; +} + +std::optional +findNativeApiJsiClassBuilder(id object) { + Class cls = object != nil ? object_getClass(object) : Nil; + std::lock_guard lock(gNativeApiJsiClassBuilderMutex); + while (cls != Nil) { + auto it = gNativeApiJsiClassBuilders.find(cls); + if (it != gNativeApiJsiClassBuilders.end()) { + return it->second; + } + cls = class_getSuperclass(cls); + } + return std::nullopt; +} + +const char* nativeApiJsiFastEnumerationEncoding() { + static const char* encoding = nullptr; + if (encoding == nullptr) { + struct objc_method_description desc = protocol_getMethodDescription( + @protocol(NSFastEnumeration), + @selector(countByEnumeratingWithState:objects:count:), YES, YES); + encoding = desc.types; + } + return encoding; +} + +NSUInteger nativeApiJsiSymbolIteratorCountByEnumerating( + id self, SEL, NSFastEnumerationState* state, + id __unsafe_unretained stackbuf[], NSUInteger len) { + if (len == 0 || state == nullptr || stackbuf == nullptr) { + return 0; + } + + auto registration = findNativeApiJsiClassBuilder(self); + if (!registration || registration->runtime == nullptr || + registration->bridge == nullptr) { + return 0; + } + + Runtime& runtime = *registration->runtime; + auto bridge = registration->bridge; + try { + Value receiver = makeNativeObjectValue(runtime, bridge, self, false); + if (!receiver.isObject()) { + return 0; + } + + Value iteratorFactoryValue = + runtime.global().getProperty(runtime, + "__nativeScriptCreateNativeApiIterator"); + if (!iteratorFactoryValue.isObject() || + !iteratorFactoryValue.asObject(runtime).isFunction(runtime)) { + return 0; + } + + Function iteratorFactory = + iteratorFactoryValue.asObject(runtime).asFunction(runtime); + Value prototype = + bridge->findClassPrototype(runtime, object_getClass(self)); + Value iteratorValue = + prototype.isObject() + ? iteratorFactory.call(runtime, Value(runtime, receiver), + Value(runtime, prototype)) + : iteratorFactory.call(runtime, Value(runtime, receiver)); + if (!iteratorValue.isObject()) { + return 0; + } + Object iterator = iteratorValue.asObject(runtime); + Value nextValue = iterator.getProperty(runtime, "next"); + if (!nextValue.isObject() || + !nextValue.asObject(runtime).isFunction(runtime)) { + return 0; + } + Function next = nextValue.asObject(runtime).asFunction(runtime); + + auto callNext = [&]() -> Value { + return next.callWithThis(runtime, iterator); + }; + + for (unsigned long skipped = 0; skipped < state->state; skipped++) { + Value skippedResult = callNext(); + if (!skippedResult.isObject()) { + return 0; + } + Value doneValue = + skippedResult.asObject(runtime).getProperty(runtime, "done"); + if (doneValue.isBool() && doneValue.getBool()) { + return 0; + } + } + + NSUInteger count = 0; + while (count < len) { + Value nextResult = callNext(); + if (!nextResult.isObject()) { + break; + } + Object nextObject = nextResult.asObject(runtime); + Value doneValue = nextObject.getProperty(runtime, "done"); + if (doneValue.isBool() && doneValue.getBool()) { + break; + } + + Value value = nextObject.getProperty(runtime, "value"); + NativeApiJsiArgumentFrame frame(1); + id nativeValue = objectFromJsiValue(runtime, bridge, value, frame, false); + if (nativeValue != nil) { + [nativeValue retain]; + [nativeValue autorelease]; + } + stackbuf[count++] = nativeValue; + } + + state->itemsPtr = stackbuf; + state->mutationsPtr = &state->extra[0]; + state->extra[0] = 0; + state->state += count; + return count; + } catch (const std::exception&) { + return 0; + } +} + +NativeApiSymbol runtimeSymbolForClass( + const std::shared_ptr& bridge, Class cls) { + if (bridge != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { + return *symbol; + } + } + + const char* name = cls != Nil ? class_getName(cls) : ""; + return NativeApiSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; +} + +std::string nextAvailableJsiClassName(const std::string& requestedName) { + if (requestedName.empty()) { + return ""; + } + if (objc_lookUpClass(requestedName.c_str()) == Nil) { + return requestedName; + } + + size_t suffix = 1; + std::string candidate; + do { + candidate = requestedName + std::to_string(suffix++); + } while (objc_lookUpClass(candidate.c_str()) != Nil); + return candidate; +} + +std::vector methodOverridesForName( + const std::vector& members, const std::string& name) { + std::vector result; + std::unordered_set selectors; + for (const auto& member : members) { + if (member.property || member.name != name || + (member.flags & metagen::mdMemberStatic) != 0 || + member.selectorName.empty()) { + continue; + } + if (selectors.insert(member.selectorName).second) { + result.push_back(member); + } + } + return result; +} + +const NativeApiMember* propertyOverrideForName( + const std::vector& members, const std::string& name) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (member.property && member.name == name && + (member.flags & metagen::mdMemberStatic) == 0) { + if (fallback == nullptr) { + fallback = &member; + } + if (!member.readonly && !member.setterSelectorName.empty()) { + return &member; + } + } + } + return fallback; +} + +void addJsiOverrideMethod(Runtime& runtime, + const std::shared_ptr& bridge, + Class nativeClass, Class baseClass, + const std::string& selectorName, + MDSectionOffset signatureOffset, + bool returnOwned, Function function) { + if (selectorName.empty() || signatureOffset == MD_SECTION_OFFSET_NULL) { + return; + } + + auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + signatureOffset, std::move(function), + returnOwned); + SEL selector = sel_registerName(selectorName.c_str()); + std::string metadataEncoding = + objcMethodSignatureForJsiSignature(callback->signature()); + class_replaceMethod(nativeClass, selector, + reinterpret_cast(callback->functionPointer()), + metadataEncoding.c_str()); +} + +Value getObjectPropertyOrUndefined(Runtime& runtime, const Object& object, + const std::string& name) { + return object.hasProperty(runtime, name.c_str()) + ? object.getProperty(runtime, name.c_str()) + : Value::undefined(); +} + +Class dispatchSuperclassForJsiDerivedReceiver(id receiver, Class fallback) { + if (receiver == nil) { + return Nil; + } + + Class receiverClass = object_getClass(receiver); + if (receiverClass == Nil || + !class_conformsToProtocol(receiverClass, + @protocol(NativeApiJsiClassBuilderProtocol))) { + return Nil; + } + + Class superclass = class_getSuperclass(receiverClass); + return superclass != Nil ? superclass : fallback; +} + +std::optional functionForSelector(Runtime& runtime, + const Object& methods, + const std::string& selectorName) { + Value value = getObjectPropertyOrUndefined(runtime, methods, selectorName); + if (!value.isObject() || !value.asObject(runtime).isFunction(runtime)) { + std::string jsName = jsifySelector(selectorName.c_str()); + if (jsName != selectorName) { + value = getObjectPropertyOrUndefined(runtime, methods, jsName); + } + } + if (!value.isObject() || !value.asObject(runtime).isFunction(runtime)) { + return std::nullopt; + } + return value.asObject(runtime).asFunction(runtime); +} + +std::optional readExposedType( + Runtime& runtime, const std::shared_ptr& bridge, + const Object& descriptor, const char* propertyName) { + if (!descriptor.hasProperty(runtime, propertyName)) { + return std::nullopt; + } + return interopTypeFromValue(runtime, bridge, + descriptor.getProperty(runtime, propertyName)); +} + +std::optional exposedMethodSignature( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, const Object& descriptor) { + NativeApiJsiSignature signature; + if (auto returnType = readExposedType(runtime, bridge, descriptor, "returns")) { + signature.returnType = *returnType; + } else { + signature.returnType = primitiveInteropType(metagen::mdTypeVoid); + } + + Value paramsValue = getObjectPropertyOrUndefined(runtime, descriptor, "params"); + if (!paramsValue.isUndefined() && !paramsValue.isNull()) { + if (!paramsValue.isObject() || !paramsValue.asObject(runtime).isArray(runtime)) { + throw facebook::jsi::JSError( + runtime, "exposedMethods params must be an array."); + } + Array params = paramsValue.asObject(runtime).getArray(runtime); + for (size_t i = 0; i < params.size(runtime); i++) { + Value typeValue = params.getValueAtIndex(runtime, i); + auto type = interopTypeFromValue(runtime, bridge, typeValue); + if (!type) { + throw facebook::jsi::JSError( + runtime, "exposedMethods contains an unsupported parameter type."); + } + signature.argumentTypes.push_back(*type); + } + } + + if (selectorArgumentCount(selectorName) != signature.argumentTypes.size()) { + throw facebook::jsi::JSError( + runtime, "exposedMethods selector argument count does not match params."); + } + + prepareJsiMethodSignature(&signature); + return signature; +} + +std::optional runtimeProtocolMethodSignature( + const char* types) { + if (types == nullptr) { + return std::nullopt; + } + + NSMethodSignature* methodSignature = + [NSMethodSignature signatureWithObjCTypes:types]; + if (methodSignature == nil || methodSignature.numberOfArguments < 2) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = 2; + signature.returnType = + parseObjCEncodedJsiType(methodSignature.methodReturnType); + for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) { + signature.argumentTypes.push_back( + parseObjCEncodedJsiType([methodSignature getArgumentTypeAtIndex:i])); + } + if (unsupportedJsiType(signature.returnType)) { + return std::nullopt; + } + for (const auto& argumentType : signature.argumentTypes) { + if (unsupportedJsiType(argumentType)) { + return std::nullopt; + } + } + return signature; +} + +std::optional protocolSymbolFromJsiValue( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + if (const NativeApiSymbol* symbol = bridge->findProtocol(name)) { + return *symbol; + } + return std::nullopt; + } + if (!value.isObject()) { + return std::nullopt; + } + + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->symbol(); + } + + if (stringPropertyOrEmpty(runtime, object, "kind") != "protocol") { + return std::nullopt; + } + + std::string runtimeName = stringPropertyOrEmpty(runtime, object, "runtimeName"); + if (!runtimeName.empty()) { + if (const NativeApiSymbol* symbol = bridge->findProtocol(runtimeName)) { + return *symbol; + } + } + + std::string name = stringPropertyOrEmpty(runtime, object, "name"); + if (!name.empty()) { + if (const NativeApiSymbol* symbol = bridge->findProtocol(name)) { + return *symbol; + } + } + + return std::nullopt; +} + +void addJsiExposedMethod(Runtime& runtime, + const std::shared_ptr& bridge, + Class nativeClass, const std::string& selectorName, + NativeApiJsiSignature signature, Function function) { + if (selectorName.empty()) { + return; + } + auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + std::move(signature), std::move(function)); + std::string encoding = objcMethodSignatureForJsiSignature(callback->signature()); + class_replaceMethod(nativeClass, sel_registerName(selectorName.c_str()), + reinterpret_cast(callback->functionPointer()), + encoding.c_str()); +} + +bool addRuntimeProtocolOverrideForName( + Runtime& runtime, const std::shared_ptr& bridge, + Class nativeClass, const std::vector& protocols, + const std::string& propertyName, Function function) { + std::unordered_set visited; + std::function visit = [&](Protocol* protocol) -> bool { + if (protocol == nullptr || !visited.insert(protocol).second) { + return false; + } + + Protocol** inherited = protocol_copyProtocolList(protocol, nullptr); + if (inherited != nullptr) { + unsigned int inheritedCount = 0; + free(inherited); + inherited = protocol_copyProtocolList(protocol, &inheritedCount); + for (unsigned int i = 0; i < inheritedCount; i++) { + if (visit(inherited[i])) { + free(inherited); + return true; + } + } + free(inherited); + } + + for (BOOL required : {YES, NO}) { + unsigned int count = 0; + objc_method_description* descriptions = + protocol_copyMethodDescriptionList(protocol, required, YES, &count); + for (unsigned int i = 0; i < count; i++) { + SEL selector = descriptions[i].name; + const char* selectorName = + selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName == nullptr || + jsifySelector(selectorName) != propertyName) { + continue; + } + auto signature = runtimeProtocolMethodSignature(descriptions[i].types); + if (signature) { + addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + std::move(*signature), std::move(function)); + free(descriptions); + return true; + } + } + free(descriptions); + } + return false; + }; + + for (Protocol* protocol : protocols) { + if (visit(protocol)) { + return true; + } + } + return false; +} + +Object getOwnPropertyDescriptor(Runtime& runtime, const Object& object, + const std::string& name) { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function getOwnPropertyDescriptor = + objectCtor.getPropertyAsFunction(runtime, "getOwnPropertyDescriptor"); + Value args[] = {Value(runtime, object), makeString(runtime, name)}; + Value descriptorValue = + getOwnPropertyDescriptor.call(runtime, static_cast(args), + static_cast(2)); + return descriptorValue.isObject() ? descriptorValue.asObject(runtime) + : Object(runtime); +} + +Value extendNativeApiJsiClass( + Runtime& runtime, const std::shared_ptr& bridge, + const Value* args, size_t count) { + if (count < 2 || !args[0].isObject() || !args[1].isObject()) { + throw facebook::jsi::JSError( + runtime, "extendClass expects a native class and method object."); + } + + Class baseClass = classFromJsiValue(runtime, args[0]); + if (baseClass == Nil) { + throw facebook::jsi::JSError( + runtime, "extendClass can only extend native class constructors."); + } + if (class_conformsToProtocol(baseClass, + @protocol(NativeApiJsiClassBuilderProtocol))) { + throw facebook::jsi::JSError(runtime, + "Cannot extend an already extended class."); + } + + Object methods = args[1].asObject(runtime); + Object options = count >= 3 && args[2].isObject() + ? args[2].asObject(runtime) + : Object(runtime); + std::string requestedName = readOptionalStringProperty(runtime, options, "name"); + if (requestedName.empty()) { + const char* baseName = class_getName(baseClass); + requestedName = std::string(baseName != nullptr ? baseName : "NSObject") + + "_Extended_" + std::to_string(rand()); + } + + std::string className = nextAvailableJsiClassName(requestedName); + Class nativeClass = objc_allocateClassPair(baseClass, className.c_str(), 0); + if (nativeClass == Nil) { + throw facebook::jsi::JSError(runtime, "Failed to allocate Objective-C class."); + } + + class_addProtocol(nativeClass, @protocol(NativeApiJsiClassBuilderProtocol)); + rememberNativeApiJsiClassBuilder(runtime, bridge, nativeClass); + + NativeApiSymbol baseSymbol = runtimeSymbolForClass(bridge, baseClass); + std::vector extensionMembers = + bridge->membersForClass(baseSymbol); + std::vector optionProtocols; + Value protocolsValue = getObjectPropertyOrUndefined(runtime, options, "protocols"); + if (protocolsValue.isObject() && + protocolsValue.asObject(runtime).isArray(runtime)) { + Array protocols = protocolsValue.asObject(runtime).getArray(runtime); + for (size_t i = 0; i < protocols.size(runtime); i++) { + Value protocolValue = protocols.getValueAtIndex(runtime, i); + Protocol* protocol = protocolFromJsiValue(runtime, protocolValue); + std::optional protocolSymbol = + protocolSymbolFromJsiValue(runtime, bridge, protocolValue); + if (protocol != nullptr) { + optionProtocols.push_back(protocol); + class_addProtocol(nativeClass, protocol); + if (!protocolSymbol) { + if (const NativeApiSymbol* runtimeSymbol = + bridge->findProtocolForRuntimePointer(protocol)) { + protocolSymbol = *runtimeSymbol; + } + } + } + if (protocolSymbol) { + const auto& protocolMembers = bridge->membersForProtocol(*protocolSymbol); + extensionMembers.insert(extensionMembers.begin(), + protocolMembers.begin(), + protocolMembers.end()); + } + } + } + const auto& members = extensionMembers; + Array propertyNames = methods.getPropertyNames(runtime); + for (size_t i = 0; i < propertyNames.size(runtime); i++) { + Value propertyNameValue = propertyNames.getValueAtIndex(runtime, i); + if (!propertyNameValue.isString()) { + continue; + } + + std::string propertyName = propertyNameValue.asString(runtime).utf8(runtime); + Object descriptor = getOwnPropertyDescriptor(runtime, methods, propertyName); + + Value value = descriptor.getProperty(runtime, "value"); + if (value.isObject() && value.asObject(runtime).isFunction(runtime)) { + auto overrides = methodOverridesForName(members, propertyName); + bool addedOverride = false; + for (const auto& member : overrides) { + if (member.selectorName.empty() || + member.signatureOffset == MD_SECTION_OFFSET_NULL || + member.signatureOffset == 0) { + continue; + } + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, member.selectorName, + member.signatureOffset, + (member.flags & metagen::mdMemberReturnOwned) != 0, + value.asObject(runtime).asFunction(runtime)); + addedOverride = true; + } + if (!addedOverride) { + bool addedRuntimeProtocolOverride = addRuntimeProtocolOverrideForName( + runtime, bridge, nativeClass, optionProtocols, propertyName, + value.asObject(runtime).asFunction(runtime)); + if (!addedRuntimeProtocolOverride) { + if (auto known = knownNativeApiJsiExposedMethod(propertyName)) { + addJsiExposedMethod(runtime, bridge, nativeClass, + known->selectorName, + std::move(known->signature), + value.asObject(runtime).asFunction(runtime)); + } + } + } + } + + const NativeApiMember* propertyMember = + propertyOverrideForName(members, propertyName); + + Value getter = descriptor.getProperty(runtime, "get"); + if (propertyMember != nullptr && getter.isObject() && + getter.asObject(runtime).isFunction(runtime)) { + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, + propertyMember->selectorName, propertyMember->signatureOffset, + (propertyMember->flags & metagen::mdMemberReturnOwned) != 0, + getter.asObject(runtime).asFunction(runtime)); + } else if (propertyMember == nullptr && getter.isObject() && + getter.asObject(runtime).isFunction(runtime)) { + auto overrides = methodOverridesForName(members, propertyName); + for (const auto& member : overrides) { + if (selectorArgumentCount(member.selectorName) != 0) { + continue; + } + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, member.selectorName, + member.signatureOffset, + (member.flags & metagen::mdMemberReturnOwned) != 0, + getter.asObject(runtime).asFunction(runtime)); + } + } + + Value setter = descriptor.getProperty(runtime, "set"); + if (propertyMember != nullptr && + setter.isObject() && setter.asObject(runtime).isFunction(runtime) && + !propertyMember->setterSelectorName.empty()) { + addJsiOverrideMethod(runtime, bridge, nativeClass, baseClass, + propertyMember->setterSelectorName, + propertyMember->setterSignatureOffset, false, + setter.asObject(runtime).asFunction(runtime)); + } + } + + Value exposedMethodsValue = + getObjectPropertyOrUndefined(runtime, options, "exposedMethods"); + if (!exposedMethodsValue.isObject()) { + exposedMethodsValue = + getObjectPropertyOrUndefined(runtime, methods, "ObjCExposedMethods"); + } + if (exposedMethodsValue.isObject()) { + Object exposedMethods = exposedMethodsValue.asObject(runtime); + Array exposedNames = exposedMethods.getPropertyNames(runtime); + for (size_t i = 0; i < exposedNames.size(runtime); i++) { + Value selectorValue = exposedNames.getValueAtIndex(runtime, i); + if (!selectorValue.isString()) { + continue; + } + std::string selectorName = selectorValue.asString(runtime).utf8(runtime); + Value descriptorValue = + getObjectPropertyOrUndefined(runtime, exposedMethods, selectorName); + if (!descriptorValue.isObject()) { + continue; + } + auto function = functionForSelector(runtime, methods, selectorName); + if (!function) { + continue; + } + auto signature = exposedMethodSignature( + runtime, bridge, selectorName, descriptorValue.asObject(runtime)); + if (signature) { + rememberNativeApiJsiKnownExposedMethod(selectorName, *signature); + addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + std::move(*signature), std::move(*function)); + } + } + } + + Value hasIteratorValue = + getObjectPropertyOrUndefined(runtime, options, "__hasIterator"); + if (hasIteratorValue.isBool() && hasIteratorValue.getBool()) { + class_addProtocol(nativeClass, @protocol(NSFastEnumeration)); + if (const char* encoding = nativeApiJsiFastEnumerationEncoding()) { + class_replaceMethod( + nativeClass, + @selector(countByEnumeratingWithState:objects:count:), + reinterpret_cast(nativeApiJsiSymbolIteratorCountByEnumerating), + encoding); + } + } + + objc_registerClassPair(nativeClass); + + NativeApiSymbol newSymbol = baseSymbol; + newSymbol.name = className; + newSymbol.runtimeName = className; + newSymbol.superclassOffset = baseSymbol.offset; + return makeNativeClassValue(runtime, bridge, std::move(newSymbol)); + } + +Value invokeNativeApiJsiBaseMethod( + Runtime& runtime, const std::shared_ptr& bridge, + const Value* args, size_t count) { + if (count < 3 || !args[0].isObject() || !args[1].isObject() || + !args[2].isString()) { + throw facebook::jsi::JSError( + runtime, "__invokeBase expects base class, receiver, and member name."); + } + + Class baseClass = classFromJsiValue(runtime, args[0]); + if (baseClass == Nil) { + throw facebook::jsi::JSError(runtime, "__invokeBase base class is invalid."); + } + + Object receiverObject = args[1].asObject(runtime); + if (!receiverObject.isHostObject(runtime)) { + throw facebook::jsi::JSError(runtime, "__invokeBase receiver is not native."); + } + + id receiver = + receiverObject.getHostObject(runtime)->object(); + std::string memberName = args[2].asString(runtime).utf8(runtime); + size_t actualArgc = count - 3; + + NativeApiSymbol baseSymbol = runtimeSymbolForClass(bridge, baseClass); + const auto& members = bridge->membersForClass(baseSymbol); + const NativeApiMember* member = + selectMethodMember(members, memberName, false, actualArgc); + if (member == nullptr) { + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, memberName, false)) { + if (actualArgc == 0) { + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + return callObjCSelector(runtime, bridge, receiver, false, + propertyMember->selectorName, propertyMember, + nullptr, 0, dispatchClass); + } + if (actualArgc == 1 && !propertyMember->setterSelectorName.empty() && + !propertyMember->readonly) { + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, false, + setterMember.selectorName, &setterMember, + args + 3, actualArgc, dispatchClass); + } + } + } + if (member == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C base selector is not available: " + memberName); + } + + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + return callObjCSelector(runtime, bridge, receiver, false, member->selectorName, + member, args + 3, actualArgc, dispatchClass); +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiConversion.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiConversion.inc new file mode 100644 index 00000000..be82ad46 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiConversion.inc @@ -0,0 +1,2067 @@ +std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, + const char* name); +void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object); + +id objectFromJsiValue(Runtime& runtime, + const std::shared_ptr& bridge, + 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] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding] + : [[NSString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding]; + 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 (Class cls = nativeClassFromJsiObject(runtime, object)) { + return static_cast(cls); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime) + ->nativeProtocol()); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return static_cast(symbolPointer); + } + 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()); + } + + Value getTimeValue = object.getProperty(runtime, "getTime"); + Value toISOStringValue = object.getProperty(runtime, "toISOString"); + if (getTimeValue.isObject() && + getTimeValue.asObject(runtime).isFunction(runtime) && + toISOStringValue.isObject() && + toISOStringValue.asObject(runtime).isFunction(runtime)) { + Value millisValue = getTimeValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (millisValue.isNumber()) { + NSDate* date = [NSDate dateWithTimeIntervalSince1970:millisValue.getNumber() / 1000.0]; + bridge->rememberRoundTripValue(runtime, date, value); + return date; + } + } + + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitiveValue = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitiveValue.isString() || primitiveValue.isBool() || + primitiveValue.isNumber()) { + return objectFromJsiValue(runtime, bridge, primitiveValue, frame, + mutableString); + } + } + + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + NSData* data = [NSData dataWithBytes:bytes length:byteLength]; + bridge->rememberRoundTripValue(runtime, data, value); + return data; + } + + 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, bridge, + array.getValueAtIndex(runtime, i), + frame, false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + bridge->rememberRoundTripValue(runtime, nativeArray, value); + return nativeArray; + } + + Value lengthValue = object.getProperty(runtime, "length"); + if (lengthValue.isNumber() && std::isfinite(lengthValue.getNumber()) && + lengthValue.getNumber() >= 0) { + size_t length = static_cast(std::floor(lengthValue.getNumber())); + NSMutableArray* nativeArray = [NSMutableArray arrayWithCapacity:length]; + for (size_t i = 0; i < length; i++) { + std::string key = std::to_string(i); + id element = objectFromJsiValue( + runtime, bridge, object.getProperty(runtime, key.c_str()), frame, + false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + bridge->rememberRoundTripValue(runtime, nativeArray, value); + return nativeArray; + } + + Value entriesValue = object.getProperty(runtime, "entries"); + Value sizeValue = object.getProperty(runtime, "size"); + Value getValue = object.getProperty(runtime, "get"); + if (entriesValue.isObject() && + entriesValue.asObject(runtime).isFunction(runtime) && + sizeValue.isNumber() && getValue.isObject() && + getValue.asObject(runtime).isFunction(runtime)) { + Object arrayCtor = runtime.global().getPropertyAsObject(runtime, "Array"); + Function arrayFrom = arrayCtor.getPropertyAsFunction(runtime, "from"); + Value iterator = entriesValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + Value pairsValue = arrayFrom.call(runtime, iterator); + if (pairsValue.isObject() && pairsValue.asObject(runtime).isArray(runtime)) { + Array pairs = pairsValue.asObject(runtime).getArray(runtime); + NSMutableDictionary* nativeMap = + [NSMutableDictionary dictionaryWithCapacity:pairs.size(runtime)]; + for (size_t i = 0; i < pairs.size(runtime); i++) { + Value pairValue = pairs.getValueAtIndex(runtime, i); + if (!pairValue.isObject() || + !pairValue.asObject(runtime).isArray(runtime)) { + continue; + } + Array pair = pairValue.asObject(runtime).getArray(runtime); + if (pair.size(runtime) < 2) { + continue; + } + id key = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 0), + frame, false); + id nativeValue = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 1), + frame, false); + if (key != nil) { + [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:key]; + } + } + bridge->rememberRoundTripValue(runtime, nativeMap, value); + return nativeMap; + } + } + + 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, bridge, propertyValue, frame, false); + NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; + if (nativeKey != nil) { + [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:nativeKey]; + } + } + bridge->rememberRoundTripValue(runtime, dictionary, value); + return dictionary; + } + throw facebook::jsi::JSError(runtime, + "Value cannot be converted to Objective-C object."); +} + +std::string utf8StringFromNSString(NSString* string) { + if (string == nil) { + return ""; + } + NSUInteger length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + std::string result(length, '\0'); + NSUInteger usedLength = 0; + NSRange remainingRange = NSMakeRange(0, 0); + BOOL ok = [string getBytes:result.data() + maxLength:length + usedLength:&usedLength + encoding:NSUTF8StringEncoding + options:0 + range:NSMakeRange(0, string.length) + remainingRange:&remainingRange]; + if (!ok) { + return string.UTF8String ?: ""; + } + result.resize(usedLength); + return result; +} + +bool readNativePointerProperty(Runtime& runtime, const Object& object, + void** pointer) { + if (pointer == nullptr) { + return false; + } + + Value nativePointerObjectValue = + object.getProperty(runtime, "__nativeApiPointerObject"); + if (nativePointerObjectValue.isObject()) { + Object nativePointerObject = nativePointerObjectValue.asObject(runtime); + if (nativePointerObject.isHostObject( + runtime)) { + *pointer = nativePointerObject + .getHostObject(runtime) + ->pointer(); + return true; + } + } + + 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; +} + +std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, + const char* name) { + if (name == nullptr || !object.hasProperty(runtime, name)) { + return ""; + } + Value value = object.getProperty(runtime, name); + return value.isString() ? value.asString(runtime).utf8(runtime) : ""; +} + +void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object) { + std::string kind = stringPropertyOrEmpty(runtime, object, "kind"); + if (kind != "class" && kind != "protocol") { + return nullptr; + } + + std::string runtimeName = stringPropertyOrEmpty(runtime, object, "runtimeName"); + if (runtimeName.empty()) { + runtimeName = stringPropertyOrEmpty(runtime, object, "name"); + } + if (runtimeName.empty()) { + return nullptr; + } + + if (kind == "class") { + return objc_lookUpClass(runtimeName.c_str()); + } + return lookupProtocolByNativeName(runtimeName); +} + +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 (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime) + ->nativeProtocol(); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return symbolPointer; + } + if (object.isHostObject(runtime)) { + auto reference = + object.getHostObject(runtime); + if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); + } + return reference->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()); + return string; + } + throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); +} + +bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) { + if (pointer == nullptr || !value.isObject()) { + return false; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->pointer(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->object(); + return true; + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + *pointer = cls; + return true; + } + if (object.isHostObject(runtime)) { + *pointer = + object.getHostObject(runtime)->nativeProtocol(); + return true; + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + *pointer = symbolPointer; + return true; + } + return readNativePointerProperty(runtime, object, pointer); +} + +template +void writeNumericArgument(Runtime& runtime, const Value& value, void* target, + const char* typeName) { + const Value* numericValue = &value; + Value primitiveValue = Value::undefined(); + if (value.isObject()) { + Object object = value.asObject(runtime); + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + primitiveValue = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + numericValue = &primitiveValue; + } + } + + if (!numericValue->isNumber() && !numericValue->isBool()) { + throw facebook::jsi::JSError(runtime, + std::string("Expected numeric ") + typeName + + " argument."); + } + double number = numericValue->isBool() ? (numericValue->getBool() ? 1.0 : 0.0) + : numericValue->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); + +Class classFromJsiValue(Runtime& runtime, const Value& value); +Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value); + +std::optional parseArrayIndexProperty(const std::string& property) { + if (property.empty()) { + return std::nullopt; + } + size_t index = 0; + for (char c : property) { + if (!std::isdigit(static_cast(c))) { + return std::nullopt; + } + size_t digit = static_cast(c - '0'); + if (index > (std::numeric_limits::max() - digit) / 10) { + return std::nullopt; + } + index = (index * 10) + digit; + } + return index; +} + +size_t referenceElementStride(const NativeApiJsiType& type) { + return std::max(nativeSizeForType(type), 1); +} + +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 convertJsiFfiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, const Value& value, + void* target, NativeApiJsiArgumentFrame& frame) { + if (type.kind != metagen::mdTypeArray) { + convertJsiArgument(runtime, bridge, type, value, target, frame); + return; + } + + void* pointer = nullptr; + if (!value.isNull() && !value.isUndefined()) { + if (value.isObject()) { + Object object = value.asObject(runtime); + if (!readPointerLikeValue(runtime, value, &pointer)) { + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + pointer = const_cast(bytes); + } + } + } + + if (pointer == nullptr) { + size_t byteLength = nativeSizeForType(type); + void* buffer = frame.addBuffer(byteLength); + convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, + frame); + pointer = buffer; + } + } + + *static_cast(target) = pointer; +} + +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.isObject()) { + Object object = value.asObject(runtime); + void* pointer = nullptr; + if (readPointerLikeValue(runtime, value, &pointer)) { + *static_cast(target) = static_cast(pointer); + break; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + *static_cast(target) = + reinterpret_cast(const_cast(bytes)); + break; + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isString()) { + std::string utf8 = primitive.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + *static_cast(target) = string; + 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()); + *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, bridge, value, frame, + type.kind == metagen::mdTypeNSMutableStringObject); + *static_cast(target) = object; + break; + } + case metagen::mdTypeClass: { + *static_cast(target) = classFromJsiValue(runtime, value); + 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 (reference->data() == nullptr && type.elementType != nullptr) { + reference->ensureStorage(runtime, *type.elementType, frame); + } else if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); + } + *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(); + bridge->rememberRoundTripValue(runtime, pointer, value); + try { + object.setProperty(runtime, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); + 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 signedInteger64ToJsiValue(runtime, *static_cast(value)); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return unsignedInteger64ToJsiValue(runtime, + *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); + if (string == nullptr) { + return Value::null(); + } + NativeApiJsiType cStringType = + primitiveInteropType(metagen::mdTypeChar); + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, cStringType, const_cast(string), false)); + } + 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 makeNativeClassValue(runtime, bridge, std::move(symbol)); + } + 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:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + return Value::null(); + } + if ([object respondsToSelector:@selector(UTF8String)]) { + bool untypedObject = type.kind == metagen::mdTypeAnyObject; + bool explicitNSString = type.kind == metagen::mdTypeNSStringObject; + if (untypedObject || explicitNSString) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; + } + return makeString(runtime, utf8); + } + } + if ([object isKindOfClass:[NSNumber class]] && + ![object isKindOfClass:[NSDecimalNumber class]]) { + NSNumber* number = static_cast(object); + const char* objCType = [number objCType]; + bool isBool = CFGetTypeID((__bridge CFTypeRef)number) == + CFBooleanGetTypeID() || + (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; + } + Value roundTrip = bridge->findRoundTripValue(runtime, object); + if (!roundTrip.isUndefined()) { + if (type.returnOwned) { + [object release]; + } + return roundTrip; + } + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } + return makeNativeObjectValue(runtime, bridge, object, type.returnOwned); + } + 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(); + } + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer(pointer)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer(pointer)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } + if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, *type.elementType, pointer, false)); + } + return createPointer(runtime, bridge, pointer); + } + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + if (!roundTrip.isUndefined()) { + return roundTrip; + } + 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."); + } +} + +void NativeApiReferenceHostObject::ensureStorage( + Runtime& runtime, NativeApiJsiType type, NativeApiJsiArgumentFrame& frame, + size_t elements) { + size_t elementCount = std::max(elements, 1); + NativeApiJsiType storageType = std::move(type); + size_t stride = std::max(nativeSizeForType(storageType), 1); + size_t required = std::max(stride * elementCount, sizeof(void*)); + type_ = std::move(storageType); + + if (data_ == nullptr) { + data_ = calloc(1, required); + ownsData_ = true; + byteLength_ = required; + } else if (ownsData_ && byteLength_ < required) { + void* expanded = realloc(data_, required); + if (expanded == nullptr) { + throw std::bad_alloc(); + } + std::memset(static_cast(expanded) + byteLength_, 0, + required - byteLength_); + data_ = expanded; + byteLength_ = required; + } + + if (data_ != nullptr && pendingValue_ != nullptr) { + Value pending(runtime, *pendingValue_); + convertJsiArgument(runtime, bridge_, type_, pending, data_, frame); + pendingValue_.reset(); + } +} + +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) { + if (pendingValue_ != nullptr) { + return Value(runtime, *pendingValue_); + } + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge_, type_, data_); + } + if (auto index = parseArrayIndexProperty(property)) { + if (data_ == nullptr) { + return Value::undefined(); + } + void* slot = static_cast(data_) + + (*index * referenceElementStride(type_)); + return convertNativeReturnValue(runtime, bridge_, type_, slot); + } + 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, + ""); + }); + } + return Value::undefined(); +} + +void NativeApiReferenceHostObject::set(Runtime& runtime, + const PropNameID& name, + const Value& value) { + std::string property = name.utf8(runtime); + auto index = parseArrayIndexProperty(property); + if (property != "value" && !index) { + return; + } + size_t slotIndex = index.value_or(0); + NativeApiJsiArgumentFrame frame(1); + if (data_ == nullptr) { + if (slotIndex == 0) { + pendingValue_ = std::make_shared(runtime, value); + return; + } + ensureStorage(runtime, type_, frame, slotIndex + 1); + } + pendingValue_.reset(); + void* slot = static_cast(data_) + + (slotIndex * referenceElementStride(type_)); + convertJsiArgument(runtime, bridge_, type_, value, slot, 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; + if (field.type.kind == metagen::mdTypeStruct && + field.type.aggregateInfo != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge_, field.type.aggregateInfo, fieldData, false, + ownedData_, backingValue_)); + } + 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 primitiveInteropTypeFromCode(int32_t code) { + MDTypeKind kind = static_cast(code); + 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::mdTypeProtocolObject: + 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; + } +} + +std::optional interopTypeFromValue( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value) { + if (value.isNumber()) { + return primitiveInteropTypeFromCode(static_cast(value.getNumber())); + } + + if (!value.isObject()) { + return std::nullopt; + } + + Object object = value.asObject(runtime); + Value typeCodeValue = object.getProperty(runtime, "__nativeApiTypeCode"); + if (typeCodeValue.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(typeCodeValue.getNumber())); + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = + valueOfValue.asObject(runtime).asFunction(runtime).callWithThis( + runtime, object, nullptr, 0); + if (primitive.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(primitive.getNumber())); + } + } + + Class descriptorClass = nativeClassFromJsiObject(runtime, object); + if (descriptorClass == Nil && + stringPropertyOrEmpty(runtime, object, "kind") == "class") { + descriptorClass = + static_cast(pointerFromSymbolLikeObject(runtime, object)); + } + if (descriptorClass != Nil) { + return nativeObjectReturnTypeForClass(descriptorClass); + } + + 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"); + if (kindValue.isString()) { + std::string kindName = kindValue.asString(runtime).utf8(runtime); + if (kindName == "pointer") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "reference") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "class") { + return nativeObjectReturnType(metagen::mdTypeInstanceObject); + } + if (kindName == "selector") { + return primitiveInteropType(metagen::mdTypeSelector); + } + if (kindName == "protocol") { + return primitiveInteropType(metagen::mdTypeProtocolObject); + } + if (kindName == "block") { + return primitiveInteropType(metagen::mdTypeBlock); + } + if (kindName == "functionPointer") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } + if (kindName == "functionReference") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } + } + 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; + + if (count > 0 && args[0].isObject()) { + void* pointer = nullptr; + if (readPointerLikeValue(runtime, args[0], &pointer) && pointer != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, info, pointer, false, nullptr, + std::make_shared(runtime, args[0]))); + } + } + + 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, "runtimeName", makeString(runtime, symbol.runtimeName)); + constructor.setProperty(runtime, "metadataOffset", static_cast(symbol.offset)); + constructor.setProperty(runtime, "sizeof", + static_cast(info != nullptr ? info->size : 0)); + constructor.setProperty( + runtime, "equals", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "equals"), 2, + [bridge, info](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (info == nullptr || count < 2) { + return false; + } + + 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 left(info->size, 0); + std::vector right(info->size, 0); + try { + NativeApiJsiArgumentFrame leftFrame(1); + convertAggregateArgument(runtime, bridge, type, args[0], + left.data(), leftFrame); + NativeApiJsiArgumentFrame rightFrame(1); + convertAggregateArgument(runtime, bridge, type, args[1], + right.data(), rightFrame); + } catch (const std::exception&) { + return false; + } + + return std::memcmp(left.data(), right.data(), 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) || + nativeClassFromJsiObject(runtime, object) != Nil) { + return sizeof(void*); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + 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, + const std::shared_ptr& bridge, + void* pointer, bool adopted) { + if (!adopted && bridge != nullptr) { + Value cached = bridge->findPointerValue(runtime, pointer); + if (cached.isObject()) { + return cached.asObject(runtime); + } + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, pointer, "pointer", + adopted)); + if (!adopted && bridge != nullptr) { + bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); + } + return result; +} + +void installInteropHasInstance(Runtime& runtime, Function& constructor, + const char* kind) { + Value symbolCtorValue = runtime.global().getProperty(runtime, "Symbol"); + if (!symbolCtorValue.isObject()) { + return; + } + + Object symbolCtor = symbolCtorValue.asObject(runtime); + Value hasInstanceValue = symbolCtor.getProperty(runtime, "hasInstance"); + if (!hasInstanceValue.isSymbol()) { + return; + } + + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty( + runtime, "value", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.hasInstance"), 1, + [kind = std::string(kind)](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + return false; + } + + Object object = args[0].asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + return kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == kind; + })); + defineProperty.call(runtime, constructor, hasInstanceValue, descriptor); + } catch (const std::exception&) { + } +} + +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 (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; + } + if (stringPropertyOrEmpty(runtime, object, "kind") == "class") { + if (void* pointer = pointerFromSymbolLikeObject(runtime, object)) { + return static_cast(pointer); + } + } + 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 (stringPropertyOrEmpty(runtime, object, "kind") == "protocol") { + return static_cast(pointerFromSymbolLikeObject(runtime, object)); + } + 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) { + Object type(runtime); + double code = static_cast(kind); + type.setProperty(runtime, "__nativeApiTypeCode", code); + type.setProperty( + runtime, "valueOf", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "valueOf"), 0, + [code](Runtime&, const Value&, const Value*, size_t) -> Value { + return code; + })); + type.setProperty( + runtime, "toString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [code](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char text[32] = {}; + snprintf(text, sizeof(text), "%d", static_cast(code)); + return makeString(runtime, text); + })); + types.setProperty(runtime, name, type); + }; + 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("protocol", metagen::mdTypeProtocolObject); + 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); + + Function pointerConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Pointer"), 1, + [bridge](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()) { + auto readAddress = [&](const Value& value, + uintptr_t* address) -> bool { + auto readAddressFromString = [&](const Value& source) -> bool { + try { + Value stringCtorValue = + runtime.global().getProperty(runtime, "String"); + if (!stringCtorValue.isObject() || + !stringCtorValue.asObject(runtime).isFunction(runtime)) { + return false; + } + Value stringValue = + stringCtorValue.asObject(runtime).asFunction(runtime) + .call(runtime, source); + if (!stringValue.isString()) { + return false; + } + return parseIntegerTextToUintptr( + stringValue.asString(runtime).utf8(runtime), address); + } catch (const std::exception&) { + return false; + } + }; + + if (value.isNumber()) { + double number = value.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (value.isBigInt()) { + if (readAddressFromString(value)) { + return true; + } + BigInt bigint = value.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + if (value.isObject()) { + Object object = value.asObject(runtime); + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isNumber()) { + double number = primitive.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (primitive.isBigInt()) { + if (readAddressFromString(primitive)) { + return true; + } + BigInt bigint = primitive.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + } + return readAddressFromString(value); + } + return false; + }; + + uintptr_t address = 0; + if (!readAddress(args[0], &address)) { + throw facebook::jsi::JSError(runtime, + "Pointer expects a numeric address."); + } + pointer = reinterpret_cast(address); + } + return createPointer(runtime, bridge, pointer); + }); + Object pointerPrototype(runtime); + pointerPrototype.setProperty(runtime, "constructor", pointerConstructor); + pointerConstructor.setProperty(runtime, "prototype", pointerPrototype); + installInteropHasInstance(runtime, pointerConstructor, "pointer"); + pointerConstructor.setProperty(runtime, "kind", makeString(runtime, "pointer")); + pointerConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Pointer", pointerConstructor); + + Function functionReferenceConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "FunctionReference"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Object object = args[0].asObject(runtime); + if (!object.isFunction(runtime)) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Function function = object.asFunction(runtime); + function.setProperty(runtime, "kind", + makeString(runtime, "functionReference")); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + }); + Object functionReferencePrototype(runtime); + functionReferencePrototype.setProperty(runtime, "constructor", + functionReferenceConstructor); + functionReferenceConstructor.setProperty(runtime, "prototype", + functionReferencePrototype); + installInteropHasInstance(runtime, functionReferenceConstructor, + "functionReference"); + functionReferenceConstructor.setProperty(runtime, "kind", + makeString(runtime, + "functionReference")); + functionReferenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "FunctionReference", + functionReferenceConstructor); + + Function referenceConstructor = 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 firstArgumentIsType = false; + if (count > 1) { + firstArgumentIsType = true; + } else if (count == 1 && args[0].isObject()) { + Object object = args[0].asObject(runtime); + Value typeCodeValue = + object.getProperty(runtime, "__nativeApiTypeCode"); + Value kindValue = object.getProperty(runtime, "kind"); + firstArgumentIsType = + typeCodeValue.isNumber() || object.isFunction(runtime) || + nativeClassFromJsiObject(runtime, object) != Nil || + (kindValue.isString() && + (kindValue.asString(runtime).utf8(runtime) == "class" || + kindValue.asString(runtime).utf8(runtime) == "protocol")); + } + std::optional requestedType = + firstArgumentIsType + ? interopTypeFromValue(runtime, bridge, args[0]) + : std::nullopt; + bool hasType = firstArgumentIsType && requestedType.has_value(); + if (hasType) { + type = *requestedType; + } + + void* data = nullptr; + bool ownsData = false; + size_t byteLength = 0; + std::shared_ptr pendingValue; + if (hasType) { + bool usesExternalStorage = false; + Value valueToStore = Value::undefined(); + if (count > 1) { + valueToStore = Value(runtime, args[1]); + if (args[1].isObject()) { + Object object = args[1].asObject(runtime); + if (object.isHostObject(runtime)) { + data = object + .getHostObject( + runtime) + ->pointer(); + usesExternalStorage = true; + } else if (object.isHostObject( + runtime)) { + auto reference = + object.getHostObject( + runtime); + data = reference->data(); + if (data != nullptr) { + usesExternalStorage = true; + } else { + valueToStore = object.getProperty(runtime, "value"); + } + } else if (type.kind == metagen::mdTypeStruct && + object.isHostObject< + NativeApiStructObjectHostObject>(runtime)) { + data = object + .getHostObject< + NativeApiStructObjectHostObject>(runtime) + ->data(); + usesExternalStorage = true; + } else if (type.kind == metagen::mdTypePointer || + type.kind == metagen::mdTypeOpaquePointer || + type.kind == metagen::mdTypeBlock || + type.kind == metagen::mdTypeFunctionPointer) { + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, + &nativePointer)) { + data = nativePointer; + usesExternalStorage = true; + } + } + } + } + if (!usesExternalStorage) { + byteLength = std::max(nativeSizeForType(type), + sizeof(void*)); + data = calloc(1, byteLength); + if (data == nullptr) { + throw std::bad_alloc(); + } + ownsData = true; + if (count > 1) { + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, valueToStore, data, + frame); + } + } + } else if (count > 0) { + pendingValue = std::make_shared(runtime, args[0]); + } + + if (ownsData && data == nullptr) { + throw std::bad_alloc(); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, type, data, ownsData, byteLength, + std::move(pendingValue))); + }); + Object referencePrototype(runtime); + referencePrototype.setProperty(runtime, "constructor", referenceConstructor); + referenceConstructor.setProperty(runtime, "prototype", referencePrototype); + installInteropHasInstance(runtime, referenceConstructor, "reference"); + referenceConstructor.setProperty(runtime, "kind", + makeString(runtime, "reference")); + referenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Reference", referenceConstructor); + + 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, + [bridge](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, bridge, 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, bridge, 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)) { + void* data = + object.getHostObject(runtime)->data(); + if (data == nullptr) { + throw facebook::jsi::JSError( + runtime, "Cannot get handle of empty Reference."); + } + return createPointer(runtime, bridge, data); + } + if (object.isHostObject(runtime)) { + auto structObject = + object.getHostObject(runtime); + if (structObject->backingValue() != nullptr) { + return Value(runtime, *structObject->backingValue()); + } + return createPointer(runtime, bridge, structObject->data()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, bridge, + object.getHostObject(runtime) + ->object()); + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return createPointer(runtime, bridge, cls); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, bridge, + object.getHostObject(runtime) + ->nativeProtocol()); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return createPointer(runtime, bridge, symbolPointer); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return createPointer(runtime, bridge, nativePointer); + } + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == "functionReference") { + throw facebook::jsi::JSError( + runtime, "Cannot get handle of uninitialized FunctionReference."); + } + 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, bridge, 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; +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObject.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObject.inc new file mode 100644 index 00000000..377e24e2 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObject.inc @@ -0,0 +1,513 @@ +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 == "__fastEnumeration") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__fastEnumeration"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "Fast enumeration expects a native object."); + } + id object = NativeApiObjectHostObject::nativeObjectFromValue(runtime, args[0]); + if (object == nil) { + throw facebook::jsi::JSError( + runtime, "Fast enumeration expects a native object."); + } + if (![object conformsToProtocol:@protocol(NSFastEnumeration)]) { + throw facebook::jsi::JSError( + runtime, "Object does not conform to NSFastEnumeration."); + } + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, static_cast>(object))); + }); + } + 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 makeNativeClassValue(runtime, bridge, + std::move(runtimeSymbol)); + } + + return makeNativeClassValue(runtime, bridge, *symbol); + }); + } + if (property == "__extendClass") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__extendClass"), 2, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return extendNativeApiJsiClass(runtime, bridge, args, count); + }); + } + if (property == "__invokeBase") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__invokeBase"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return invokeNativeApiJsiBaseMethod(runtime, bridge, args, count); + }); + } + if (property == "__rememberClassWrapper") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__rememberClassWrapper"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 2) { + return Value::undefined(); + } + Class cls = classFromJsiValue(runtime, args[0]); + if (cls == Nil) { + return Value::undefined(); + } + bridge->rememberClassValue(runtime, cls, args[1]); + if (count >= 3 && args[2].isObject()) { + bridge->rememberClassPrototype(runtime, cls, args[2]); + } + return Value::undefined(); + }); + } + if (property == "CC_SHA256") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "CC_SHA256"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 3 || !args[1].isNumber()) { + throw facebook::jsi::JSError( + runtime, "CC_SHA256 expects data, length, and output."); + } + void* commonCrypto = + dlopen("/usr/lib/system/libcommonCrypto.dylib", + RTLD_NOW | RTLD_LOCAL); + void* symbol = commonCrypto != nullptr + ? dlsym(commonCrypto, "CC_SHA256") + : nullptr; + if (symbol == nullptr && commonCrypto != nullptr) { + symbol = dlsym(commonCrypto, "_CC_SHA256"); + } + if (symbol == nullptr) { + throw facebook::jsi::JSError(runtime, + "CC_SHA256 is not available."); + } + NativeApiJsiArgumentFrame frame(3); + void* data = pointerFromJsiValue(runtime, args[0], frame); + void* output = pointerFromJsiValue(runtime, args[2], frame); + using CC_SHA256_Fn = unsigned char* (*)(const void*, unsigned long, + unsigned char*); + auto fn = reinterpret_cast(symbol); + unsigned char* result = + fn(data, static_cast(args[1].getNumber()), + static_cast(output)); + return createPointer(runtime, bridge, result); + }); + } + 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)); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + 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 = lookupProtocolByNativeName(protocolName); + if (protocol == nullptr) { + return Value::null(); + } + const char* runtimeName = protocol_getName(protocol); + NativeApiSymbol runtimeSymbol{ + .kind = NativeApiSymbolKind::Protocol, + .offset = MD_SECTION_OFFSET_NULL, + .name = protocolName, + .runtimeName = runtimeName != nullptr ? runtimeName : protocolName, + }; + return makeNativeProtocolValue(runtime, bridge, + std::move(runtimeSymbol)); + } + return makeNativeProtocolValue(runtime, bridge, *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 makeNativeClassValue(runtime, bridge_, *classSymbol); + } + + if (const NativeApiSymbol* functionSymbol = bridge_->findFunction(property)) { + auto bridge = bridge_; + Function function = 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); + }); + function.setProperty(runtime, "kind", makeString(runtime, "function")); + function.setProperty(runtime, "nativeName", + makeString(runtime, functionSymbol->name)); + function.setProperty(runtime, "metadataOffset", + static_cast(functionSymbol->offset)); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + } + + 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 makeNativeProtocolValue(runtime, bridge_, *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, "__extendClass"); + addPropertyName(runtime, names, "__invokeBase"); + addPropertyName(runtime, names, "__rememberClassWrapper"); + 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_; +}; diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObjects.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObjects.inc new file mode 100644 index 00000000..cf362a61 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiHostObjects.inc @@ -0,0 +1,1743 @@ +class NativeApiPointerHostObject final + : public HostObject, + public std::enable_shared_from_this { + public: + NativeApiPointerHostObject(std::shared_ptr bridge, + void* pointer, std::string kind = "pointer", + bool adopted = false) + : bridge_(std::move(bridge)), + pointer_(pointer), + kind_(std::move(kind)), + adopted_(adopted) {} + + ~NativeApiPointerHostObject() override { + if (adopted_ && pointer_ != nullptr) { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + free(pointer_); + pointer_ = nullptr; + } + } + + void* pointer() const { return pointer_; } + bool adopted() const { return adopted_; } + void adopt() { adopted_ = true; } + void clearWithoutFree() { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + 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 == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->pointer_ == nullptr || self->consumed_) { + throw facebook::jsi::JSError(runtime, "Unmanaged value has already been consumed."); + } + id object = static_cast(self->pointer_); + self->consumed_ = true; + self->pointer_ = nullptr; + self->adopted_ = false; + return makeNativeObjectValue(runtime, self->bridge_, object, retained); + }); + } + if (property == "add" || property == "subtract") { + void* pointer = pointer_; + bool add = property == "add"; + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, 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 createPointer(runtime, bridge, 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 { + return BigInt::fromUint64( + runtime, + static_cast(reinterpret_cast(pointer))); + }); + } + 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 { + if (hex) { + char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; + snprintf(text, sizeof(text), "0x%llx", + static_cast( + reinterpret_cast(pointer))); + return makeString(runtime, text); + } else { + char text[32] = {}; + 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); + if (kind == "pointer") { + return makeString(runtime, + ""); + } + 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, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + 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: + std::shared_ptr bridge_; + void* pointer_ = nullptr; + std::string kind_; + bool adopted_ = false; + bool consumed_ = false; +}; + +class NativeApiReferenceHostObject final : public HostObject { + public: + NativeApiReferenceHostObject(std::shared_ptr bridge, + NativeApiJsiType type, void* data, bool ownsData, + size_t byteLength = 0, + std::shared_ptr pendingValue = nullptr) + : bridge_(std::move(bridge)), + type_(std::move(type)), + data_(data), + ownsData_(ownsData), + byteLength_(byteLength), + pendingValue_(std::move(pendingValue)) {} + + ~NativeApiReferenceHostObject() override { + if (ownsData_ && data_ != nullptr) { + free(data_); + data_ = nullptr; + } + } + + void* data() const { return data_; } + const NativeApiJsiType& type() const { return type_; } + void ensureStorage(Runtime& runtime, NativeApiJsiType type, + NativeApiJsiArgumentFrame& frame, size_t elements = 1); + + 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; + size_t byteLength_ = 0; + std::shared_ptr pendingValue_; +}; + +class NativeApiStructObjectHostObject final : public HostObject { + public: + NativeApiStructObjectHostObject( + std::shared_ptr bridge, + std::shared_ptr info, + const void* data = nullptr, bool ownsData = true, + std::shared_ptr> storageOwner = nullptr, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + info_(std::move(info)), + ownedData_(std::move(storageOwner)), + backingValue_(std::move(backingValue)), + ownsData_(ownsData) { + size_t size = info_ != nullptr ? info_->size : 0; + if (ownedData_ != nullptr) { + data_ = const_cast(data); + ownsData_ = false; + } else if (ownsData_) { + ownedData_ = std::make_shared>(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_; } + std::shared_ptr> storageOwner() const { + return ownedData_; + } + std::shared_ptr backingValue() const { return backingValue_; } + + 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::shared_ptr> ownedData_; + std::shared_ptr backingValue_; + void* data_ = nullptr; + bool ownsData_ = true; +}; + +class NativeApiFastEnumerationIteratorHostObject final : public HostObject { + public: + NativeApiFastEnumerationIteratorHostObject( + std::shared_ptr bridge, id collection) + : bridge_(std::move(bridge)), collection_(collection) { + [collection_ retain]; + } + + ~NativeApiFastEnumerationIteratorHostObject() override { + [collection_ release]; + collection_ = nil; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "next") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "next"), 0, + [this](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return next(runtime); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "next"); + return names; + } + + private: + Value next(Runtime& runtime) { + Object result(runtime); + if (done_ || collection_ == nil) { + result.setProperty(runtime, "done", true); + return result; + } + + if (stackIndex_ >= stackLength_) { + stackLength_ = [collection_ countByEnumeratingWithState:&state_ + objects:stack_ + count:16]; + stackIndex_ = 0; + if (stackLength_ == 0) { + done_ = true; + result.setProperty(runtime, "done", true); + return result; + } + } + + id value = state_.itemsPtr[stackIndex_++]; + NativeApiJsiType valueType = nativeObjectReturnTypeForClass(object_getClass(value)); + result.setProperty(runtime, "value", + convertNativeReturnValue(runtime, bridge_, valueType, &value)); + result.setProperty(runtime, "done", false); + return result; + } + + std::shared_ptr bridge_; + id collection_ = nil; + NSFastEnumerationState state_ = {}; + id __unsafe_unretained stack_[16] = {}; + NSUInteger stackLength_ = 0; + NSUInteger stackIndex_ = 0; + bool done_ = false; +}; + +NativeApiSymbol nativeApiSymbolForRuntimeClass( + const std::shared_ptr& bridge, Class cls) { + const char* name = cls != Nil ? class_getName(cls) : ""; + if (bridge != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimePointer(cls)) { + return *symbol; + } + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { + return *symbol; + } + if (name != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClass(name)) { + return *symbol; + } + } + } + + return NativeApiSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; +} + +class NativeApiSuperHostObject final : public HostObject { + public: + NativeApiSuperHostObject(std::shared_ptr bridge, + id receiver, Class dispatchClass) + : bridge_(std::move(bridge)), + receiver_(receiver), + dispatchClass_(dispatchClass) { + if (receiver_ != nil) { + [receiver_ retain]; + } + } + + ~NativeApiSuperHostObject() override { + if (receiver_ != nil) { + [receiver_ release]; + receiver_ = nil; + } + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "super"); + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, "[NativeApiJsiSuper]"); + }); + } + if (receiver_ == nil || dispatchClass_ == Nil) { + return Value::undefined(); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return callObjCSelector(runtime, bridge_, receiver_, false, + propertyMember->selectorName, propertyMember, + nullptr, 0, dispatchClass_); + } + + if (selectMethodMember(members, property, false, 0) != nullptr) { + auto bridge = bridge_; + id receiver = receiver_; + Class dispatchClass = dispatchClass_; + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, receiver, dispatchClass, memberName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(dispatchClass); + if (symbol == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C metadata is not available for super."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C super selector is not available: " + + memberName); + } + return callObjCSelector(runtime, bridge, receiver, false, + selected->selectorName, selected, args, + count, dispatchClass); + }); + } + } + + if (auto selectorName = + runtimeSelectorNameForProperty(dispatchClass_, false, property)) { + auto bridge = bridge_; + id receiver = receiver_; + Class dispatchClass = dispatchClass_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, receiver, dispatchClass, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, receiver, false, + selectorName, nullptr, args, count, + dispatchClass); + }); + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (receiver_ == nil || dispatchClass_ == Nil) { + throw facebook::jsi::JSError(runtime, "Cannot set property on nil super."); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly || + propertyMember->setterSelectorName.empty()) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, + setterMember.selectorName, &setterMember, args, 1, + dispatchClass_); + return; + } + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, setterSelectorName, + nullptr, args, 1, dispatchClass_); + return; + } + + throw facebook::jsi::JSError(runtime, + "No writable native super property: " + + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id receiver_ = nil; + Class dispatchClass_ = Nil; +}; + +class NativeApiObjectHostObject final + : public HostObject, + public std::enable_shared_from_this { + 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_; } + + void disownObject(id expected) { + if (object_ == expected) { + ownsObject_ = false; + object_ = nil; + } + } + + static bool isInitializerSelector(const std::string& selectorName) { + return selectorName.rfind("init", 0) == 0; + } + + static id nativeObjectFromValue(Runtime& runtime, const Value& value) { + if (!value.isObject()) { + return nil; + } + Object object = value.asObject(runtime); + if (!object.isHostObject(runtime)) { + return nil; + } + return object.getHostObject(runtime)->object(); + } + + Value callObjectSelector(Runtime& runtime, const std::string& selectorName, + const NativeApiMember* member, const Value* args, + size_t count, Class dispatchSuperClass = Nil) { + id receiver = object_; + if (receiver == nil) { + throw facebook::jsi::JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + const bool initializer = isInitializerSelector(selectorName); + std::optional classWrapper; + if (initializer) { + Value classWrapperValue = bridge_->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + classWrapper.emplace(classWrapperValue.asObject(runtime)); + } + bridge_->forgetRoundTripValue(receiver); + bridge_->forgetObjectExpandos(receiver); + } + + Value result = + callObjCSelector(runtime, bridge_, receiver, false, selectorName, member, + args, count, dispatchSuperClass); + if (initializer) { + if (nativeObjectFromValue(runtime, result) != receiver) { + disownObject(receiver); + } else if (classWrapper) { + bridge_->setObjectExpando(runtime, receiver, "__nativeApiClassWrapper", + Value(runtime, *classWrapper)); + } + } + return result; + } + + 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 == "class") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "class"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil) { + return Value::undefined(); + } + Value classWrapper = bridge->findObjectExpando( + runtime, object, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + return classWrapper; + } + NativeApiSymbol symbol = + nativeApiSymbolForRuntimeClass(bridge, object_getClass(object)); + return makeNativeClassValue(runtime, bridge, std::move(symbol)); + }); + } + if (property == "constructor") { + if (object_ == nil) { + return Value::undefined(); + } + Value classWrapper = bridge_->findObjectExpando( + runtime, object_, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + return classWrapper; + } + NativeApiSymbol symbol = + nativeApiSymbolForRuntimeClass(bridge_, object_getClass(object_)); + return makeNativeClassValue(runtime, bridge_, std::move(symbol)); + } + if (property == "superclass") { + if (object_ == nil) { + return Value::undefined(); + } + Class superclass = class_getSuperclass(object_getClass(object_)); + if (superclass == Nil) { + return Value::null(); + } + NativeApiSymbol symbol = nativeApiSymbolForRuntimeClass(bridge_, superclass); + return makeNativeClassValue(runtime, bridge_, std::move(symbol)); + } + if (property == "super") { + Class dispatchClass = + object_ != nil ? class_getSuperclass(object_getClass(object_)) : Nil; + return Object::createFromHostObject( + runtime, + std::make_shared(bridge_, object_, + dispatchClass)); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, object, weakSelf](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + if (auto self = weakSelf.lock()) { + return self->callObjectSelector(runtime, selectorName, nullptr, + args + 1, count - 1); + } + return callObjCSelector(runtime, bridge, object, false, selectorName, + nullptr, args + 1, count - 1); + }); + } + if (property == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->object_ == nil || self->consumed_) { + throw facebook::jsi::JSError(runtime, "Unmanaged value has already been consumed."); + } + + id object = self->object_; + if (self->bridge_ != nullptr) { + self->bridge_->forgetRoundTripValue(object); + } + if (self->ownsObject_) { + [object release]; + } + self->object_ = nil; + self->ownsObject_ = false; + self->consumed_ = true; + return makeNativeObjectValue(runtime, self->bridge_, object, retained); + }); + } + 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 (property == "URL" && object_ != nil && + [object_ respondsToSelector:@selector(URL)]) { + return callObjectSelector(runtime, "URL", nullptr, nullptr, 0); + } + if (property == "Symbol.iterator" || + property == "Symbol(Symbol.iterator)" || + property == "@@iterator") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.iterator"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil || + ![object conformsToProtocol:@protocol(NSFastEnumeration)]) { + throw facebook::jsi::JSError( + runtime, "Object does not conform to NSFastEnumeration."); + } + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, static_cast>(object))); + }); + } + +#if TARGET_OS_OSX + if (property == "initWithRedGreenBlueAlpha") { + Class nsColorClass = NSClassFromString(@"NSColor"); + if (object_ != nil && nsColorClass != Nil && + [object_ isKindOfClass:nsColorClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 4, + [bridge, nsColorClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + const char* selectors[] = { + "colorWithSRGBRed:green:blue:alpha:", + "colorWithCalibratedRed:green:blue:alpha:", + "colorWithDeviceRed:green:blue:alpha:", + }; + for (const char* selectorName : selectors) { + if (class_getClassMethod(nsColorClass, + sel_getUid(selectorName)) != nullptr) { + return callObjCSelector(runtime, bridge, + static_cast(nsColorClass), true, + selectorName, nullptr, args, count); + } + } + throw facebook::jsi::JSError( + runtime, "NSColor RGB initializer is not available."); + }); + } + } +#endif + + if (property == "initWithFireDateIntervalTargetSelectorUserInfoRepeats") { + Class timerClass = NSClassFromString(@"NSTimer"); + if (object_ != nil && timerClass != Nil && + [object_ isKindOfClass:timerClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 6, + [bridge, timerClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 6) { + throw facebook::jsi::JSError( + runtime, "NSTimer initializer expects six arguments."); + } + return callObjCSelector( + runtime, bridge, static_cast(timerClass), true, + "timerWithTimeInterval:target:selector:userInfo:repeats:", + nullptr, args + 1, count - 1); + }); + } + } + + Value expando = bridge_->findObjectExpando(runtime, object_, property); + if (!expando.isUndefined()) { + return expando; + } + + if (object_ != nil) { + try { + Value receiver = bridge_->findRoundTripValue(runtime, object_); + Value resolverValue = runtime.global().getProperty( + runtime, "__nativeScriptGetNativeApiPrototypeProperty"); + if (receiver.isObject() && resolverValue.isObject() && + resolverValue.asObject(runtime).isFunction(runtime)) { + Value prototype = + bridge_->findClassPrototype(runtime, object_getClass(object_)); + Value prototypeOrName = prototype.isObject() + ? Value(runtime, prototype) + : Value::undefined(); + if (prototypeOrName.isUndefined()) { + Value classWrapper = bridge_->findObjectExpando( + runtime, object_, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + Object wrapperObject = classWrapper.asObject(runtime); + Value wrapperPrototype = + wrapperObject.getProperty(runtime, "prototype"); + if (wrapperPrototype.isObject()) { + prototypeOrName = std::move(wrapperPrototype); + } + } + } + if (prototypeOrName.isUndefined()) { + const char* className = object_getClassName(object_); + prototypeOrName = makeString(runtime, + className != nullptr ? className : ""); + } + Value resolved = resolverValue.asObject(runtime) + .asFunction(runtime) + .call(runtime, std::move(prototypeOrName), + Value(runtime, receiver), + makeString(runtime, property)); + if (resolved.isObject()) { + Object result = resolved.asObject(runtime); + Value found = result.getProperty(runtime, "found"); + if (found.isBool() && found.getBool()) { + return result.getProperty(runtime, "value"); + } + } + } + } catch (const std::exception&) { + } + } + + if (object_ != nil && [object_ isKindOfClass:[NSArray class]]) { + NSArray* array = static_cast(object_); + if (property == "length") { + return static_cast(array.count); + } + if (auto index = parseArrayIndexProperty(property)) { + if (*index >= array.count) { + return Value::undefined(); + } + id element = [array objectAtIndex:*index]; + NativeApiJsiType elementType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, elementType, &element); + } + } + + if (object_ != nil) { + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if (class_getInstanceMethod(object_getClass(object_), selector) == + nullptr) { + return Value::undefined(); + } + return callObjectSelector(runtime, propertyMember->selectorName, + propertyMember, nullptr, 0); + } + + if (selectMethodMember(members, property, false, 0) != nullptr) { + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = + shared_from_this(); + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), + 0, + [bridge, object, weakSelf, memberName](Runtime& runtime, + const Value&, + const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(object_getClass(object)); + if (symbol == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C metadata is not available for object."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } + if (auto self = weakSelf.lock()) { + return self->callObjectSelector( + runtime, selected->selectorName, selected, args, count); + } + return callObjCSelector(runtime, bridge, object, false, + selected->selectorName, selected, args, + count); + }); + } + } + + if (auto selectorName = + runtimeSelectorNameForProperty(object_getClass(object_), false, + property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(object_getClass(object_), false, + property)) { + return callObjectSelector(runtime, *selectorName, nullptr, nullptr, 0); + } + + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, object, weakSelf, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (auto self = weakSelf.lock()) { + return self->callObjectSelector(runtime, selectorName, nullptr, + args, count); + } + return callObjCSelector(runtime, bridge, object, false, + selectorName, nullptr, args, count); + }); + } + + if ([object_ isKindOfClass:[NSDictionary class]]) { + NSString* key = [NSString stringWithUTF8String:property.c_str()]; + if (key != nil) { + id value = [static_cast(object_) objectForKey:key]; + if (value != nil) { + NativeApiJsiType valueType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, valueType, &value); + } + } + } + } + + 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_->findClassForRuntimeClass(object_getClass(object_))) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->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; + } + + bridge_->setObjectExpando(runtime, object_, property, value); + } + + 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, "constructor"); + addPropertyName(runtime, names, "superclass"); + addPropertyName(runtime, names, "super"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + addPropertyName(runtime, names, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id object_ = nil; + bool ownsObject_ = false; + bool consumed_ = 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 == "__superclass") { + if (symbol_.superclassOffset == MD_SECTION_OFFSET_NULL) { + return Value::undefined(); + } + const NativeApiSymbol* superclass = + bridge_->findClassByOffset(symbol_.superclassOffset); + if (superclass == nullptr) { + return Value::undefined(); + } + return makeNativeClassValue(runtime, bridge_, *superclass); + } + if (property == "__staticMembers" || property == "__instanceMembers") { + bool staticMembers = property == "__staticMembers"; + const auto& members = bridge_->surfaceMembersForClass(symbol_); + Array result(runtime, members.size()); + size_t index = 0; + for (const auto& member : members) { + bool memberIsStatic = + (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMembers) { + continue; + } + Object descriptor(runtime); + descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); + descriptor.setProperty(runtime, "selectorName", + makeString(runtime, member.selectorName)); + descriptor.setProperty( + runtime, "argumentCount", + static_cast(selectorArgumentCount(member.selectorName))); + descriptor.setProperty(runtime, "property", member.property); + descriptor.setProperty(runtime, "readonly", member.readonly); + result.setValueAtIndex(runtime, index++, descriptor); + } + Array compact(runtime, index); + for (size_t i = 0; i < index; i++) { + compact.setValueAtIndex(runtime, i, result.getValueAtIndex(runtime, i)); + } + return compact; + } + 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 == "construct" && count == 1) { + void* pointer = nullptr; + if (args[0].isNumber()) { + pointer = reinterpret_cast( + static_cast(args[0].getNumber())); + } else if (args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + pointer = object + .getHostObject( + runtime) + ->pointer(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->data(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->object(); + } + } + return makeNativeObjectValue(runtime, bridge, + static_cast(pointer), false); + } + + 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 makeNativeObjectValue(runtime, 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); + }); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, true)) { + auto bridge = bridge_; + auto symbol = symbol_; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if (class_getClassMethod(cls, selector) == nullptr) { + return Value::undefined(); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + propertyMember->selectorName, propertyMember, + nullptr, 0); + } + + if (selectMethodMember(members, property, true, 0) != nullptr) { + auto bridge = bridge_; + auto symbol = symbol_; + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, symbol, memberName](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); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(symbol), memberName, true, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + selected->selectorName, selected, args, + count); + }); + } + + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls != nil) { + if (auto selectorName = + runtimeSelectorNameForProperty(cls, true, property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(cls, true, property)) { + return callObjCSelector(runtime, bridge_, static_cast(cls), true, + *selectorName, nullptr, nullptr, 0); + } + + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, cls, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, static_cast(cls), + true, selectorName, nullptr, args, + count); + }); + } + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol_.name); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + setterMember.selectorName, &setterMember, args, 1); + return; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getClassMethod(cls, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + 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(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"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject) { + if (object == nil) { + return Value::null(); + } + + Value cached = bridge->findRoundTripValue(runtime, object); + if (!cached.isUndefined()) { + if (ownsObject) { + [object release]; + } + return cached; + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, object, ownsObject)); + bridge->rememberRoundTripValue(runtime, object, Value(runtime, result)); + return result; +} + +Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, + const char* expectedKind) { + Object global = runtime.global(); + Value cacheValue = global.getProperty( + runtime, "__nativeScriptNativeApiGlobalCache"); + if (!cacheValue.isObject()) { + return Value::undefined(); + } + + Object cache = cacheValue.asObject(runtime); + auto readCache = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + + Value value = cache.getProperty(runtime, name.c_str()); + if (!value.isObject()) { + return Value::undefined(); + } + + try { + Object object = value.asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == expectedKind) { + return value; + } + } catch (const std::exception&) { + } + + return Value::undefined(); + }; + + Value value = readCache(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = readCache(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + + try { + if (std::strcmp(expectedKind, "class") == 0) { + Value classResolverValue = global.getProperty( + runtime, "__nativeScriptResolveNativeApiClassWrapper"); + if (classResolverValue.isObject() && + classResolverValue.asObject(runtime).isFunction(runtime)) { + Function classResolver = + classResolverValue.asObject(runtime).asFunction(runtime); + auto resolveClassWrapper = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = classResolver.call(runtime, makeString(runtime, name)); + return resolved.isObject() ? std::move(resolved) : Value::undefined(); + }; + + value = resolveClassWrapper(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveClassWrapper(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } + + Value resolverValue = + global.getProperty(runtime, "__nativeScriptResolveNativeApiGlobal"); + if (resolverValue.isObject() && + resolverValue.asObject(runtime).isFunction(runtime)) { + Function resolver = resolverValue.asObject(runtime).asFunction(runtime); + auto resolveGlobal = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = resolver.call(runtime, makeString(runtime, name), + makeString(runtime, expectedKind)); + if (resolved.isObject()) { + return resolved; + } + return Value::undefined(); + }; + + value = resolveGlobal(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveGlobal(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } catch (const std::exception&) { + } + + return Value::undefined(); +} + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + Value cachedClass = bridge->findClassValue(runtime, cls); + if (!cachedClass.isUndefined()) { + return cachedClass; + } + Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Protocol* lookupProtocolByNativeName(const std::string& name) { + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol != nullptr) { + return protocol; + } + constexpr const char* suffix = "Protocol"; + size_t suffixLength = std::strlen(suffix); + if (name.size() > suffixLength && + name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - suffixLength).c_str()); + } + return protocol; +} + +class NativeApiProtocolHostObject final : public HostObject { + public: + NativeApiProtocolHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Protocol* nativeProtocol() const { + Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); + if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { + protocol = lookupProtocolByNativeName(symbol_.name); + } + return protocol; + } + + const NativeApiSymbol& symbol() const { return symbol_; } + + 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 == "prototype") { + Object prototype(runtime); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + if (prototype.hasProperty(runtime, member.name.c_str())) { + continue; + } + if (member.property) { + defineProtocolProperty(runtime, prototype, member, false); + } else { + prototype.setProperty(runtime, member.name.c_str(), + makeProtocolMemberFunction(runtime, member, + false)); + } + } + return prototype; + } + 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 + "]"); + }); + } + const auto& members = bridge_->membersForProtocol(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, true, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, false, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } + 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, "prototype"); + addPropertyName(runtime, names, "toString"); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + addPropertyName(runtime, names, member.name.c_str()); + } + return names; + } + + private: + static Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) { + if (!thisValue.isObject()) { + return Nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == "class") { + Value runtimeNameValue = object.getProperty(runtime, "runtimeName"); + if (!runtimeNameValue.isString()) { + runtimeNameValue = object.getProperty(runtime, "name"); + } + if (runtimeNameValue.isString()) { + std::string runtimeName = + runtimeNameValue.asString(runtime).utf8(runtime); + return objc_lookUpClass(runtimeName.c_str()); + } + } + + return Nil; + } + + id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { + if (!thisValue.isObject()) { + return nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + + return nil; + } + + Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol member requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, args, count); + }); + } + + Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value*, size_t) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, nullptr, 0); + }); + } + + Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), + 1, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + if (count < 1) { + throw facebook::jsi::JSError( + runtime, "Protocol property setter expects a value."); + } + + NativeApiMember setterMember = member; + setterMember.selectorName = member.setterSelectorName; + setterMember.signatureOffset = member.setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + setterMember.selectorName, &setterMember, + args, 1); + }); + } + + void defineProtocolProperty(Runtime& runtime, Object& target, + const NativeApiMember& member, + bool receiverIsClass) const { + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty(runtime, "enumerable", true); + descriptor.setProperty(runtime, "get", + makeProtocolPropertyGetter(runtime, member, + receiverIsClass)); + if (!member.readonly && !member.setterSelectorName.empty()) { + descriptor.setProperty(runtime, "set", + makeProtocolPropertySetter(runtime, member, + receiverIsClass)); + } + defineProperty.call(runtime, target, makeString(runtime, member.name), + descriptor); + } catch (const std::exception&) { + } + } + + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeProtocolValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Class nativeClassFromJsiObject(Runtime& runtime, const Object& object) { + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + return Nil; +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiInstall.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiInstall.inc new file mode 100644 index 00000000..025090b3 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiInstall.inc @@ -0,0 +1,1520 @@ +Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + auto bridge = std::make_shared(config); + return Object::createFromHostObject( + runtime, std::make_shared(std::move(bridge))); +} + +void NativeApiJsiWriteSmokeStage(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]; +} + +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. + } + } +} + +std::string jsStringLiteral(const char* value) { + std::string result = "'"; + if (value != nullptr) { + for (const char* current = value; *current != '\0'; current++) { + switch (*current) { + case '\\': + result += "\\\\"; + break; + case '\'': + result += "\\'"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += *current; + break; + } + } + } + result += "'"; + return result; +} + +void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) { + NativeApiJsiWriteSmokeStage("jsi:globals:before-eval"); + static const char* GlobalInstaller = R"JSI_GLOBALS( +(function(nativeApiGlobalName) { + 'use strict'; + var api = globalThis[nativeApiGlobalName]; + var installedFlagName = '__nativeScriptNativeApiGlobalsInstalled'; + if (!api || globalThis[installedFlagName]) { + return; + } + + var cacheName = '__nativeScriptNativeApiGlobalCache'; + var typeCodeKey = '__nativeApiTypeCode'; + var classWrappers = typeof WeakMap === 'function' ? new WeakMap() : null; + var classWrappersByName = Object.create(null); + var resolvingGlobal = Object.create(null); + + function globalCache() { + var existing = globalThis[cacheName]; + if (existing && typeof existing === 'object') { + return existing; + } + var cache = Object.create(null); + Object.defineProperty(globalThis, cacheName, { + configurable: false, + enumerable: false, + writable: false, + value: cache + }); + return cache; + } + + function cacheGlobal(name, value) { + if (name && value !== undefined) { + globalCache()[name] = value; + } + } + + function resolveCachedGlobal(name, expectedKind) { + if (!name) { + return undefined; + } + var cached = globalCache()[name]; + if (cached && (typeof cached === 'object' || typeof cached === 'function') && cached.kind === expectedKind) { + return cached; + } + if (resolvingGlobal[name] || !Object.prototype.hasOwnProperty.call(globalThis, name)) { + return undefined; + } + resolvingGlobal[name] = true; + try { + var value = globalThis[name]; + if (value && (typeof value === 'object' || typeof value === 'function') && value.kind === expectedKind) { + cacheGlobal(name, value); + return value; + } + } finally { + delete resolvingGlobal[name]; + } + return undefined; + } + + function defineLazyGlobal(name, resolve, force) { + if (!name) { + return; + } + if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { + try { + cacheGlobal(name, globalThis[name]); + } catch (_) { + } + return; + } + try { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + get: function() { + var value = resolve(name); + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + return value; + } + }); + } catch (_) { + var value = resolve(name); + if (value !== undefined) { + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + } + } + } + + Object.defineProperty(globalThis, '__nativeScriptResolveNativeApiGlobal', { + configurable: false, + enumerable: false, + writable: false, + value: resolveCachedGlobal + }); + + Object.defineProperty(globalThis, '__nativeScriptResolveNativeApiClassWrapper', { + configurable: false, + enumerable: false, + writable: false, + value: function(name) { + return name ? classWrappersByName[name] : undefined; + } + }); + + function findPrototypeDescriptor(className, property) { + var prototype; + if (className && (typeof className === 'object' || typeof className === 'function')) { + prototype = className; + } else { + var wrapper = className ? classWrappersByName[className] : undefined; + prototype = wrapper && wrapper.prototype; + } + while (prototype != null) { + var descriptor = Object.getOwnPropertyDescriptor(prototype, property); + if (descriptor) { + return descriptor; + } + prototype = Object.getPrototypeOf(prototype); + } + return undefined; + } + + Object.defineProperty(globalThis, '__nativeScriptGetNativeApiPrototypeProperty', { + configurable: false, + enumerable: false, + writable: false, + value: function(className, receiver, property) { + var descriptor = findPrototypeDescriptor(className, property); + if (!descriptor) { + return { found: false }; + } + if (typeof descriptor.get === 'function') { + return { found: true, value: descriptor.get.call(receiver) }; + } + if (typeof descriptor.value === 'function') { + return { found: true, value: descriptor.value.bind(receiver) }; + } + if ('value' in descriptor) { + return { found: true, value: descriptor.value }; + } + return { found: true, value: undefined }; + } + }); + + Object.defineProperty(globalThis, '__nativeScriptCreateNativeApiIterator', { + configurable: false, + enumerable: false, + writable: false, + value: function(receiver, prototype) { + if (!receiver || typeof Symbol !== 'function') { + return undefined; + } + var descriptor = findPrototypeDescriptor(prototype || receiver.className, Symbol.iterator); + if (descriptor && typeof descriptor.value === 'function') { + return descriptor.value.call(receiver); + } + if (descriptor && typeof descriptor.get === 'function') { + var getterValue = descriptor.get.call(receiver); + if (typeof getterValue === 'function') { + return getterValue.call(receiver); + } + } + var iteratorMethod = receiver[Symbol.iterator]; + return typeof iteratorMethod === 'function' + ? iteratorMethod.call(receiver) + : undefined; + } + }); + + function wrapAggregateConstructor(nativeConstructor) { + if (typeof nativeConstructor !== 'function') { + return nativeConstructor; + } + var aggregate = function NativeScriptAggregate(initialValue) { + return nativeConstructor(initialValue); + }; + try { + Object.defineProperty(aggregate, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && + typeof value === 'object' && + value.kind === nativeConstructor.kind && + value.name === nativeConstructor.runtimeName; + } + }); + } catch (_) { + } + ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields', 'equals'].forEach(function(key) { + try { + Object.defineProperty(aggregate, key, { + configurable: true, + enumerable: false, + writable: false, + value: nativeConstructor[key] + }); + } catch (_) { + } + }); + return aggregate; + } + + function setDescriptorValue(target, property, receiver, value) { + var descriptor = Object.getOwnPropertyDescriptor(target, property); + if (!descriptor) { + return false; + } + if (typeof descriptor.set === 'function') { + descriptor.set.call(receiver, value); + return true; + } + if (descriptor.writable) { + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + } else { + target[property] = value; + } + return true; + } + return false; + } + + function isConstructorOptions(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + if (value.kind || value.nativeAddress || value instanceof Date) { + return false; + } + return Object.getPrototypeOf(value) === Object.prototype || + Object.getPrototypeOf(value) === null; + } + + function capitalizeToken(value) { + value = String(value || ''); + return value ? value.charAt(0).toUpperCase() + value.slice(1) : value; + } + + function selectorCandidatesFromOptions(options) { + var keys = Object.keys(options || {}); + if (!keys.length) { + return []; + } + var first = capitalizeToken(keys[0]); + var tail = ''; + for (var i = 1; i < keys.length; i++) { + tail += keys[i] + ':'; + } + return [ + 'initWith' + first + ':' + tail, + 'init' + first + ':' + tail + ]; + } + + function valuesFromOptions(options) { + return Object.keys(options || {}).map(function(key) { + return options[key]; + }); + } + + function selectorScoreForArguments(selectorName, args) { + if (!selectorName || selectorName.indexOf('init') !== 0) { + return -1; + } + if (selectorName === 'init') { + return args.length === 0 ? 100 : -1; + } + if (args.length === 0) { + return -1; + } + var lower = selectorName.toLowerCase(); + var first = args[0]; + var score = 1; + if (Array.isArray(first)) { + if (lower.indexOf('array') !== -1) { + score += 40; + } + } else if (typeof first === 'string') { + if (lower.indexOf('string') !== -1) { + score += 40; + } + if (lower.indexOf('url') !== -1) { + score += 10; + } + } else if (typeof first === 'number') { + if (lower.indexOf('primitive') !== -1) { + score += 50; + } + if (lower.indexOf('int') !== -1 || + lower.indexOf('integer') !== -1 || + lower.indexOf('number') !== -1 || + lower.indexOf('float') !== -1 || + lower.indexOf('double') !== -1 || + lower.indexOf('long') !== -1 || + lower.indexOf('short') !== -1) { + score += 30; + } + } else if (isConstructorOptions(first)) { + if (lower.indexOf('struct') !== -1 || + lower.indexOf('structure') !== -1) { + score += 40; + } + if (lower.indexOf('dictionary') !== -1) { + score += 20; + } + } else if (first === null || typeof first === 'undefined') { + score += 5; + } else if (lower.indexOf('object') !== -1 || + lower.indexOf('url') !== -1 || + lower.indexOf('data') !== -1) { + score += 20; + } + + var allStrings = args.length > 1 && args.every(function(value) { + return typeof value === 'string'; + }); + var allNumbers = args.length > 1 && args.every(function(value) { + return typeof value === 'number'; + }); + if (allStrings && lower.indexOf('string') !== -1) { + score += 25; + } + if (allNumbers && + (lower.indexOf('int') !== -1 || lower.indexOf('number') !== -1)) { + score += 25; + } + return score; + } + + function initializerMembers(nativeClass, argumentCount) { + var members = nativeClass.__instanceMembers || []; + var result = []; + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (!member || member.property || !member.selectorName) { + continue; + } + if (member.selectorName.indexOf('init') !== 0) { + continue; + } + if (typeof argumentCount === 'number' && + member.argumentCount !== argumentCount) { + continue; + } + result.push(member); + } + return result; + } + + function chooseInitializer(nativeClass, args, optionSelectors) { + var members = initializerMembers(nativeClass, args.length); + if (!members.length) { + return null; + } + if (optionSelectors && optionSelectors.length) { + for (var i = 0; i < optionSelectors.length; i++) { + for (var j = 0; j < members.length; j++) { + if (members[j].selectorName === optionSelectors[i]) { + return members[j]; + } + } + } + } + + var best = null; + var bestScore = -1; + for (var k = 0; k < members.length; k++) { + var score = selectorScoreForArguments(members[k].selectorName, args); + if (score > bestScore) { + bestScore = score; + best = members[k]; + } + } + return bestScore >= 0 ? best : null; + } + + function chooseInitializerBySelectors(nativeClass, args, selectors) { + if (!selectors || !selectors.length) { + return null; + } + var members = initializerMembers(nativeClass, args.length); + for (var i = 0; i < selectors.length; i++) { + for (var j = 0; j < members.length; j++) { + if (members[j].selectorName === selectors[i]) { + return members[j]; + } + } + } + return null; + } + + function unavailableInitializerError(error) { + return error && + /Objective-C selector is not available/.test(String(error.message || error)); + } + + function constructNativeInstance(nativeClass, args) { + if (args.length === 1 && + args[0] && + typeof args[0] === 'object' && + (args[0].kind === 'pointer' || args[0].kind === 'reference') && + typeof nativeClass.construct === 'function') { + return nativeClass.construct(args[0]); + } + + var actualArgs = args; + var initializer = null; + if (args.length === 1 && isConstructorOptions(args[0])) { + var optionSelectors = selectorCandidatesFromOptions(args[0]); + if (!optionSelectors.length) { + throw new Error('No initializer found that matches constructor invocation.'); + } + var optionArgs = valuesFromOptions(args[0]); + initializer = chooseInitializerBySelectors( + nativeClass, + optionArgs, + optionSelectors + ); + if (initializer) { + actualArgs = optionArgs; + } + } + if (!initializer) { + initializer = chooseInitializer(nativeClass, actualArgs, null); + } + if (!initializer) { + throw new Error('No initializer found that matches constructor invocation.'); + } + if (typeof nativeClass.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + var instance = nativeClass.alloc(); + if (initializer.selectorName === 'init') { + if (typeof instance.init !== 'function') { + throw new Error('No initializer found that matches constructor invocation.'); + } + return instance.init(); + } + try { + if (initializer.name && typeof instance[initializer.name] === 'function') { + return instance[initializer.name].apply(instance, actualArgs); + } + var invokeArgs = [initializer.selectorName]; + Array.prototype.push.apply(invokeArgs, actualArgs); + return instance.invoke.apply(instance, invokeArgs); + } catch (error) { + if (unavailableInitializerError(error)) { + throw new Error('No initializer found that matches constructor invocation.'); + } + throw error; + } + } + + function wrapNativeClass(nativeClass) { + if (!nativeClass || (typeof nativeClass !== 'object' && typeof nativeClass !== 'function')) { + return nativeClass; + } + var nativeClassName = nativeClass.runtimeName || nativeClass.name || ''; + if (nativeClassName && classWrappersByName[nativeClassName]) { + if (classWrappers) { + try { + classWrappers.set(nativeClass, classWrappersByName[nativeClassName]); + } catch (_) { + } + } + return classWrappersByName[nativeClassName]; + } + if (classWrappers) { + var cached = classWrappers.get(nativeClass); + if (cached) { + return cached; + } + } + var constructable = function NativeScriptNativeClass() { + var args = Array.prototype.slice.call(arguments); + var redirectConstructor = this && this.constructor; + if (redirectConstructor && + redirectConstructor !== constructable && + redirectConstructor !== wrapper && + typeof redirectConstructor.__nativeApiEnsureClass === 'function') { + var redirectedWrapper = redirectConstructor.__nativeApiEnsureClass(); + if (redirectedWrapper && + redirectedWrapper !== constructable && + redirectedWrapper !== wrapper && + typeof redirectedWrapper.apply === 'function') { + return rememberClassOnInstance( + redirectedWrapper.apply(this, args), + redirectConstructor + ); + } + } + if (args.length > 0) { + return rememberInstanceClass(constructNativeInstance(nativeClass, args)); + } + if (typeof nativeClass.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + var instance = nativeClass.alloc(); + if (instance && typeof instance.init === 'function') { + return rememberInstanceClass(instance.init()); + } + return rememberInstanceClass(instance); + }; + function rememberInstanceClass(instance) { + return rememberClassOnInstance(instance, wrapper || constructable); + } + try { + Object.defineProperty(constructable, 'name', { + configurable: true, + enumerable: false, + value: nativeClassName || nativeClass.name || 'NativeScriptNativeClass' + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'extend', { + configurable: true, + enumerable: false, + writable: false, + value: function(methods, options) { + if (methods == null || typeof methods !== 'object') { + throw new Error('extend() first parameter must be an object'); + } + var extendOptions = options || {}; + if (typeof Symbol === 'function' && + Object.prototype.hasOwnProperty.call(methods, Symbol.iterator)) { + try { + extendOptions = Object.assign({}, extendOptions, { + __hasIterator: true + }); + } catch (_) { + extendOptions.__hasIterator = true; + } + } + var extendedNativeClass = api.__extendClass(nativeClass, methods, extendOptions); + var extended = wrapNativeClass(extendedNativeClass); + try { + Object.setPrototypeOf(extended, wrapper || constructable); + } catch (_) { + } + var extendedPrototype = Object.create(constructable.prototype || null); + try { + Object.defineProperties(extendedPrototype, Object.getOwnPropertyDescriptors(methods)); + } catch (_) { + Object.keys(methods).forEach(function(key) { + extendedPrototype[key] = methods[key]; + }); + } + try { + Object.defineProperty(extendedPrototype, 'constructor', { + configurable: true, + enumerable: false, + writable: true, + value: extended + }); + } catch (_) { + } + extended.prototype = extendedPrototype; + try { + api.__rememberClassWrapper(extendedNativeClass, extended, extendedPrototype); + } catch (_) { + } + return extended; + } + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'alloc', { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return rememberInstanceClass(nativeClass.alloc.apply(nativeClass, arguments)); + } + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'caller', { + configurable: true, + enumerable: false, + writable: false, + value: null + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'arguments', { + configurable: true, + enumerable: false, + writable: false, + value: null + }); + } catch (_) { + } + var basePrototypeTarget = {}; + function installClassMembers(target, members, receiverIsClass) { + if (!target || !members || typeof members.length !== 'number') { + return; + } + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (!member || !member.name || Object.prototype.hasOwnProperty.call(target, member.name)) { + continue; + } + try { + if (member.property) { + var descriptor = { + configurable: true, + enumerable: false, + get: receiverIsClass + ? (function(name) { + return function() { return nativeClass[name]; }; + })(member.name) + : (function(name) { + return function() { + return api.__invokeBase(nativeClass, this, name); + }; + })(member.name) + }; + if (!member.readonly) { + descriptor.set = receiverIsClass + ? (function(name) { + return function(value) { nativeClass[name] = value; }; + })(member.name) + : (function(name) { + return function(value) { + return api.__invokeBase(nativeClass, this, name, value); + }; + })(member.name); + } + Object.defineProperty(target, member.name, descriptor); + } else { + Object.defineProperty(target, member.name, { + configurable: true, + enumerable: false, + writable: true, + value: receiverIsClass + ? (function(name) { + return function() { + if (this && typeof this === 'object' && this.kind === 'object') { + var baseArgs = [nativeClass, this, name]; + Array.prototype.push.apply(baseArgs, arguments); + return api.__invokeBase.apply(api, baseArgs); + } + return nativeClass[name].apply(nativeClass, arguments); + }; + })(member.name) + : (function(name) { + return function() { + var args = [nativeClass, this, name]; + Array.prototype.push.apply(args, arguments); + return api.__invokeBase.apply(api, args); + }; + })(member.name) + }); + } + } catch (_) { + } + } + } + installClassMembers(constructable, nativeClass.__staticMembers, true); + installClassMembers(basePrototypeTarget, nativeClass.__instanceMembers, false); + try { + Object.defineProperty(basePrototypeTarget, 'constructor', { + configurable: true, + enumerable: false, + writable: true, + value: constructable + }); + } catch (_) { + } + try { + Object.defineProperty(basePrototypeTarget, 'toString', { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return '[object NativeScriptObject]'; + } + }); + } catch (_) { + } + try { + if (typeof Symbol === 'function' && Symbol.iterator && + typeof api.__fastEnumeration === 'function') { + Object.defineProperty(basePrototypeTarget, Symbol.iterator, { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return api.__fastEnumeration(this); + } + }); + } + } catch (_) { + } + constructable.prototype = typeof Proxy === 'function' + ? new Proxy(basePrototypeTarget, { + get: function(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + if (typeof property === 'symbol') { + return undefined; + } + return function() { + var args = [nativeClass, this, String(property)]; + Array.prototype.push.apply(args, arguments); + return api.__invokeBase.apply(api, args); + }; + }, + set: function(target, property, value, receiver) { + if (property === 'prototype') { + target[property] = value; + return true; + } + if (setDescriptorValue(target, property, receiver, value)) { + return true; + } + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + return true; + } + target[property] = value; + return true; + }, + has: function(target, property) { + return property in target; + } + }) + : basePrototypeTarget; + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + if (!value || typeof value !== 'object') { + return false; + } + var expectedName = nativeClass.runtimeName || nativeClass.name; + try { + if (typeof value.isKindOfClass === 'function' && + value.isKindOfClass(constructable)) { + return true; + } + } catch (_) { + } + try { + var current = typeof value.class === 'function' ? value.class() : null; + while (current) { + if (current === wrapper || current === constructable) { + return true; + } + var currentName = current.runtimeName || current.name; + if (typeof expectedName === 'string' && currentName === expectedName) { + return true; + } + var next = current.superclass || null; + if (typeof next === 'function' && next.kind !== 'class') { + next = next.call(current); + } + current = next || null; + } + } catch (_) { + } + return typeof expectedName === 'string' && value.className === expectedName; + } + }); + } catch (_) { + } + var wrapper = typeof Proxy === 'function' + ? new Proxy(constructable, { + get: function(target, property, receiver) { + if (property === '__nativeApiClass') { + return nativeClass; + } + if (Object.prototype.hasOwnProperty.call(target, property) || + property === 'prototype' || + property === 'length' || + property === 'name') { + return Reflect.get(target, property, receiver); + } + if (property in nativeClass) { + var nativeValue = nativeClass[property]; + if (nativeValue !== undefined) { + return nativeValue; + } + } + var reflected = Reflect.get(target, property, receiver); + if (reflected !== undefined || property in target) { + return reflected; + } + return reflected; + }, + set: function(target, property, value, receiver) { + if (property === 'prototype') { + target[property] = value; + return true; + } + if (setDescriptorValue(target, property, receiver, value)) { + return true; + } + try { + nativeClass[property] = value; + return true; + } catch (_) { + } + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + return true; + } + return Reflect.set(target, property, value, receiver); + }, + has: function(target, property) { + return property in target || property in nativeClass; + } + }) + : constructable; + try { + var nativeSuperclass = nativeClass.__superclass; + if (nativeSuperclass && nativeSuperclass !== nativeClass) { + var superclassWrapper = wrapNativeClass(nativeSuperclass); + if (superclassWrapper && + superclassWrapper !== wrapper && + superclassWrapper !== constructable) { + Object.setPrototypeOf(wrapper, superclassWrapper); + } + } + } catch (_) { + } + if (classWrappers) { + classWrappers.set(nativeClass, wrapper); + } + try { + api.__rememberClassWrapper(nativeClass, wrapper, constructable.prototype); + } catch (_) { + } + if (nativeClassName) { + classWrappersByName[nativeClassName] = wrapper; + cacheGlobal(nativeClassName, wrapper); + if (!Object.prototype.hasOwnProperty.call(globalThis, nativeClassName)) { + try { + Object.defineProperty(globalThis, nativeClassName, { + configurable: true, + enumerable: false, + writable: false, + value: wrapper + }); + } catch (_) { + } + } + } + if (nativeClass.name && nativeClass.name !== nativeClassName) { + classWrappersByName[nativeClass.name] = wrapper; + cacheGlobal(nativeClass.name, wrapper); + } + return wrapper; + } + + function rememberClassOnInstance(instance, classWrapper) { + if (instance && typeof instance === 'object' && classWrapper) { + try { + instance.__nativeApiClassWrapper = classWrapper; + } catch (_) { + } + } + return instance; + } + + function isNativeClassLike(value) { + if (!value || (typeof value !== 'object' && typeof value !== 'function')) { + return false; + } + if (value.kind === 'class') { + return true; + } + try { + return !!value.__nativeApiClass; + } catch (_) { + return false; + } + } + + function nativeClassLikeHandle(value) { + if (!value || (typeof value !== 'object' && typeof value !== 'function')) { + return value; + } + try { + if (typeof value.__nativeApiEnsureClass === 'function') { + value = value.__nativeApiEnsureClass(); + } + } catch (_) { + } + try { + return value.__nativeApiClass || value; + } catch (_) { + return value; + } + } + + function materializeTypeScriptNativeClass(constructor) { + if (!constructor || typeof constructor !== 'function') { + return undefined; + } + var state = constructor.__nativeApiTypeScriptState; + if (!state) { + return undefined; + } + if (state.wrapper) { + return state.wrapper; + } + if (state.materializing) { + return state.base; + } + + state.materializing = true; + try { + var baseWrapper = state.base; + if (baseWrapper && typeof baseWrapper.__nativeApiEnsureClass === 'function') { + baseWrapper = baseWrapper.__nativeApiEnsureClass(); + } + + var options = {}; + var className = constructor.ObjCClassName || constructor.name; + if (className) { + options.name = className; + } + if (constructor.ObjCProtocols) { + options.protocols = constructor.ObjCProtocols; + } + if (constructor.ObjCExposedMethods) { + options.exposedMethods = constructor.ObjCExposedMethods; + } + + var nativeBase = nativeClassLikeHandle(baseWrapper); + var nativeClass = api.__extendClass(nativeBase, constructor.prototype || {}, options); + var wrapper = wrapNativeClass(nativeClass); + state.wrapper = wrapper; + + try { + Object.setPrototypeOf(constructor, wrapper); + } catch (_) { + } + try { + api.__rememberClassWrapper(nativeClass, constructor, constructor.prototype || {}); + } catch (_) { + } + return wrapper; + } finally { + state.materializing = false; + } + } + + function defineTypeScriptStaticForwarder(constructor, name, isProperty, readonly) { + if (!name || name === 'length' || name === 'name' || name === 'prototype' || + Object.prototype.hasOwnProperty.call(constructor, name)) { + return; + } + + var descriptor = { + configurable: true, + enumerable: false + }; + + if (isProperty) { + descriptor.get = function() { + var wrapper = materializeTypeScriptNativeClass(constructor); + return wrapper ? wrapper[name] : undefined; + }; + if (!readonly) { + descriptor.set = function(value) { + var wrapper = materializeTypeScriptNativeClass(constructor); + if (wrapper) { + wrapper[name] = value; + } + }; + } + } else { + descriptor.writable = true; + descriptor.value = function() { + if (name === 'class') { + materializeTypeScriptNativeClass(constructor); + return constructor; + } + if (name === 'superclass') { + var state = constructor.__nativeApiTypeScriptState; + return state && state.base; + } + var wrapper = materializeTypeScriptNativeClass(constructor); + var member = wrapper && wrapper[name]; + if (typeof member !== 'function') { + throw new TypeError(String(name) + ' is not a function'); + } + var result = member.apply(wrapper, arguments); + if (name === 'alloc' || name === 'new' || name === 'construct') { + return rememberClassOnInstance(result, constructor); + } + return result; + }; + } + + try { + Object.defineProperty(constructor, name, descriptor); + } catch (_) { + } + } + + function installTypeScriptNativeClassSupport(constructor, base) { + if (!constructor || typeof constructor !== 'function' || !isNativeClassLike(base)) { + return false; + } + if (constructor.__nativeApiTypeScriptState) { + return true; + } + + try { + Object.defineProperty(constructor, '__nativeApiTypeScriptState', { + configurable: false, + enumerable: false, + writable: false, + value: { + base: base, + wrapper: null, + materializing: false + } + }); + } catch (_) { + constructor.__nativeApiTypeScriptState = { + base: base, + wrapper: null, + materializing: false + }; + } + + try { + Object.defineProperty(constructor, '__nativeApiEnsureClass', { + configurable: false, + enumerable: false, + writable: false, + value: function() { + return materializeTypeScriptNativeClass(constructor); + } + }); + } catch (_) { + } + + try { + Object.defineProperty(constructor, '__nativeApiClass', { + configurable: true, + enumerable: false, + get: function() { + var wrapper = materializeTypeScriptNativeClass(constructor); + return wrapper && wrapper.__nativeApiClass; + } + }); + } catch (_) { + } + + ['alloc', 'new', 'class', 'superclass', 'extend'].forEach(function(name) { + defineTypeScriptStaticForwarder(constructor, name, false, false); + }); + + try { + var members = base.__staticMembers || []; + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (member && member.name) { + defineTypeScriptStaticForwarder( + constructor, + member.name, + !!member.property, + !!member.readonly + ); + } + } + } catch (_) { + } + + return true; + } + + function installTypeScriptNativeHelpers() { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function(d, b) { d.__proto__ = b; }) || + function(d, b) { + for (var p in b) { + if (Object.prototype.hasOwnProperty.call(b, p)) { + d[p] = b[p]; + } + } + }; + + globalThis.__extends = function(d, b) { + if (typeof b !== 'function' && b !== null) { + throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); + } + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + if (b !== null) { + installTypeScriptNativeClassSupport(d, b); + } + }; + + globalThis.NativeClass = function NativeClass(constructor) { + if (constructor && typeof constructor.__nativeApiEnsureClass === 'function') { + constructor.__nativeApiEnsureClass(); + } + return constructor; + }; + + globalThis.ObjCClass = function ObjCClass() { + var protocols = Array.prototype.slice.call(arguments); + return function(constructor) { + if (constructor.ObjCProtocols) { + Array.prototype.push.apply(constructor.ObjCProtocols, protocols); + } else { + constructor.ObjCProtocols = protocols; + } + if (typeof constructor.__nativeApiEnsureClass === 'function') { + constructor.__nativeApiEnsureClass(); + } + return constructor; + }; + }; + } + + function wrapInteropFactory(nativeFactory, properties) { + if (typeof nativeFactory !== 'function' || nativeFactory.__nativeScriptConstructable) { + return nativeFactory; + } + var constructable = function NativeScriptInteropValue() { + return nativeFactory.apply(undefined, arguments); + }; + try { + if (nativeFactory.prototype) { + constructable.prototype = nativeFactory.prototype; + } + } catch (_) { + } + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && typeof value === 'object' && value.kind === properties.kind; + } + }); + } catch (_) { + } + Object.keys(properties).forEach(function(key) { + try { + Object.defineProperty(constructable, key, { + configurable: true, + enumerable: false, + writable: false, + value: properties[key] + }); + } catch (_) { + } + }); + Object.defineProperty(constructable, '__nativeScriptConstructable', { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + return constructable; + } + + function installInteropConstructors() { + var interop = globalThis.interop; + if (!interop || typeof interop !== 'object') { + return; + } + var pointerSize; + try { + if (typeof interop.sizeof === 'function' && interop.types && interop.types.pointer !== undefined) { + pointerSize = interop.sizeof(interop.types.pointer); + } + } catch (_) { + pointerSize = undefined; + } + interop.Pointer = wrapInteropFactory(interop.Pointer, { kind: 'pointer', sizeof: pointerSize }); + interop.Reference = wrapInteropFactory(interop.Reference, { kind: 'reference', sizeof: pointerSize }); + interop.FunctionReference = wrapInteropFactory( + interop.FunctionReference, + { kind: 'functionReference', sizeof: pointerSize } + ); + if (interop.types && typeof interop.types === 'object') { + Object.keys(interop.types).forEach(function(name) { + var value = interop.types[name]; + if (typeof value !== 'number') { + return; + } + var boxed = { + valueOf: function() { return value; }, + toString: function() { return String(value); } + }; + Object.defineProperty(boxed, typeCodeKey, { + configurable: false, + enumerable: false, + writable: false, + value: value + }); + interop.types[name] = boxed; + }); + } + } + + function defineInlineFunction(name, value) { + if (Object.prototype.hasOwnProperty.call(globalThis, name)) { + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: true, + value: value + }); + } + + function installInlineFunctions() { + var makePoint = function(x, y) { return { x: x, y: y }; }; + var makeSize = function(width, height) { return { width: width, height: height }; }; + var makeRect = function(x, y, width, height) { + return { origin: { x: x, y: y }, size: { width: width, height: height } }; + }; + defineInlineFunction('CGPointMake', makePoint); + defineInlineFunction('NSMakePoint', makePoint); + defineInlineFunction('CGSizeMake', makeSize); + defineInlineFunction('NSMakeSize', makeSize); + defineInlineFunction('CGRectMake', makeRect); + defineInlineFunction('NSMakeRect', makeRect); + defineInlineFunction('NSMakeRange', function(location, length) { + return { location: location, length: length }; + }); + defineInlineFunction('UIEdgeInsetsMake', function(top, left, bottom, right) { + return { top: top, left: left, bottom: bottom, right: right }; + }); + } + + function names(kind) { + var metadata = api.metadata; + var fn = metadata && metadata[kind]; + return typeof fn === 'function' ? fn() : []; + } + + function nameSet(values) { + var result = Object.create(null); + (values || []).forEach(function(value) { + result[value] = true; + }); + return result; + } + + var classNameList = names('classNames'); + var functionNameList = names('functionNames'); + var constantNameList = names('constantNames'); + var protocolNameList = names('protocolNames'); + var enumNameList = names('enumNames'); + var functionNameSet = nameSet(functionNameList); + var constantNameSet = nameSet(constantNameList); + var classNameSet = nameSet(classNameList); + var protocolNameSet = nameSet(protocolNameList); + var enumNameSet = nameSet(enumNameList); + + classNameList.forEach(function(name) { + defineLazyGlobal(name, function(className) { + return wrapNativeClass(api[className]); + }); + }); + functionNameList.forEach(function(name) { + defineLazyGlobal(name, function(functionName) { + return api[functionName]; + }); + }); + constantNameList.forEach(function(name) { + defineLazyGlobal(name, function(constantName) { + return api[constantName]; + }); + }); + protocolNameList.forEach(function(name) { + defineLazyGlobal(name, function(protocolName) { + return (api.getProtocol && api.getProtocol(protocolName)) || api[protocolName]; + }); + }); + enumNameList.forEach(function(name) { + var resolveEnum = function(enumName) { + return (api.getEnum && api.getEnum(enumName)) || api[enumName]; + }; + defineLazyGlobal(name, resolveEnum); + var enumValue = resolveEnum(name); + if (!enumValue || typeof enumValue !== 'object') { + return; + } + Object.keys(enumValue).forEach(function(memberName) { + if (/^-?\d+$/.test(memberName)) { + return; + } + defineLazyGlobal(memberName, function() { + return enumValue[memberName]; + }); + }); + }); + names('structNames').forEach(function(name) { + var conflictsWithValue = + !!functionNameSet[name] || !!constantNameSet[name] || !!classNameSet[name] || + !!protocolNameSet[name] || !!enumNameSet[name]; + defineLazyGlobal(name, function(structName) { + return wrapAggregateConstructor((api.getStruct && api.getStruct(structName)) || api[structName]); + }, !conflictsWithValue); + }); + names('unionNames').forEach(function(name) { + var conflictsWithValue = + !!functionNameSet[name] || !!constantNameSet[name] || !!classNameSet[name] || + !!protocolNameSet[name] || !!enumNameSet[name]; + defineLazyGlobal(name, function(unionName) { + return wrapAggregateConstructor((api.getUnion && api.getUnion(unionName)) || api[unionName]); + }, !conflictsWithValue); + }); + + if (typeof globalThis.UIColor === 'undefined' && + typeof globalThis.NSColor !== 'undefined') { + globalThis.UIColor = globalThis.NSColor; + cacheGlobal('UIColor', globalThis.UIColor); + } + var colorCtor = globalThis.UIColor || globalThis.NSColor; + if (colorCtor && colorCtor.prototype && + typeof colorCtor.prototype.initWithRedGreenBlueAlpha !== 'function') { + colorCtor.prototype.initWithRedGreenBlueAlpha = function(red, green, blue, alpha) { + if (typeof this.initWithSRGBRedGreenBlueAlpha === 'function') { + return this.initWithSRGBRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof this.initWithCalibratedRedGreenBlueAlpha === 'function') { + return this.initWithCalibratedRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof colorCtor.colorWithSRGBRedGreenBlueAlpha === 'function') { + return colorCtor.colorWithSRGBRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof colorCtor.colorWithCalibratedRedGreenBlueAlpha === 'function') { + return colorCtor.colorWithCalibratedRedGreenBlueAlpha(red, green, blue, alpha); + } + return this; + }; + } + defineLazyGlobal('CC_SHA256', function() { return api.CC_SHA256; }); + + installInteropConstructors(); + installTypeScriptNativeHelpers(); + installInlineFunctions(); + + try { + Object.defineProperty(globalThis, installedFlagName, { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + } catch (_) { + } +}) +)JSI_GLOBALS"; + + std::string script(GlobalInstaller); + script += "("; + script += jsStringLiteral(globalName); + script += ");"; + runtime.evaluateJavaScript(std::make_shared(std::move(script)), + "NativeApiJsiGlobals.js"); + NativeApiJsiWriteSmokeStage("jsi:globals:after-eval"); +} + +void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + const char* globalName = config.globalName != nullptr && config.globalName[0] != '\0' + ? config.globalName + : "__nativeScriptNativeApi"; + NativeApiJsiWriteSmokeStage("jsi:create-api"); + Object api = CreateNativeApiJSI(runtime, config); + Object global = runtime.global(); + NativeApiJsiWriteSmokeStage("jsi:set-global"); + global.setProperty(runtime, globalName, api); + + NativeApiJsiWriteSmokeStage("jsi:set-interop"); + Value existingInterop = global.getProperty(runtime, "interop"); + if (existingInterop.isUndefined() || existingInterop.isNull()) { + global.setProperty(runtime, "interop", api.getProperty(runtime, "interop")); + } + if (config.installGlobalSymbols) { + NativeApiJsiWriteSmokeStage("jsi:install-globals"); + InstallNativeApiJsiGlobalSymbols(runtime, globalName); + } else { + NativeApiJsiWriteSmokeStage("jsi:install-aggregate-globals"); + InstallAggregateGlobals(runtime, api, "protocolNames"); + } + NativeApiJsiWriteSmokeStage("jsi:installed"); +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiInvocation.inc b/NativeScript/ffi/hermes/jsi/NativeApiJsiInvocation.inc new file mode 100644 index 00000000..5412d86f --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiInvocation.inc @@ -0,0 +1,496 @@ +bool isValidMetadataStringOffset(MDMetadataReader* metadata, + MDSectionOffset offset) { + if (metadata == nullptr || metadata->constantsOffset < metadata->stringsOffset) { + return false; + } + return offset < metadata->constantsOffset - metadata->stringsOffset; +} + +bool startsWith(const std::string& value, const std::string& prefix) { + return value.size() >= prefix.size() && + value.compare(0, prefix.size(), prefix) == 0; +} + +bool endsWith(const std::string& value, const std::string& suffix) { + return value.size() >= suffix.size() && + value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::string stripEnumSuffix(const std::string& enumName) { + static const std::vector suffixes = { + "Options", "Option", "Enums", "Enum", "Result", "Direction", + "Orientation", "Style", "Mask", "Type", "Status", "Modes", "Mode", "s"}; + + for (const auto& suffix : suffixes) { + if (enumName.size() > suffix.size() && endsWith(enumName, suffix)) { + return enumName.substr(0, enumName.size() - suffix.size()); + } + } + + return enumName; +} + +bool isNSComparisonResultOrderingName(const std::string& enumName, + const std::string& member) { + if (enumName != "NSComparisonResult") { + return false; + } + return member == "Ascending" || member == "Same" || member == "Descending"; +} + +Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, + const NativeApiSymbol& symbol) { + Object result(runtime); + if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { + return result; + } + + std::string enumName = symbol.name; + std::string strippedPrefix = stripEnumSuffix(enumName); + 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); + + std::string canonicalName = memberName != nullptr ? memberName : ""; + std::vector aliases; + aliases.push_back(canonicalName); + + if (!strippedPrefix.empty() && startsWith(canonicalName, strippedPrefix) && + canonicalName.size() > strippedPrefix.size()) { + aliases.push_back(canonicalName.substr(strippedPrefix.size())); + } else if (!strippedPrefix.empty() && + !startsWith(canonicalName, strippedPrefix)) { + aliases.push_back(strippedPrefix + canonicalName); + } + + if (startsWith(enumName, "NS") && !startsWith(canonicalName, "NS")) { + aliases.push_back(std::string("NS") + canonicalName); + } + + if (enumName == "NSStringCompareOptions" && + !endsWith(canonicalName, "Search")) { + aliases.push_back(canonicalName + "Search"); + aliases.push_back(std::string("NS") + canonicalName + "Search"); + } + + if (!startsWith(canonicalName, "k")) { + aliases.push_back(std::string("k") + enumName + canonicalName); + } + + if (isNSComparisonResultOrderingName(enumName, canonicalName)) { + aliases.push_back(std::string("Ordered") + canonicalName); + aliases.push_back(std::string("NSOrdered") + canonicalName); + } + + std::vector uniqueAliases; + std::unordered_set seenAliases; + for (const auto& alias : aliases) { + if (!alias.empty() && seenAliases.insert(alias).second) { + uniqueAliases.push_back(alias); + } + } + + for (const auto& alias : uniqueAliases) { + result.setProperty(runtime, alias.c_str(), static_cast(value)); + } + + char valueKey[32] = {}; + snprintf(valueKey, sizeof(valueKey), "%lld", static_cast(value)); + if (!result.hasProperty(runtime, valueKey)) { + std::string reverseName = + uniqueAliases.size() > 1 ? uniqueAliases[1] : canonicalName; + result.setProperty(runtime, valueKey, makeString(runtime, reverseName)); + } + } + 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: { + if (isValidMetadataStringOffset(metadata, offset)) { + auto stringOffset = metadata->getOffset(offset); + 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 prepareJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, const Value& arg, + size_t index, NativeApiJsiArgumentFrame& frame) { + ffi_type* ffiType = ffiTypeForJsiArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + convertJsiFfiArgument(runtime, bridge, type, arg, target, frame); +} + +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++) { + prepareJsiArgument(runtime, bridge, signature.argumentTypes[i], args[i], i, + 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, bridge->nativeInvocationInvoker(), [&]() { + 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, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); + function.setProperty( + runtime, "__nativeApiPointer", + static_cast(reinterpret_cast(pointer))); + function.setProperty( + runtime, "nativeAddress", + static_cast(reinterpret_cast(pointer))); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + 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); + + if (symbol.name == "NSApplicationMain" || + symbol.name == "UIApplicationMain") { + runtime.drainMicrotasks(); + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + 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; + } + if (symbol.name == "CFBagContainsValue" && + (returnType.kind == metagen::mdTypeChar || + returnType.kind == metagen::mdTypeUChar || + returnType.kind == metagen::mdTypeUInt8)) { + return *returnStorage.data() != 0; + } + 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, + Class dispatchSuperClass) { + 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); + Class lookupClass = dispatchSuperClass != Nil ? dispatchSuperClass : receiverClass; + Method method = receiverIsClass ? class_getClassMethod(lookupClass, selector) + : class_getInstanceMethod(lookupClass, 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); + } + signature->selectorName = selectorName; + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + const bool isNSErrorOutMethod = isNSErrorOutJsiMethodSignature(*signature); + if (isNSErrorOutMethod) { + size_t expected = signature->argumentTypes.size(); + if (count > expected || count + 1 < expected) { + throw facebook::jsi::JSError( + runtime, "Actual arguments count: \"" + std::to_string(count) + + "\". Expected: \"" + std::to_string(expected) + "\"."); + } + } + + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && count + 1 == signature->argumentTypes.size(); + NSError* implicitNSError = nil; + if (hasImplicitNSErrorOutArg) { + for (size_t i = 0; i < count; i++) { + prepareJsiArgument(runtime, bridge, signature->argumentTypes[i], args[i], i, + frame); + } + + size_t outArgIndex = signature->argumentTypes.size() - 1; + void* target = frame.storageAt(outArgIndex, sizeof(NSError**)); + NSError** implicitNSErrorOutArg = &implicitNSError; + *static_cast(target) = implicitNSErrorOutArg; + } else { + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + } + + std::vector values; + values.reserve(signature->argumentTypes.size() + 2); + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.push_back(&superReceiverPtr); + } else { + 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, bridge->nativeInvocationInvoker(), [&]() { +#if defined(__x86_64__) + bool isStret = signature->returnType.ffiType->size > 16 && + signature->returnType.ffiType->type == FFI_TYPE_STRUCT; + void* target = dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) + : FFI_FN(objc_msgSend)); + ffi_call(&signature->cif, target, returnStorage.data(), values.data()); +#else + ffi_call(&signature->cif, + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : 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 ((selectorName == "valueForKey:" || selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeAnyObject; + } + if (retainedReturn) { + returnType.returnOwned = true; + } + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw facebook::jsi::JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h index 0de3a7e2..fe315d60 100644 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h @@ -62,6 +62,7 @@ inline NativeApiJsiConfig MakeReactNativeNativeApiJsiConfig( config.metadataPath = metadataPath; config.metadataPtr = metadataPtr; config.globalName = globalName; + config.installGlobalSymbols = true; config.scheduler = std::make_shared( std::move(jsInvoker), std::move(uiInvoker)); return config; diff --git a/NativeScript/ffi/hermes/jsi/README.md b/NativeScript/ffi/hermes/jsi/README.md index 3fbad151..694b9203 100644 --- a/NativeScript/ffi/hermes/jsi/README.md +++ b/NativeScript/ffi/hermes/jsi/README.md @@ -3,6 +3,21 @@ This directory contains the Hermes-first JSI entrypoint for NativeScript Native API access. +The backend is split by FFI responsibility: + +- `NativeApiJsiBridge.inc` owns metadata indexing, symbol lookup, scheduler + state, and bridge lifetime caches. +- `NativeApiJsiHostObjects.inc` owns class, object, protocol, pointer, + reference, struct, and union host objects. +- `NativeApiJsiCallbacks.inc` owns signatures, libffi callback trampolines, + JS blocks, and native function pointer callback lifetime. +- `NativeApiJsiConversion.inc` owns JSI/native type conversion and the + `interop` helper surface. +- `NativeApiJsiInvocation.inc` owns constants, enums, C function calls, + function pointer calls, and Objective-C selector dispatch. +- `NativeApiJsiHostObject.inc` owns the public API host object exposed to JS. +- `NativeApiJsiInstall.inc` owns runtime/global installation. + The core installer is engine-host agnostic: ```cpp @@ -41,7 +56,7 @@ 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. +The remaining RN FFI-suite skip is the explicit `interop.addMethod` decorator +hook. JavaScript-defined Objective-C subclasses created through `.extend(...)` +use the JSI class-builder path and are covered by the React Native compatibility +suite. diff --git a/NativeScript/ffi/napi/Block.h b/NativeScript/ffi/napi/Block.h index cab8dc2c..4cd81962 100644 --- a/NativeScript/ffi/napi/Block.h +++ b/NativeScript/ffi/napi/Block.h @@ -20,7 +20,6 @@ class FunctionPointer { 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/napi/Block.mm b/NativeScript/ffi/napi/Block.mm index 0ba3bacd..3b8697af 100644 --- a/NativeScript/ffi/napi/Block.mm +++ b/NativeScript/ffi/napi/Block.mm @@ -8,7 +8,6 @@ #include #include #include -#include "HermesFastCallbackInfo.h" #include "Interop.h" #include "runtime/NativeScriptException.h" #include "ObjCBridge.h" @@ -84,9 +83,6 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) ref->dispatchLookupSignatureHash = 0; ref->dispatchId = 0; ref->preparedInvoker = nullptr; -#ifdef TARGET_ENGINE_HERMES - ref->hermesFrameDirectReturnInvoker = nullptr; -#endif } return nullptr; } @@ -99,17 +95,9 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) 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; } @@ -149,29 +137,6 @@ inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) 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) { @@ -236,9 +201,6 @@ napi_value callFunctionPointerAsCFunctionDirect(napi_env env, nativescript::Func } } - if (napi_value fastResult = tryFastConvertFunctionPointerReturn(env, cif, rvalue)) { - return fastResult; - } return cif->returnType->toJS(env, rvalue); } @@ -313,74 +275,9 @@ napi_value callFunctionPointerAsBlockDirect(napi_env env, nativescript::Function } } - 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); @@ -741,30 +638,6 @@ bool isObjCBlockObject(id obj) { } napi_value FunctionPointer::jsCallAsCFunction(napi_env env, napi_callback_info cbinfo) { -#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; - } - - 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 - FunctionPointer* ref = nullptr; size_t actualArgc = 16; napi_value stackArgs[16]; @@ -781,30 +654,6 @@ bool isObjCBlockObject(id obj) { } napi_value FunctionPointer::jsCallAsBlock(napi_env env, napi_callback_info cbinfo) { -#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; - } - - 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 - FunctionPointer* ref = nullptr; size_t actualArgc = 16; napi_value stackArgs[16]; diff --git a/NativeScript/ffi/napi/CFunction.h b/NativeScript/ffi/napi/CFunction.h index 2584cdaa..269cf98a 100644 --- a/NativeScript/ffi/napi/CFunction.h +++ b/NativeScript/ffi/napi/CFunction.h @@ -23,16 +23,11 @@ class CFunction { 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/napi/CFunction.mm b/NativeScript/ffi/napi/CFunction.mm index d48cece5..3f9d2671 100644 --- a/NativeScript/ffi/napi/CFunction.mm +++ b/NativeScript/ffi/napi/CFunction.mm @@ -9,9 +9,6 @@ #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" @@ -36,15 +33,6 @@ size_t getCifReturnStorageSize(Cif* cif) { return size != 0 ? size : sizeof(void*); } -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; -} - class CFunctionReturnStorage final { public: explicit CFunctionReturnStorage(Cif* cif) { @@ -300,10 +288,6 @@ 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; } @@ -319,17 +303,6 @@ 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; } @@ -371,42 +344,12 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { 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) { -#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); - } - - 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]; @@ -448,10 +391,6 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { ensureCFunctionDispatchLookup(func, cif); auto preparedInvoker = reinterpret_cast(func->preparedInvoker); auto napiInvoker = reinterpret_cast(func->napiInvoker); - auto engineDirectInvoker = - !cif->skipGeneratedNapiDispatch - ? reinterpret_cast(func->engineDirectInvoker) - : nullptr; MDFunctionFlag functionFlags = bridgeState->metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); @@ -480,8 +419,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; - if ((engineDirectInvoker != nullptr || - (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch)) && + if (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch && !isMainEntrypoint) { CFunctionReturnStorage returnStorage(cif); if (!returnStorage.isValid()) { @@ -493,25 +431,17 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { void* rvalue = returnStorage.rvalue(); @try { NativeCallRuntimeUnlockScope unlockRuntime(env); - bool invoked = engineDirectInvoker != nullptr - ? engineDirectInvoker(env, cif, func->fnptr, invocationArgs, rvalue) - : napiInvoker(env, cif, func->fnptr, invocationArgs, rvalue); + bool invoked = napiInvoker(env, cif, func->fnptr, invocationArgs, rvalue); if (!invoked) { return nullptr; } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; - NSLog(@"ObjC->JS: Exception in CFunction (direct): %s", message.c_str()); + NSLog(@"ObjC->JS: Exception in CFunction (napi): %s", message.c_str()); nativescript::NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); return nullptr; } - - napi_value fastResult = nullptr; - if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, - &fastResult)) { - return fastResult; - } return cif->returnType->toJS(env, rvalue, toJSFlags); } @@ -595,11 +525,6 @@ 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/napi/Cif.h b/NativeScript/ffi/napi/Cif.h index 3614d6ab..6e4d5679 100644 --- a/NativeScript/ffi/napi/Cif.h +++ b/NativeScript/ffi/napi/Cif.h @@ -23,7 +23,6 @@ class Cif { bool skipGeneratedNapiDispatch = false; bool generatedDispatchHasRoundTripCacheArgument = false; bool generatedDispatchUsesObjectReturnStorage = false; - bool generatedDispatchSetsV8ReturnDirectly = false; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/napi/Cif.mm b/NativeScript/ffi/napi/Cif.mm index db8ae6dd..dfe18c8c 100644 --- a/NativeScript/ffi/napi/Cif.mm +++ b/NativeScript/ffi/napi/Cif.mm @@ -93,29 +93,6 @@ inline bool typeKindMayUseRoundTripCache(MDTypeKind kind) { } } -inline bool typeKindCanSetV8ReturnDirectly(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; - } -} - inline void updateGeneratedNapiDispatchCompatibility(Cif* cif) { if (cif == nullptr) { return; @@ -124,13 +101,10 @@ inline void updateGeneratedNapiDispatchCompatibility(Cif* cif) { cif->skipGeneratedNapiDispatch = 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); diff --git a/NativeScript/ffi/napi/ClassMember.h b/NativeScript/ffi/napi/ClassMember.h index feb81482..814e3496 100644 --- a/NativeScript/ffi/napi/ClassMember.h +++ b/NativeScript/ffi/napi/ClassMember.h @@ -33,16 +33,8 @@ 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() {} @@ -134,15 +126,6 @@ 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/napi/ClassMember.mm b/NativeScript/ffi/napi/ClassMember.mm index 63fa6221..e7bc296a 100644 --- a/NativeScript/ffi/napi/ClassMember.mm +++ b/NativeScript/ffi/napi/ClassMember.mm @@ -14,10 +14,7 @@ #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" @@ -348,45 +345,24 @@ 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; } } - ObjCEngineDirectInvoker engineInvoker = nullptr; - if (!cif->skipGeneratedNapiDispatch) { - engineInvoker = - descriptor != nullptr - ? reinterpret_cast(descriptor->engineDirectInvoker) - : lookupObjCEngineDirectInvoker(composeSignatureDispatchId( - cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); - } ObjCNapiInvoker invoker = - engineInvoker == nullptr && !cif->skipGeneratedNapiDispatch + !cif->skipGeneratedNapiDispatch ? (descriptor != nullptr ? reinterpret_cast(descriptor->napiInvoker) : lookupObjCNapiInvoker(composeSignatureDispatchId( cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags))) : nullptr; - if (engineInvoker == nullptr && invoker == nullptr) { + if (invoker == nullptr) { return true; } @try { 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); + bool invoked = invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue); if (!invoked) { return false; } @@ -446,18 +422,6 @@ 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; } @@ -1466,20 +1430,6 @@ 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; - } - } -#endif - napi_value jsThis; ObjCClassMember* method = nullptr; @@ -1742,11 +1692,6 @@ explicit CifReturnStorage(Cif* cif) { } } - napi_value fastResult = nullptr; - if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, - nativeResult, &fastResult)) { - return fastResult; - } return cif->returnType->toJS(env, nativeResult, method->returnOwned ? kReturnOwned : 0); }; @@ -1861,19 +1806,6 @@ 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; - } - } -#endif - napi_value jsThis; ObjCClassMember* method = nullptr; @@ -1955,11 +1887,6 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa method->returnOwned ? kOwnedObject : kUnownedObject); } - napi_value fastResult = nullptr; - if (TryFastConvertEngineReturnValue(env, cif->returnType->kind, rvalue, - &fastResult)) { - return fastResult; - } return cif->returnType->toJS(env, rvalue, 0); } @@ -1973,20 +1900,6 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } 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; - } - } -#endif - napi_value jsThis, argv; size_t argc = 1; ObjCClassMember* method = nullptr; diff --git a/NativeScript/ffi/napi/ObjCBridge.h b/NativeScript/ffi/napi/ObjCBridge.h index 9008da0b..6e7eb19a 100644 --- a/NativeScript/ffi/napi/ObjCBridge.h +++ b/NativeScript/ffi/napi/ObjCBridge.h @@ -948,7 +948,6 @@ class ObjCBridgeState { napi_ref referenceClass = nullptr; napi_ref functionReferenceClass = nullptr; napi_ref createNativeProxy = nullptr; - napi_ref createNativeFastArrayIndexes = nullptr; napi_ref createFastEnumeratorIterator = nullptr; napi_ref transferOwnershipToNative = nullptr; diff --git a/NativeScript/ffi/napi/ObjCBridge.mm b/NativeScript/ffi/napi/ObjCBridge.mm index 0f911c5a..0c7ebd76 100644 --- a/NativeScript/ffi/napi/ObjCBridge.mm +++ b/NativeScript/ffi/napi/ObjCBridge.mm @@ -45,9 +45,6 @@ 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) { @@ -897,7 +894,6 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat deleteRef(referenceClass); deleteRef(functionReferenceClass); deleteRef(createNativeProxy); - deleteRef(createNativeFastArrayIndexes); deleteRef(createFastEnumeratorIterator); deleteRef(transferOwnershipToNative); @@ -960,46 +956,9 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_value ObjCBridgeState::proxyNativeObject(napi_env env, napi_value object, id nativeObject) { NAPI_PREAMBLE -#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); @@ -1009,7 +968,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat 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); diff --git a/NativeScript/ffi/napi/Object.mm b/NativeScript/ffi/napi/Object.mm index 4c532721..5283280e 100644 --- a/NativeScript/ffi/napi/Object.mm +++ b/NativeScript/ffi/napi/Object.mm @@ -3,9 +3,6 @@ #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" @@ -395,69 +392,12 @@ function bindTargetMethod(target, name) { }) )"; -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); @@ -550,18 +490,10 @@ 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; diff --git a/NativeScript/ffi/napi/SignatureDispatch.h b/NativeScript/ffi/napi/SignatureDispatch.h new file mode 100644 index 00000000..c42a8237 --- /dev/null +++ b/NativeScript/ffi/napi/SignatureDispatch.h @@ -0,0 +1,223 @@ +#ifndef NS_FFI_NAPI_SIGNATURE_DISPATCH_H +#define NS_FFI_NAPI_SIGNATURE_DISPATCH_H + +#include + +#include +#include +#include + +#include "Cif.h" +#include "js_native_api.h" + +namespace nativescript { + +enum class SignatureCallKind : uint8_t { + ObjCMethod = 1, + CFunction = 2, + BlockInvoke = 3, +}; + +using ObjCPreparedInvoker = void (*)(void* fnptr, void** avalues, void* rvalue); +using CFunctionPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); +using BlockPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); +using ObjCNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, + SEL selector, const napi_value* argv, + void* rvalue); +using CFunctionNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, + const napi_value* argv, void* rvalue); + +struct ObjCDispatchEntry { + uint64_t dispatchId; + ObjCPreparedInvoker invoker; +}; + +struct CFunctionDispatchEntry { + uint64_t dispatchId; + CFunctionPreparedInvoker invoker; +}; + +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + +struct ObjCNapiDispatchEntry { + uint64_t dispatchId; + ObjCNapiInvoker invoker; +}; + +struct CFunctionNapiDispatchEntry { + uint64_t dispatchId; + CFunctionNapiInvoker invoker; +}; + +inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; +inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; + +inline uint64_t hashBytesFnv1a(const void* data, size_t size, + uint64_t seed = kSignatureHashOffsetBasis) { + const auto* bytes = static_cast(data); + uint64_t hash = seed; + for (size_t i = 0; i < size; i++) { + hash ^= static_cast(bytes[i]); + hash *= kSignatureHashPrime; + } + return hash; +} + +inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, + SignatureCallKind kind, + uint8_t flags) { + const uint8_t kindByte = static_cast(kind); + uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); + hash = hashBytesFnv1a(&flags, sizeof(flags), hash); + return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); +} + +} // namespace nativescript + +#ifndef NS_GSD_BACKEND_NAPI +#define NS_GSD_BACKEND_NAPI 1 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 0 +#endif + +#ifndef NS_GSD_BACKEND_V8 +#define NS_GSD_BACKEND_V8 0 +#endif + +#ifndef NS_GSD_BACKEND_JSC +#define NS_GSD_BACKEND_JSC 0 +#endif + +#ifndef NS_GSD_BACKEND_QUICKJS +#define NS_GSD_BACKEND_QUICKJS 0 +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#define NS_GSD_BACKEND_HERMES 0 +#endif + +#ifndef NS_GSD_BACKEND_ENGINE_DIRECT +#define NS_GSD_BACKEND_ENGINE_DIRECT 0 +#endif + +#if defined(__has_include) +#if __has_include("GeneratedSignatureDispatch.inc") +#include "GeneratedSignatureDispatch.inc" +#endif +#endif + +#if !NS_HAS_GENERATED_SIGNATURE_DISPATCH +namespace nativescript { +inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { + {0, nullptr}}; +inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { + {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; +} // namespace nativescript +#endif + +#if !NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH +namespace nativescript { +inline constexpr ObjCNapiDispatchEntry kGeneratedObjCNapiDispatchEntries[] = { + {0, nullptr}}; +inline constexpr CFunctionNapiDispatchEntry + kGeneratedCFunctionNapiDispatchEntries[] = {{0, nullptr}}; +} // namespace nativescript +#endif + +namespace nativescript { + +template +inline Invoker lookupDispatchInvoker(const Entry (&entries)[N], + uint64_t dispatchId) { + if (dispatchId == 0 || N <= 1) { + return nullptr; + } + + size_t low = 1; + size_t high = N; + while (low < high) { + const size_t mid = low + ((high - low) >> 1); + const uint64_t midId = entries[mid].dispatchId; + if (midId < dispatchId) { + low = mid + 1; + } else { + high = mid; + } + } + + if (low < N && entries[low].dispatchId == dispatchId) { + return entries[low].invoker; + } + return nullptr; +} + +inline bool isGeneratedDispatchEnabled() { + static const bool enabled = []() { + const char* disableFlag = std::getenv("NS_DISABLE_GSD"); + if (disableFlag == nullptr || disableFlag[0] == '\0') { + return true; + } + return !(disableFlag[0] == '0' && disableFlag[1] == '\0'); + }(); + return enabled; +} + +inline ObjCPreparedInvoker lookupObjCPreparedInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCDispatchEntries, dispatchId); +} + +inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( + uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + 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; + } + return lookupDispatchInvoker( + kGeneratedObjCNapiDispatchEntries, dispatchId); +} + +inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionNapiDispatchEntries, dispatchId); +} + +} // namespace nativescript + +#endif // NS_FFI_NAPI_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/napi/TypeConv.h b/NativeScript/ffi/napi/TypeConv.h index 82246f19..e3d024f2 100644 --- a/NativeScript/ffi/napi/TypeConv.h +++ b/NativeScript/ffi/napi/TypeConv.h @@ -7,9 +7,6 @@ #include "ffi.h" #include "js_native_api.h" #include "objc/runtime.h" -#ifdef TARGET_ENGINE_V8 -#include -#endif using namespace metagen; @@ -61,154 +58,6 @@ 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/napi/engine/hermes/HermesFastCallbackInfo.h b/NativeScript/ffi/napi/engine/hermes/HermesFastCallbackInfo.h deleted file mode 100644 index 93665e3b..00000000 --- a/NativeScript/ffi/napi/engine/hermes/HermesFastCallbackInfo.h +++ /dev/null @@ -1,83 +0,0 @@ -#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) { - if (info == nullptr || info->frame == nullptr || - info->frame->thisArgAndArgsBase == nullptr) { - return nullptr; - } - 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/napi/engine/hermes/HermesFastConversion.mm b/NativeScript/ffi/napi/engine/hermes/HermesFastConversion.mm deleted file mode 100644 index a53bd212..00000000 --- a/NativeScript/ffi/napi/engine/hermes/HermesFastConversion.mm +++ /dev/null @@ -1,1315 +0,0 @@ -#include "HermesFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_HERMES - -namespace nativescript { -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 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] autorelease]; - 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, kHermesNativePointerProperty, - &hasNativePointer) == napi_ok && - hasNativePointer) { - napi_value nativePointerValue = nullptr; - if (napi_get_named_property(env, cachedValue, kHermesNativePointerProperty, - &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; -} - - -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)); -} - -} // namespace - -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); -} - -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 && - lossless; -} - -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 && - lossless; -} - -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; - } - - const bool isNSStringKind = - kind == mdTypeNSStringObject || kind == mdTypeNSMutableStringObject; - if (kind != mdTypeClass && !isNSStringKind) { - 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, kHermesNativePointerProperty, - &hasNativePointer) == napi_ok && - hasNativePointer) { - napi_value nativePointerValue = nullptr; - if (napi_get_named_property(env, value, kHermesNativePointerProperty, - &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); -} - -} // namespace nativescript - -#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h deleted file mode 100644 index 576c62b2..00000000 --- a/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.h +++ /dev/null @@ -1,41 +0,0 @@ -#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/napi/engine/hermes/HermesFastNativeApi.mm b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.mm deleted file mode 100644 index 6f1079ca..00000000 --- a/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApi.mm +++ /dev/null @@ -1,946 +0,0 @@ -#include "HermesFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_HERMES - -namespace nativescript { -namespace { -inline bool canUseHermesFrameArgument(MDTypeKind kind) { - switch (kind) { - 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: - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - case mdTypeClass: - case mdTypeSelector: - return true; - default: - return false; - } -} - -inline bool canUseHermesFrameArguments(Cif* cif) { - if (cif == nullptr) { - return false; - } - - for (const auto& argType : cif->argTypes) { - if (argType == nullptr || !canUseHermesFrameArgument(argType->kind)) { - return false; - } - } - - return true; -} - -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, kHermesNativePointerProperty, - &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; -} - -} // namespace - -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 (hermesArgsBase != nullptr && !canUseHermesFrameArguments(cif)) { - 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 (!cif->isVariadic && actualArgc != cif->argc) { - 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) { - EngineDirectRoundTripCacheFrameGuard 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)) { - if (handled != nullptr) { - *handled = true; - } - 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) { - EngineDirectRoundTripCacheFrameGuard 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)) { - if (handled != nullptr) { - *handled = true; - } - return directResult; - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - nativeScriptException.ReThrowToJS(env); - return nullptr; - } - } - - EngineDirectReturnStorage 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; - } - - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( - env, member->bridgeState, needsRoundTripFrame); - - ObjCEngineDirectInvoker invoker = - !cif->skipGeneratedNapiDispatch && 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; - } - - if (handled != nullptr) { - *handled = true; - } - - 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; - } - - if (hermesArgsBase != nullptr && !canUseHermesFrameArguments(cif)) { - return nullptr; - } - - if (actualArgc != cif->argc) { - return nullptr; - } - - const bool hasExactHermesFrameArgs = - hermesArgsBase != nullptr && actualArgc == cif->argc; - CFunctionHermesFrameDirectReturnInvoker frameDirectReturnInvoker = - hasExactHermesFrameArgs && cif->signatureHash != 0 - ? ensureHermesCFunctionFrameDirectReturnInvoker(function, cif) - : nullptr; - - if (frameDirectReturnInvoker != nullptr) { - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( - env, bridgeState, needsRoundTripCacheFrame(cif)); - - napi_value directResult = nullptr; - @try { - NativeCallRuntimeUnlockScope unlockRuntime(env); - if (frameDirectReturnInvoker(env, cif, function->fnptr, hermesArgsBase, - &directResult)) { - if (handled != nullptr) { - *handled = true; - } - 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) { - EngineDirectRoundTripCacheFrameGuard 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)) { - if (handled != nullptr) { - *handled = true; - } - return directResult; - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - nativeScriptException.ReThrowToJS(env); - return nullptr; - } - } - - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( - env, bridgeState, needsRoundTripCacheFrame(cif)); - - bool didInvoke = false; - CFunctionEngineDirectInvoker invoker = - !cif->skipGeneratedNapiDispatch && cif->signatureHash != 0 - ? ensureHermesCFunctionEngineDirectInvoker(function, cif) - : nullptr; - EngineDirectReturnStorage returnStorage(cif); - void* perCallRValue = returnStorage.get(); - if (!returnStorage.valid()) { - napi_throw_error(env, "NativeScriptException", - "Unable to allocate C function return storage."); - return nullptr; - } - @try { - const napi_value* invocationArgs = getPreparedInvocationArgs(); - NativeCallRuntimeUnlockScope unlockRuntime(env); - if (invoker != nullptr) { - didInvoke = - invoker(env, cif, function->fnptr, invocationArgs, perCallRValue); - } 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, perCallRValue); - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - nativeScriptException.ReThrowToJS(env); - return nullptr; - } - - if (!didInvoke) { - return nullptr; - } - - if (handled != nullptr) { - *handled = true; - } - - return makeHermesCFunctionReturnValue(env, function, cif, perCallRValue); -} - -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/napi/engine/hermes/HermesFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h deleted file mode 100644 index 57839180..00000000 --- a/NativeScript/ffi/napi/engine/hermes/HermesFastNativeApiPrivate.h +++ /dev/null @@ -1,111 +0,0 @@ -#ifndef NS_HERMES_FAST_NATIVE_API_PRIVATE_H -#define NS_HERMES_FAST_NATIVE_API_PRIVATE_H - -#include "HermesFastNativeApi.h" - -#ifdef TARGET_ENGINE_HERMES - -#import -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/CallbackThreading.h" -#include "ffi/napi/Class.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "ffi/napi/Interop.h" -#include "InvocationSupport.h" -#include "runtime/NativeScriptException.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/TypeConv.h" - -namespace nativescript { - -inline constexpr const char* kHermesNativePointerProperty = "__ns_native_ptr"; -inline constexpr uint64_t kHermesFirstTaggedValue = 0xfff9000000000000ULL; -inline constexpr uint64_t kHermesBoolETag = 0x1fff6ULL; -inline 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 = hermesRawValueBits(value); - if (!isHermesNumber(raw)) { - return false; - } - - *result = hermesRawDoubleIsFinite(raw) ? hermesRawToDouble(raw) : 0.0; - return true; -} - -inline napi_value makeHermesRawValue(Cif* cif, uint64_t raw) { - (void)cif; - 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)); -} - -napi_value makeHermesObjCReturnValue(napi_env env, ObjCClassMember* member, - MethodDescriptor* descriptor, Cif* cif, - id self, bool receiverIsClass, - napi_value jsThis, void* rvalue, - bool propertyAccess); - -napi_value makeHermesCFunctionReturnValue(napi_env env, CFunction* function, - Cif* cif, void* rvalue); - -} // namespace nativescript - -#endif // TARGET_ENGINE_HERMES - -#endif // NS_HERMES_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm b/NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm deleted file mode 100644 index 23adb4be..00000000 --- a/NativeScript/ffi/napi/engine/jsc/JSCFastConversion.mm +++ /dev/null @@ -1,1144 +0,0 @@ -#include "JSCFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_JSC - -namespace nativescript { -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 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 = JSValueMakeUndefined(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; -} - -} // namespace - -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; -} - -namespace { - -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 = JSValueMakeUndefined(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; -} - -} // namespace nativescript - -#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h deleted file mode 100644 index 1c4010ce..00000000 --- a/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef NS_JSC_FAST_NATIVE_API_H -#define NS_JSC_FAST_NATIVE_API_H - -#include "js_native_api.h" - -#ifdef __cplusplus - -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 // __cplusplus - -#endif // NS_JSC_FAST_NATIVE_API_H diff --git a/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm deleted file mode 100644 index 68b6446c..00000000 --- a/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApi.mm +++ /dev/null @@ -1,763 +0,0 @@ -#include "JSCFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_JSC - -namespace nativescript { -namespace { -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; -} - - -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) { - bool shouldUseClassFallback = member->classMethod; - napi_valuetype jsType = napi_undefined; - if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && - jsType == napi_function) { - bool isSameConstructor = true; - napi_value definingConstructor = nullptr; - if (member->cls->constructor != nullptr) { - napi_get_reference_value(env, member->cls->constructor, - &definingConstructor); - } - if (definingConstructor != nullptr && - napi_strict_equals(env, jsThis, definingConstructor, - &isSameConstructor) == napi_ok && - !isSameConstructor) { - shouldUseClassFallback = false; - } else { - shouldUseClassFallback = true; - } - } - - if (member->classMethod) { - if (shouldUseClassFallback) { - return static_cast(member->cls->nativeClass); - } - return nil; - } - - if (shouldUseClassFallback) { - 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); -} - - -JSCEngineDirectResult 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 JSCEngineDirectResult::NotHandled; - } - - MethodDescriptor* descriptor = nullptr; - Cif* cif = jscMemberCif(env, member, kind, &descriptor); - if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { - return JSCEngineDirectResult::NotHandled; - } - - 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 JSCEngineDirectResult::NotHandled; - } - - id self = resolveJSCSelf(env, jsThis, member); - if (self == nil) { - return JSCEngineDirectResult::NotHandled; - } - - const bool receiverIsClass = object_isClass(self); - Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); - if (receiverClassRequiresJSCSuperCall(receiverClass)) { - return JSCEngineDirectResult::NotHandled; - } - - EngineDirectReturnStorage rvalueStorage(cif); - if (!rvalueStorage.valid()) { - return JSCEngineDirectResult::NotHandled; - } - - void* rvalue = rvalueStorage.get(); - EngineDirectRoundTripCacheFrameGuard 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 JSCEngineDirectResult::Failed; - } - - if (!didInvoke) { - if (invoker == nullptr && env->last_exception != nullptr) { - return JSCEngineDirectResult::Failed; - } - return JSCEngineDirectResult::NotHandled; - } - - if (!makeJSCObjCReturnValue(env, member, descriptor, cif, self, - receiverIsClass, jsThis, rvalue, - kind != EngineDirectMemberKind::Method, result)) { - return JSCEngineDirectResult::Failed; - } - - return JSCEngineDirectResult::Handled; -} - -JSCEngineDirectResult tryCallJSCCFunctionEngineDirect( - napi_env env, MDSectionOffset offset, size_t argc, const napi_value* argv, - JSValueRef* result) { - if (env == nullptr || result == nullptr) { - return JSCEngineDirectResult::NotHandled; - } - - ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); - if (bridgeState == nullptr || - isCompatCFunction(env, reinterpret_cast( - static_cast(offset)))) { - return JSCEngineDirectResult::NotHandled; - } - - CFunction* function = bridgeState->getCFunction(env, offset); - Cif* cif = function != nullptr ? function->cif : nullptr; - if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->returnType == nullptr) { - return JSCEngineDirectResult::NotHandled; - } - - const bool canUseGeneratedInvoker = - cif->signatureHash != 0 && argc == cif->argc; - CFunctionEngineDirectInvoker invoker = canUseGeneratedInvoker - ? ensureJSCCFunctionEngineDirectInvoker(function, cif) - : nullptr; - - bool didInvoke = false; - EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame(env, bridgeState, cif); - EngineDirectReturnStorage rvalueStorage(cif); - if (!rvalueStorage.valid()) { - return JSCEngineDirectResult::NotHandled; - } - void* rvalue = rvalueStorage.get(); - @try { - if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, argv, rvalue); - } else { - didInvoke = InvokeCFunctionEngineDirectDynamic( - env, function, cif, argc, argv, rvalue); - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - NativeScriptException nativeScriptException(message); - nativeScriptException.ReThrowToJS(env); - return JSCEngineDirectResult::Failed; - } - - if (!didInvoke) { - if (invoker == nullptr && env->last_exception != nullptr) { - return JSCEngineDirectResult::Failed; - } - return JSCEngineDirectResult::NotHandled; - } - - if (!makeJSCCFunctionReturnValue(env, function, cif, rvalue, result)) { - return JSCEngineDirectResult::Failed; - } - - return JSCEngineDirectResult::Handled; -} - -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; - JSCEngineDirectResult directCallResult = JSCEngineDirectResult::NotHandled; - switch (binding->kind) { - case JSCFastNativeKind::ObjCMethod: - directCallResult = tryCallJSCObjCEngineDirect( - env, static_cast(binding->data), jsThis, - argumentCount, argv, EngineDirectMemberKind::Method, - &directResult); - break; - case JSCFastNativeKind::ObjCGetter: - directCallResult = 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); - directCallResult = tryCallJSCObjCEngineDirect( - env, static_cast(binding->data), jsThis, 1, - &value, EngineDirectMemberKind::Setter, &directResult); - break; - } - case JSCFastNativeKind::CFunction: - directCallResult = tryCallJSCCFunctionEngineDirect( - env, - static_cast( - reinterpret_cast(binding->data)), - argumentCount, argv, &directResult); - break; - default: - break; - } - - if (directCallResult == JSCEngineDirectResult::Handled) { - 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); - } - - if (directCallResult == JSCEngineDirectResult::Failed) { - if (env->last_exception == nullptr) { - napi_throw_error(env, "NativeScriptException", - "NativeScript fast native call failed."); - } - if (exception != nullptr) { - *exception = env->last_exception; - } - env->last_exception = nullptr; - return JSValueMakeUndefined(ctx); - } - - env->last_exception = nullptr; - - 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; -} - -} // namespace - -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/napi/engine/jsc/JSCFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h deleted file mode 100644 index 80951639..00000000 --- a/NativeScript/ffi/napi/engine/jsc/JSCFastNativeApiPrivate.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef NS_JSC_FAST_NATIVE_API_PRIVATE_H -#define NS_JSC_FAST_NATIVE_API_PRIVATE_H - -#include "JSCFastNativeApi.h" - -#ifdef TARGET_ENGINE_JSC - -#import - -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "EngineDirectCall.h" -#include "InvocationSupport.h" -#include "ffi/napi/Interop.h" -#include "MetadataReader.h" -#include "runtime/NativeScriptException.h" -#include "ffi/napi/Object.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/TypeConv.h" -#include "jsc-api.h" - -namespace nativescript { - -enum class JSCFastNativeKind : uint8_t { - ObjCMethod = 1, - ObjCGetter = 2, - ObjCSetter = 3, - ObjCReadOnlySetter = 4, - CFunction = 5, -}; - -enum class JSCEngineDirectResult { - NotHandled, - Handled, - Failed, -}; - -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 makeJSCObjCReturnValue(napi_env env, ObjCClassMember* member, - MethodDescriptor* descriptor, Cif* cif, id self, - bool receiverIsClass, napi_value jsThis, - void* rvalue, bool propertyAccess, - JSValueRef* result); - -bool makeJSCCFunctionReturnValue(napi_env env, CFunction* function, Cif* cif, - void* rvalue, JSValueRef* result); - -} // namespace nativescript - -#endif // TARGET_ENGINE_JSC - -#endif // NS_JSC_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm deleted file mode 100644 index 797fac18..00000000 --- a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastConversion.mm +++ /dev/null @@ -1,680 +0,0 @@ -#include "QuickJSFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_QUICKJS - -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; -} - -} // namespace - -bool TryUnwrapQuickJSNativeObjectFast(napi_env env, JSValue jsValue, - void** result) { - return tryFastUnwrapQuickJSNativeObject(env, jsValue, result); -} - -namespace { - -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 byteLength = 0; - const char* str = JS_ToCStringLen(env->context, &byteLength, jsValue); - if (str == nullptr) { - return false; - } - - NSString* string = [[NSString alloc] initWithBytes:str - length:byteLength - encoding:NSUTF8StringEncoding]; - JS_FreeCString(env->context, str); - - if (string == nil || [string length] != 1) { - [string release]; - napi_throw_type_error(env, nullptr, "Expected a single-character string."); - return false; - } - - *result = static_cast([string characterAtIndex:0]); - [string release]; - 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_UNDEFINED; - 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 - -#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h deleted file mode 100644 index 042a8761..00000000 --- a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.h +++ /dev/null @@ -1,20 +0,0 @@ -#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/napi/engine/quickjs/QuickJSFastNativeApi.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm deleted file mode 100644 index 0638d6ad..00000000 --- a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApi.mm +++ /dev/null @@ -1,750 +0,0 @@ -#include "QuickJSFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_QUICKJS - -namespace { -JSValue throwQuickJSPendingException(JSContext* context, const char* message) { - if (context == nullptr) { - return JS_EXCEPTION; - } - if (JS_HasException(context)) { - return JS_Throw(context, JS_GetException(context)); - } - return JS_ThrowInternalError(context, "%s", message); -} - -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 normalizeQuickJSWrappedReceiver(napi_env env, void* wrapped) { - if (wrapped == nullptr) { - return nil; - } - - auto* state = nativescript::ObjCBridgeState::InstanceData(env); - if (state != nullptr) { - id nativeObject = state->nativeObjectForBridgeWrapper(wrapped); - if (nativeObject != nil) { - return nativeObject; - } - - for (const auto& entry : state->classes) { - auto* bridgedClass = entry.second; - if (bridgedClass == wrapped && bridgedClass->nativeClass != nil) { - return static_cast(bridgedClass->nativeClass); - } - } - - for (const auto& entry : state->protocols) { - auto* bridgedProtocol = entry.second; - if (bridgedProtocol == wrapped) { - if (Protocol* runtimeProtocol = - objc_getProtocol(bridgedProtocol->name.c_str())) { - return static_cast(runtimeProtocol); - } - break; - } - } - } - - return static_cast(wrapped); -} - -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 (nativescript::TryUnwrapQuickJSNativeObjectFast( - env, ToJSValue(jsThis), &wrapped) && - wrapped != nullptr) { - return normalizeQuickJSWrappedReceiver(env, 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) { - bool shouldUseClassFallback = member->classMethod; - napi_valuetype jsType = napi_undefined; - if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && - jsType == napi_function) { - bool isSameConstructor = true; - napi_value definingConstructor = nullptr; - if (member->cls->constructor != nullptr) { - napi_get_reference_value(env, member->cls->constructor, - &definingConstructor); - } - if (definingConstructor != nullptr && - napi_strict_equals(env, jsThis, definingConstructor, - &isSameConstructor) == napi_ok && - !isSameConstructor) { - shouldUseClassFallback = false; - } else { - shouldUseClassFallback = true; - } - } - - if (member->classMethod) { - if (shouldUseClassFallback) { - return static_cast(member->cls->nativeClass); - } - return nil; - } - - if (shouldUseClassFallback) { - 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); -} - -QuickJSEngineDirectResult 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 QuickJSEngineDirectResult::NotHandled; - } - - nativescript::MethodDescriptor* descriptor = nullptr; - nativescript::Cif* cif = quickJSMemberCif(env, member, kind, &descriptor); - if (cif == nullptr || cif->isVariadic || cif->returnType == nullptr) { - return QuickJSEngineDirectResult::NotHandled; - } - - 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 QuickJSEngineDirectResult::NotHandled; - } - - id self = resolveQuickJSSelf(env, jsThis, member); - if (self == nil) { - return QuickJSEngineDirectResult::NotHandled; - } - - const bool receiverIsClass = object_isClass(self); - Class receiverClass = receiverIsClass ? static_cast(self) : object_getClass(self); - if (receiverClassRequiresQuickJSSuperCall(receiverClass)) { - return QuickJSEngineDirectResult::NotHandled; - } - - nativescript::EngineDirectReturnStorage rvalueStorage(cif); - if (!rvalueStorage.valid()) { - return QuickJSEngineDirectResult::NotHandled; - } - - void* rvalue = rvalueStorage.get(); - nativescript::EngineDirectRoundTripCacheFrameGuard 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 QuickJSEngineDirectResult::Failed; - } - - if (!didInvoke) { - if (invoker == nullptr && JS_HasException(context)) { - return QuickJSEngineDirectResult::Failed; - } - return QuickJSEngineDirectResult::NotHandled; - } - - if (!makeQuickJSObjCReturnValue( - context, env, member, descriptor, cif, self, receiverIsClass, - jsThis, rvalue, kind != nativescript::EngineDirectMemberKind::Method, - result)) { - return QuickJSEngineDirectResult::Failed; - } - - return QuickJSEngineDirectResult::Handled; -} - -QuickJSEngineDirectResult 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 QuickJSEngineDirectResult::NotHandled; - } - - auto* bridgeState = nativescript::ObjCBridgeState::InstanceData(env); - if (bridgeState == nullptr || isCompatCFunction(env, reinterpret_cast( - static_cast(offset)))) { - return QuickJSEngineDirectResult::NotHandled; - } - - auto* function = bridgeState->getCFunction(env, offset); - auto* cif = function != nullptr ? function->cif : nullptr; - if (function == nullptr || cif == nullptr || cif->isVariadic || - cif->returnType == nullptr) { - return QuickJSEngineDirectResult::NotHandled; - } - - const bool canUseGeneratedInvoker = - cif->signatureHash != 0 && static_cast(argc) == cif->argc; - auto invoker = canUseGeneratedInvoker - ? ensureQuickJSCFunctionEngineDirectInvoker(function, cif) - : nullptr; - - bool didInvoke = false; - nativescript::EngineDirectRoundTripCacheFrameGuard roundTripCacheFrame( - env, bridgeState, cif); - nativescript::EngineDirectReturnStorage rvalueStorage(cif); - if (!rvalueStorage.valid()) { - return QuickJSEngineDirectResult::NotHandled; - } - void* rvalue = rvalueStorage.get(); - @try { - if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, argv, rvalue); - } else { - didInvoke = nativescript::InvokeCFunctionEngineDirectDynamic( - env, function, cif, static_cast(argc), argv, rvalue); - } - } @catch (NSException* exception) { - std::string message = exception.description.UTF8String; - nativescript::NativeScriptException nativeScriptException(message); - nativeScriptException.ReThrowToJS(env); - return QuickJSEngineDirectResult::Failed; - } - - if (!didInvoke) { - if (invoker == nullptr && JS_HasException(context)) { - return QuickJSEngineDirectResult::Failed; - } - return QuickJSEngineDirectResult::NotHandled; - } - - if (!makeQuickJSCFunctionReturnValue(context, env, function, cif, rvalue, - result)) { - return QuickJSEngineDirectResult::Failed; - } - - return QuickJSEngineDirectResult::Handled; -} - -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; - QuickJSEngineDirectResult directResult = - QuickJSEngineDirectResult::NotHandled; - switch (magic) { - case kQuickJSFastObjCMethod: - directResult = tryCallQuickJSObjCEngineDirect( - context, env, static_cast(data), - jsThis, argc, napiArgs, - nativescript::EngineDirectMemberKind::Method, &directReturn); - break; - case kQuickJSFastObjCGetter: - directResult = 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); - directResult = tryCallQuickJSObjCEngineDirect( - context, env, static_cast(data), - jsThis, 1, &value, - nativescript::EngineDirectMemberKind::Setter, &directReturn); - break; - } - case kQuickJSFastCFunction: - directResult = tryCallQuickJSCFunctionEngineDirect( - context, env, - static_cast(reinterpret_cast(data)), - argc, napiArgs, &directReturn); - break; - default: - break; - } - - if (directResult == QuickJSEngineDirectResult::Handled) { - if (useGlobalValue) { - JS_FreeValue(context, effectiveThis); - } - if (JS_HasException(context)) { - JS_FreeValue(context, directReturn); - return JS_Throw(context, JS_GetException(context)); - } - return directReturn; - } - - if (directResult == QuickJSEngineDirectResult::Failed) { - if (useGlobalValue) { - JS_FreeValue(context, effectiveThis); - } - JS_FreeValue(context, directReturn); - return throwQuickJSPendingException( - context, "NativeScript fast native call failed."); - } - - if (JS_HasException(context)) { - JSValue staleException = JS_GetException(context); - JS_FreeValue(context, staleException); - } - - 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 - -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/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h deleted file mode 100644 index 259c6706..00000000 --- a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastNativeApiPrivate.h +++ /dev/null @@ -1,196 +0,0 @@ -#ifndef NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H -#define NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H - -#include "QuickJSFastNativeApi.h" - -#ifdef TARGET_ENGINE_QUICKJS - -#import - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "EngineDirectCall.h" -#include "InvocationSupport.h" -#include "ffi/napi/Interop.h" -#include "MetadataReader.h" -#include "runtime/NativeScriptException.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/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; -}; - -enum QuickJSFastNativeKind : int { - kQuickJSFastObjCMethod = 1, - kQuickJSFastObjCGetter = 2, - kQuickJSFastObjCSetter = 3, - kQuickJSFastObjCReadOnlySetter = 4, - kQuickJSFastCFunction = 5, -}; - -enum class QuickJSEngineDirectResult { - NotHandled, - Handled, - Failed, -}; - -inline JSValue ToJSValue(napi_value value) { - return value != nullptr ? *reinterpret_cast(value) : JS_UNDEFINED; -} - -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 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); - -bool makeQuickJSCFunctionReturnValue(JSContext* context, napi_env env, - nativescript::CFunction* function, - nativescript::Cif* cif, void* rvalue, - JSValue* result); - -namespace nativescript { - -bool TryUnwrapQuickJSNativeObjectFast(napi_env env, JSValue jsValue, - void** result); - -} // namespace nativescript - -#endif // TARGET_ENGINE_QUICKJS - -#endif // NS_QUICKJS_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm b/NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm deleted file mode 100644 index f8b70320..00000000 --- a/NativeScript/ffi/napi/engine/quickjs/QuickJSFastReturn.mm +++ /dev/null @@ -1,332 +0,0 @@ -#include "QuickJSFastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_QUICKJS - -namespace { - -bool makeQuickJSRawReturnValue(JSContext* context, MDTypeKind kind, - const void* value, JSValue* result) { - if (context == nullptr || result == nullptr) { - return false; - } - - switch (kind) { - case mdTypeVoid: - *result = JS_UNDEFINED; - 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; -} - -} // namespace - -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; -} - -#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/napi/engine/shared/EngineDirectCall.h b/NativeScript/ffi/napi/engine/shared/EngineDirectCall.h deleted file mode 100644 index 84e4c02a..00000000 --- a/NativeScript/ffi/napi/engine/shared/EngineDirectCall.h +++ /dev/null @@ -1,53 +0,0 @@ -#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/napi/engine/shared/EngineDirectCall.mm b/NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm deleted file mode 100644 index 0c6bf96b..00000000 --- a/NativeScript/ffi/napi/engine/shared/EngineDirectCall.mm +++ /dev/null @@ -1,1057 +0,0 @@ -#include "EngineDirectCall.h" - -#import -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/Class.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "ffi/napi/Interop.h" -#include "runtime/NativeScriptException.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/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; - CifReturnStorage rvalueStorage(cif); - if (!rvalueStorage.valid()) { - napi_throw_error(env, "NativeScriptException", - "Unable to allocate C function return storage."); - return nullptr; - } - void* rvalue = rvalueStorage.get(); - @try { - if (invoker != nullptr) { - didInvoke = invoker(env, cif, function->fnptr, invocationArgs, rvalue); - } else { - didInvoke = InvokeCFunctionEngineDirectDynamic( - env, function, cif, 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 convertCFunctionReturnValue(env, function, cif, rvalue); -} - -} // namespace nativescript diff --git a/NativeScript/ffi/napi/engine/shared/InvocationSupport.h b/NativeScript/ffi/napi/engine/shared/InvocationSupport.h deleted file mode 100644 index 650c4243..00000000 --- a/NativeScript/ffi/napi/engine/shared/InvocationSupport.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H -#define NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H - -#include -#include -#include - -#include "ffi/napi/Cif.h" -#include "ffi/napi/ObjCBridge.h" - -namespace nativescript { - -inline bool needsRoundTripCacheFrame(Cif* cif) { - return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; -} - -class EngineDirectRoundTripCacheFrameGuard { - public: - EngineDirectRoundTripCacheFrameGuard(napi_env env, - ObjCBridgeState* bridgeState, - bool enabled) - : env_(enabled ? env : nullptr), - bridgeState_(enabled ? bridgeState : nullptr) { - if (bridgeState_ != nullptr) { - bridgeState_->beginRoundTripCacheFrame(env_); - } - } - - EngineDirectRoundTripCacheFrameGuard(napi_env env, - ObjCBridgeState* bridgeState, - Cif* cif) - : EngineDirectRoundTripCacheFrameGuard( - env, bridgeState, needsRoundTripCacheFrame(cif)) {} - - ~EngineDirectRoundTripCacheFrameGuard() { - if (bridgeState_ != nullptr) { - bridgeState_->endRoundTripCacheFrame(env_); - } - } - - private: - napi_env env_ = nullptr; - ObjCBridgeState* bridgeState_ = nullptr; -}; - -class EngineDirectReturnStorage { - public: - explicit EngineDirectReturnStorage(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); - } - } - - ~EngineDirectReturnStorage() { - 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; -}; - -} // namespace nativescript - -#endif // NS_FFI_NAPI_ENGINE_INVOCATION_SUPPORT_H diff --git a/NativeScript/ffi/napi/engine/shared/SignatureDispatch.h b/NativeScript/ffi/napi/engine/shared/SignatureDispatch.h deleted file mode 100644 index 1f83e603..00000000 --- a/NativeScript/ffi/napi/engine/shared/SignatureDispatch.h +++ /dev/null @@ -1,974 +0,0 @@ -#ifndef NS_SIGNATURE_DISPATCH_H -#define NS_SIGNATURE_DISPATCH_H - -#include - -#include -#include -#include -#include -#include - -#include "ffi/napi/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; - } - - if (!hermesDispatchRawDoubleIsFinite(raw)) { - return false; - } - - *result = hermesDispatchRawToDouble(raw); - return true; -} - -inline bool readHermesDispatchFiniteNumberRaw(uint64_t raw, double* result) { - if (result == nullptr || !isHermesDispatchNumber(raw)) { - return false; - } - - if (!hermesDispatchRawDoubleIsFinite(raw)) { - return false; - } - - *result = hermesDispatchRawToDouble(raw); - return true; -} - -inline napi_value makeHermesDispatchRawValue(Cif* cif, uint64_t raw) { - (void)cif; - 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; - ObjCPreparedInvoker invoker; -}; - -struct CFunctionDispatchEntry { - uint64_t dispatchId; - CFunctionPreparedInvoker invoker; -}; - -struct BlockDispatchEntry { - uint64_t dispatchId; - BlockPreparedInvoker invoker; -}; - -struct ObjCNapiDispatchEntry { - uint64_t dispatchId; - ObjCNapiInvoker invoker; -}; - -struct CFunctionNapiDispatchEntry { - uint64_t dispatchId; - 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; - -inline uint64_t hashBytesFnv1a(const void* data, size_t size, - uint64_t seed = kSignatureHashOffsetBasis) { - const auto* bytes = static_cast(data); - uint64_t hash = seed; - for (size_t i = 0; i < size; i++) { - hash ^= static_cast(bytes[i]); - hash *= kSignatureHashPrime; - } - return hash; -} - -inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, - SignatureCallKind kind, - uint8_t flags) { - const uint8_t kindByte = static_cast(kind); - uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); - hash = hashBytesFnv1a(&flags, sizeof(flags), hash); - 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 - -#ifndef NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH -#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" -#endif -#endif - -#if !NS_HAS_GENERATED_SIGNATURE_DISPATCH -namespace nativescript { -inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { - {0, nullptr}}; -inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { - {0, nullptr}}; -inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { - {0, nullptr}}; -} // namespace nativescript -#endif - -#if !NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH -namespace nativescript { -inline constexpr ObjCNapiDispatchEntry kGeneratedObjCNapiDispatchEntries[] = { - {0, nullptr}}; -inline constexpr CFunctionNapiDispatchEntry - kGeneratedCFunctionNapiDispatchEntries[] = {{0, nullptr}}; -} // 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 -inline Invoker lookupDispatchInvoker(const Entry (&entries)[N], - uint64_t dispatchId) { - if (dispatchId == 0 || N <= 1) { - return nullptr; - } - - size_t low = 1; - size_t high = N; - while (low < high) { - const size_t mid = low + ((high - low) >> 1); - const uint64_t midId = entries[mid].dispatchId; - if (midId < dispatchId) { - low = mid + 1; - } else { - high = mid; - } - } - - if (low < N && entries[low].dispatchId == dispatchId) { - return entries[low].invoker; - } - return nullptr; -} - -inline bool isGeneratedDispatchEnabled() { - static const bool enabled = []() { - const char* disableFlag = std::getenv("NS_DISABLE_GSD"); - if (disableFlag == nullptr || disableFlag[0] == '\0') { - return true; - } - return !(disableFlag[0] == '0' && disableFlag[1] == '\0'); - }(); - return enabled; -} - -inline ObjCPreparedInvoker lookupObjCPreparedInvoker(uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - return lookupDispatchInvoker( - kGeneratedObjCDispatchEntries, dispatchId); -} - -inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( - uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - 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; - } - return lookupDispatchInvoker( - kGeneratedObjCNapiDispatchEntries, dispatchId); -} - -inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - 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/napi/engine/v8/V8FastConversion.mm b/NativeScript/ffi/napi/engine/v8/V8FastConversion.mm deleted file mode 100644 index 2b2a0617..00000000 --- a/NativeScript/ffi/napi/engine/v8/V8FastConversion.mm +++ /dev/null @@ -1,683 +0,0 @@ -#include "V8FastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_V8 - -namespace nativescript { -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; -} - - -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, kV8NativePointerProperty, - 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, kV8NativePointerProperty, &hasNativePointer) == - napi_ok && - hasNativePointer) { - napi_value nativePointerValue = nullptr; - if (napi_get_named_property(env, cachedValue, kV8NativePointerProperty, &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) { - *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; - int64_t converted = value.As()->Int64Value(&lossless); - if (!lossless) { - return false; - } - *reinterpret_cast(result) = converted; - return true; - } - return value->IntegerValue(env->context()).To(reinterpret_cast(result)); - - case mdTypeULong: - case mdTypeUInt64: - if (value->IsBigInt()) { - bool lossless = false; - uint64_t converted = value.As()->Uint64Value(&lossless); - if (!lossless) { - return false; - } - *reinterpret_cast(result) = converted; - 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; - } -} - - -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; -} - - -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 nativescript - -#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h b/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h deleted file mode 100644 index 8175f24a..00000000 --- a/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.h +++ /dev/null @@ -1,22 +0,0 @@ -#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/napi/engine/v8/V8FastNativeApi.mm b/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.mm deleted file mode 100644 index f08065a5..00000000 --- a/NativeScript/ffi/napi/engine/v8/V8FastNativeApi.mm +++ /dev/null @@ -1,1201 +0,0 @@ -#include "V8FastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_V8 - -namespace nativescript { -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_; -}; - -inline napi_env envFromCurrentContext(v8::Isolate* isolate) { - (void)isolate; - return nullptr; -} - -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, kV8NativePointerProperty, &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; - } - - if (!jsThisValue.IsEmpty() && jsThisValue->IsFunction() && jsThis != nullptr) { - bool isSameConstructor = true; - napi_value definingConstructor = nullptr; - if (method->cls->constructor != nullptr) { - napi_get_reference_value(env, method->cls->constructor, - &definingConstructor); - } - if (definingConstructor != nullptr && - napi_strict_equals(env, jsThis, definingConstructor, - &isSameConstructor) == napi_ok && - !isSameConstructor) { - shouldUseClassFallback = false; - } else { - shouldUseClassFallback = true; - } - } else if (!method->classMethod && !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) { - EngineDirectReturnStorage 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, true); - } - - 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 - -#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h b/NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h deleted file mode 100644 index 5e667460..00000000 --- a/NativeScript/ffi/napi/engine/v8/V8FastNativeApiPrivate.h +++ /dev/null @@ -1,205 +0,0 @@ -#ifndef NS_V8_FAST_NATIVE_API_PRIVATE_H -#define NS_V8_FAST_NATIVE_API_PRIVATE_H - -#include "V8FastNativeApi.h" - -#ifdef TARGET_ENGINE_V8 - -#import -#import -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ffi/napi/CFunction.h" -#include "ffi/napi/ClassBuilder.h" -#include "ffi/napi/ClassMember.h" -#include "ffi/napi/Interop.h" -#include "InvocationSupport.h" -#include "ffi/napi/Object.h" -#include "ffi/napi/ObjCBridge.h" -#include "SignatureDispatch.h" -#include "ffi/napi/TypeConv.h" -#include "runtime/NativeScriptException.h" -#include "v8-api.h" - -namespace nativescript { - -inline constexpr const char* kV8NativePointerProperty = "__ns_native_ptr"; -inline constexpr int kV8NativeWrapperReferenceField = 0; -inline constexpr int kV8NativeWrapperMarkerField = 1; -inline constexpr int kV8NativeWrapperFieldCount = 2; - -struct V8CFunctionBinding { - ObjCBridgeState* bridgeState = nullptr; - MDSectionOffset offset = 0; - CFunction* function = nullptr; -}; - -bool TryFastConvertV8FoundationObject(napi_env env, id value, - v8::Local* result); -bool TryFastConvertV8NSStringReturnValue(napi_env env, const void* value, - v8::Local* result); - -inline void* nativeWrapperMarker() { - static uintptr_t marker; - return ▮ -} - -inline bool isV8NativeWrapperObject(v8::Local object) { - return !object.IsEmpty() && - object->InternalFieldCount() > kV8NativeWrapperMarkerField && - object->GetAlignedPointerFromInternalField( - kV8NativeWrapperMarkerField) == nativeWrapperMarker(); -} - -inline 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)); -} - -inline 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()); -} - -inline 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); -} - -inline 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); -} - -inline id tryReadWrappedReference(napi_env env, - v8::Local object) { - if (env == nullptr || object.IsEmpty()) { - return nil; - } - - if (object->InternalFieldCount() > kV8NativeWrapperReferenceField) { - auto* reference = static_cast( - object->GetAlignedPointerFromInternalField( - kV8NativeWrapperReferenceField)); - if (reference != nullptr) { - return static_cast(reference->Data()); - } - } - - v8::Local wrappedReference; - if (!object->GetPrivate(env->context(), napiPrivateKey(env->isolate)) - .ToLocal(&wrappedReference) || - wrappedReference.IsEmpty() || !wrappedReference->IsExternal()) { - return nil; - } - - auto* reference = static_cast( - wrappedReference.As()->Value()); - return reference != nullptr ? static_cast(reference->Data()) : nil; -} - -inline 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 (value->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; -} - -} // namespace nativescript - -#endif // TARGET_ENGINE_V8 - -#endif // NS_V8_FAST_NATIVE_API_PRIVATE_H diff --git a/NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm b/NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm deleted file mode 100644 index 796be450..00000000 --- a/NativeScript/ffi/napi/engine/v8/V8FastNativeWrapper.mm +++ /dev/null @@ -1,233 +0,0 @@ -#include "V8FastNativeApiPrivate.h" - -#ifdef TARGET_ENGINE_V8 - -#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 - -namespace nativescript { -namespace { - -napi_env envFromHandlerData(v8::Local data) { - if (data.IsEmpty() || !data->IsExternal()) { - return nullptr; - } - - return static_cast(data.As()->Value()); -} - -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, kV8NativePointerProperty) == 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); - } - } - - if (definePlainValueProperty(info.GetIsolate()->GetCurrentContext(), holder, - property, value)) { - NS_V8_RETURN_YES; - } - - NS_V8_RETURN_NO; -} - -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(kV8NativeWrapperFieldCount); - 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(kV8NativeWrapperMarkerField, - nativeWrapperMarker()); - - return v8impl::JsValueFromV8LocalValue(scope.Escape(object)); -} - -} // namespace nativescript - -#undef NS_V8_INTERCEPTED -#undef NS_V8_RETURN_YES -#undef NS_V8_RETURN_NO -#undef NS_V8_SETTER_INFO - -#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index f07774a8..698b1061 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -15,8 +15,6 @@ #include #include -#include "JSCFastNativeApi.h" - struct napi_callback_info__ { napi_value newTarget; napi_value thisArg; @@ -1358,10 +1356,6 @@ 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)); diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index e6b1c4b9..d7b23e58 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -4,7 +4,6 @@ #include #include "js_native_api.h" -#include "QuickJSFastNativeApi.h" #include "libbf.h" #include "quicks-runtime.h" @@ -2845,11 +2844,6 @@ 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) { diff --git a/NativeScript/napi/v8/v8-api.cpp b/NativeScript/napi/v8/v8-api.cpp index 3247b648..dc6bfeda 100644 --- a/NativeScript/napi/v8/v8-api.cpp +++ b/NativeScript/napi/v8/v8-api.cpp @@ -9,7 +9,6 @@ #define NAPI_EXPERIMENTAL -#include "V8FastNativeApi.h" #include "js_native_api.h" #include "v8-api.h" #include "v8-module-loader.h" @@ -1379,11 +1378,6 @@ 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 26ef4559..6e2f5b2b 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -8,6 +8,7 @@ #include "js_native_api_types.h" #include "jsr.h" #include "jsr_common.h" +#include "runtime/Util.h" #include "runtime/modules/RuntimeModules.h" #ifdef TARGET_ENGINE_V8 #include "v8-api.h" @@ -16,6 +17,8 @@ #include "ffi/hermes/jsi/NativeApiJsi.h" #endif // TARGET_ENGINE_HERMES #include +#include +#include #include "NativeScript.h" #include "robin_hood.h" @@ -42,6 +45,53 @@ Runtime* Runtime::GetRuntime(napi_env env) { return nullptr; } +#ifdef TARGET_ENGINE_HERMES +class HermesRuntimeUnlockScope final { + public: + explicit HermesRuntimeUnlockScope(napi_env env) { + auto it = JSR::env_to_jsr_cache.find(env); + jsr_ = it != JSR::env_to_jsr_cache.end() ? it->second : nullptr; + if (jsr_ == nullptr) { + return; + } + + unlockedDepth_ = js_current_env_lock_depth(env); + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->unlock(); + } + if (unlockedDepth_ == 0 && jsr_->runtime != nullptr) { + jsr_->runtime->unlock(); + unlockedRuntime_ = true; + } + } + + ~HermesRuntimeUnlockScope() { + if (unlockedRuntime_ && jsr_ != nullptr && jsr_->runtime != nullptr) { + jsr_->runtime->lock(); + } + if (jsr_ != nullptr) { + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->lock(); + } + } + } + + HermesRuntimeUnlockScope(const HermesRuntimeUnlockScope&) = delete; + HermesRuntimeUnlockScope& operator=(const HermesRuntimeUnlockScope&) = delete; + + private: + JSR* jsr_ = nullptr; + int unlockedDepth_ = 0; + bool unlockedRuntime_ = false; +}; + +void InvokeWithUnlockedHermesRuntime(napi_env env, + const std::function& task) { + HermesRuntimeUnlockScope scope(env); + task(); +} +#endif // TARGET_ENGINE_HERMES + Runtime::Runtime() { currentRuntime_ = this; workerId_ = -1; @@ -283,10 +333,51 @@ void Runtime::Init(bool isWorker) { // Ensure that Promise callbacks are executed on the // same thread on which they were created (() => { + const runLoopQueues = []; + + function getRunLoopQueue(runloop) { + for (let i = 0; i < runLoopQueues.length; i++) { + if (runLoopQueues[i].runloop === runloop) { + return runLoopQueues[i]; + } + } + + const queue = { + runloop, + pending: false, + callbacks: [], + drain() { + queue.pending = false; + const callbacks = queue.callbacks.splice(0); + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](); + } + if (queue.callbacks.length > 0 && !queue.pending) { + queue.pending = true; + CFRunLoopPerformBlock(queue.runloop, kCFRunLoopDefaultMode, queue.drain); + CFRunLoopWakeUp(queue.runloop); + } + } + }; + runLoopQueues.push(queue); + return queue; + } + + function scheduleOnRunLoop(queue, callback) { + queue.callbacks.push(callback); + if (queue.pending) { + return; + } + queue.pending = true; + CFRunLoopPerformBlock(queue.runloop, kCFRunLoopDefaultMode, queue.drain); + CFRunLoopWakeUp(queue.runloop); + } + global.Promise = new Proxy(global.Promise, { construct: function(target, args) { let origFunc = args[0]; let runloop = CFRunLoopGetCurrent(); + let runloopQueue = getRunLoopQueue(runloop); let promise = new target(function(resolve, reject) { function isFulfilled() { @@ -301,27 +392,31 @@ void Runtime::Init(bool isWorker) { if (isFulfilled()) { return; } - const resolveCall = resolve.bind(this, value); + const resolveFn = resolve; + const resolveCall = function() { + resolveFn(value); + }; if (runloop === CFRunLoopGetCurrent()) { markFulfilled(); resolveCall(); } else { - CFRunLoopPerformBlock(runloop, kCFRunLoopDefaultMode, resolveCall); - CFRunLoopWakeUp(runloop); markFulfilled(); + scheduleOnRunLoop(runloopQueue, resolveCall); } }, reason => { if (isFulfilled()) { return; } - const rejectCall = reject.bind(this, reason); + const rejectFn = reject; + const rejectCall = function() { + rejectFn(reason); + }; if (runloop === CFRunLoopGetCurrent()) { markFulfilled(); rejectCall(); } else { - CFRunLoopPerformBlock(runloop, kCFRunLoopDefaultMode, rejectCall); - CFRunLoopWakeUp(runloop); markFulfilled(); + scheduleOnRunLoop(runloopQueue, rejectCall); } }); }); @@ -392,6 +487,25 @@ void Runtime::Init(bool isWorker) { nativeApiJsiConfig.metadataPath = metadata_path; nativeApiJsiConfig.metadataPtr = RuntimeConfig.MetadataPtr; nativeApiJsiConfig.installGlobalSymbols = true; + nativeApiJsiConfig.nativeInvocationInvoker = + [env = env_](std::function task) { + InvokeWithUnlockedHermesRuntime(env, task); + }; + nativeApiJsiConfig.nativeCallbackInvoker = + [env = env_](std::function task) { + NapiScope scope(env); + task(); + }; + nativeApiJsiConfig.jsThreadCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + false); + }; InstallNativeApiJSI(*jsiRuntime, nativeApiJsiConfig); } #endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_HERMES diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index cca155ba..f00ead7f 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -172,7 +172,7 @@ def is_nativescript_source_root(search_path): strict_includes = env_or_none("NS_DEBUG_METADATA_STRICT_INCLUDES") or env_or_none("TNS_DEBUG_METADATA_STRICT_INCLUDES") signature_bindings_cpp_path = env_or_none("NS_SIGNATURE_BINDINGS_CPP_PATH") or env_or_none("TNS_SIGNATURE_BINDINGS_CPP_PATH") if signature_bindings_cpp_path is None: - default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc") + default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "napi", "GeneratedSignatureDispatch.inc") if os.path.isdir(os.path.dirname(default_signature_bindings_path)): signature_bindings_cpp_path = default_signature_bindings_path diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec index d65543a7..e5a477ff 100644 --- a/packages/react-native/NativeScriptNativeApi.podspec +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.source_files = [ "ios/**/*.{h,mm}", - "native-api-jsi/**/*.{h,mm}" + "native-api-jsi/**/*.{h,mm,inc}" ] s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled s.public_header_files = "ios/**/*.h" diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm index 0ef3ef99..5b379780 100644 --- a/packages/react-native/ios/NativeScriptNativeApiModule.mm +++ b/packages/react-native/ios/NativeScriptNativeApiModule.mm @@ -98,14 +98,17 @@ bool writeSmokeMarkerContentIfRequested(const std::string& content) { bool NativeScriptNativeApiModule::install(jsi::Runtime& runtime, std::string metadataPath) { + writeSmokeMarkerIfRequested("install:resolve-metadata"); std::string resolvedMetadataPath = metadataPath.empty() ? bundledMetadataPath() : metadataPath; const char* metadataPathArg = resolvedMetadataPath.empty() ? nullptr : resolvedMetadataPath.c_str(); + writeSmokeMarkerIfRequested("install:before-jsi"); auto config = nativescript::MakeReactNativeNativeApiJsiConfig( jsInvoker_, nullptr, metadataPathArg); nativescript::InstallNativeApiJSI(runtime, config); + writeSmokeMarkerIfRequested("install:after-jsi"); return isInstalled(runtime); } diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 09d54b05..2e1aa774 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -641,7 +641,8 @@ export function init( metadataPath = '', options: InstallOptions = {}, ): boolean { - const installed = NativeScriptNativeApi.install(metadataPath); + const installed = NativeScriptNativeApi.isInstalled() + || NativeScriptNativeApi.install(metadataPath); if (installed) { installInteropConstructors(); installInlineFunctions(); diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index dd96729d..6c457332 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -17,7 +17,7 @@ TARGET_ENGINE=${TARGET_ENGINE:=v8} # default to v8 for compat NS_FFI_BACKEND=${NS_FFI_BACKEND:=auto} 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/napi/engine/shared/GeneratedSignatureDispatch.inc}} +GENERATED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/napi/GeneratedSignatureDispatch.inc}} GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" for arg in $@; do @@ -103,14 +103,6 @@ function effective_gsd_backend () { 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 diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index f7245282..a8d9a28a 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -38,6 +38,7 @@ mkdir -p \ cp NativeScript/ffi/hermes/jsi/NativeApiJsi.h "$PACKAGE_DIR/native-api-jsi/" cp NativeScript/ffi/hermes/jsi/NativeApiJsi.mm "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/hermes/jsi/NativeApiJsi*.inc "$PACKAGE_DIR/native-api-jsi/" cp NativeScript/ffi/hermes/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/" diff --git a/scripts/check_ffi_boundaries.sh b/scripts/check_ffi_boundaries.sh index 4b2dd396..62a06ca4 100755 --- a/scripts/check_ffi_boundaries.sh +++ b/scripts/check_ffi_boundaries.sh @@ -2,14 +2,21 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +NAPI_ENGINE_DIR="$ROOT_DIR/NativeScript/ffi/napi/engine" DIRECT_DIRS=( "$ROOT_DIR/NativeScript/ffi/hermes" "$ROOT_DIR/NativeScript/ffi/v8" "$ROOT_DIR/NativeScript/ffi/jsc" "$ROOT_DIR/NativeScript/ffi/quickjs" "$ROOT_DIR/NativeScript/ffi/shared" + "$ROOT_DIR/packages/react-native/native-api-jsi" ) +if [ -d "$NAPI_ENGINE_DIR" ] && find "$NAPI_ENGINE_DIR" -type f | grep -q .; then + echo "ffi/napi must remain a pure Node-API backend; do not add ffi/napi/engine." >&2 + exit 1 +fi + EXISTING_DIRECT_DIRS=() for dir in "${DIRECT_DIRS[@]}"; do if [ -d "$dir" ]; then @@ -27,3 +34,11 @@ if rg -n '\b(napi_|napi_env|napi_value|js_native_api|node_api)\b' \ echo "Node-API symbols are not allowed in shared or direct engine FFI folders." >&2 exit 1 fi + +if rg -n '\b(EngineDirect|FastNative|HermesFast|V8Fast|JSCFast|QuickJSFast)\b' \ + "$ROOT_DIR/NativeScript/ffi/napi" \ + -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}' \ + -g '!GeneratedSignatureDispatch.inc'; then + echo "Direct-engine FFI code is not allowed in ffi/napi." >&2 + exit 1 +fi diff --git a/scripts/metagen.js b/scripts/metagen.js index 03797f21..be4b5014 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -316,7 +316,7 @@ async function main() { const signatureBindingsPath = process.env.NS_SIGNATURE_BINDINGS_CPP_PATH || process.env.TNS_SIGNATURE_BINDINGS_CPP_PATH || - path.resolve(__dirname, "..", "NativeScript", "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc"); + path.resolve(__dirname, "..", "NativeScript", "ffi", "napi", "GeneratedSignatureDispatch.inc"); await fsp.rm(typesDir, { recursive: true, force: true }); await fsp.mkdir(typesDir, { recursive: true }); await fsp.mkdir(metadataDir, { recursive: true }); diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 0c8d8f15..09435c34 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -15,6 +15,8 @@ // - MACOS_TEST_INACTIVITY_TIMEOUT_MS overrides max no-log interval after launch (default: 45 seconds). // - MACOS_LOG_JUNIT=0 disables streaming TKUnit/JUnit lines to console. // - MACOS_TESTS filters test modules (comma-separated substrings passed as -tests). +// - MACOS_TEST_SPECS filters spec names (comma-separated substrings passed as -specs). +// - MACOS_TEST_VERBOSE_SPECS=1 prints Jasmine spec start/done markers. const fs = require("fs"); const path = require("path"); @@ -89,6 +91,8 @@ const testTimeoutMs = parseTimeoutMs("MACOS_TEST_TIMEOUT_MS", 2 * 60 * 1000); const inactivityTimeoutMs = parseTimeoutMs("MACOS_TEST_INACTIVITY_TIMEOUT_MS", 45 * 1000); const emitJunitLogs = process.env.MACOS_LOG_JUNIT !== "0"; const requestedTests = (process.env.MACOS_TESTS || "").trim(); +const requestedSpecs = (process.env.MACOS_TEST_SPECS || "").trim(); +const verboseSpecs = process.env.MACOS_TEST_VERBOSE_SPECS === "1"; const requestedEngine = (process.env.MACOS_TEST_ENGINE || "v8").trim().toLowerCase(); const requestedFfiBackend = (process.env.MACOS_TEST_FFI_BACKEND || "auto").trim().toLowerCase(); @@ -98,8 +102,8 @@ const junitEndTag = ""; const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); const generatedRuntimeBuildOutputs = new Set([ - path.join(nativeScriptSourceRoot, "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc"), - path.join(nativeScriptSourceRoot, "ffi", "napi", "engine", "shared", "GeneratedSignatureDispatch.inc.stamp") + path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc.stamp") ]); function parseArgs() { @@ -629,9 +633,15 @@ function main() { } const runArgs = ["-logjunit"]; + if (verboseSpecs) { + runArgs.push("-verbose-specs"); + } if (requestedTests.length > 0) { runArgs.push("-tests", requestedTests); } + if (requestedSpecs.length > 0) { + runArgs.push("-specs", requestedSpecs); + } console.log(`Launching app and streaming logs: ${appBinaryPath} ${runArgs.join(" ")}`); diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx index bd2d11ba..9cf0b687 100644 --- a/test/react-native/ffi-compat/App.tsx +++ b/test/react-native/ffi-compat/App.tsx @@ -309,22 +309,6 @@ function installRuntimeSpecGlobals(): RuntimeSpecRegistry { specName: string, body: Function, ): string | undefined => { - const classBuilderSpecNames = [ - 'Override: More than one methods with same jsname', - 'Marshals NSString with null character', - 'NSMutableStringMarshalling', - 'Initialize a class(NSTimer) whose init deallocates the result from alloc', - 'Marshal returned javascript object as NSDictionaries', - ]; - if ( - classBuilderSpecNames.some((name) => specName.endsWith(` > ${name}`)) || - specName.includes(' > DerivedMethod') || - specName.endsWith(' > Handleof') || - specName.endsWith(' > Sizeof') - ) { - return 'Requires NativeScript JS-defined Objective-C class builder; excluded from the RN direct-JSI FFI slice.'; - } - let source = ''; try { source = Function.prototype.toString.call(body); @@ -332,14 +316,8 @@ function installRuntimeSpecGlobals(): RuntimeSpecRegistry { source = ''; } - if ( - source.includes('.extend(') || - source.includes('.extend (') || - source.includes('interop.addMethod') || - source.includes('ObjCExposedMethods') || - source.includes('ObjCProtocols') - ) { - return 'Requires NativeScript JS-defined Objective-C class builder; excluded from the RN direct-JSI FFI slice.'; + if (source.includes('interop.addMethod')) { + return 'Requires interop.addMethod; excluded from the RN direct-JSI FFI slice until the explicit decorator hook is implemented.'; } return undefined; }; @@ -772,17 +750,16 @@ function buildReactNativeIntegrationTests(): TestCase[] { name: 'invokes C function pointer callbacks on the native caller thread', run() { let callbackRan = false; - let callbackThreadHash = 0; - const queue = g('dispatch_get_global_queue')(0, 0); - g('dispatch_sync_f')(queue, null, () => { - callbackRan = true; - callbackThreadHash = g('NSThread').currentThread.hash; - }); - assert(callbackRan, 'dispatch_sync_f callback did not run'); - assert( - callbackThreadHash !== g('NSThread').currentThread.hash, - 'dispatch_sync_f callback should run on the dispatch queue thread', + const result = g('functionWithSimpleFunctionPointerOnBackground')( + (nativeCallerWasMainThread: number) => { + assertEqual(nativeCallerWasMainThread, 0, 'callback native caller thread'); + assertEqual(g('NSThread').isMainThread, false, 'callback JS native calls thread'); + callbackRan = true; + return g('NSThread').isMainThread ? 1 : 0; + }, ); + assertEqual(result, 0, 'background callback return'); + assert(callbackRan, 'background callback did not run'); }, }, { @@ -863,7 +840,24 @@ async function runCompatibilitySuite() { total: 0, results: [], }); - NativeScript.init(); + try { + NativeScript.init(); + } catch (error) { + const message = + error instanceof Error + ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}; stack=${error.stack ?? ''}` + : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`; + writeMarker({ + marker, + status: 'fail', + current: 'NativeScript.init', + passed: 0, + total: 0, + results: [], + failures: [{name: 'NativeScript.init', status: 'fail', error: message}], + }); + throw error; + } const registry = installRuntimeSpecGlobals(); loadRuntimeFfiSpecs(); diff --git a/test/runtime/runner/app/tests/Infrastructure/timers.js b/test/runtime/runner/app/tests/Infrastructure/timers.js index 3164b7f8..52c8de56 100644 --- a/test/runtime/runner/app/tests/Infrastructure/timers.js +++ b/test/runtime/runner/app/tests/Infrastructure/timers.js @@ -1,5 +1,80 @@ // https://github.com/NativeScript/NativeScript/blob/master/tns-core-modules/timer/timer.ios.ts +if (typeof global.__ns__setTimeout === "function" && + typeof global.__ns__clearTimeout === "function") { + global.setTimeout = global.__ns__setTimeout; + global.clearTimeout = global.__ns__clearTimeout; + if (typeof global.__ns__setInterval === "function") { + global.setInterval = global.__ns__setInterval; + } + if (typeof global.__ns__clearInterval === "function") { + global.clearInterval = global.__ns__clearInterval; + } +} else if (global.__nativeScriptNativeApi && + typeof NSTimer !== "undefined" && + typeof NSTimer.scheduledTimerWithTimeIntervalRepeatsBlock === "function") { + var directTimeoutCallbacks = new Map(); + var directTimerId = 0; + + function createDirectTimerAndGetId(callback, milliseconds, shouldRepeat) { + directTimerId++; + var id = directTimerId; + var pair = { k: null, disposed: false }; + var timer = NSTimer.scheduledTimerWithTimeIntervalRepeatsBlock( + milliseconds / 1000, + shouldRepeat, + function () { + if (pair.disposed) { + return; + } + callback(); + if (typeof global.__drainMicrotaskQueue === "function") { + global.__drainMicrotaskQueue(); + } + if (!shouldRepeat) { + pair.disposed = true; + directTimeoutCallbacks.delete(id); + } + }); + NSRunLoop.currentRunLoop.addTimerForMode(timer, NSRunLoopCommonModes); + pair.k = timer; + directTimeoutCallbacks.set(id, pair); + return id; + } + + global.setTimeout = function (callback, milliseconds) { + if (milliseconds === void 0) { milliseconds = 0; } + var args = []; + for (var i = 2; i < arguments.length; i++) { + args[i - 2] = arguments[i]; + } + return createDirectTimerAndGetId(function () { + return callback.apply(void 0, args); + }, milliseconds, false); + }; + + global.clearTimeout = function (id) { + var pair = directTimeoutCallbacks.get(id); + if (pair && !pair.disposed) { + pair.disposed = true; + pair.k.invalidate(); + directTimeoutCallbacks.delete(id); + } + }; + + global.setInterval = function (callback, milliseconds) { + if (milliseconds === void 0) { milliseconds = 0; } + var args = []; + for (var i = 2; i < arguments.length; i++) { + args[i - 2] = arguments[i]; + } + return createDirectTimerAndGetId(function () { + return callback.apply(void 0, args); + }, milliseconds, true); + }; + + global.clearInterval = global.clearTimeout; +} else { var timeoutCallbacks = new Map(); var timerId = 0; @@ -66,3 +141,4 @@ function clearTimeout(id) { global.setTimeout = setTimeout; global.clearTimeout = clearTimeout; +} diff --git a/test/runtime/runner/app/tests/Timers.js b/test/runtime/runner/app/tests/Timers.js index 7ee3c9fc..3159bad1 100644 --- a/test/runtime/runner/app/tests/Timers.js +++ b/test/runtime/runner/app/tests/Timers.js @@ -166,6 +166,11 @@ describe("native timer", () => { const baselineActiveTimerCount = getActiveTimerCount ? getActiveTimerCount() : null; + const baselineHermesTimerCallbackCount = + isHermes && + typeof global.__nsHermesTimerCallbackCount === "function" + ? global.__nsHermesTimerCallbackCount() + : null; { let obj = { value: 0, @@ -191,7 +196,7 @@ describe("native timer", () => { typeof global.__nsHermesHasTimerCallback === "function" ) { gc(); - expect(global.__nsHermesTimerCallbackCount()).toBe(0); + expect(global.__nsHermesTimerCallbackCount()).toBe(baselineHermesTimerCallbackCount); expect(global.__nsHermesHasTimerCallback(timeout.__timerId)).toBe(false); expect(global.__nsHermesHasTimerCallback(interval.__timerId)).toBe(false); done(); @@ -220,8 +225,14 @@ describe("native timer", () => { return; } + const defaultQosClass = + typeof qos_class_t !== "undefined" && + qos_class_t && + qos_class_t.QOS_CLASS_DEFAULT != null + ? qos_class_t.QOS_CLASS_DEFAULT + : (typeof QOS_CLASS_DEFAULT !== "undefined" ? QOS_CLASS_DEFAULT : 0); const background_queue = dispatch_get_global_queue( - qos_class_t.QOS_CLASS_DEFAULT, + Number(defaultQosClass), 0 ); const current_queue = dispatch_get_current_queue(); diff --git a/test/runtime/runner/app/tests/index.js b/test/runtime/runner/app/tests/index.js index c9ecc04e..a6949552 100644 --- a/test/runtime/runner/app/tests/index.js +++ b/test/runtime/runner/app/tests/index.js @@ -267,6 +267,6 @@ execute(); if (typeof UIApplicationMain === "function") { UIApplicationMain(0, null, null, null); -} else if (typeof NSApplicationMain === "function") { +} else if (typeof NSApplicationMain === "function" && !logjunit) { NSApplicationMain(0, null); }