diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 627aeb287..b0a5aa548 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,6 @@ jobs: IOS_BUILD_TIMEOUT_MS: "600000" IOS_TEST_TIMEOUT_MS: "600000" IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000" - IOS_TEST_VERBOSE_SPECS: "1" + IOS_LOG_JUNIT: "1" IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000" run: npm run test:ios diff --git a/.gitignore b/.gitignore index edc6abe58..31b025ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,8 @@ package-lock.json v8_build .npmrc /Frameworks/ +/.kiro/ +/opencode.json /llvm/ @@ -65,11 +67,16 @@ packages/*/types SwiftBindgen # Generated Objective-C/C dispatch wrappers -NativeScript/ffi/napi/GeneratedSignatureDispatch.inc -NativeScript/ffi/napi/GeneratedSignatureDispatch.inc.stamp +NativeScript/ffi/*/GeneratedSignatureDispatch.inc +NativeScript/ffi/*/GeneratedSignatureDispatch.inc.stamp +NativeScript/ffi/*/GeneratedGsdSignatureDispatch.inc +NativeScript/ffi/*/GeneratedGsdSignatureDispatch.inc.stamp + +# Packaged native framework artifacts +packages/*/NativeScript.xcframework/ # React Native TurboModule package staging packages/react-native/dist/ packages/react-native/ios/vendor/ packages/react-native/metadata/ -packages/react-native/native-api-jsi/ +packages/react-native/native-api/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index b9386f462..f2f77274a 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -20,12 +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_FFI_BACKEND "auto" CACHE STRING "FFI backend: auto, napi, v8, jsc, quickjs, or hermes") 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_FFI_BACKEND PROPERTY STRINGS auto napi v8 jsc quickjs hermes) set_property(CACHE NS_GSD_BACKEND PROPERTY STRINGS auto v8 jsc quickjs hermes napi none) if (BUILD_MACOS_NODE_API) @@ -139,36 +139,46 @@ 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 OR TARGET_ENGINE_V8 OR TARGET_ENGINE_JSC OR TARGET_ENGINE_QUICKJS) - set(NS_EFFECTIVE_FFI_BACKEND "direct") + elseif(TARGET_ENGINE_HERMES) + set(NS_EFFECTIVE_FFI_BACKEND "hermes") + elseif(TARGET_ENGINE_V8) + set(NS_EFFECTIVE_FFI_BACKEND "v8") + elseif(TARGET_ENGINE_JSC) + set(NS_EFFECTIVE_FFI_BACKEND "jsc") + elseif(TARGET_ENGINE_QUICKJS) + set(NS_EFFECTIVE_FFI_BACKEND "quickjs") else() set(NS_EFFECTIVE_FFI_BACKEND "napi") endif() -elseif(NS_FFI_BACKEND STREQUAL "napi" OR NS_FFI_BACKEND STREQUAL "direct") +elseif(NS_FFI_BACKEND STREQUAL "napi" OR + NS_FFI_BACKEND STREQUAL "v8" OR + NS_FFI_BACKEND STREQUAL "jsc" OR + NS_FFI_BACKEND STREQUAL "quickjs" OR + NS_FFI_BACKEND STREQUAL "hermes") 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 +if(NOT NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" 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") + message(FATAL_ERROR + "NS_FFI_BACKEND=${NS_EFFECTIVE_FFI_BACKEND} requires an embedded JS runtime build") 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")) +if(NOT NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" AND + NOT NS_EFFECTIVE_FFI_BACKEND STREQUAL "${TARGET_ENGINE}") 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.") + "NS_FFI_BACKEND=${NS_EFFECTIVE_FFI_BACKEND} requires TARGET_ENGINE=${NS_EFFECTIVE_FFI_BACKEND}") endif() +message(STATUS "NS_FFI_BACKEND = ${NS_FFI_BACKEND} (${NS_EFFECTIVE_FFI_BACKEND})") + if(NS_GSD_BACKEND STREQUAL "auto") - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") - set(NS_EFFECTIVE_GSD_BACKEND "none") - else() + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") set(NS_EFFECTIVE_GSD_BACKEND "napi") + else() + set(NS_EFFECTIVE_GSD_BACKEND "${NS_EFFECTIVE_FFI_BACKEND}") endif() elseif(NS_GSD_BACKEND STREQUAL "v8" OR NS_GSD_BACKEND STREQUAL "jsc" OR @@ -200,13 +210,18 @@ if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" AND "NS_FFI_BACKEND=napi is the pure Node-API FFI backend and only supports " "NS_GSD_BACKEND=napi or none.") endif() +if(NOT NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" AND + NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi") + message(FATAL_ERROR + "NS_FFI_BACKEND=${NS_EFFECTIVE_FFI_BACKEND} cannot use NS_GSD_BACKEND=napi. " + "Use the matching engine backend or none.") +endif() message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND})") # Set up sources include_directories( ./ - ffi/shared ../metadata-generator/include napi/common libffi/${LIBFFI_BUILD}/include @@ -239,33 +254,33 @@ set(FFI_NAPI_SOURCE_FILES ffi/napi/ClassBuilder.mm ) -set(FFI_DIRECT_SHARED_SOURCE_FILES - ffi/shared/direct/EmbeddedMetadata.mm +set(FFI_ENGINE_SHARED_SOURCE_FILES + ffi/shared/MetadataState.mm ) -set(FFI_HERMES_DIRECT_SOURCE_FILES - ${FFI_DIRECT_SHARED_SOURCE_FILES} - ffi/hermes/jsi/NativeApiJsi.mm +set(FFI_HERMES_ENGINE_SOURCE_FILES + ${FFI_ENGINE_SHARED_SOURCE_FILES} + ffi/hermes/NativeApiJsi.mm ) -set(FFI_V8_DIRECT_SOURCE_FILES - ${FFI_DIRECT_SHARED_SOURCE_FILES} +set(FFI_V8_ENGINE_SOURCE_FILES + ${FFI_ENGINE_SHARED_SOURCE_FILES} ffi/v8/NativeApiV8.mm ffi/v8/NativeApiV8HostObjects.mm ffi/v8/NativeApiV8Runtime.mm ffi/v8/NativeApiV8Value.mm ) -set(FFI_JSC_DIRECT_SOURCE_FILES - ${FFI_DIRECT_SHARED_SOURCE_FILES} +set(FFI_JSC_ENGINE_SOURCE_FILES + ${FFI_ENGINE_SHARED_SOURCE_FILES} ffi/jsc/NativeApiJSC.mm ffi/jsc/NativeApiJSCHostObjects.mm ffi/jsc/NativeApiJSCRuntime.mm ffi/jsc/NativeApiJSCValue.mm ) -set(FFI_QUICKJS_DIRECT_SOURCE_FILES - ${FFI_DIRECT_SHARED_SOURCE_FILES} +set(FFI_QUICKJS_ENGINE_SOURCE_FILES + ${FFI_ENGINE_SHARED_SOURCE_FILES} ffi/quickjs/NativeApiQuickJSHostObjects.mm ffi/quickjs/NativeApiQuickJS.mm ffi/quickjs/NativeApiQuickJSRuntime.mm @@ -328,10 +343,10 @@ if(ENABLE_JS_RUNTIME) napi/v8/SimpleAllocator.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "v8") set(SOURCE_FILES ${SOURCE_FILES} - ${FFI_V8_DIRECT_SOURCE_FILES} + ${FFI_V8_ENGINE_SOURCE_FILES} ) endif() @@ -359,10 +374,10 @@ if(ENABLE_JS_RUNTIME) napi/hermes/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "hermes") set(SOURCE_FILES ${SOURCE_FILES} - ${FFI_HERMES_DIRECT_SOURCE_FILES} + ${FFI_HERMES_ENGINE_SOURCE_FILES} ) endif() @@ -398,10 +413,10 @@ if(ENABLE_JS_RUNTIME) napi/quickjs/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "quickjs") set(SOURCE_FILES ${SOURCE_FILES} - ${FFI_QUICKJS_DIRECT_SOURCE_FILES} + ${FFI_QUICKJS_ENGINE_SOURCE_FILES} ) endif() @@ -417,10 +432,10 @@ if(ENABLE_JS_RUNTIME) napi/jsc/jsr.cpp ) - if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "jsc") set(SOURCE_FILES ${SOURCE_FILES} - ${FFI_JSC_DIRECT_SOURCE_FILES} + ${FFI_JSC_ENGINE_SOURCE_FILES} ) endif() @@ -511,40 +526,48 @@ 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) -set(NS_FFI_BACKEND_DIRECT_VALUE 0) +set(NS_GSD_BACKEND_PREPARED_VALUE 0) set(NS_FFI_BACKEND_NAPI_VALUE 0) +set(NS_FFI_BACKEND_V8_VALUE 0) +set(NS_FFI_BACKEND_JSC_VALUE 0) +set(NS_FFI_BACKEND_QUICKJS_VALUE 0) +set(NS_FFI_BACKEND_HERMES_VALUE 0) if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8") - set(NS_GSD_BACKEND_V8_VALUE 1) + set(NS_GSD_BACKEND_PREPARED_VALUE 1) elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "jsc") - set(NS_GSD_BACKEND_JSC_VALUE 1) + set(NS_GSD_BACKEND_PREPARED_VALUE 1) elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "quickjs") - set(NS_GSD_BACKEND_QUICKJS_VALUE 1) + set(NS_GSD_BACKEND_PREPARED_VALUE 1) elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "hermes") set(NS_GSD_BACKEND_HERMES_VALUE 1) 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") +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") set(NS_FFI_BACKEND_NAPI_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "v8") + set(NS_FFI_BACKEND_V8_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "jsc") + set(NS_FFI_BACKEND_JSC_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "quickjs") + set(NS_FFI_BACKEND_QUICKJS_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "hermes") + set(NS_FFI_BACKEND_HERMES_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_GSD_BACKEND_PREPARED=${NS_GSD_BACKEND_PREPARED_VALUE} NS_FFI_BACKEND_NAPI=${NS_FFI_BACKEND_NAPI_VALUE} + NS_FFI_BACKEND_V8=${NS_FFI_BACKEND_V8_VALUE} + NS_FFI_BACKEND_JSC=${NS_FFI_BACKEND_JSC_VALUE} + NS_FFI_BACKEND_QUICKJS=${NS_FFI_BACKEND_QUICKJS_VALUE} + NS_FFI_BACKEND_HERMES=${NS_FFI_BACKEND_HERMES_VALUE} ) set(FRAMEWORK_VERSION_VALUE "${VERSION}") @@ -657,13 +680,13 @@ if(TARGET_ENGINE_V8) # Prefer universal sim slice if present set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64-simulator/libv8_monolith.framework") if(NOT EXISTS "${V8_SLICE_DIR}") - set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64-simulator/libv8_monolith.framework") # fallback + set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64-simulator/libv8_monolith.framework") endif() elseif(TARGET_PLATFORM STREQUAL "ios") # Prefer universal sim slice if present set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64/libv8_monolith.framework") if(NOT EXISTS "${V8_SLICE_DIR}") - set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64/libv8_monolith.framework") # fallback + set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/ios-arm64/libv8_monolith.framework") endif() elseif(TARGET_PLATFORM STREQUAL "visionos-sim") set(V8_SLICE_DIR "${V8_XCFRAMEWORK}/xrsimulator-arm64/libv8_monolith.framework") diff --git a/NativeScript/ffi/hermes/NativeApiJsi.h b/NativeScript/ffi/hermes/NativeApiJsi.h new file mode 100644 index 000000000..ea3039614 --- /dev/null +++ b/NativeScript/ffi/hermes/NativeApiJsi.h @@ -0,0 +1,26 @@ +#ifndef NATIVE_API_JSI_H +#define NATIVE_API_JSI_H + +#include + +#include "ffi/shared/NativeApiBackendConfig.h" + +namespace nativescript { + +using NativeApiJsiScheduler = NativeApiBackendScheduler; +using NativeApiJsiConfig = NativeApiBackendConfig; + +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/hermes/NativeApiJsi.mm b/NativeScript/ffi/hermes/NativeApiJsi.mm new file mode 100644 index 000000000..e7f508d7a --- /dev/null +++ b/NativeScript/ffi/hermes/NativeApiJsi.mm @@ -0,0 +1,351 @@ +#include "NativeApiJsi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.h" +#include "ffi.h" +#include "NativeApiJsiSignatureDispatch.h" + +@protocol NativeApiClassBuilderProtocol +@end + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace nativescript { +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::StringBuffer; +using facebook::jsi::Value; +using facebook::jsi::JSError; + +using NativeApiConfig = NativeApiJsiConfig; +using NativeApiScheduler = NativeApiJsiScheduler; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +void SetNativeApiObjectPrototype(Runtime& runtime, Object& object, + const Object& prototype) { + Object objectConstructor = + runtime.global().getPropertyAsObject(runtime, "Object"); + Function setPrototypeOf = + objectConstructor.getPropertyAsFunction(runtime, "setPrototypeOf"); + setPrototypeOf.call(runtime, Value(runtime, object), Value(runtime, prototype)); +} + +// clang-format off +#define NATIVESCRIPT_NATIVE_API_RUNTIME_NAME "jsi" +#define NATIVESCRIPT_NATIVE_API_BACKEND_NAME "hermes" +#define NATIVESCRIPT_NATIVE_API_HOST_SET_VOID 1 +#define NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE 1 +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_SELECTOR_GROUP_FUNCTION 1 +#include "../shared/bridge/ObjCBridge.mm" +#include "../shared/bridge/HostObjects.mm" +#include "../shared/bridge/Callbacks.mm" +#include "../shared/bridge/TypeConv.mm" +#include "../shared/bridge/Invocation.mm" +#include "../shared/bridge/ClassBuilder.mm" +#include "../shared/bridge/HostObject.mm" +// clang-format on + +#include "NativeApiJsiGsd.mm" + + +void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { + return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); +} + +bool tryCallGeneratedEngineObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass, Value* result) { + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount) { + return false; + } + + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, args, + prepared.signature.returnType}; + if (!invoker(ctx)) { + return false; + } + *result = std::move(ctx.result); + return true; +} + +Function CreateNativeApiSelectorGroupFunctionImpl( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver = {}, + std::shared_ptr boundReceiverState = + nullptr) { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__nativeSelectorGroup"), 0, + [bridge = std::move(bridge), lookupClass, receiverIsClass, + selectors = std::move(selectors), + preparedInvocations = std::move(preparedInvocations), + boundReceiver = std::move(boundReceiver), + boundReceiverState = std::move(boundReceiverState), + cachedReceiverClass = Class(Nil), + cachedDispatchClass = Class(Nil)]( + Runtime& runtime, const Value& thisValue, const Value* args, + size_t count) mutable -> Value { + NativeApiRoundTripCacheFrameGuard roundTripFrame(bridge); + if (count >= selectors->size() || + (*selectors)[count].selectorName.empty()) { + throw JSError(runtime, + "Objective-C selector is not available for the provided " + "arguments count."); + } + + NativeApiSelectorGroupEntry& entry = (*selectors)[count]; + auto& prepared = (*preparedInvocations)[count]; + Class selectorLookupClass = lookupClass; + id receiver = receiverIsClass ? static_cast(lookupClass) : nil; + std::shared_ptr receiverHostObject; + if (!receiverIsClass) { + if (boundReceiverState != nullptr) { + receiver = boundReceiverState->object(); + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + } else if (thisValue.isObject()) { + Object receiverObject = thisValue.asObject(runtime); + if (receiverObject.isHostObject( + runtime)) { + receiverHostObject = + receiverObject.getHostObject( + runtime); + receiver = receiverHostObject->object(); + } + } + } + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + + const bool propertyGetterCall = + entry.hasMember && entry.member.property && count == 0; + const std::string* selectorNamePtr = &entry.selectorName; + const NativeApiMember* selectedMember = + entry.hasMember ? &entry.member : nullptr; + bool callTargetCanPrepare = true; + if (prepared == nullptr || propertyGetterCall) { + NativeApiSelectorGroupCallTarget callTarget = + selectorGroupCallTargetForEntry(receiver, selectorLookupClass, + receiverIsClass, entry, count); + selectorNamePtr = callTarget.selectorName; + selectedMember = callTarget.member; + callTargetCanPrepare = callTarget.canPrepare; + if (prepared != nullptr && prepared->selectorName != *selectorNamePtr) { + prepared = nullptr; + } + } + const std::string& selectorName = + prepared != nullptr && !propertyGetterCall ? prepared->selectorName + : *selectorNamePtr; + + if (receiverIsClass) { + Class methodClass = prepared != nullptr ? prepared->receiverClass : Nil; + if (methodClass == Nil) { + SEL selector = sel_registerName(selectorName.c_str()); + methodClass = + NativeApiClassHostObject::classRespondingToClassSelector( + lookupClass, selector); + } + if (methodClass == Nil) { + throw JSError(runtime, + "Objective-C selector is not available: " + + entry.selectorName); + } + selectorLookupClass = methodClass; + receiver = static_cast(methodClass); + } + if (propertyGetterCall && !callTargetCanPrepare) { + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + selectorName, selectedMember, nullptr, 0); + } + + if (prepared == nullptr) { + if (!receiverIsClass) { + SEL selector = sel_registerName(selectorName.c_str()); + if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { + Class receiverClass = object_getClass(receiver); + if (class_getInstanceMethod(receiverClass, selector) != nullptr) { + selectorLookupClass = receiverClass; + } + } + } + prepared = prepareNativeApiObjCInvocation( + runtime, bridge, selectorLookupClass, receiverIsClass, selectorName, + selectedMember); + // Look up the engine-neutral GSD invoker for this signature. + if (prepared->engineInvoker == nullptr) { + uint64_t dispatchId = dispatchIdForEngineSignature( + prepared->signature, SignatureCallKind::ObjCMethod); + if (auto gsdInvoker = lookupObjCGsdInvoker(dispatchId)) { + prepared->engineInvoker = reinterpret_cast(gsdInvoker); + configureGeneratedEngineObjCInvocation(*prepared); + } + } + } + + // Memoized dispatch-superclass resolution (pure function of the + // receiver's class + lookupClass) — avoids a per-call + // class_conformsToProtocol probe. + Class gsdDispatchClass = Nil; + if (!receiverIsClass) { + Class receiverClass = object_getClass(receiver); + if (receiverClass == cachedReceiverClass) { + gsdDispatchClass = cachedDispatchClass; + } else { + gsdDispatchClass = + dispatchSuperclassForEngineDerivedReceiver(receiver, lookupClass); + cachedReceiverClass = receiverClass; + cachedDispatchClass = gsdDispatchClass; + } + } + // GSD fast path: read jsi args directly, call objc_msgSend with a + // typed cast, produce the jsi return value — bypassing all generic + // marshalling. Only engages for plain calls (no super dispatch, init + // disown handling, or implicit NSError-out argument). + if (prepared->gsdEngineCallable && gsdDispatchClass == Nil && + count == prepared->gsdEngineArgumentCount && + !(!receiverIsClass && prepared->isInitMethod)) { + auto invoker = + reinterpret_cast(prepared->engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared->selector, + args, prepared->signature.returnType}; + if (invoker(ctx)) { + return std::move(ctx.result); + } + } + + if (receiverIsClass) { + return callPreparedObjCSelector(runtime, bridge, receiver, true, + *prepared, args, count, Nil); + } + if (!receiverHostObject) { + if (boundReceiverState != nullptr) { + if (auto bound = boundReceiver.lock()) { + receiverHostObject = std::move(bound); + } + } else if (thisValue.isObject()) { + Object receiverObject = thisValue.asObject(runtime); + if (receiverObject.isHostObject( + runtime)) { + receiverHostObject = + receiverObject.getHostObject( + runtime); + } + } + } + if (!receiverHostObject) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + return receiverHostObject->callPreparedObjectSelector( + runtime, *prepared, args, count, gsdDispatchClass); + }); +} + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), {}, nullptr); +} + +Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, false, std::move(selectors), + std::move(preparedInvocations), receiverHostObject, + receiverHostObject != nullptr ? receiverHostObject->lifetimeState() + : nullptr); +} + +} // namespace + +#include "../shared/bridge/Install.mm" + +Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + return CreateNativeApi(runtime, config); +} + +void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + InstallNativeApi(runtime, config); +} + +} // 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/NativeApiJsiGsd.mm b/NativeScript/ffi/hermes/NativeApiJsiGsd.mm new file mode 100644 index 000000000..9bcf76829 --- /dev/null +++ b/NativeScript/ffi/hermes/NativeApiJsiGsd.mm @@ -0,0 +1,169 @@ +// --- GSD (Generated Signature Dispatch) for Hermes/JSI --- +// GsdObjCContext is the engine-neutral interface the generated invokers use: +// it reads jsi::Value arguments and writes the jsi::Value return value using +// the shared engine-neutral conversion helpers (which already operate on the +// jsi value type). Readers require the fast representation; anything else +// makes a reader return false so the invoker falls back to the generic path. +struct GsdObjCContext; +using ObjCGsdInvoker = bool (*)(GsdObjCContext&); +struct ObjCGsdDispatchEntry { + uint64_t dispatchId; + ObjCGsdInvoker invoker; +}; + +struct GsdObjCContext { + Runtime& runtime; + const std::shared_ptr& bridge; + id self; + SEL selector; + const Value* arguments; + const NativeApiType& returnType; + Value result = Value::undefined(); + + template + void invokeNative(Invocation&& invocation) { + performGeneratedObjCInvocation(runtime, bridge, [&]() { invocation(); }); + } + + bool readNumber(size_t i, double* out) { + const Value& v = arguments[i]; + if (!v.isNumber()) return false; + *out = v.asNumber(); + return true; + } + bool readBool(size_t i, uint8_t* out) { + const Value& v = arguments[i]; + if (!v.isBool()) return false; + *out = v.getBool() ? 1 : 0; + return true; + } + template + bool readSigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + template + bool readUnsigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readFloat(size_t i, float* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readDouble(size_t i, double* out) { return readNumber(i, out); } + bool readSelector(size_t i, SEL* out) { + return readFastEngineSelectorArgument(runtime, arguments[i], out); + } + bool readClass(size_t i, Class* out) { + Class cls = classFromEngineValue(runtime, arguments[i]); + if (cls == Nil) return false; + *out = cls; + return true; + } + bool readObject(size_t i, id* out) { + const Value& v = arguments[i]; + if (v.isNull() || v.isUndefined()) { + *out = nil; + return true; + } + if (!v.isObject()) return false; + Object o = v.getObject(runtime); + if (o.isHostObject(runtime)) { + *out = o.getHostObject(runtime)->object(); + return true; + } + if (o.isHostObject(runtime)) { + *out = static_cast( + o.getHostObject(runtime)->nativeClass()); + return true; + } + Class cls = classFromEngineValue(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + if (o.isHostObject(runtime)) { + *out = static_cast( + o.getHostObject(runtime) + ->nativeProtocol()); + return true; + } + return false; + } + + void setVoid() { result = Value::undefined(); } + void setBool(bool v) { result = Value(v); } + void setInt32(int32_t v) { result = Value(static_cast(v)); } + void setUInt32(uint32_t v) { result = Value(static_cast(v)); } + void setUInt16(uint16_t v) { + if (v >= 32 && v <= 126) { + result = makeString(runtime, std::string(1, static_cast(v))); + } else { + result = Value(static_cast(v)); + } + } + void setInt64(int64_t v) { result = signedInteger64ToEngineValue(runtime, v); } + void setUInt64(uint64_t v) { + result = unsignedInteger64ToEngineValue(runtime, v); + } + void setDouble(double v) { result = Value(v); } + void setSelector(SEL v) { + const char* name = v != nullptr ? sel_getName(v) : nullptr; + result = name != nullptr ? makeString(runtime, name) : Value::null(); + } + void setClass(Class v) { + if (v == nil) { + result = Value::null(); + return; + } + const char* name = class_getName(v); + 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; + } + result = makeNativeClassValue(runtime, bridge, std::move(symbol)); + } + void setObject(id obj) { + result = convertNativeReturnValue(runtime, bridge, returnType, &obj); + } +}; + +// Close the anonymous namespace so the generated dispatch table lives in +// namespace nativescript; GsdObjCContext/ObjCGsdDispatchEntry stay reachable +// via the unnamed namespace's implicit using-directive. +} // namespace (temporary close for GSD .inc) + +#if defined(__has_include) +#if __has_include("GeneratedGsdSignatureDispatch.inc") +#include "GeneratedGsdSignatureDispatch.inc" +#endif +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH +inline constexpr ObjCGsdDispatchEntry kGeneratedObjCGsdDispatchEntries[] = { + {0, nullptr}}; +#endif + +ObjCGsdInvoker lookupObjCGsdInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCGsdDispatchEntries, dispatchId); +} + +namespace { // reopen anonymous namespace + +// --- End GSD --- diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/hermes/NativeApiJsiReactNative.h similarity index 100% rename from NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h rename to NativeScript/ffi/hermes/NativeApiJsiReactNative.h diff --git a/NativeScript/ffi/hermes/NativeApiJsiSignatureDispatch.h b/NativeScript/ffi/hermes/NativeApiJsiSignatureDispatch.h new file mode 100644 index 000000000..279dbd742 --- /dev/null +++ b/NativeScript/ffi/hermes/NativeApiJsiSignatureDispatch.h @@ -0,0 +1,14 @@ +#ifndef NS_FFI_HERMES_NATIVE_API_JSI_SIGNATURE_DISPATCH_H +#define NS_FFI_HERMES_NATIVE_API_JSI_SIGNATURE_DISPATCH_H + +#include "ffi/shared/SignatureDispatchCore.h" + +#if defined(__has_include) +#if __has_include("GeneratedSignatureDispatch.inc") +#include "GeneratedSignatureDispatch.inc" +#endif +#endif + +#include "ffi/shared/PreparedSignatureDispatch.h" + +#endif // NS_FFI_HERMES_NATIVE_API_JSI_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/hermes/README.md b/NativeScript/ffi/hermes/README.md new file mode 100644 index 000000000..35c04d97a --- /dev/null +++ b/NativeScript/ffi/hermes/README.md @@ -0,0 +1,23 @@ +# Native API Hermes JSI backend + +This directory owns the Hermes-facing Native API entrypoint: + +- `NativeApiJsi.h` exposes the public JSI install/create API. +- `NativeApiJsi.mm` binds Hermes JSI types to the Hermes-owned bridge + implementation files in this directory. +- `NativeApiJsiReactNative.h` adapts React Native `CallInvoker`s to the JSI + scheduler config used by the TurboModule. +- `NativeApiJsiSignatureDispatch.h` wires Hermes generated signature dispatch + tables into native invocation. + +Hermes is the only backend that exposes the real `facebook::jsi` API. V8, JSC, +and QuickJS own their bridge implementations in their respective engine +directories. + +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); +``` diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h deleted file mode 100644 index 82df76143..000000000 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h +++ /dev/null @@ -1,44 +0,0 @@ -#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; - std::function)> nativeInvocationInvoker = nullptr; - std::function)> nativeCallbackInvoker = nullptr; - std::function)> jsThreadCallbackInvoker = nullptr; - bool invokeCallbacksOnNativeCallerThread = false; - bool installGlobalSymbols = false; -}; - -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/hermes/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm deleted file mode 100644 index 70a80bbfe..000000000 --- a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm +++ /dev/null @@ -1,89 +0,0 @@ -#include "NativeApiJsi.h" - -#ifdef TARGET_ENGINE_HERMES - -#import -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Metadata.h" -#include "MetadataReader.h" -#include "ffi.h" - -@protocol NativeApiJsiClassBuilderProtocol -@end - -#ifdef EMBED_METADATA_SIZE -extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; -#endif - -namespace nativescript { -namespace { - -using facebook::jsi::Array; -using facebook::jsi::ArrayBuffer; -using facebook::jsi::BigInt; -using facebook::jsi::Function; -using facebook::jsi::HostObject; -using facebook::jsi::MutableBuffer; -using facebook::jsi::Object; -using facebook::jsi::PropNameID; -using facebook::jsi::Runtime; -using facebook::jsi::String; -using facebook::jsi::StringBuffer; -using facebook::jsi::Value; -using metagen::MDMemberFlag; -using metagen::MDMetadataReader; -using metagen::MDSectionOffset; -using metagen::MDTypeKind; - -// clang-format off -#include "jsi/NativeApiJsiBridge.h" -#include "jsi/NativeApiJsiHostObjects.h" -#include "jsi/NativeApiJsiCallbacks.h" -#include "jsi/NativeApiJsiConversion.h" -#include "jsi/NativeApiJsiInvocation.h" -#include "jsi/NativeApiJsiClassBuilder.h" -#include "jsi/NativeApiJsiHostObject.h" -// clang-format on - -} // namespace - -#include "jsi/NativeApiJsiInstall.h" - -} // 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/README.md b/NativeScript/ffi/hermes/jsi/README.md deleted file mode 100644 index ca07222b5..000000000 --- a/NativeScript/ffi/hermes/jsi/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Native API JSI bridge - -This directory contains the Hermes-first JSI entrypoint for NativeScript Native -API access. - -The backend is split by FFI responsibility: - -- `../../shared/jsi/NativeApiJsiBridge.h` owns metadata indexing, symbol lookup, scheduler - state, and bridge lifetime caches. -- `../../shared/jsi/NativeApiJsiHostObjects.h` owns class, object, protocol, pointer, - reference, struct, and union host objects. -- `../../shared/jsi/NativeApiJsiCallbacks.h` owns signatures, libffi callback trampolines, - JS blocks, and native function pointer callback lifetime. -- `../../shared/jsi/NativeApiJsiConversion.h` owns JSI/native type conversion and the - `interop` helper surface. -- `../../shared/jsi/NativeApiJsiInvocation.h` owns constants, enums, C function calls, - function pointer calls, and Objective-C selector dispatch. -- `../../shared/jsi/NativeApiJsiHostObject.h` owns the public API host object exposed to JS. -- `../../shared/jsi/NativeApiJsiInstall.h` owns runtime/global installation. - -The core installer is engine-host agnostic: - -```cpp -nativescript::NativeApiJsiConfig config; -config.metadataPath = metadataPath; -config.metadataPtr = metadataPtr; -nativescript::InstallNativeApiJSI(runtime, config); -``` - -NativeScript's Hermes runtime installs this automatically as -`globalThis.__nativeScriptNativeApi`. - -React Native integrations should include `NativeApiJsiReactNative.h` from a -TurboModule implementation and pass the module's JS/UI `CallInvoker`s: - -```cpp -nativescript::InstallReactNativeNativeApiJSI( - runtime, jsInvoker, uiInvoker, metadataPath, metadataPtr); -``` - -The React Native adapter is intentionally only a scheduler/config shim. The -native API host object, metadata loading, primitive C function dispatch, -Objective-C class/object handles, and selector invocation live in the shared -JSI implementation so they can be used by both NativeScript Hermes and a React -Native TurboModule without going through Node-API. - -The direct JSI backend is still moving toward full NativeScript bridge parity. -It covers the metadata-backed Objective-C class/function/constant/enum paths -needed by the React Native TurboModule, plus metadata-backed structs/unions, -primitive array/vector value marshalling, JS blocks, C function pointer -callbacks, protocol wrappers, pointer/reference helpers, and the core `interop` -helpers (`Pointer`, `Reference`, `sizeof`, `alloc`, `free`, `adopt`, -`handleof`, `stringFromCString`, `bufferFromData`, and `addProtocol`). Struct -and union constructors, plus protocol symbols, are installed on `globalThis` -along with `interop` so common NativeScript-style calls such as -`CGRect({ origin, size })`, `interop.sizeof(CGRect)`, and -`interop.handleof(value)` work through JSI. - -The remaining 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/jsc/NativeApiJSC.h b/NativeScript/ffi/jsc/NativeApiJSC.h index 0bf60c969..7b013f558 100644 --- a/NativeScript/ffi/jsc/NativeApiJSC.h +++ b/NativeScript/ffi/jsc/NativeApiJSC.h @@ -1,19 +1,20 @@ #ifndef NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H #define NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H -#include "ffi/shared/direct/NativeApiDirect.h" +#include "ffi/shared/NativeApiBackendConfig.h" #include namespace nativescript { -using NativeApiJSCConfig = NativeApiDirectConfig; +using NativeApiScheduler = NativeApiBackendScheduler; +using NativeApiConfig = NativeApiBackendConfig; -void InstallNativeApiJSC(JSGlobalContextRef context, - const NativeApiJSCConfig& config = NativeApiJSCConfig{}); +void InstallNativeApi(JSGlobalContextRef context, + const NativeApiConfig& config = NativeApiConfig{}); } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiJSC(JSGlobalContextRef context, +extern "C" void NativeScriptInstallNativeApi(JSGlobalContextRef context, const char* metadataPath); #endif // NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H diff --git a/NativeScript/ffi/jsc/NativeApiJSC.mm b/NativeScript/ffi/jsc/NativeApiJSC.mm index b1a6fa398..227af4c60 100644 --- a/NativeScript/ffi/jsc/NativeApiJSC.mm +++ b/NativeScript/ffi/jsc/NativeApiJSC.mm @@ -3,68 +3,69 @@ #ifdef TARGET_ENGINE_JSC #include "NativeApiJSCRuntime.h" +#include "SignatureDispatch.h" namespace nativescript { -using NativeApiJsiConfig = NativeApiDirectConfig; -using NativeApiJsiScheduler = NativeApiDirectScheduler; - namespace { -using facebook::jsi::Array; -using facebook::jsi::ArrayBuffer; -using facebook::jsi::BigInt; -using facebook::jsi::Function; -using facebook::jsi::HostObject; -using facebook::jsi::MutableBuffer; -using facebook::jsi::Object; -using facebook::jsi::PropNameID; -using facebook::jsi::Runtime; -using facebook::jsi::String; -using facebook::jsi::StringBuffer; -using facebook::jsi::Value; +using nativescript::engine::Array; +using nativescript::engine::ArrayBuffer; +using nativescript::engine::BigInt; +using nativescript::engine::Function; +using nativescript::engine::HostObject; +using nativescript::engine::MutableBuffer; +using nativescript::engine::Object; +using nativescript::engine::PropNameID; +using nativescript::engine::Runtime; +using nativescript::engine::String; +using nativescript::engine::StringBuffer; +using nativescript::engine::Value; +using nativescript::engine::JSError; using metagen::MDMemberFlag; using metagen::MDMetadataReader; using metagen::MDSectionOffset; using metagen::MDTypeKind; // clang-format off -#include "jsi/NativeApiJsiBridge.h" -#include "jsi/NativeApiJsiHostObjects.h" +#define NATIVESCRIPT_NATIVE_API_BACKEND_NAME "jsc" +#include "../shared/bridge/ObjCBridge.mm" // clang-format on #define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_SELECTOR_GROUP_FUNCTION 1 -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { - return std::make_shared(runtime.state()); -} +#include "NativeApiJSCRuntimeSupport.mm" // clang-format off -#include "jsi/NativeApiJsiCallbacks.h" -#include "jsi/NativeApiJsiConversion.h" -#include "jsi/NativeApiJsiInvocation.h" -#include "jsi/NativeApiJsiClassBuilder.h" -#include "jsi/NativeApiJsiHostObject.h" +#include "../shared/bridge/HostObjects.mm" +#include "../shared/bridge/Callbacks.mm" +#include "../shared/bridge/TypeConv.mm" +#include "../shared/bridge/Invocation.mm" +#include "../shared/bridge/ClassBuilder.mm" +#include "../shared/bridge/HostObject.mm" // clang-format on +#include "NativeApiJSCSelectorGroups.mm" + } // namespace -#include "jsi/NativeApiJsiInstall.h" +#include "../shared/bridge/Install.mm" -void InstallNativeApiJSC(JSGlobalContextRef context, const NativeApiJSCConfig& config) { +void InstallNativeApi(JSGlobalContextRef context, const NativeApiConfig& config) { if (context == nullptr) { return; } Runtime runtime(context); - InstallNativeApiJSI(runtime, config); + InstallNativeApi(runtime, config); } } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiJSC(JSGlobalContextRef context, +extern "C" void NativeScriptInstallNativeApi(JSGlobalContextRef context, const char* metadataPath) { - nativescript::NativeApiJSCConfig config; + nativescript::NativeApiConfig config; config.metadataPath = metadataPath; - nativescript::InstallNativeApiJSC(context, config); + nativescript::InstallNativeApi(context, config); } #endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCGsd.mm b/NativeScript/ffi/jsc/NativeApiJSCGsd.mm new file mode 100644 index 000000000..a0e89498b --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCGsd.mm @@ -0,0 +1,316 @@ +// --- GSD (Generated Signature Dispatch) for JSC --- +// GsdObjCContext is the engine-neutral interface the generated invokers use: +// it reads JS arguments and writes the JS return value via the JSC API. The +// readers/setters mirror JSC's generic conversions exactly; any value not in +// the fast representation makes a reader return false so the invoker falls +// back to the fully correct generic path. Number readers require an actual +// JS number so coercion edge cases (numeric strings, single-char unichar +// arguments) defer to the generic path. +struct GsdObjCContext; +using ObjCGsdInvoker = bool (*)(GsdObjCContext&); +struct ObjCGsdDispatchEntry { + uint64_t dispatchId; + ObjCGsdInvoker invoker; +}; + +struct GsdObjCContext { + Runtime& runtime; + const std::shared_ptr& bridge; + id self; + SEL selector; + JSContextRef context; + const JSValueRef* arguments; + const NativeApiType& returnType; + JSValueRef result = nullptr; + const Value* valueArguments = nullptr; + bool materializeValueResult = false; + Value valueResult = Value::undefined(); + + template + void invokeNative(Invocation&& invocation) { + performGeneratedObjCInvocation(runtime, bridge, [&]() { invocation(); }); + } + + bool readNumber(size_t i, double* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (!v.isNumber()) return false; + *out = v.getNumber(); + return true; + } + JSValueRef v = arguments[i]; + if (!JSValueIsNumber(context, v)) return false; + JSValueRef exception = nullptr; + double converted = JSValueToNumber(context, v, &exception); + if (exception != nullptr) return false; + *out = converted; + return true; + } + bool readBool(size_t i, uint8_t* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (!v.isBool()) return false; + *out = v.getBool() ? 1 : 0; + return true; + } + JSValueRef v = arguments[i]; + if (!JSValueIsBoolean(context, v)) return false; + *out = JSValueToBoolean(context, v) ? 1 : 0; + return true; + } + template + bool readSigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + template + bool readUnsigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readFloat(size_t i, float* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readDouble(size_t i, double* out) { return readNumber(i, out); } + bool readSelector(size_t i, SEL* out) { + if (valueArguments != nullptr) { + return readFastEngineSelectorArgument(runtime, valueArguments[i], out); + } + return readJSCEngineSelectorArgument(runtime, arguments[i], out); + } + bool readClass(size_t i, Class* out) { + if (valueArguments != nullptr) { + Class cls = classFromEngineValue(runtime, valueArguments[i]); + if (cls == Nil) return false; + *out = cls; + return true; + } + if (auto* c = jscHostObjectRaw( + runtime, arguments[i])) { + *out = c->nativeClass(); + return true; + } + Class cls = jscNativeClassArgument(runtime, arguments[i]); + if (cls == Nil) return false; + *out = cls; + return true; + } + bool readObject(size_t i, id* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (v.isNull() || v.isUndefined()) { + *out = nil; + return true; + } + if (!v.isObject()) return false; + Object object = v.asObject(runtime); + if (object.isHostObject(runtime)) { + *out = object.getHostObject(runtime)->object(); + return true; + } + if (object.isHostObject(runtime)) { + *out = static_cast( + object.getHostObject(runtime)->nativeClass()); + return true; + } + Class cls = classFromEngineValue(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + if (object.isHostObject(runtime)) { + *out = static_cast( + object.getHostObject(runtime) + ->nativeProtocol()); + return true; + } + return false; + } + JSValueRef v = arguments[i]; + if (v == nullptr || JSValueIsNull(context, v) || + JSValueIsUndefined(context, v)) { + *out = nil; + return true; + } + if (auto* h = jscHostObjectRaw(runtime, v)) { + *out = h->object(); + return true; + } + if (auto* c = jscHostObjectRaw(runtime, v)) { + *out = static_cast(c->nativeClass()); + return true; + } + if (JSValueIsObject(context, v)) { + Class cls = jscNativeClassArgument(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + } + if (auto* p = jscHostObjectRaw(runtime, v)) { + *out = static_cast(p->nativeProtocol()); + return true; + } + return false; + } + + void setVoid() { + if (materializeValueResult) { + valueResult = Value::undefined(); + return; + } + result = JSValueMakeUndefined(context); + } + void setBool(bool v) { + if (materializeValueResult) { + valueResult = Value(v); + return; + } + result = JSValueMakeBoolean(context, v); + } + void setInt32(int32_t v) { + if (materializeValueResult) { + valueResult = Value(static_cast(v)); + return; + } + result = JSValueMakeNumber(context, v); + } + void setUInt32(uint32_t v) { + if (materializeValueResult) { + valueResult = Value(static_cast(v)); + return; + } + result = JSValueMakeNumber(context, v); + } + void setUInt16(uint16_t v) { + if (materializeValueResult) { + if (v >= 32 && v <= 126) { + valueResult = makeString(runtime, std::string(1, static_cast(v))); + } else { + valueResult = Value(static_cast(v)); + } + return; + } + if (v >= 32 && v <= 126) { + char buffer[2] = {static_cast(v), '\0'}; + JSStringRef string = engine::jscengine::makeJSString(buffer); + result = JSValueMakeString(context, string); + JSStringRelease(string); + } else { + result = JSValueMakeNumber(context, v); + } + } + void setInt64(int64_t v) { + if (materializeValueResult) { + valueResult = signedInteger64ToEngineValue(runtime, v); + return; + } + result = jscInteger64Value(runtime, v); + } + void setUInt64(uint64_t v) { + if (materializeValueResult) { + valueResult = unsignedInteger64ToEngineValue(runtime, v); + return; + } + result = jscUnsignedInteger64Value(runtime, v); + } + void setDouble(double v) { + if (materializeValueResult) { + valueResult = Value(v); + return; + } + result = JSValueMakeNumber(context, v); + } + void setSelector(SEL v) { + const char* name = v != nullptr ? sel_getName(v) : nullptr; + if (materializeValueResult) { + valueResult = name != nullptr ? makeString(runtime, name) : Value::null(); + return; + } + if (name == nullptr) { + result = JSValueMakeNull(context); + return; + } + JSStringRef string = engine::jscengine::makeJSString(name); + result = JSValueMakeString(context, string); + JSStringRelease(string); + } + void setClass(Class v) { + if (materializeValueResult) { + if (v == nil) { + valueResult = Value::null(); + return; + } + const char* name = class_getName(v); + 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; + } + valueResult = makeNativeClassValue(runtime, bridge, std::move(symbol)); + return; + } + if (v == nil) { + result = JSValueMakeNull(context); + return; + } + const char* name = class_getName(v); + 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; + } + Value classValue = makeNativeClassValue(runtime, bridge, std::move(symbol)); + result = classValue.local(runtime); + } + void setObject(id obj) { + if (materializeValueResult) { + valueResult = convertNativeReturnValue(runtime, bridge, returnType, &obj); + return; + } + result = setJSCEngineObjectReturn(runtime, bridge, returnType, obj); + } +}; + +// Close the anonymous namespace so the generated dispatch table lives in +// namespace nativescript. GsdObjCContext/ObjCGsdDispatchEntry remain reachable +// via the unnamed namespace's implicit using-directive. +} // namespace (temporary close for GSD .inc) + +#if defined(__has_include) +#if __has_include("GeneratedGsdSignatureDispatch.inc") +#include "GeneratedGsdSignatureDispatch.inc" +#endif +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH +inline constexpr ObjCGsdDispatchEntry kGeneratedObjCGsdDispatchEntries[] = { + {0, nullptr}}; +#endif + +ObjCGsdInvoker lookupObjCGsdInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCGsdDispatchEntries, dispatchId); +} + +namespace { // reopen anonymous namespace + +// --- End GSD --- diff --git a/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm b/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm index ab2da20a8..7af17b3e4 100644 --- a/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm +++ b/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm @@ -2,13 +2,98 @@ #ifdef TARGET_ENGINE_JSC -namespace facebook { -namespace jsi { +namespace nativescript { +class NativeApiObjectHostObject; +} + +namespace nativescript { +namespace engine { -namespace jscdirect { +namespace jscengine { JSClassRef hostClass(Runtime& runtime); JSClassRef functionClass(Runtime& runtime); +void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function); + +template +class StackValueArray { + public: + explicit StackValueArray(size_t count) : count_(count) { + if (count_ > InlineCount) { + values_ = static_cast(::operator new(sizeof(Value) * count_)); + } else { + values_ = reinterpret_cast(inlineStorage_); + } + } + + ~StackValueArray() { + for (size_t i = 0; i < constructed_; i++) { + values_[i].~Value(); + } + if (count_ > InlineCount) { + ::operator delete(values_); + } + } + + StackValueArray(const StackValueArray&) = delete; + StackValueArray& operator=(const StackValueArray&) = delete; + + void emplace(size_t index, Value&& value) { + new (&values_[index]) Value(std::move(value)); + constructed_++; + } + + Value* data() { return count_ == 0 ? nullptr : values_; } + size_t size() const { return count_; } + + private: + size_t count_ = 0; + size_t constructed_ = 0; + Value* values_ = nullptr; + alignas(Value) unsigned char inlineStorage_[sizeof(Value) * InlineCount]; +}; + +bool isNativeInstancePrototypeBypassExcluded(JSStringRef propertyName) { + return JSStringIsEqualToUTF8CString(propertyName, "kind") || + JSStringIsEqualToUTF8CString(propertyName, "className") || + JSStringIsEqualToUTF8CString(propertyName, "nativeAddress") || + JSStringIsEqualToUTF8CString(propertyName, "class") || + JSStringIsEqualToUTF8CString(propertyName, "constructor") || + JSStringIsEqualToUTF8CString(propertyName, "super") || + JSStringIsEqualToUTF8CString(propertyName, "invoke") || + JSStringIsEqualToUTF8CString(propertyName, "send") || + JSStringIsEqualToUTF8CString(propertyName, "takeRetainedValue") || + JSStringIsEqualToUTF8CString(propertyName, "takeUnretainedValue") || + JSStringIsEqualToUTF8CString(propertyName, "toString"); +} + +bool shouldDeferToNativeInstancePrototype(JSContextRef context, + JSObjectRef object, + JSStringRef propertyName, + HostObjectHolder* holder) { + if (context == nullptr || object == nullptr || propertyName == nullptr || + holder == nullptr || + holder->typeToken != hostObjectTypeToken() || + isNativeInstancePrototypeBypassExcluded(propertyName)) { + return false; + } + + JSValueRef prototypeValue = JSObjectGetPrototype(context, object); + if (prototypeValue == nullptr || !JSValueIsObject(context, prototypeValue)) { + return false; + } + + JSValueRef exception = nullptr; + JSObjectRef prototypeObject = + JSValueToObject(context, prototypeValue, &exception); + if (exception != nullptr || prototypeObject == nullptr) { + return false; + } + + exception = nullptr; + bool found = JSObjectHasProperty(context, prototypeObject, propertyName); + return exception == nullptr && found; +} JSValueRef hostGetProperty(JSContextRef context, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) { @@ -16,6 +101,10 @@ JSValueRef hostGetProperty(JSContextRef context, JSObjectRef object, JSStringRef if (holder == nullptr || holder->hostObject == nullptr) { return nullptr; } + if (shouldDeferToNativeInstancePrototype(context, object, propertyName, + holder)) { + return nullptr; + } Runtime runtime(holder->state); try { Value result = holder->hostObject->get(runtime, PropNameID(stringToUtf8(propertyName))); @@ -34,8 +123,8 @@ bool hostSetProperty(JSContextRef context, JSObjectRef object, JSStringRef prope } Runtime runtime(holder->state); try { - holder->hostObject->set(runtime, PropNameID(stringToUtf8(propertyName)), Value(runtime, value)); - return true; + return holder->hostObject->set(runtime, PropNameID(stringToUtf8(propertyName)), + Value::borrowed(runtime, value)); } catch (const std::exception& error) { setException(context, exception, error); return true; @@ -70,15 +159,15 @@ JSValueRef functionCall(JSContextRef context, JSObjectRef function, JSObjectRef return JSValueMakeUndefined(context); } Runtime runtime(holder->state); - std::vector args; - args.reserve(argumentCount); + StackValueArray<8> args(argumentCount); for (size_t i = 0; i < argumentCount; i++) { - args.emplace_back(runtime, arguments[i]); + args.emplace(i, Value::borrowed(runtime, arguments[i])); } try { - Value thisValue(runtime, thisObject); + Value thisValue = Value::borrowed(runtime, thisObject); Value result = - holder->callback(runtime, thisValue, args.empty() ? nullptr : args.data(), args.size()); + holder->callback(runtime, thisValue, args.size() == 0 ? nullptr : args.data(), + args.size()); return result.local(runtime); } catch (const std::exception& error) { setException(context, exception, error); @@ -94,7 +183,7 @@ JSClassRef hostClass(Runtime& runtime) { auto state = runtime.state(); if (state->hostClass == nullptr) { JSClassDefinition definition = kJSClassDefinitionEmpty; - definition.className = "NativeScriptDirectHostObject"; + definition.className = "NativeScriptEngineHostObject"; definition.getProperty = hostGetProperty; definition.setProperty = hostSetProperty; definition.getPropertyNames = hostGetPropertyNames; @@ -108,7 +197,7 @@ JSClassRef functionClass(Runtime& runtime) { auto state = runtime.state(); if (state->functionClass == nullptr) { JSClassDefinition definition = kJSClassDefinitionEmpty; - definition.className = "NativeScriptDirectFunction"; + definition.className = "NativeScriptEngineFunction"; definition.callAsFunction = functionCall; definition.finalize = functionFinalize; state->functionClass = JSClassCreate(&definition); @@ -149,24 +238,24 @@ void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function) { JSObjectSetPrototype(context, function, prototypeValue); } -} // namespace jscdirect +} // namespace jscengine Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken) { - auto* holder = new jscdirect::HostObjectHolder(runtime.state(), std::move(host), typeToken); - JSObjectRef object = JSObjectMake(runtime.context(), jscdirect::hostClass(runtime), holder); + auto* holder = new jscengine::HostObjectHolder(runtime.state(), std::move(host), typeToken); + JSObjectRef object = JSObjectMake(runtime.context(), jscengine::hostClass(runtime), holder); return Object::fromValueStorage(Value(runtime, object).storage_); } Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, HostFunctionType callback) { - auto* holder = new jscdirect::FunctionHolder(runtime.state(), std::move(callback)); - JSObjectRef function = JSObjectMake(runtime.context(), jscdirect::functionClass(runtime), holder); - jscdirect::setFunctionPrototype(runtime.context(), function); + auto* holder = new jscengine::FunctionHolder(runtime.state(), std::move(callback)); + JSObjectRef function = JSObjectMake(runtime.context(), jscengine::functionClass(runtime), holder); + jscengine::setFunctionPrototype(runtime.context(), function); std::string functionName = name.utf8(runtime); if (!functionName.empty()) { - JSStringRef property = jscdirect::makeJSString("name"); - JSStringRef valueString = jscdirect::makeJSString(functionName); + JSStringRef property = jscengine::makeJSString("name"); + JSStringRef valueString = jscengine::makeJSString(functionName); JSValueRef value = JSValueMakeString(runtime.context(), valueString); JSObjectSetProperty(runtime.context(), function, property, value, kJSPropertyAttributeReadOnly, nullptr); @@ -176,7 +265,7 @@ void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function) { return Function(Object::fromValueStorage(Value(runtime, function).storage_)); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCMarshalling.mm b/NativeScript/ffi/jsc/NativeApiJSCMarshalling.mm new file mode 100644 index 000000000..c5b5290e5 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCMarshalling.mm @@ -0,0 +1,461 @@ +// Included by NativeApiJSCSelectorGroups.mm inside the NativeScript anonymous namespace. + +std::string jscValueToUtf8(Runtime& runtime, JSValueRef value) { + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(runtime.context(), value, &exception); + if (string == nullptr || exception != nullptr) { + if (string != nullptr) { + JSStringRelease(string); + } + return {}; + } + std::string result = engine::jscengine::stringToUtf8(string); + JSStringRelease(string); + return result; +} + +bool jscNumberValue(Runtime& runtime, JSValueRef value, double* result) { + if (result == nullptr) { + return false; + } + JSValueRef exception = nullptr; + double converted = JSValueToNumber(runtime.context(), value, &exception); + if (exception != nullptr) { + return false; + } + *result = converted; + return true; +} + +template +std::shared_ptr jscHostObject(Runtime& runtime, JSValueRef value) { + if (value == nullptr || !JSValueIsObject(runtime.context(), value)) { + return nullptr; + } + JSValueRef exception = nullptr; + JSObjectRef object = JSValueToObject(runtime.context(), value, &exception); + if (exception != nullptr || object == nullptr) { + return nullptr; + } + auto* holder = static_cast( + JSObjectGetPrivate(object)); + if (holder == nullptr || + holder->typeToken != engine::jscengine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); +} + +template +T* jscHostObjectRaw(Runtime& runtime, JSValueRef value) { + if (value == nullptr || !JSValueIsObject(runtime.context(), value)) { + return nullptr; + } + JSValueRef exception = nullptr; + JSObjectRef object = JSValueToObject(runtime.context(), value, &exception); + if (exception != nullptr || object == nullptr) { + return nullptr; + } + auto* holder = static_cast( + JSObjectGetPrivate(object)); + if (holder == nullptr || + holder->typeToken != engine::jscengine::hostObjectTypeToken()) { + return nullptr; + } + return static_cast(holder->hostObject.get()); +} + +id jscNativeObjectArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiType& type, JSValueRef value, + NativeApiArgumentFrame& frame) { + if (value == nullptr || JSValueIsNull(runtime.context(), value) || + JSValueIsUndefined(runtime.context(), value)) { + return nil; + } + if (JSValueIsString(runtime.context(), value)) { + std::string utf8 = jscValueToUtf8(runtime, value); + id string = type.kind == metagen::mdTypeNSMutableStringObject + ? [[NSMutableString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding] + : [[NSString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding]; + if (string != nil) { + frame.addObject(string); + } + return string; + } + if (JSValueIsBoolean(runtime.context(), value)) { + return [NSNumber numberWithBool:JSValueToBoolean(runtime.context(), value)]; + } + if (JSValueIsNumber(runtime.context(), value)) { + double converted = 0; + if (jscNumberValue(runtime, value, &converted)) { + return [NSNumber numberWithDouble:converted]; + } + } + if (!JSValueIsObject(runtime.context(), value)) { + return nil; + } + if (auto objectHost = jscHostObject(runtime, value)) { + return objectHost->object(); + } + if (auto classHost = jscHostObject(runtime, value)) { + return static_cast(classHost->nativeClass()); + } + if (auto protocolHost = + jscHostObject(runtime, value)) { + return static_cast(protocolHost->nativeProtocol()); + } + if (auto pointerHost = + jscHostObject(runtime, value)) { + return static_cast(pointerHost->pointer()); + } + if (auto referenceHost = + jscHostObject(runtime, value)) { + return static_cast(referenceHost->data()); + } + if (auto structHost = + jscHostObject(runtime, value)) { + return static_cast(structHost->data()); + } + + JSValueRef exception = nullptr; + JSObjectRef object = JSValueToObject(runtime.context(), value, &exception); + if (exception == nullptr && object != nullptr) { + JSStringRef property = engine::jscengine::makeJSString("__nativeApiClass"); + JSValueRef wrappedClassValue = + JSObjectGetProperty(runtime.context(), object, property, nullptr); + JSStringRelease(property); + if (auto classHost = + jscHostObject(runtime, + wrappedClassValue)) { + return static_cast(classHost->nativeClass()); + } + } + + Value wrapped = Value::borrowed(runtime, value); + return objectFromEngineValue(runtime, bridge, wrapped, frame, + type.kind == + metagen::mdTypeNSMutableStringObject); +} + +Class jscNativeClassArgument(Runtime& runtime, JSValueRef value) { + if (value == nullptr || JSValueIsNull(runtime.context(), value) || + JSValueIsUndefined(runtime.context(), value)) { + return Nil; + } + if (auto classHost = jscHostObject(runtime, value)) { + return classHost->nativeClass(); + } + if (JSValueIsObject(runtime.context(), value)) { + JSValueRef exception = nullptr; + JSObjectRef object = JSValueToObject(runtime.context(), value, &exception); + if (exception == nullptr && object != nullptr) { + JSStringRef property = engine::jscengine::makeJSString("__nativeApiClass"); + JSValueRef wrappedClassValue = + JSObjectGetProperty(runtime.context(), object, property, nullptr); + JSStringRelease(property); + if (auto classHost = + jscHostObject(runtime, + wrappedClassValue)) { + return classHost->nativeClass(); + } + } + } + Value wrapped = Value::borrowed(runtime, value); + return classFromEngineValue(runtime, wrapped); +} + +bool readJSCEngineSelectorArgument(Runtime& runtime, JSValueRef value, + SEL* result) { + if (result == nullptr) { + return false; + } + if (value == nullptr || JSValueIsNull(runtime.context(), value) || + JSValueIsUndefined(runtime.context(), value)) { + *result = nullptr; + return true; + } + if (!JSValueIsString(runtime.context(), value)) { + return false; + } + std::string selectorName = jscValueToUtf8(runtime, value); + *result = sel_registerName(selectorName.c_str()); + return true; +} + +template +bool writeJSCNumber(Runtime& runtime, JSValueRef value, void* target) { + double converted = 0; + if (!jscNumberValue(runtime, value, &converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; +} + +bool prepareJSCEngineArgument( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, JSValueRef value, + NativeApiArgumentFrame& frame, size_t index) { + ffi_type* ffiType = ffiTypeForEngineArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + + switch (type.kind) { + case metagen::mdTypeBool: + if (!JSValueIsBoolean(runtime.context(), value)) { + return false; + } + *static_cast(target) = + JSValueToBoolean(runtime.context(), value) ? 1 : 0; + return true; + case metagen::mdTypeChar: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeSShort: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeUShort: + if (JSValueIsString(runtime.context(), value)) { + std::string text = jscValueToUtf8(runtime, value); + if (text.size() != 1) { + return false; + } + *static_cast(target) = + static_cast(static_cast(text[0])); + return true; + } + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeSInt: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeUInt: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeFloat: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeDouble: + return writeJSCNumber(runtime, value, target); + case metagen::mdTypeSelector: + return readJSCEngineSelectorArgument(runtime, value, + static_cast(target)); + case metagen::mdTypeClass: { + Class cls = jscNativeClassArgument(runtime, value); + if (cls == Nil) { + return false; + } + *static_cast(target) = cls; + return true; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + *static_cast(target) = + jscNativeObjectArgument(runtime, bridge, type, value, frame); + return true; + default: + break; + } + + Value wrapped = Value::borrowed(runtime, value); + convertEngineFfiArgument(runtime, bridge, type, wrapped, target, frame); + return true; +} + +JSValueRef jscInteger64Value(Runtime& runtime, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return JSValueMakeNumber(runtime.context(), static_cast(value)); + } + Value bigint = BigInt::fromInt64(runtime, value); + return bigint.local(runtime); +} + +JSValueRef jscUnsignedInteger64Value(Runtime& runtime, uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return JSValueMakeNumber(runtime.context(), static_cast(value)); + } + Value bigint = BigInt::fromUint64(runtime, value); + return bigint.local(runtime); +} + +JSValueRef setJSCEngineObjectReturn( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, id object) { + if (object == nil) { + return JSValueMakeNull(runtime.context()); + } + Value roundTrip = + findCachedNativeObjectReturn(runtime, bridge, type, object); + if (!roundTrip.isUndefined()) { + JSValueRef result = roundTrip.local(runtime); + if (type.returnOwned) { + [object release]; + } + return result; + } + if (nativeObjectReturnMayCoerceToString(type) && + nativeObjectIsStringLike(object)) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; + } + JSStringRef string = engine::jscengine::makeJSString(utf8); + JSValueRef result = JSValueMakeString(runtime.context(), string); + JSStringRelease(string); + return result; + } + if ([object isKindOfClass:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + return JSValueMakeNull(runtime.context()); + } + 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); + JSValueRef result = + isBool ? JSValueMakeBoolean(runtime.context(), [number boolValue]) + : JSValueMakeNumber(runtime.context(), [number doubleValue]); + if (type.returnOwned) { + [object release]; + } + return result; + } + + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + Value result = makeNativeClassValue(runtime, bridge, *classSymbol); + if (type.returnOwned) { + [object release]; + } + return result.local(runtime); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + Value result = makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + if (type.returnOwned) { + [object release]; + } + return result.local(runtime); + } + Value result = makeNativeObjectValue(runtime, bridge, object, type.returnOwned); + return result.local(runtime); +} + +JSValueRef setJSCEngineReturnValue( + Runtime& runtime, const std::shared_ptr& bridge, + NativeApiType type, void* value, const std::string& selectorName) { + switch (type.kind) { + case metagen::mdTypeVoid: + return JSValueMakeUndefined(runtime.context()); + case metagen::mdTypeBool: + return JSValueMakeBoolean(runtime.context(), + *static_cast(value) != 0); + case metagen::mdTypeChar: + return JSValueMakeNumber(runtime.context(), + *static_cast(value)); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return JSValueMakeNumber(runtime.context(), + *static_cast(value)); + case metagen::mdTypeSShort: + return JSValueMakeNumber(runtime.context(), + *static_cast(value)); + case metagen::mdTypeUShort: { + uint16_t raw = *static_cast(value); + if (raw >= 32 && raw <= 126) { + char buffer[2] = {static_cast(raw), '\0'}; + JSStringRef string = engine::jscengine::makeJSString(buffer); + JSValueRef result = JSValueMakeString(runtime.context(), string); + JSStringRelease(string); + return result; + } + return JSValueMakeNumber(runtime.context(), raw); + } + case metagen::mdTypeSInt: + return JSValueMakeNumber(runtime.context(), + *static_cast(value)); + case metagen::mdTypeUInt: + return JSValueMakeNumber(runtime.context(), + *static_cast(value)); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return jscInteger64Value(runtime, *static_cast(value)); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return jscUnsignedInteger64Value(runtime, + *static_cast(value)); + case metagen::mdTypeFloat: + return JSValueMakeNumber(runtime.context(), *static_cast(value)); + case metagen::mdTypeDouble: + return JSValueMakeNumber(runtime.context(), *static_cast(value)); + case metagen::mdTypeClass: { + Class cls = *static_cast(value); + if (cls == nil) { + return JSValueMakeNull(runtime.context()); + } + 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; + } + Value result = makeNativeClassValue(runtime, bridge, std::move(symbol)); + return result.local(runtime); + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + if ((selectorName == "valueForKey:" || + selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(type)) { + type.kind = metagen::mdTypeAnyObject; + } + return setJSCEngineObjectReturn(runtime, bridge, type, + *static_cast(value)); + case metagen::mdTypeSelector: { + SEL selector = *static_cast(value); + const char* selectorNameValue = + selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorNameValue == nullptr) { + return JSValueMakeNull(runtime.context()); + } + JSStringRef string = engine::jscengine::makeJSString(selectorNameValue); + JSValueRef result = JSValueMakeString(runtime.context(), string); + JSStringRelease(string); + return result; + } + default: + break; + } + Value result = convertNativeReturnValue(runtime, bridge, type, value); + return result.local(runtime); +} diff --git a/NativeScript/ffi/jsc/NativeApiJSCRuntime.h b/NativeScript/ffi/jsc/NativeApiJSCRuntime.h index 305399f41..ecf8db1d3 100644 --- a/NativeScript/ffi/jsc/NativeApiJSCRuntime.h +++ b/NativeScript/ffi/jsc/NativeApiJSCRuntime.h @@ -37,15 +37,15 @@ #include "MetadataReader.h" #include "ffi.h" -@protocol NativeApiJsiClassBuilderProtocol +@protocol NativeApiClassBuilderProtocol @end #ifdef EMBED_METADATA_SIZE extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { class Runtime; class Value; @@ -100,13 +100,13 @@ class HostObject { public: virtual ~HostObject() = default; virtual Value get(Runtime& runtime, const PropNameID& name); - virtual void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); virtual std::vector getPropertyNames(Runtime& runtime); }; using HostFunctionType = std::function; -namespace jscdirect { +namespace jscengine { inline std::string stringToUtf8(JSStringRef string) { if (string == nullptr) { @@ -188,11 +188,15 @@ struct RuntimeState { if (functionClass != nullptr) { JSClassRelease(functionClass); } + if (selectorGroupFunctionClass != nullptr) { + JSClassRelease(selectorGroupFunctionClass); + } } JSGlobalContextRef context = nullptr; JSClassRef hostClass = nullptr; JSClassRef functionClass = nullptr; + JSClassRef selectorGroupFunctionClass = nullptr; }; struct ValueStorage { @@ -202,12 +206,13 @@ struct ValueStorage { Bool, Number, JSC, + JSCBorrowed, }; explicit ValueStorage(Kind kind) : kind(kind) {} ~ValueStorage() { - if (context != nullptr && value != nullptr) { + if (kind == Kind::JSC && context != nullptr && value != nullptr) { JSValueUnprotect(context, value); } } @@ -249,24 +254,26 @@ struct ArrayBufferHolder { std::shared_ptr buffer; }; -} // namespace jscdirect +void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function); + +} // namespace jscengine class Runtime { public: explicit Runtime(JSGlobalContextRef context) - : state_(std::make_shared(context)) {} + : state_(std::make_shared(context)) {} - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} JSGlobalContextRef context() const { return state_->context; } - std::shared_ptr state() const { return state_; } + std::shared_ptr state() const { return state_; } Object global(); Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); void drainMicrotasks() {} private: - std::shared_ptr state_; + std::shared_ptr state_; }; class String { @@ -275,14 +282,14 @@ class String { String(Runtime& runtime, JSStringRef string); static String createFromUtf8(Runtime& runtime, const char* value) { - JSStringRef string = jscdirect::makeJSString(value); + JSStringRef string = jscengine::makeJSString(value); String result(runtime, string); JSStringRelease(string); return result; } static String createFromUtf8(Runtime& runtime, const std::string& value) { - JSStringRef string = jscdirect::makeJSString(value); + JSStringRef string = jscengine::makeJSString(value); String result(runtime, string); JSStringRelease(string); return result; @@ -299,130 +306,149 @@ class String { private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class Value { public: - Value() - : storage_( - std::make_shared(jscdirect::ValueStorage::Kind::Undefined)) {} + Value() : kind_(jscengine::ValueStorage::Kind::Undefined) {} - Value(bool value) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::Bool)) { - storage_->boolValue = value; - } + Value(bool value) : kind_(jscengine::ValueStorage::Kind::Bool), boolValue_(value) {} - Value(double value) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::Number)) { - storage_->numberValue = value; - } + Value(double value) : kind_(jscengine::ValueStorage::Kind::Number), numberValue_(value) {} Value(int value) : Value(static_cast(value)) {} Value(uint32_t value) : Value(static_cast(value)) {} - Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} - Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == jscengine::ValueStorage::Kind::JSCBorrowed) { + // Promote borrowed to owned + storage_ = std::make_shared(jscengine::ValueStorage::Kind::JSC); + storage_->context = runtime.context(); + storage_->value = value.borrowedValue_ != nullptr ? value.borrowedValue_ + : JSValueMakeUndefined(runtime.context()); + JSValueProtect(runtime.context(), storage_->value); + kind_ = jscengine::ValueStorage::Kind::JSC; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedContext_ = value.borrowedContext_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedContext_(value.borrowedContext_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value); Value(Runtime& runtime, const Object& object); Value(Runtime& runtime, const Function& function); Value(Runtime& runtime, const Array& array); Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); Value(Runtime& runtime, const BigInt& bigint); Value(Runtime& runtime, JSValueRef value) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + : kind_(jscengine::ValueStorage::Kind::JSC), + storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { storage_->context = runtime.context(); storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); JSValueProtect(runtime.context(), storage_->value); } + static Value borrowed(Runtime& runtime, JSValueRef value) { + Value result; + result.kind_ = jscengine::ValueStorage::Kind::JSCBorrowed; + result.borrowedContext_ = runtime.context(); + result.borrowedValue_ = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); + return result; + } + static Value undefined() { return Value(); } static Value null() { Value value; - value.storage_ = std::make_shared(jscdirect::ValueStorage::Kind::Null); + value.kind_ = jscengine::ValueStorage::Kind::Null; return value; } bool isUndefined() const { - return storage_->kind == jscdirect::ValueStorage::Kind::Undefined || - (storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsUndefined(storage_->context, storage_->value)); + return kind_ == jscengine::ValueStorage::Kind::Undefined || + (isJSC() && JSValueIsUndefined(jscContext(), jscValue())); } bool isNull() const { - return storage_->kind == jscdirect::ValueStorage::Kind::Null || - (storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsNull(storage_->context, storage_->value)); + return kind_ == jscengine::ValueStorage::Kind::Null || + (isJSC() && JSValueIsNull(jscContext(), jscValue())); } bool isBool() const { - return storage_->kind == jscdirect::ValueStorage::Kind::Bool || - (storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsBoolean(storage_->context, storage_->value)); + return kind_ == jscengine::ValueStorage::Kind::Bool || + (isJSC() && JSValueIsBoolean(jscContext(), jscValue())); } bool getBool() const { - if (storage_->kind == jscdirect::ValueStorage::Kind::Bool) { - return storage_->boolValue; + if (kind_ == jscengine::ValueStorage::Kind::Bool) { + return boolValue_; } - if (storage_->kind == jscdirect::ValueStorage::Kind::JSC) { - return JSValueToBoolean(storage_->context, storage_->value); - } - return false; + return isJSC() && JSValueToBoolean(jscContext(), jscValue()); } bool isNumber() const { - return storage_->kind == jscdirect::ValueStorage::Kind::Number || - (storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsNumber(storage_->context, storage_->value)); + return kind_ == jscengine::ValueStorage::Kind::Number || + (isJSC() && JSValueIsNumber(jscContext(), jscValue())); } double getNumber() const { - if (storage_->kind == jscdirect::ValueStorage::Kind::Number) { - return storage_->numberValue; + if (kind_ == jscengine::ValueStorage::Kind::Number) { + return numberValue_; } - if (storage_->kind == jscdirect::ValueStorage::Kind::JSC) { - return JSValueToNumber(storage_->context, storage_->value, nullptr); - } - return 0; + return isJSC() ? JSValueToNumber(jscContext(), jscValue(), nullptr) : 0; } - bool isObject() const { - return storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsObject(storage_->context, storage_->value); - } - bool isString() const { - return storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsString(storage_->context, storage_->value); - } + bool isObject() const { return isJSC() && JSValueIsObject(jscContext(), jscValue()); } + bool isString() const { return isJSC() && JSValueIsString(jscContext(), jscValue()); } bool isBigInt() const { - if (storage_->kind != jscdirect::ValueStorage::Kind::JSC) { + if (!isJSC()) { return false; } if (__builtin_available(macOS 15.0, iOS 18.0, *)) { - return JSValueIsBigInt(storage_->context, storage_->value); + return JSValueIsBigInt(jscContext(), jscValue()); } return false; } - bool isSymbol() const { - return storage_->kind == jscdirect::ValueStorage::Kind::JSC && - JSValueIsSymbol(storage_->context, storage_->value); - } + bool isSymbol() const { return isJSC() && JSValueIsSymbol(jscContext(), jscValue()); } Object asObject(Runtime& runtime) const; String asString(Runtime& runtime) const; BigInt getBigInt(Runtime& runtime) const; JSValueRef local(Runtime& runtime) const { - switch (storage_->kind) { - case jscdirect::ValueStorage::Kind::Undefined: + switch (kind_) { + case jscengine::ValueStorage::Kind::Undefined: return JSValueMakeUndefined(runtime.context()); - case jscdirect::ValueStorage::Kind::Null: + case jscengine::ValueStorage::Kind::Null: return JSValueMakeNull(runtime.context()); - case jscdirect::ValueStorage::Kind::Bool: - return JSValueMakeBoolean(runtime.context(), storage_->boolValue); - case jscdirect::ValueStorage::Kind::Number: - return JSValueMakeNumber(runtime.context(), storage_->numberValue); - case jscdirect::ValueStorage::Kind::JSC: + case jscengine::ValueStorage::Kind::Bool: + return JSValueMakeBoolean(runtime.context(), boolValue_); + case jscengine::ValueStorage::Kind::Number: + return JSValueMakeNumber(runtime.context(), numberValue_); + case jscengine::ValueStorage::Kind::JSC: return storage_->value; + case jscengine::ValueStorage::Kind::JSCBorrowed: + return borrowedValue_; } } + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.storage_ = std::move(s); + return v; + } + private: friend class Runtime; friend class Object; @@ -431,20 +457,38 @@ class Value { friend class ArrayBuffer; friend class Function; friend class Array; - std::shared_ptr storage_; + + bool isJSC() const { + return kind_ == jscengine::ValueStorage::Kind::JSC || + kind_ == jscengine::ValueStorage::Kind::JSCBorrowed; + } + JSContextRef jscContext() const { + return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedContext_ + : storage_->context; + } + JSValueRef jscValue() const { + return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedValue_ : storage_->value; + } + + jscengine::ValueStorage::Kind kind_ = jscengine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + JSGlobalContextRef borrowedContext_ = nullptr; + JSValueRef borrowedValue_ = nullptr; + std::shared_ptr storage_; }; class Object { public: Object() = default; explicit Object(Runtime& runtime) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { storage_->context = runtime.context(); storage_->value = JSObjectMake(runtime.context(), nullptr, nullptr); JSValueProtect(runtime.context(), storage_->value); } - static Object fromValueStorage(std::shared_ptr storage) { + static Object fromValueStorage(std::shared_ptr storage) { Object object; object.storage_ = std::move(storage); return object; @@ -454,17 +498,17 @@ class Object { static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { auto baseHost = std::static_pointer_cast(std::move(host)); return createFromHostObjectWithToken(runtime, std::move(baseHost), - jscdirect::hostObjectTypeToken()); + jscengine::hostObjectTypeToken()); } Value getProperty(Runtime& runtime, const char* name) const { - JSStringRef property = jscdirect::makeJSString(name); + JSStringRef property = jscengine::makeJSString(name); JSValueRef exception = nullptr; JSValueRef result = JSObjectGetProperty(runtime.context(), local(runtime), property, &exception); JSStringRelease(property); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -478,7 +522,7 @@ class Object { JSValueRef result = JSObjectGetPropertyForKey(runtime.context(), local(runtime), key.local(runtime), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -490,13 +534,13 @@ class Object { Function getPropertyAsFunction(Runtime& runtime, const char* name) const; void setProperty(Runtime& runtime, const char* name, const Value& value) { - JSStringRef property = jscdirect::makeJSString(name); + JSStringRef property = jscengine::makeJSString(name); JSValueRef exception = nullptr; JSObjectSetProperty(runtime.context(), local(runtime), property, value.local(runtime), kJSPropertyAttributeNone, &exception); JSStringRelease(property); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } } @@ -523,12 +567,12 @@ class Object { JSObjectSetPropertyForKey(runtime.context(), local(runtime), key.local(runtime), value.local(runtime), kJSPropertyAttributeNone, &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } } bool hasProperty(Runtime& runtime, const char* name) const { - JSStringRef property = jscdirect::makeJSString(name); + JSStringRef property = jscengine::makeJSString(name); bool result = JSObjectHasProperty(runtime.context(), local(runtime), property); JSStringRelease(property); return result; @@ -539,7 +583,7 @@ class Object { } bool isArray(Runtime& runtime) const { - JSStringRef name = jscdirect::makeJSString("Array"); + JSStringRef name = jscengine::makeJSString("Array"); JSValueRef constructorValue = JSObjectGetProperty( runtime.context(), JSContextGetGlobalObject(runtime.context()), name, nullptr); JSStringRelease(name); @@ -568,13 +612,13 @@ class Object { template bool isHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == jscdirect::hostObjectTypeToken(); + return holder != nullptr && holder->typeToken == jscengine::hostObjectTypeToken(); } template std::shared_ptr getHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != jscdirect::hostObjectTypeToken()) { + if (holder == nullptr || holder->typeToken != jscengine::hostObjectTypeToken()) { return nullptr; } return std::static_pointer_cast(holder->hostObject); @@ -584,11 +628,7 @@ class Object { return reinterpret_cast(const_cast(storage_->value)); } - operator Value() const { - Value value; - value.storage_ = storage_; - return value; - } + operator Value() const { return Value::fromStorage(storage_); } protected: friend class Value; @@ -597,17 +637,17 @@ class Object { friend class Array; friend class ArrayBuffer; - explicit Object(std::shared_ptr storage) + explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken); - jscdirect::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { - return static_cast(JSObjectGetPrivate(local(runtime))); + jscengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { + return static_cast(JSObjectGetPrivate(local(runtime))); } - std::shared_ptr storage_; + std::shared_ptr storage_; }; class Function : public Object { @@ -629,7 +669,7 @@ class Function : public Object { runtime.context(), local(runtime), JSContextGetGlobalObject(runtime.context()), argv.size(), argv.empty() ? nullptr : argv.data(), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -662,7 +702,7 @@ class Function : public Object { JSObjectCallAsFunction(runtime.context(), local(runtime), thisObject.local(runtime), argv.size(), argv.empty() ? nullptr : argv.data(), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -677,7 +717,7 @@ class Function : public Object { JSValueRef result = JSObjectCallAsConstructor(runtime.context(), local(runtime), argv.size(), argv.empty() ? nullptr : argv.data(), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -698,14 +738,14 @@ class Function : public Object { class Array : public Object { public: explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { std::vector initial(size, JSValueMakeUndefined(runtime.context())); JSValueRef exception = nullptr; storage_->context = runtime.context(); storage_->value = JSObjectMakeArray(runtime.context(), initial.size(), initial.data(), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } JSValueProtect(runtime.context(), storage_->value); } @@ -722,7 +762,7 @@ class Array : public Object { JSValueRef result = JSObjectGetPropertyAtIndex(runtime.context(), local(runtime), static_cast(index), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } return Value(runtime, result); } @@ -732,7 +772,7 @@ class Array : public Object { JSObjectSetPropertyAtIndex(runtime.context(), local(runtime), static_cast(index), value.local(runtime), &exception); if (exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } } void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { @@ -744,7 +784,7 @@ class BigInt { public: BigInt() = default; BigInt(Runtime& runtime, JSValueRef value) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { storage_->context = runtime.context(); storage_->value = value; JSValueProtect(runtime.context(), storage_->value); @@ -778,7 +818,7 @@ class BigInt { JSValueRef exception = nullptr; JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), &exception); if (string == nullptr || exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } String result(runtime, string); JSStringRelease(string); @@ -787,33 +827,29 @@ class BigInt { JSValueRef local(Runtime& runtime) const { return storage_->value; } - operator Value() const { - Value value; - value.storage_ = storage_; - return value; - } + operator Value() const { return Value::fromStorage(storage_); } private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class ArrayBuffer : public Object { public: ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { - auto* holder = new jscdirect::ArrayBufferHolder(std::move(buffer)); + : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + auto* holder = new jscengine::ArrayBufferHolder(std::move(buffer)); JSValueRef exception = nullptr; storage_->context = runtime.context(); storage_->value = JSObjectMakeArrayBufferWithBytesNoCopy( runtime.context(), holder->buffer->data(), holder->buffer->size(), [](void*, void* deallocatorContext) { - delete static_cast(deallocatorContext); + delete static_cast(deallocatorContext); }, holder, &exception); if (exception != nullptr) { delete holder; - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } JSValueProtect(runtime.context(), storage_->value); } @@ -831,8 +867,8 @@ class ArrayBuffer : public Object { JSObjectGetArrayBufferBytesPtr(runtime.context(), local(runtime), &exception)); } }; -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm b/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm index 3396af697..da5aa2ce8 100644 --- a/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm +++ b/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm @@ -2,8 +2,8 @@ #ifdef TARGET_ENGINE_JSC -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { Object Runtime::global() { return Object::fromValueStorage(Value(*this, JSContextGetGlobalObject(context())).storage_); @@ -13,18 +13,18 @@ const std::string& sourceURL) { JSStringRef source = JSStringCreateWithUTF8CString( buffer != nullptr ? std::string(buffer->data(), buffer->size()).c_str() : ""); - JSStringRef url = jscdirect::makeJSString(sourceURL); + JSStringRef url = jscengine::makeJSString(sourceURL); JSValueRef exception = nullptr; JSValueRef result = JSEvaluateScript(context(), source, nullptr, url, 1, &exception); JSStringRelease(source); JSStringRelease(url); if (exception != nullptr) { - throw JSError(*this, jscdirect::valueToUtf8(context(), exception)); + throw JSError(*this, jscengine::valueToUtf8(context(), exception)); } return Value(*this, result); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCRuntimeSupport.mm b/NativeScript/ffi/jsc/NativeApiJSCRuntimeSupport.mm new file mode 100644 index 000000000..8d1849d19 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCRuntimeSupport.mm @@ -0,0 +1,12 @@ +// Included by NativeApiJSC.mm inside the NativeScript anonymous namespace. + +std::shared_ptr retainNativeApiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} + +void SetNativeApiObjectPrototype(Runtime& runtime, Object& object, + const Object& prototype) { + JSObjectSetPrototype(runtime.context(), object.local(runtime), + prototype.local(runtime)); +} + diff --git a/NativeScript/ffi/jsc/NativeApiJSCSelectorGroups.mm b/NativeScript/ffi/jsc/NativeApiJSCSelectorGroups.mm new file mode 100644 index 000000000..52f775102 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCSelectorGroups.mm @@ -0,0 +1,458 @@ +// Included by NativeApiJSC.mm inside the NativeScript anonymous namespace. + +struct NativeApiSelectorGroupData { + NativeApiSelectorGroupData( + std::shared_ptr state, + std::shared_ptr bridge, Class lookupClass, + bool receiverIsClass, + std::shared_ptr> + selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver = {}, + std::shared_ptr boundReceiverState = + nullptr) + : state(state), + bridge(std::move(bridge)), + lookupClass(lookupClass), + receiverIsClass(receiverIsClass), + selectors(std::move(selectors)), + preparedInvocations(std::move(preparedInvocations)), + boundReceiver(std::move(boundReceiver)), + boundReceiverState(std::move(boundReceiverState)), + runtime(state) {} + + std::shared_ptr state; + std::shared_ptr bridge; + Class lookupClass = Nil; + bool receiverIsClass = false; + std::shared_ptr> selectors; + std::shared_ptr< + std::vector>> + preparedInvocations; + std::weak_ptr boundReceiver; + std::shared_ptr boundReceiverState; + // Reused per call (avoids per-call shared_ptr refcount + dispatch-superclass + // probe on the hot path). + Runtime runtime; + Class cachedReceiverClass = Nil; + Class cachedDispatchClass = Nil; +}; + +#include "NativeApiJSCMarshalling.mm" + +#include "NativeApiJSCGsd.mm" + + +void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { + return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); +} + +bool tryCallGeneratedEngineObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass, Value* result) { + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount) { + return false; + } + + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, + runtime.context(), nullptr, prepared.signature.returnType}; + ctx.valueArguments = args; + ctx.materializeValueResult = true; + if (!invoker(ctx)) { + return false; + } + *result = std::move(ctx.valueResult); + return true; +} + +JSValueRef setJSCEnginePreparedObjCResult( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const std::shared_ptr& receiverHostObject, + const std::optional& initializerClassWrapper, + size_t providedCount, const JSValueRef arguments[], + Class dispatchSuperClass) { + const NativeApiSignature& signature = prepared.signature; + if (receiver == nil || signature.variadic || + unsupportedEngineType(signature.returnType)) { + throw JSError(runtime, + "Objective-C selector is not supported by JSC engine: " + + prepared.selectorName); + } + + const bool isNSErrorOutMethod = prepared.isNSErrorOutMethod; + if (isNSErrorOutMethod) { + size_t expected = signature.argumentTypes.size(); + if (providedCount > expected || providedCount + 1 < expected) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + std::to_string(expected) + "\"."); + } + } else if (providedCount != signature.argumentTypes.size()) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + + std::to_string(signature.argumentTypes.size()) + "\"."); + } + + // GSD fast path: the generated invoker reads args directly from the JSC + // arguments, calls objc_msgSend with a typed cast, and produces the JS + // return value — bypassing all generic marshalling. + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod) { + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, + runtime.context(), arguments, signature.returnType}; + if (invoker(ctx)) { + return ctx.result; + } + } + + if (dispatchSuperClass == Nil && !initializerClassWrapper && + providedCount <= 2) { + Value fastArgs[2]; + for (size_t i = 0; i < providedCount; i++) { + fastArgs[i] = Value::borrowed(runtime, arguments[i]); + } + Value fastResult; + if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared, + fastArgs, providedCount, Nil, + &fastResult)) { + return fastResult.local(runtime); + } + } + + NativeApiArgumentFrame frame(signature.argumentTypes.size()); + for (size_t i = 0; i < providedCount; i++) { + if (!prepareJSCEngineArgument(runtime, bridge, signature.argumentTypes[i], + arguments[i], frame, i)) { + throw JSError(runtime, + "Objective-C argument is not supported by JSC engine: " + + prepared.selectorName); + } + } + + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && providedCount + 1 == signature.argumentTypes.size(); + NSError* implicitNSError = nil; + if (hasImplicitNSErrorOutArg) { + size_t outArgIndex = signature.argumentTypes.size() - 1; + void* target = frame.storageAt(outArgIndex, sizeof(NSError**)); + NSError** implicitNSErrorOutArg = &implicitNSError; + *static_cast(target) = implicitNSErrorOutArg; + } + + NativeApiPointerFrame values(signature.argumentTypes.size() + 2); + size_t valueIndex = 0; + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.set(valueIndex++, &superReceiverPtr); + } else { + values.set(valueIndex++, &receiver); + } + values.set(valueIndex++, const_cast(&prepared.selector)); + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + values.set(valueIndex++, frame.values()[i]); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature.returnType)); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (prepared.preparedInvoker != nullptr && dispatchSuperClass == Nil) { + prepared.preparedInvoker(reinterpret_cast(objc_msgSend), + values.data(), returnStorage.data()); + } else { +#if defined(__x86_64__) + bool isStret = signature.returnType.ffiType->size > 16 && + signature.returnType.ffiType->type == FFI_TYPE_STRUCT; + void (*target)(void) = + dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend)); + ffi_call(const_cast(&signature.cif), target, + returnStorage.data(), values.data()); +#else + ffi_call(const_cast(&signature.cif), + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#endif + } + }); + + NativeApiType returnType = signature.returnType; + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + if (initializerClassWrapper) { + id resultObject = nil; + if (isObjectiveCObjectType(returnType)) { + resultObject = *static_cast(returnStorage.data()); + } + if (receiverHostObject != nullptr && resultObject != receiver) { + receiverHostObject->disownObject(receiver); + } + if (resultObject != nil) { + bridge->setObjectExpando(runtime, resultObject, + "__nativeApiClassWrapper", + Value(runtime, *initializerClassWrapper)); + } + } + return setJSCEngineReturnValue(runtime, bridge, returnType, + returnStorage.data(), prepared.selectorName); +} + +JSValueRef NativeApiSelectorGroupCall( + JSContextRef context, JSObjectRef function, JSObjectRef thisObject, + size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { + auto* data = + static_cast(JSObjectGetPrivate(function)); + if (data == nullptr || data->selectors == nullptr || + data->preparedInvocations == nullptr) { + return JSValueMakeUndefined(context); + } + + Runtime& runtime = data->runtime; + try { + NativeApiRoundTripCacheFrameGuard roundTripFrame(data->bridge); + if (argumentCount >= data->selectors->size() || + (*data->selectors)[argumentCount].selectorName.empty()) { + throw JSError(runtime, + "Objective-C selector is not available for the provided arguments " + "count."); + } + + NativeApiSelectorGroupEntry& entry = (*data->selectors)[argumentCount]; + auto& prepared = (*data->preparedInvocations)[argumentCount]; + Class selectorLookupClass = data->lookupClass; + id receiver = data->receiverIsClass ? static_cast(data->lookupClass) : nil; + std::shared_ptr receiverHostObject; + if (!data->receiverIsClass) { + if (data->boundReceiverState != nullptr) { + receiver = data->boundReceiverState->object(); + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + } else if (thisObject != nullptr) { + auto* holder = static_cast( + JSObjectGetPrivate(thisObject)); + if (holder != nullptr && + holder->typeToken == + engine::jscengine::hostObjectTypeToken< + NativeApiObjectHostObject>()) { + receiver = + static_cast(holder->hostObject.get()) + ->object(); + } + } + } + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + + const bool propertyGetterCall = + entry.hasMember && entry.member.property && argumentCount == 0; + const std::string* selectorNamePtr = &entry.selectorName; + const NativeApiMember* selectedMember = + entry.hasMember ? &entry.member : nullptr; + bool callTargetCanPrepare = true; + if (prepared == nullptr || propertyGetterCall) { + NativeApiSelectorGroupCallTarget callTarget = + selectorGroupCallTargetForEntry(receiver, selectorLookupClass, + data->receiverIsClass, entry, + argumentCount); + selectorNamePtr = callTarget.selectorName; + selectedMember = callTarget.member; + callTargetCanPrepare = callTarget.canPrepare; + if (prepared != nullptr && prepared->selectorName != *selectorNamePtr) { + prepared = nullptr; + } + } + const std::string& selectorName = + prepared != nullptr && !propertyGetterCall ? prepared->selectorName + : *selectorNamePtr; + + if (data->receiverIsClass) { + Class methodClass = prepared != nullptr ? prepared->receiverClass : Nil; + if (methodClass == Nil) { + SEL selector = sel_registerName(selectorName.c_str()); + methodClass = + NativeApiClassHostObject::classRespondingToClassSelector( + data->lookupClass, selector); + } + if (methodClass == Nil) { + throw JSError(runtime, + "Objective-C selector is not available: " + + entry.selectorName); + } + selectorLookupClass = methodClass; + receiver = static_cast(methodClass); + } + if (propertyGetterCall && !callTargetCanPrepare) { + return callObjCSelector(runtime, data->bridge, receiver, + data->receiverIsClass, selectorName, + selectedMember, nullptr, 0) + .local(runtime); + } + + if (prepared == nullptr) { + if (!data->receiverIsClass) { + SEL selector = sel_registerName(selectorName.c_str()); + if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { + Class receiverClass = object_getClass(receiver); + if (class_getInstanceMethod(receiverClass, selector) != nullptr) { + selectorLookupClass = receiverClass; + } + } + } + prepared = prepareNativeApiObjCInvocation( + runtime, data->bridge, selectorLookupClass, data->receiverIsClass, + selectorName, selectedMember); + // Look up the engine-neutral GSD invoker for this signature. + if (prepared->engineInvoker == nullptr) { + uint64_t dispatchId = dispatchIdForEngineSignature( + prepared->signature, SignatureCallKind::ObjCMethod); + if (auto gsdInvoker = lookupObjCGsdInvoker(dispatchId)) { + prepared->engineInvoker = reinterpret_cast(gsdInvoker); + configureGeneratedEngineObjCInvocation(*prepared); + } + } + } + + std::optional initializerClassWrapper; + if (!data->receiverIsClass && prepared->isInitMethod) { + if (!receiverHostObject) { + if (data->boundReceiverState != nullptr) { + if (auto boundReceiver = data->boundReceiver.lock()) { + receiverHostObject = std::move(boundReceiver); + } + } else if (thisObject != nullptr) { + auto* holder = static_cast( + JSObjectGetPrivate(thisObject)); + if (holder != nullptr && + holder->typeToken == + engine::jscengine::hostObjectTypeToken< + NativeApiObjectHostObject>()) { + receiverHostObject = + std::static_pointer_cast( + holder->hostObject); + } + } + } + Value classWrapperValue = data->bridge->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + initializerClassWrapper.emplace(classWrapperValue.asObject(runtime)); + } + data->bridge->forgetRoundTripValue(receiver); + data->bridge->forgetObjectExpandos(receiver); + } + + Class dispatchClass = Nil; + if (!data->receiverIsClass) { + Class receiverClass = object_getClass(receiver); + if (receiverClass == data->cachedReceiverClass) { + dispatchClass = data->cachedDispatchClass; + } else { + dispatchClass = dispatchSuperclassForEngineDerivedReceiver( + receiver, data->lookupClass); + data->cachedReceiverClass = receiverClass; + data->cachedDispatchClass = dispatchClass; + } + } + return setJSCEnginePreparedObjCResult( + runtime, data->bridge, receiver, *prepared, receiverHostObject, + initializerClassWrapper, argumentCount, arguments, dispatchClass); + } catch (const std::exception& error) { + engine::jscengine::setException(context, exception, error); + return JSValueMakeUndefined(context); + } +} + +void NativeApiSelectorGroupFinalize(JSObjectRef function) { + delete static_cast( + JSObjectGetPrivate(function)); +} + +JSClassRef NativeApiSelectorGroupFunctionClass(Runtime& runtime) { + auto state = runtime.state(); + if (state->selectorGroupFunctionClass == nullptr) { + JSClassDefinition definition = kJSClassDefinitionEmpty; + definition.className = "NativeScriptEngineSelectorGroupFunction"; + definition.callAsFunction = NativeApiSelectorGroupCall; + definition.finalize = NativeApiSelectorGroupFinalize; + state->selectorGroupFunctionClass = JSClassCreate(&definition); + } + return state->selectorGroupFunctionClass; +} + +Function CreateNativeApiSelectorGroupFunctionImpl( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver, + std::shared_ptr boundReceiverState = + nullptr) { + auto* data = new NativeApiSelectorGroupData( + runtime.state(), std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), + std::move(boundReceiver), std::move(boundReceiverState)); + JSObjectRef function = + JSObjectMake(runtime.context(), + NativeApiSelectorGroupFunctionClass(runtime), data); + engine::jscengine::setFunctionPrototype(runtime.context(), function); + + JSStringRef property = engine::jscengine::makeJSString("name"); + JSStringRef functionName = + engine::jscengine::makeJSString("__nativeSelectorGroup"); + JSValueRef value = JSValueMakeString(runtime.context(), functionName); + JSObjectSetProperty(runtime.context(), function, property, value, + kJSPropertyAttributeReadOnly, nullptr); + JSStringRelease(functionName); + JSStringRelease(property); + + Value functionValue(runtime, function); + return functionValue.asObject(runtime).asFunction(runtime); +} + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), {}, nullptr); +} + +Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, false, std::move(selectors), + std::move(preparedInvocations), receiverHostObject, + receiverHostObject != nullptr ? receiverHostObject->lifetimeState() + : nullptr); +} diff --git a/NativeScript/ffi/jsc/NativeApiJSCValue.mm b/NativeScript/ffi/jsc/NativeApiJSCValue.mm index ca4071567..f66913761 100644 --- a/NativeScript/ffi/jsc/NativeApiJSCValue.mm +++ b/NativeScript/ffi/jsc/NativeApiJSCValue.mm @@ -2,43 +2,68 @@ #ifdef TARGET_ENGINE_JSC -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } -void HostObject::set(Runtime&, const PropNameID&, const Value&) {} +bool HostObject::set(Runtime&, const PropNameID&, const Value&) { return true; } std::vector HostObject::getPropertyNames(Runtime&) { return {}; } String::String(Runtime& runtime, JSStringRef string) - : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { storage_->context = runtime.context(); storage_->value = JSValueMakeString(runtime.context(), string); JSValueProtect(runtime.context(), storage_->value); } std::string String::utf8(Runtime& runtime) const { - return jscdirect::valueToUtf8(runtime.context(), storage_->value); + return jscengine::valueToUtf8(runtime.context(), storage_->value); } -String::operator Value() const { - Value value; - value.storage_ = storage_; - return value; -} +String::operator Value() const { return Value::fromStorage(storage_); } -Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} -Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} -Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} -Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} -Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} +Value::Value(Runtime&, const String& value) { + storage_ = value.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Object& object) { + storage_ = object.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Function& function) { + storage_ = function.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Array& array) { + storage_ = array.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) { + storage_ = arrayBuffer.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const BigInt& bigint) { + storage_ = bigint.storage_; + kind_ = storage_ ? storage_->kind : jscengine::ValueStorage::Kind::Undefined; +} -Object Value::asObject(Runtime&) const { return Object::fromValueStorage(storage_); } +Object Value::asObject(Runtime& runtime) const { + if (storage_) { + return Object::fromValueStorage(storage_); + } + // Promote borrowed to owned storage for Object. + auto s = std::make_shared(jscengine::ValueStorage::Kind::JSC); + s->context = runtime.context(); + s->value = borrowedValue_ != nullptr ? borrowedValue_ : JSValueMakeUndefined(runtime.context()); + JSValueProtect(runtime.context(), s->value); + return Object::fromValueStorage(std::move(s)); +} String Value::asString(Runtime& runtime) const { JSValueRef exception = nullptr; JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), &exception); if (string == nullptr || exception != nullptr) { - throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); } String result(runtime, string); JSStringRelease(string); @@ -77,7 +102,7 @@ setProperty(runtime, name, Value(runtime, value)); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/SignatureDispatch.h b/NativeScript/ffi/jsc/SignatureDispatch.h new file mode 100644 index 000000000..c53da941f --- /dev/null +++ b/NativeScript/ffi/jsc/SignatureDispatch.h @@ -0,0 +1,14 @@ +#ifndef NATIVESCRIPT_FFI_JSC_SIGNATURE_DISPATCH_H +#define NATIVESCRIPT_FFI_JSC_SIGNATURE_DISPATCH_H + +#include "ffi/shared/SignatureDispatchCore.h" + +#if defined(__has_include) +#if __has_include("GeneratedSignatureDispatch.inc") +#include "GeneratedSignatureDispatch.inc" +#endif +#endif + +#include "ffi/shared/PreparedSignatureDispatch.h" + +#endif // NATIVESCRIPT_FFI_JSC_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/napi/CFunction.mm b/NativeScript/ffi/napi/CFunction.mm index 3f9d26714..2ee6eb74a 100644 --- a/NativeScript/ffi/napi/CFunction.mm +++ b/NativeScript/ffi/napi/CFunction.mm @@ -13,7 +13,7 @@ #include "ObjCBridge.h" #include "SignatureDispatch.h" #include "runtime/NativeScriptException.h" -#include "Tasks.h" +#include "ffi/shared/Tasks.h" #ifdef ENABLE_JS_RUNTIME #include "jsr.h" #endif diff --git a/NativeScript/ffi/napi/CallbackThreading.h b/NativeScript/ffi/napi/CallbackThreading.h index 63c17990f..08ffcea22 100644 --- a/NativeScript/ffi/napi/CallbackThreading.h +++ b/NativeScript/ffi/napi/CallbackThreading.h @@ -4,6 +4,7 @@ #include "js_native_api.h" #include +#include #include #if defined(ENABLE_JS_RUNTIME) @@ -66,8 +67,9 @@ class NativeCallRuntimeUnlockScope final { jsr_->unlock(); } if (unlockedDepth_ == 0 && jsr_->runtime != nullptr) { - runtime_ = jsr_->runtime.get(); - runtime_->unlock(); + auto* runtime = jsr_->runtime.get(); + runtime->unlock(); + relockRuntime_ = [runtime]() { runtime->lock(); }; unlockedRuntime_ = true; } if (unlockedDepth_ > 0 || unlockedRuntime_) { @@ -91,8 +93,8 @@ class NativeCallRuntimeUnlockScope final { jsr_->lock(); } } - if (unlockedRuntime_ && runtime_ != nullptr) { - runtime_->lock(); + if (unlockedRuntime_ && relockRuntime_) { + relockRuntime_(); } #endif } @@ -104,7 +106,7 @@ class NativeCallRuntimeUnlockScope final { private: #if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) JSR* jsr_ = nullptr; - facebook::jsi::ThreadSafeRuntime* runtime_ = nullptr; + std::function relockRuntime_; #endif int unlockedDepth_ = 0; bool unlockedRuntime_ = false; diff --git a/NativeScript/ffi/napi/Cif.mm b/NativeScript/ffi/napi/Cif.mm index dfe18c8c3..bd1f24dda 100644 --- a/NativeScript/ffi/napi/Cif.mm +++ b/NativeScript/ffi/napi/Cif.mm @@ -9,53 +9,13 @@ #include "Metadata.h" #include "MetadataReader.h" #include "ObjCBridge.h" +#include "ffi/shared/SignatureDispatchCore.h" #include "TypeConv.h" #include "Util.h" namespace nativescript { namespace { -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; -} - -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 appendIntegralToHash(uint64_t* hash, 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); - } -} - -bool appendMetadataSignatureHash(MDMetadataReader* reader, MDSectionOffset signatureOffset, - std::unordered_set* activeSignatures, - uint64_t* hash); - inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptr& type) { if (type == nullptr) { return false; @@ -123,135 +83,6 @@ inline void updateGeneratedNapiDispatchCompatibility(Cif* cif) { } } -bool appendMetadataTypeHash(MDMetadataReader* reader, MDSectionOffset* offset, - std::unordered_set* activeSignatures, uint64_t* hash) { - if (reader == nullptr || offset == nullptr || hash == nullptr || activeSignatures == nullptr) { - return false; - } - - const MDTypeKind kindWithFlags = reader->getTypeKind(*offset); - *offset += sizeof(MDTypeKind); - const MDTypeKind rawKind = - static_cast((kindWithFlags & ~mdTypeFlagNext) & ~mdTypeFlagVariadic); - - appendIntegralToHash(hash, 0xB0); - const MDTypeKind canonicalKind = canonicalizeSignatureTypeKind(rawKind); - appendIntegralToHash(hash, static_cast(canonicalKind)); - - switch (rawKind) { - case mdTypeArray: - case mdTypeVector: - case mdTypeExtVector: - case mdTypeComplex: { - const auto arraySize = reader->getArraySize(*offset); - *offset += sizeof(uint16_t); - appendIntegralToHash(hash, arraySize); - if (!appendMetadataTypeHash(reader, offset, activeSignatures, hash)) { - return false; - } - break; - } - - case mdTypeStruct: { - const auto structOffset = reader->getOffset(*offset); - *offset += sizeof(MDSectionOffset); - appendIntegralToHash(hash, structOffset); - break; - } - - case mdTypeClassObject: { - auto classOffset = reader->getOffset(*offset); - *offset += sizeof(MDSectionOffset); - bool hasNext = (classOffset & mdSectionOffsetNext) != 0; - while (hasNext) { - auto protocolOffset = reader->getOffset(*offset); - *offset += sizeof(MDSectionOffset); - hasNext = (protocolOffset & mdSectionOffsetNext) != 0; - } - break; - } - - case mdTypeProtocolObject: { - bool hasNext = true; - while (hasNext) { - auto protocolOffset = reader->getOffset(*offset); - *offset += sizeof(MDSectionOffset); - hasNext = (protocolOffset & mdSectionOffsetNext) != 0; - } - break; - } - - case mdTypePointer: - if (!appendMetadataTypeHash(reader, offset, activeSignatures, hash)) { - return false; - } - break; - - case mdTypeBlock: - case mdTypeFunctionPointer: { - const auto nestedSignatureOffset = reader->getOffset(*offset); - *offset += sizeof(MDSectionOffset); - if (nestedSignatureOffset != MD_SECTION_OFFSET_NULL) { - const auto nestedAbsoluteOffset = reader->signaturesOffset + nestedSignatureOffset; - if (!appendMetadataSignatureHash(reader, nestedAbsoluteOffset, activeSignatures, hash)) { - return false; - } - } - break; - } - - default: - break; - } - - appendIntegralToHash(hash, 0xBF); - return true; -} - -bool appendMetadataSignatureHash(MDMetadataReader* reader, MDSectionOffset signatureOffset, - std::unordered_set* activeSignatures, - uint64_t* hash) { - if (reader == nullptr || hash == nullptr || activeSignatures == nullptr) { - return false; - } - - if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { - appendIntegralToHash(hash, 0xEE); - return true; - } - activeSignatures->insert(signatureOffset); - - MDSectionOffset offset = signatureOffset; - const MDTypeKind returnTypeKind = reader->getTypeKind(offset); - bool next = (returnTypeKind & mdTypeFlagNext) != 0; - const bool isVariadic = (returnTypeKind & mdTypeFlagVariadic) != 0; - - appendIntegralToHash(hash, 0xA0); - appendIntegralToHash(hash, isVariadic ? 1 : 0); - - if (!appendMetadataTypeHash(reader, &offset, activeSignatures, hash)) { - activeSignatures->erase(signatureOffset); - return false; - } - - uint32_t argCount = 0; - while (next) { - const MDTypeKind argTypeKind = reader->getTypeKind(offset); - next = (argTypeKind & mdTypeFlagNext) != 0; - if (!appendMetadataTypeHash(reader, &offset, activeSignatures, hash)) { - activeSignatures->erase(signatureOffset); - return false; - } - argCount++; - } - - appendIntegralToHash(hash, argCount); - appendIntegralToHash(hash, 0xAF); - - activeSignatures->erase(signatureOffset); - return true; -} - } // namespace // Essentially, we cache libffi structures per unique method signature, @@ -497,14 +328,7 @@ bool appendMetadataSignatureHash(MDMetadataReader* reader, MDSectionOffset signa rvalue = malloc(cif.rtype->size); rvalueLength = cif.rtype->size; - if (signatureStart != MD_SECTION_OFFSET_NULL) { - uint64_t canonicalSignatureHash = kFNV64OffsetBasis; - std::unordered_set activeSignatures; - if (appendMetadataSignatureHash(reader, signatureStart, &activeSignatures, - &canonicalSignatureHash)) { - signatureHash = canonicalSignatureHash; - } - } + signatureHash = metadataSignatureHash(reader, signatureStart); updateGeneratedNapiDispatchCompatibility(this); } diff --git a/NativeScript/ffi/napi/ClassMember.mm b/NativeScript/ffi/napi/ClassMember.mm index e7bc296a6..de88b84dd 100644 --- a/NativeScript/ffi/napi/ClassMember.mm +++ b/NativeScript/ffi/napi/ClassMember.mm @@ -407,8 +407,13 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, bool isStret = cif->returnType->type->size > 16 && cif->returnType->type->type == FFI_TYPE_STRUCT; #endif + NapiNativeCallbackExceptionCapture callbackException; + ScopedNapiNativeCallbackExceptionCapture callbackExceptionCapture( + &callbackException); + @try { if (!supercall) { + bool preparedInvoked = false; if (cif != nullptr && cif->signatureHash != 0) { if (descriptor != nullptr && (!descriptor->dispatchLookupCached || @@ -432,22 +437,24 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, if (invoker != nullptr) { NativeCallRuntimeUnlockScope unlockRuntime(env); invoker((void*)objc_msgSend, avalues, rvalue); - return true; + preparedInvoked = true; } } + if (!preparedInvoked) { #if defined(__x86_64__) - if (isStret) { - NativeCallRuntimeUnlockScope unlockRuntime(env); - ffi_call(&cif->cif, FFI_FN(objc_msgSend_stret), rvalue, avalues); - } else { + 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); - } -#else - NativeCallRuntimeUnlockScope unlockRuntime(env); - ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); #endif + } } else { Class superClass = classMethod ? class_getSuperclass(object_getClass((id)receiverClass)) : class_getSuperclass(receiverClass); @@ -475,6 +482,10 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, return false; } + if (rethrowNapiNativeCallbackException(env, callbackException)) { + return false; + } + return true; } diff --git a/NativeScript/ffi/napi/Closure.h b/NativeScript/ffi/napi/Closure.h index 7ceafd49c..d81a43f4d 100644 --- a/NativeScript/ffi/napi/Closure.h +++ b/NativeScript/ffi/napi/Closure.h @@ -17,6 +17,33 @@ namespace nativescript { class ObjCBridgeState; +struct NapiNativeCallbackExceptionCapture { + napi_env env = nullptr; + napi_ref errorRef = nullptr; + + ~NapiNativeCallbackExceptionCapture(); + void clear(); +}; + +class ScopedNapiNativeCallbackExceptionCapture { + public: + explicit ScopedNapiNativeCallbackExceptionCapture( + NapiNativeCallbackExceptionCapture* capture); + ~ScopedNapiNativeCallbackExceptionCapture(); + + ScopedNapiNativeCallbackExceptionCapture( + const ScopedNapiNativeCallbackExceptionCapture&) = delete; + ScopedNapiNativeCallbackExceptionCapture& operator=( + const ScopedNapiNativeCallbackExceptionCapture&) = delete; + + private: + NapiNativeCallbackExceptionCapture* capture_ = nullptr; +}; + +bool recordNapiNativeCallbackException(napi_env env, napi_value error); +bool rethrowNapiNativeCallbackException( + napi_env env, NapiNativeCallbackExceptionCapture& capture); + class Closure { public: static void callBlockFromMainThread(napi_env env, napi_value js_cb, diff --git a/NativeScript/ffi/napi/Closure.mm b/NativeScript/ffi/napi/Closure.mm index ab90659ca..d3c2e2777 100644 --- a/NativeScript/ffi/napi/Closure.mm +++ b/NativeScript/ffi/napi/Closure.mm @@ -28,6 +28,9 @@ namespace { +thread_local std::vector + gNativeCallbackExceptionCaptureStack; + inline void deleteClosureOnOwningThread(Closure* closure) { if (closure == nullptr) { return; @@ -61,6 +64,73 @@ inline void deleteClosureOnOwningThread(Closure* closure) { } // namespace +NapiNativeCallbackExceptionCapture::~NapiNativeCallbackExceptionCapture() { + clear(); +} + +void NapiNativeCallbackExceptionCapture::clear() { + if (env != nullptr && errorRef != nullptr) { + napi_delete_reference(env, errorRef); + } + env = nullptr; + errorRef = nullptr; +} + +ScopedNapiNativeCallbackExceptionCapture:: + ScopedNapiNativeCallbackExceptionCapture( + NapiNativeCallbackExceptionCapture* capture) + : capture_(capture) { + gNativeCallbackExceptionCaptureStack.push_back(capture_); +} + +ScopedNapiNativeCallbackExceptionCapture:: + ~ScopedNapiNativeCallbackExceptionCapture() { + if (!gNativeCallbackExceptionCaptureStack.empty() && + gNativeCallbackExceptionCaptureStack.back() == capture_) { + gNativeCallbackExceptionCaptureStack.pop_back(); + } +} + +bool recordNapiNativeCallbackException(napi_env env, napi_value error) { + if (env == nullptr || error == nullptr || + gNativeCallbackExceptionCaptureStack.empty()) { + return false; + } + + auto* capture = gNativeCallbackExceptionCaptureStack.back(); + if (capture == nullptr || capture->errorRef != nullptr) { + return capture != nullptr; + } + + capture->env = env; + return napi_create_reference(env, error, 1, &capture->errorRef) == napi_ok; +} + +bool rethrowNapiNativeCallbackException( + napi_env env, NapiNativeCallbackExceptionCapture& capture) { + if (env == nullptr || capture.errorRef == nullptr) { + return false; + } + + napi_value error = nullptr; + napi_ref errorRef = capture.errorRef; + capture.errorRef = nullptr; + napi_get_reference_value(env, errorRef, &error); + napi_delete_reference(env, errorRef); + capture.env = nullptr; + + if (error != nullptr) { + NativeScriptException nativeScriptException( + env, error, "JS implemented closure threw an exception"); + nativeScriptException.ReThrowToJS(env); + } else { + NativeScriptException nativeScriptException( + "Unable to obtain the error thrown by the JS implemented closure"); + nativeScriptException.ReThrowToJS(env); + } + return true; +} + void Closure::destroyOnOwningThread(Closure* closure) { deleteClosureOnOwningThread(closure); } inline bool selectorEndsWithErrorParam(SEL selector) { @@ -135,7 +205,11 @@ inline void JSCallbackInner(Closure* closure, napi_value func, napi_value thisAr napi_create_error(env, code, msg, &result); } - NativeScriptException::OnUncaughtError(env, result); + if (recordNapiNativeCallbackException(env, result)) { + napi_get_undefined(env, &result); + } else { + NativeScriptException::OnUncaughtError(env, result); + } } // Even if call was failed and result is just undefined, let's still try to @@ -251,7 +325,11 @@ void JSMethodCallback(ffi_cif* cif, void* ret, void* args[], void* data) { napi_create_error(env, code, msg, &result); } - NativeScriptException::OnUncaughtError(env, result); + if (recordNapiNativeCallbackException(env, result)) { + napi_get_undefined(env, &result); + } else { + NativeScriptException::OnUncaughtError(env, result); + } } bool shouldFree; diff --git a/NativeScript/ffi/napi/ObjCBridge.mm b/NativeScript/ffi/napi/ObjCBridge.mm index 0c7ebd76a..d3b250123 100644 --- a/NativeScript/ffi/napi/ObjCBridge.mm +++ b/NativeScript/ffi/napi/ObjCBridge.mm @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,18 @@ const unsigned char __attribute__((section("__objc_metadata,__objc_metadata"))) std::atomic gNextBridgeStateToken{1}; constexpr const char* kNativePointerProperty = "__ns_native_ptr"; +bool envFlagEnabled(const char* name) { + const char* value = std::getenv(name); + if (value == nullptr || value[0] == '\0') { + return false; + } + + return std::strcmp(value, "0") != 0 && std::strcmp(value, "false") != 0 && + std::strcmp(value, "FALSE") != 0 && std::strcmp(value, "False") != 0 && + std::strcmp(value, "off") != 0 && std::strcmp(value, "OFF") != 0 && + std::strcmp(value, "no") != 0 && std::strcmp(value, "NO") != 0; +} + inline void deleteReferenceNow(napi_env env, napi_ref ref, bool unrefFirst) { if (env == nullptr || ref == nullptr) { return; @@ -958,7 +971,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_value result = object; const bool nativeIsArray = [nativeObject isKindOfClass:NSArray.class]; - bool shouldProxyArray = nativeIsArray; + bool shouldProxyArray = nativeIsArray && !envFlagEnabled("NS_DISABLE_NAPI_ARRAY_PROXY"); if (shouldProxyArray) { napi_value factory = get_ref_value(env, createNativeProxy); napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); diff --git a/NativeScript/ffi/napi/SignatureDispatch.h b/NativeScript/ffi/napi/SignatureDispatch.h index c42a82376..7df628f80 100644 --- a/NativeScript/ffi/napi/SignatureDispatch.h +++ b/NativeScript/ffi/napi/SignatureDispatch.h @@ -3,47 +3,18 @@ #include -#include -#include -#include - #include "Cif.h" +#include "ffi/shared/SignatureDispatchCore.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; @@ -54,29 +25,6 @@ struct CFunctionNapiDispatchEntry { 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 @@ -91,30 +39,33 @@ inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, #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 +#ifndef NS_GSD_BACKEND_PREPARED +#define NS_GSD_BACKEND_PREPARED 0 #endif +#define NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH \ + (NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_NAPI || NS_GSD_BACKEND_PREPARED) + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" +#elif NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH +#error GeneratedSignatureDispatch.inc is required when generated signature dispatch is enabled. #endif +#elif NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH +#error __has_include is required to validate GeneratedSignatureDispatch.inc. +#endif + +#if NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH && !NS_HAS_GENERATED_SIGNATURE_DISPATCH +#error GeneratedSignatureDispatch.inc did not enable this generated signature dispatch backend. +#endif + +#if NS_GSD_BACKEND_NAPI && !NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH +#error GeneratedSignatureDispatch.inc did not enable Node-API generated signature dispatch. #endif #if !NS_HAS_GENERATED_SIGNATURE_DISPATCH @@ -139,42 +90,6 @@ inline constexpr CFunctionNapiDispatchEntry 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; diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJS.h b/NativeScript/ffi/quickjs/NativeApiQuickJS.h index 73c31984d..513a05c34 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJS.h +++ b/NativeScript/ffi/quickjs/NativeApiQuickJS.h @@ -1,20 +1,21 @@ #ifndef NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H #define NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H -#include "ffi/shared/direct/NativeApiDirect.h" +#include "ffi/shared/NativeApiBackendConfig.h" #include "quickjs.h" namespace nativescript { -using NativeApiQuickJSConfig = NativeApiDirectConfig; +using NativeApiScheduler = NativeApiBackendScheduler; +using NativeApiConfig = NativeApiBackendConfig; -void InstallNativeApiQuickJS(JSContext* context, - const NativeApiQuickJSConfig& config = - NativeApiQuickJSConfig{}); +void InstallNativeApi(JSContext* context, + const NativeApiConfig& config = + NativeApiConfig{}); } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiQuickJS(JSContext* context, +extern "C" void NativeScriptInstallNativeApi(JSContext* context, const char* metadataPath); #endif // NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJS.mm b/NativeScript/ffi/quickjs/NativeApiQuickJS.mm index 57988a26f..8f234d1bd 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJS.mm +++ b/NativeScript/ffi/quickjs/NativeApiQuickJS.mm @@ -3,157 +3,73 @@ #ifdef TARGET_ENGINE_QUICKJS #include "NativeApiQuickJSRuntime.h" +#include "SignatureDispatch.h" namespace nativescript { -using NativeApiJsiConfig = NativeApiDirectConfig; -using NativeApiJsiScheduler = NativeApiDirectScheduler; - namespace { -using facebook::jsi::Array; -using facebook::jsi::ArrayBuffer; -using facebook::jsi::BigInt; -using facebook::jsi::Function; -using facebook::jsi::HostObject; -using facebook::jsi::MutableBuffer; -using facebook::jsi::Object; -using facebook::jsi::PropNameID; -using facebook::jsi::Runtime; -using facebook::jsi::String; -using facebook::jsi::StringBuffer; -using facebook::jsi::Value; +using nativescript::engine::Array; +using nativescript::engine::ArrayBuffer; +using nativescript::engine::BigInt; +using nativescript::engine::Function; +using nativescript::engine::HostObject; +using nativescript::engine::MutableBuffer; +using nativescript::engine::Object; +using nativescript::engine::PropNameID; +using nativescript::engine::Runtime; +using nativescript::engine::String; +using nativescript::engine::StringBuffer; +using nativescript::engine::Value; +using nativescript::engine::JSError; using metagen::MDMemberFlag; using metagen::MDMetadataReader; using metagen::MDSectionOffset; using metagen::MDTypeKind; // clang-format off -#include "jsi/NativeApiJsiBridge.h" +#define NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE 1 +#define NATIVESCRIPT_NATIVE_API_BACKEND_NAME "quickjs" +#include "../shared/bridge/ObjCBridge.mm" // clang-format on #define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS 1 #define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_SELECTOR_GROUP_FUNCTION 1 -static JSValue NativeApiQuickJSLazyGlobalGetter(JSContext* context, JSValueConst, int, - JSValueConst*, int, JSValueConst* data) { - JSValue global = JS_GetGlobalObject(context); - JSValue resolver = JS_GetPropertyStr(context, global, "__nativeScriptResolveNativeApiLazyGlobal"); - if (!JS_IsFunction(context, resolver)) { - JS_FreeValue(context, resolver); - JS_FreeValue(context, global); - return JS_UNDEFINED; - } - - JSValueConst args[] = {data[0], data[1]}; - JSValue result = JS_Call(context, resolver, global, 2, args); - JS_FreeValue(context, resolver); - if (JS_IsException(result)) { - JS_FreeValue(context, global); - return result; - } - - JSAtom atom = JS_ValueToAtom(context, data[0]); - if (atom != JS_ATOM_NULL) { - JS_DefinePropertyValue(context, global, atom, JS_DupValue(context, result), - JS_PROP_CONFIGURABLE); - JS_FreeAtom(context, atom); - } - JS_FreeValue(context, global); - return result; -} - -bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, - const std::string& name, const std::string& kind, - bool force) { - if (name.empty() || kind.empty()) { - return false; - } - - JSContext* context = runtime.context(); - JSValue global = JS_GetGlobalObject(context); - JSAtom atom = JS_NewAtomLen(context, name.data(), name.size()); - if (atom == JS_ATOM_NULL) { - JS_FreeValue(context, global); - return false; - } - - int hasProperty = JS_HasProperty(context, global, atom); - if (!force && hasProperty > 0) { - JS_FreeAtom(context, atom); - JS_FreeValue(context, global); - return false; - } - if (hasProperty < 0) { - JS_FreeAtom(context, atom); - JS_FreeValue(context, global); - return false; - } - - JSValue data[] = { - JS_NewStringLen(context, name.data(), name.size()), - JS_NewStringLen(context, kind.data(), kind.size()), - }; - if (JS_IsException(data[0]) || JS_IsException(data[1])) { - JS_FreeValue(context, data[0]); - JS_FreeValue(context, data[1]); - JS_FreeAtom(context, atom); - JS_FreeValue(context, global); - return false; - } - - JSValue getter = JS_NewCFunctionData(context, NativeApiQuickJSLazyGlobalGetter, 0, 0, 2, data); - JS_FreeValue(context, data[0]); - JS_FreeValue(context, data[1]); - if (JS_IsException(getter)) { - JS_FreeAtom(context, atom); - JS_FreeValue(context, global); - return false; - } - - int status = - JS_DefinePropertyGetSet(context, global, atom, getter, JS_UNDEFINED, JS_PROP_CONFIGURABLE); - JS_FreeAtom(context, atom); - JS_FreeValue(context, global); - return status >= 0; -} +#include "NativeApiQuickJSRuntimeSupport.mm" // clang-format off -#include "jsi/NativeApiJsiHostObjects.h" +#include "../shared/bridge/HostObjects.mm" +#include "../shared/bridge/Callbacks.mm" +#include "../shared/bridge/TypeConv.mm" +#include "../shared/bridge/Invocation.mm" +#include "../shared/bridge/ClassBuilder.mm" +#include "../shared/bridge/HostObject.mm" // clang-format on -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { - return std::make_shared(runtime.state()); -} - -// clang-format off -#include "jsi/NativeApiJsiCallbacks.h" -#include "jsi/NativeApiJsiConversion.h" -#include "jsi/NativeApiJsiInvocation.h" -#include "jsi/NativeApiJsiClassBuilder.h" -#include "jsi/NativeApiJsiHostObject.h" -// clang-format on +#include "NativeApiQuickJSSelectorGroups.mm" } // namespace -#include "jsi/NativeApiJsiInstall.h" +#include "../shared/bridge/Install.mm" -void InstallNativeApiQuickJS(JSContext* context, const NativeApiQuickJSConfig& config) { +void InstallNativeApi(JSContext* context, const NativeApiConfig& config) { if (context == nullptr) { return; } - auto state = facebook::jsi::quickjsdirect::stateForContext(context); - facebook::jsi::Runtime runtime(state); - facebook::jsi::quickjsdirect::ensureClasses(runtime); - InstallNativeApiJSI(runtime, config); + auto state = engine::quickjsengine::stateForContext(context); + nativescript::engine::Runtime runtime(state); + engine::quickjsengine::ensureClasses(runtime); + InstallNativeApi(runtime, config); } } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiQuickJS(JSContext* context, const char* metadataPath) { - nativescript::NativeApiQuickJSConfig config; +extern "C" void NativeScriptInstallNativeApi(JSContext* context, const char* metadataPath) { + nativescript::NativeApiConfig config; config.metadataPath = metadataPath; - nativescript::InstallNativeApiQuickJS(context, config); + nativescript::InstallNativeApi(context, config); } #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSGsd.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSGsd.mm new file mode 100644 index 000000000..434920110 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSGsd.mm @@ -0,0 +1,301 @@ +// --- GSD (Generated Signature Dispatch) for QuickJS --- +// GsdObjCContext is the engine-neutral interface the generated invokers use: +// it reads JS arguments and writes the JS return value via the QuickJS API. +// Readers require an actual JS number so coercion edge cases defer to the +// fully correct generic path. +struct GsdObjCContext; +using ObjCGsdInvoker = bool (*)(GsdObjCContext&); +struct ObjCGsdDispatchEntry { + uint64_t dispatchId; + ObjCGsdInvoker invoker; +}; + +struct GsdObjCContext { + Runtime& runtime; + const std::shared_ptr& bridge; + id self; + SEL selector; + JSContext* context; + JSValueConst* arguments; + const NativeApiType& returnType; + JSValue result = JS_UNDEFINED; + const Value* valueArguments = nullptr; + bool materializeValueResult = false; + Value valueResult = Value::undefined(); + + template + void invokeNative(Invocation&& invocation) { + performGeneratedObjCInvocation(runtime, bridge, [&]() { invocation(); }); + } + + bool readNumber(size_t i, double* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (!v.isNumber()) return false; + *out = v.getNumber(); + return true; + } + JSValueConst v = arguments[i]; + if (!JS_IsNumber(v)) return false; + return quickJSNumberValue(context, v, out); + } + bool readBool(size_t i, uint8_t* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (!v.isBool()) return false; + *out = v.getBool() ? 1 : 0; + return true; + } + JSValueConst v = arguments[i]; + if (!JS_IsBool(v)) return false; + *out = JS_ToBool(context, v) != 0 ? 1 : 0; + return true; + } + template + bool readSigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + template + bool readUnsigned(size_t i, T* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readFloat(size_t i, float* out) { + double tmp = 0; + if (!readNumber(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readDouble(size_t i, double* out) { return readNumber(i, out); } + bool readSelector(size_t i, SEL* out) { + if (valueArguments != nullptr) { + return readFastEngineSelectorArgument(runtime, valueArguments[i], out); + } + return readQuickJSEngineSelectorArgument(runtime, arguments[i], out); + } + bool readClass(size_t i, Class* out) { + if (valueArguments != nullptr) { + Class cls = classFromEngineValue(runtime, valueArguments[i]); + if (cls == Nil) return false; + *out = cls; + return true; + } + if (auto* c = quickJSHostObjectRaw( + runtime, arguments[i])) { + *out = c->nativeClass(); + return true; + } + Class cls = quickJSNativeClassArgument(runtime, arguments[i]); + if (cls == Nil) return false; + *out = cls; + return true; + } + bool readObject(size_t i, id* out) { + if (valueArguments != nullptr) { + const Value& v = valueArguments[i]; + if (v.isNull() || v.isUndefined()) { + *out = nil; + return true; + } + if (!v.isObject()) return false; + Object object = v.asObject(runtime); + if (object.isHostObject(runtime)) { + *out = object.getHostObject(runtime)->object(); + return true; + } + if (object.isHostObject(runtime)) { + *out = static_cast( + object.getHostObject(runtime)->nativeClass()); + return true; + } + Class cls = classFromEngineValue(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + if (object.isHostObject(runtime)) { + *out = static_cast( + object.getHostObject(runtime) + ->nativeProtocol()); + return true; + } + return false; + } + JSValueConst v = arguments[i]; + if (JS_IsNull(v) || JS_IsUndefined(v)) { + *out = nil; + return true; + } + if (auto* h = quickJSHostObjectRaw(runtime, v)) { + *out = h->object(); + return true; + } + if (auto* c = quickJSHostObjectRaw(runtime, v)) { + *out = static_cast(c->nativeClass()); + return true; + } + if (JS_IsObject(v)) { + Class cls = quickJSNativeClassArgument(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + } + if (auto* p = + quickJSHostObjectRaw(runtime, v)) { + *out = static_cast(p->nativeProtocol()); + return true; + } + return false; + } + + void setVoid() { + if (materializeValueResult) { + valueResult = Value::undefined(); + return; + } + result = JS_UNDEFINED; + } + void setBool(bool v) { + if (materializeValueResult) { + valueResult = Value(v); + return; + } + result = JS_NewBool(context, v); + } + void setInt32(int32_t v) { + if (materializeValueResult) { + valueResult = Value(static_cast(v)); + return; + } + result = JS_NewInt32(context, v); + } + void setUInt32(uint32_t v) { + if (materializeValueResult) { + valueResult = Value(static_cast(v)); + return; + } + result = JS_NewUint32(context, v); + } + void setUInt16(uint16_t v) { + if (materializeValueResult) { + if (v >= 32 && v <= 126) { + valueResult = makeString(runtime, std::string(1, static_cast(v))); + } else { + valueResult = Value(static_cast(v)); + } + return; + } + if (v >= 32 && v <= 126) { + char buffer[2] = {static_cast(v), '\0'}; + result = JS_NewStringLen(context, buffer, 1); + } else { + result = JS_NewUint32(context, v); + } + } + void setInt64(int64_t v) { + if (materializeValueResult) { + valueResult = signedInteger64ToEngineValue(runtime, v); + return; + } + result = quickJSInteger64Value(runtime, v); + } + void setUInt64(uint64_t v) { + if (materializeValueResult) { + valueResult = unsignedInteger64ToEngineValue(runtime, v); + return; + } + result = quickJSUnsignedInteger64Value(runtime, v); + } + void setDouble(double v) { + if (materializeValueResult) { + valueResult = Value(v); + return; + } + result = JS_NewFloat64(context, v); + } + void setSelector(SEL v) { + const char* name = v != nullptr ? sel_getName(v) : nullptr; + if (materializeValueResult) { + valueResult = name != nullptr ? makeString(runtime, name) : Value::null(); + return; + } + result = name == nullptr ? JS_NULL : JS_NewString(context, name); + } + void setClass(Class v) { + if (materializeValueResult) { + if (v == nil) { + valueResult = Value::null(); + return; + } + const char* name = class_getName(v); + 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; + } + valueResult = makeNativeClassValue(runtime, bridge, std::move(symbol)); + return; + } + if (v == nil) { + result = JS_NULL; + return; + } + const char* name = class_getName(v); + 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; + } + Value classValue = makeNativeClassValue(runtime, bridge, std::move(symbol)); + result = classValue.local(runtime); + } + void setObject(id obj) { + if (materializeValueResult) { + valueResult = convertNativeReturnValue(runtime, bridge, returnType, &obj); + return; + } + result = setQuickJSEngineObjectReturn(runtime, bridge, returnType, obj); + } +}; + +// Close the anonymous namespace so the generated dispatch table lives in +// namespace nativescript; GsdObjCContext/ObjCGsdDispatchEntry stay reachable +// via the unnamed namespace's implicit using-directive. +} // namespace (temporary close for GSD .inc) + +#if defined(__has_include) +#if __has_include("GeneratedGsdSignatureDispatch.inc") +#include "GeneratedGsdSignatureDispatch.inc" +#endif +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH +inline constexpr ObjCGsdDispatchEntry kGeneratedObjCGsdDispatchEntries[] = { + {0, nullptr}}; +#endif + +ObjCGsdInvoker lookupObjCGsdInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCGsdDispatchEntries, dispatchId); +} + +namespace { // reopen anonymous namespace + +// --- End GSD --- diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm index 1cd706a30..5917fd686 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm @@ -2,10 +2,14 @@ #ifdef TARGET_ENGINE_QUICKJS -namespace facebook { -namespace jsi { +namespace nativescript { +class NativeApiObjectHostObject; +} + +namespace nativescript { +namespace engine { -namespace quickjsdirect { +namespace quickjsengine { JSClassID gHostClassId = 0; JSClassID gFunctionClassId = 0; @@ -22,6 +26,44 @@ } } // namespace +template +class StackValueArray { + public: + explicit StackValueArray(size_t count) : count_(count) { + if (count_ > InlineCount) { + values_ = static_cast(::operator new(sizeof(Value) * count_)); + } else { + values_ = reinterpret_cast(inlineStorage_); + } + } + + ~StackValueArray() { + for (size_t i = 0; i < constructed_; i++) { + values_[i].~Value(); + } + if (count_ > InlineCount) { + ::operator delete(values_); + } + } + + StackValueArray(const StackValueArray&) = delete; + StackValueArray& operator=(const StackValueArray&) = delete; + + void emplace(size_t index, Value&& value) { + new (&values_[index]) Value(std::move(value)); + constructed_++; + } + + Value* data() { return count_ == 0 ? nullptr : values_; } + size_t size() const { return count_; } + + private: + size_t count_ = 0; + size_t constructed_ = 0; + Value* values_ = nullptr; + alignas(Value) unsigned char inlineStorage_[sizeof(Value) * InlineCount]; +}; + std::shared_ptr stateForContext(JSContext* context) { std::lock_guard lock(runtimeStatesMutex()); auto& states = runtimeStates(); @@ -34,16 +76,119 @@ return state; } +static bool isNativeInstancePrototypeBypassExcluded(JSContext* ctx, + JSAtom atom) { + const char* name = JS_AtomToCString(ctx, atom); + if (name == nullptr) { + return true; + } + bool excluded = + std::strcmp(name, "kind") == 0 || + std::strcmp(name, "className") == 0 || + std::strcmp(name, "nativeAddress") == 0 || + std::strcmp(name, "class") == 0 || + std::strcmp(name, "constructor") == 0 || + std::strcmp(name, "super") == 0 || + std::strcmp(name, "invoke") == 0 || + std::strcmp(name, "send") == 0 || + std::strcmp(name, "takeRetainedValue") == 0 || + std::strcmp(name, "takeUnretainedValue") == 0 || + std::strcmp(name, "toString") == 0; + JS_FreeCString(ctx, name); + return excluded; +} + +static void freePropertyDescriptor(JSContext* ctx, + JSPropertyDescriptor& desc) { + JS_FreeValue(ctx, desc.getter); + JS_FreeValue(ctx, desc.setter); + JS_FreeValue(ctx, desc.value); +} + +static JSValue nativePrototypeProperty(JSContext* ctx, JSValueConst obj, + JSAtom atom, JSValueConst receiver, + HostObjectHolder* holder, + bool* handled) { + *handled = false; + if (holder == nullptr || + holder->typeToken != hostObjectTypeToken()) { + return JS_UNDEFINED; + } + + JSValue prototype = JS_GetPrototype(ctx, obj); + if (JS_IsException(prototype)) { + *handled = true; + return prototype; + } + + for (size_t depth = 0; depth < 64 && JS_IsObject(prototype); depth++) { + JSPropertyDescriptor desc = {}; + int found = JS_GetOwnProperty(ctx, &desc, prototype, atom); + if (found < 0) { + JS_FreeValue(ctx, prototype); + *handled = true; + return JS_EXCEPTION; + } + if (found > 0) { + if (isNativeInstancePrototypeBypassExcluded(ctx, atom)) { + freePropertyDescriptor(ctx, desc); + JS_FreeValue(ctx, prototype); + return JS_UNDEFINED; + } + + *handled = true; + JS_FreeValue(ctx, prototype); + if ((desc.flags & JS_PROP_GETSET) != 0) { + JSValue getter = desc.getter; + JS_FreeValue(ctx, desc.setter); + JS_FreeValue(ctx, desc.value); + if (JS_IsUndefined(getter)) { + JS_FreeValue(ctx, getter); + return JS_UNDEFINED; + } + JSValue result = JS_Call(ctx, getter, receiver, 0, nullptr); + JS_FreeValue(ctx, getter); + return result; + } + + JS_FreeValue(ctx, desc.getter); + JS_FreeValue(ctx, desc.setter); + return desc.value; + } + + JSValue nextPrototype = JS_GetPrototype(ctx, prototype); + JS_FreeValue(ctx, prototype); + if (JS_IsException(nextPrototype)) { + *handled = true; + return nextPrototype; + } + prototype = nextPrototype; + } + + JS_FreeValue(ctx, prototype); + return JS_UNDEFINED; +} + static JSValue nativeHostGet(JSContext* ctx, JSValueConst obj, JSAtom atom, JSValueConst receiver) { - (void)receiver; Runtime runtime(stateForContext(ctx)); auto* holder = static_cast(JS_GetOpaque(obj, gHostClassId)); if (holder == nullptr || holder->hostObject == nullptr) { return JS_UNDEFINED; } try { + bool handledByPrototype = false; + JSValue prototypeResult = + nativePrototypeProperty(ctx, obj, atom, receiver, holder, + &handledByPrototype); + if (handledByPrototype) { + return prototypeResult; + } + Value result = holder->hostObject->get(runtime, PropNameID(atomToUtf8(ctx, atom))); - return result.local(runtime); + if (!result.isUndefined()) { + return result.local(runtime); + } + return JS_UNDEFINED; } catch (const std::exception& error) { return throwError(ctx, error); } @@ -57,8 +202,10 @@ static int nativeHostSet(JSContext* ctx, JSValueConst obj, JSAtom atom, JSValueC return 0; } try { - holder->hostObject->set(runtime, PropNameID(atomToUtf8(ctx, atom)), Value(runtime, value)); - return 1; + bool handled = holder->hostObject->set( + runtime, PropNameID(atomToUtf8(ctx, atom)), + Value::borrowed(runtime, value)); + return handled ? 1 : 0; } catch (const std::exception& error) { throwError(ctx, error); return -1; @@ -114,15 +261,14 @@ static JSValue invokeFunctionHolder(JSContext* ctx, FunctionHolder* holder, JSVa if (holder == nullptr || !holder->callback) { return JS_UNDEFINED; } - std::vector args; - args.reserve(argc); + StackValueArray<8> args(static_cast(argc)); for (int i = 0; i < argc; i++) { - args.emplace_back(runtime, argv[i]); + args.emplace(static_cast(i), Value::borrowed(runtime, argv[i])); } try { - Value self(runtime, thisValue); + Value self = Value::borrowed(runtime, thisValue); Value result = - holder->callback(runtime, self, args.empty() ? nullptr : args.data(), args.size()); + holder->callback(runtime, self, args.size() == 0 ? nullptr : args.data(), args.size()); return result.local(runtime); } catch (const std::exception& error) { return throwError(ctx, error); @@ -164,7 +310,7 @@ void ensureClasses(Runtime& runtime) { } if (!state->hostClassRegistered) { JSClassDef def = {}; - def.class_name = "NativeScriptDirectHostObject"; + def.class_name = "NativeScriptEngineHostObject"; def.exotic = &hostExoticMethods; def.finalizer = nativeHostFinalize; JS_NewClass(rt, gHostClassId, &def); @@ -176,7 +322,7 @@ void ensureClasses(Runtime& runtime) { } if (!state->functionClassRegistered) { JSClassDef def = {}; - def.class_name = "NativeScriptDirectFunction"; + def.class_name = "NativeScriptEngineFunction"; def.call = nativeFunctionCall; def.finalizer = nativeFunctionFinalize; JS_NewClass(rt, gFunctionClassId, &def); @@ -185,22 +331,22 @@ void ensureClasses(Runtime& runtime) { } } -} // namespace quickjsdirect +} // namespace quickjsengine -quickjsdirect::HostObjectHolder* Object::hostObjectHolder(Runtime& runtime) const { - quickjsdirect::ensureClasses(runtime); +quickjsengine::HostObjectHolder* Object::hostObjectHolder(Runtime& runtime) const { + quickjsengine::ensureClasses(runtime); JSValue object = local(runtime); - auto* holder = static_cast( - JS_GetOpaque(object, quickjsdirect::gHostClassId)); + auto* holder = static_cast( + JS_GetOpaque(object, quickjsengine::gHostClassId)); JS_FreeValue(runtime.context(), object); return holder; } Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken) { - quickjsdirect::ensureClasses(runtime); - auto* holder = new quickjsdirect::HostObjectHolder(runtime.state(), std::move(host), typeToken); - JSValue object = JS_NewObjectClass(runtime.context(), quickjsdirect::gHostClassId); + quickjsengine::ensureClasses(runtime); + auto* holder = new quickjsengine::HostObjectHolder(runtime.state(), std::move(host), typeToken); + JSValue object = JS_NewObjectClass(runtime.context(), quickjsengine::gHostClassId); JS_SetOpaque(object, holder); Object result = Object::fromValueStorage(Value(runtime, object).storage_); JS_FreeValue(runtime.context(), object); @@ -209,16 +355,16 @@ void ensureClasses(Runtime& runtime) { Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int parameterCount, HostFunctionType callback) { - quickjsdirect::ensureClasses(runtime); - auto* holder = new quickjsdirect::FunctionHolder(runtime.state(), std::move(callback)); - JSValue data = JS_NewObjectClass(runtime.context(), quickjsdirect::gFunctionClassId); + quickjsengine::ensureClasses(runtime); + auto* holder = new quickjsengine::FunctionHolder(runtime.state(), std::move(callback)); + JSValue data = JS_NewObjectClass(runtime.context(), quickjsengine::gFunctionClassId); if (JS_IsException(data)) { delete holder; throw JSError(runtime, "QuickJS host function data allocation failed."); } JS_SetOpaque(data, holder); - JSValue function = JS_NewCFunctionData(runtime.context(), quickjsdirect::nativeFunctionCallData, + JSValue function = JS_NewCFunctionData(runtime.context(), quickjsengine::nativeFunctionCallData, static_cast(parameterCount), 0, 1, &data); JS_FreeValue(runtime.context(), data); if (JS_IsException(function)) { @@ -233,7 +379,7 @@ void ensureClasses(Runtime& runtime) { return result; } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSMarshalling.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSMarshalling.mm new file mode 100644 index 000000000..fed137d9f --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSMarshalling.mm @@ -0,0 +1,461 @@ +// Included by NativeApiQuickJSSelectorGroups.mm inside the NativeScript anonymous namespace. + +std::string quickJSValueToUtf8(JSContext* context, JSValueConst value) { + size_t length = 0; + const char* text = JS_ToCStringLen(context, &length, value); + if (text == nullptr) { + return {}; + } + std::string result(text, length); + JS_FreeCString(context, text); + return result; +} + +bool quickJSNumberValue(JSContext* context, JSValueConst value, + double* result) { + if (result == nullptr) { + return false; + } + double converted = 0; + if (JS_ToFloat64(context, &converted, value) < 0) { + return false; + } + *result = converted; + return true; +} + +template +std::shared_ptr quickJSHostObject(Runtime& runtime, JSValueConst value) { + if (!JS_IsObject(value)) { + return nullptr; + } + engine::quickjsengine::ensureClasses(runtime); + auto* holder = static_cast( + JS_GetOpaque(value, engine::quickjsengine::gHostClassId)); + if (holder == nullptr || + holder->typeToken != engine::quickjsengine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); +} + +template +T* quickJSHostObjectRaw(Runtime& runtime, JSValueConst value) { + if (!JS_IsObject(value)) { + return nullptr; + } + engine::quickjsengine::ensureClasses(runtime); + auto* holder = static_cast( + JS_GetOpaque(value, engine::quickjsengine::gHostClassId)); + if (holder == nullptr || + holder->typeToken != engine::quickjsengine::hostObjectTypeToken()) { + return nullptr; + } + return static_cast(holder->hostObject.get()); +} + +id quickJSNativeObjectArgument( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, JSValueConst value, + NativeApiArgumentFrame& frame) { + JSContext* context = runtime.context(); + if (JS_IsNull(value) || JS_IsUndefined(value)) { + return nil; + } + if (JS_IsString(value)) { + std::string utf8 = quickJSValueToUtf8(context, value); + id string = type.kind == metagen::mdTypeNSMutableStringObject + ? [[NSMutableString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding] + : [[NSString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding]; + if (string != nil) { + frame.addObject(string); + } + return string; + } + if (JS_IsBool(value)) { + return [NSNumber numberWithBool:JS_ToBool(context, value) != 0]; + } + if (JS_IsNumber(value) || JS_IsBigInt(context, value)) { + double converted = 0; + if (quickJSNumberValue(context, value, &converted)) { + return [NSNumber numberWithDouble:converted]; + } + } + if (!JS_IsObject(value)) { + return nil; + } + if (auto objectHost = + quickJSHostObject(runtime, value)) { + return objectHost->object(); + } + if (auto classHost = + quickJSHostObject(runtime, value)) { + return static_cast(classHost->nativeClass()); + } + if (auto protocolHost = + quickJSHostObject(runtime, value)) { + return static_cast(protocolHost->nativeProtocol()); + } + if (auto pointerHost = + quickJSHostObject(runtime, value)) { + return static_cast(pointerHost->pointer()); + } + if (auto referenceHost = + quickJSHostObject(runtime, value)) { + return static_cast(referenceHost->data()); + } + if (auto structHost = + quickJSHostObject(runtime, value)) { + return static_cast(structHost->data()); + } + + JSValue wrappedClassValue = + JS_GetPropertyStr(context, value, "__nativeApiClass"); + if (!JS_IsException(wrappedClassValue)) { + if (auto classHost = quickJSHostObject( + runtime, wrappedClassValue)) { + JS_FreeValue(context, wrappedClassValue); + return static_cast(classHost->nativeClass()); + } + } + JS_FreeValue(context, wrappedClassValue); + + Value wrapped = Value::borrowed(runtime, value); + return objectFromEngineValue(runtime, bridge, wrapped, frame, + type.kind == + metagen::mdTypeNSMutableStringObject); +} + +Class quickJSNativeClassArgument(Runtime& runtime, JSValueConst value) { + if (JS_IsNull(value) || JS_IsUndefined(value)) { + return Nil; + } + if (auto classHost = + quickJSHostObject(runtime, value)) { + return classHost->nativeClass(); + } + if (JS_IsObject(value)) { + JSValue wrappedClassValue = + JS_GetPropertyStr(runtime.context(), value, "__nativeApiClass"); + if (!JS_IsException(wrappedClassValue)) { + if (auto classHost = quickJSHostObject( + runtime, wrappedClassValue)) { + JS_FreeValue(runtime.context(), wrappedClassValue); + return classHost->nativeClass(); + } + } + JS_FreeValue(runtime.context(), wrappedClassValue); + } + Value wrapped = Value::borrowed(runtime, value); + return classFromEngineValue(runtime, wrapped); +} + +bool readQuickJSEngineSelectorArgument(Runtime& runtime, JSValueConst value, + SEL* result) { + if (result == nullptr) { + return false; + } + if (JS_IsNull(value) || JS_IsUndefined(value)) { + *result = nullptr; + return true; + } + if (!JS_IsString(value)) { + return false; + } + std::string selectorName = quickJSValueToUtf8(runtime.context(), value); + *result = sel_registerName(selectorName.c_str()); + return true; +} + +template +bool writeQuickJSNumber(JSContext* context, JSValueConst value, void* target) { + double converted = 0; + if (!quickJSNumberValue(context, value, &converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; +} + +bool prepareQuickJSEngineArgument( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, JSValueConst value, + NativeApiArgumentFrame& frame, size_t index) { + ffi_type* ffiType = ffiTypeForEngineArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + JSContext* context = runtime.context(); + + switch (type.kind) { + case metagen::mdTypeBool: + if (!JS_IsBool(value)) { + return false; + } + *static_cast(target) = JS_ToBool(context, value) != 0 ? 1 : 0; + return true; + case metagen::mdTypeChar: + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeSShort: + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeUShort: + if (JS_IsString(value)) { + std::string text = quickJSValueToUtf8(context, value); + if (text.size() != 1) { + return false; + } + *static_cast(target) = + static_cast(static_cast(text[0])); + return true; + } + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeSInt: { + int32_t converted = 0; + if (JS_ToInt32(context, &converted, value) < 0) { + return false; + } + *static_cast(target) = converted; + return true; + } + case metagen::mdTypeUInt: { + uint32_t converted = 0; + if (JS_ToUint32(context, &converted, value) < 0) { + return false; + } + *static_cast(target) = converted; + return true; + } + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: { + int64_t converted = 0; + if (JS_ToInt64Ext(context, &converted, value) < 0) { + return false; + } + *static_cast(target) = converted; + return true; + } + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: { + uint64_t converted = 0; + if (JS_IsBigInt(context, value)) { + if (JS_ToBigUint64(context, &converted, value) < 0) { + return false; + } + } else { + int64_t signedValue = 0; + if (JS_ToInt64Ext(context, &signedValue, value) < 0) { + return false; + } + converted = static_cast(signedValue); + } + *static_cast(target) = converted; + return true; + } + case metagen::mdTypeFloat: + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeDouble: + return writeQuickJSNumber(context, value, target); + case metagen::mdTypeSelector: + return readQuickJSEngineSelectorArgument(runtime, value, + static_cast(target)); + case metagen::mdTypeClass: { + Class cls = quickJSNativeClassArgument(runtime, value); + if (cls == Nil) { + return false; + } + *static_cast(target) = cls; + return true; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + *static_cast(target) = + quickJSNativeObjectArgument(runtime, bridge, type, value, frame); + return true; + default: + break; + } + + Value wrapped = Value::borrowed(runtime, value); + convertEngineFfiArgument(runtime, bridge, type, wrapped, target, frame); + return true; +} + +JSValue quickJSInteger64Value(Runtime& runtime, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return JS_NewFloat64(runtime.context(), static_cast(value)); + } + return JS_NewBigInt64(runtime.context(), value); +} + +JSValue quickJSUnsignedInteger64Value(Runtime& runtime, uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return JS_NewFloat64(runtime.context(), static_cast(value)); + } + return JS_NewBigUint64(runtime.context(), value); +} + +JSValue setQuickJSEngineObjectReturn( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, id object) { + JSContext* context = runtime.context(); + if (object == nil) { + return JS_NULL; + } + Value roundTrip = + findCachedNativeObjectReturn(runtime, bridge, type, object); + if (!roundTrip.isUndefined()) { + JSValue result = roundTrip.local(runtime); + if (type.returnOwned) { + [object release]; + } + return result; + } + if (nativeObjectReturnMayCoerceToString(type) && + nativeObjectIsStringLike(object)) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; + } + return JS_NewStringLen(context, utf8.data(), utf8.size()); + } + if ([object isKindOfClass:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + return JS_NULL; + } + 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); + JSValue result = isBool ? JS_NewBool(context, [number boolValue]) + : JS_NewFloat64(context, [number doubleValue]); + if (type.returnOwned) { + [object release]; + } + return result; + } + + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + Value result = makeNativeClassValue(runtime, bridge, *classSymbol); + if (type.returnOwned) { + [object release]; + } + return result.local(runtime); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + Value result = makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + if (type.returnOwned) { + [object release]; + } + return result.local(runtime); + } + Value result = makeNativeObjectValue(runtime, bridge, object, type.returnOwned); + return result.local(runtime); +} + +JSValue setQuickJSEngineReturnValue( + Runtime& runtime, const std::shared_ptr& bridge, + NativeApiType type, void* value, const std::string& selectorName) { + JSContext* context = runtime.context(); + switch (type.kind) { + case metagen::mdTypeVoid: + return JS_UNDEFINED; + case metagen::mdTypeBool: + return JS_NewBool(context, *static_cast(value) != 0); + case metagen::mdTypeChar: + return JS_NewInt32(context, *static_cast(value)); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return JS_NewUint32(context, *static_cast(value)); + case metagen::mdTypeSShort: + return JS_NewInt32(context, *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 JS_NewStringLen(context, buffer, 1); + } + return JS_NewUint32(context, raw); + } + case metagen::mdTypeSInt: + return JS_NewInt32(context, *static_cast(value)); + case metagen::mdTypeUInt: + return JS_NewUint32(context, *static_cast(value)); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return quickJSInteger64Value(runtime, *static_cast(value)); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return quickJSUnsignedInteger64Value(runtime, + *static_cast(value)); + case metagen::mdTypeFloat: + return JS_NewFloat64(context, *static_cast(value)); + case metagen::mdTypeDouble: + return JS_NewFloat64(context, *static_cast(value)); + case metagen::mdTypeClass: { + Class cls = *static_cast(value); + if (cls == nil) { + return JS_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; + } + Value result = makeNativeClassValue(runtime, bridge, std::move(symbol)); + return result.local(runtime); + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + if ((selectorName == "valueForKey:" || + selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(type)) { + type.kind = metagen::mdTypeAnyObject; + } + return setQuickJSEngineObjectReturn(runtime, bridge, type, + *static_cast(value)); + case metagen::mdTypeSelector: { + SEL selector = *static_cast(value); + const char* selectorNameValue = + selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorNameValue == nullptr) { + return JS_NULL; + } + return JS_NewString(context, selectorNameValue); + } + default: + break; + } + Value result = convertNativeReturnValue(runtime, bridge, type, value); + return result.local(runtime); +} diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h index 438a78166..c20ad480a 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h @@ -36,15 +36,15 @@ #include "ffi.h" #include "quickjs.h" -@protocol NativeApiJsiClassBuilderProtocol +@protocol NativeApiClassBuilderProtocol @end #ifdef EMBED_METADATA_SIZE extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { class Runtime; class Value; @@ -96,13 +96,13 @@ class HostObject { public: virtual ~HostObject() = default; virtual Value get(Runtime& runtime, const PropNameID& name); - virtual void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); virtual std::vector getPropertyNames(Runtime& runtime); }; using HostFunctionType = std::function; -namespace quickjsdirect { +namespace quickjsengine { template const void* hostObjectTypeToken() { @@ -115,6 +115,7 @@ struct RuntimeState { JSContext* context = nullptr; bool hostClassRegistered = false; bool functionClassRegistered = false; + bool selectorGroupDataClassRegistered = false; }; extern JSClassID gHostClassId; @@ -129,11 +130,12 @@ struct ValueStorage { Bool, Number, QuickJS, + QuickJSBorrowed, }; explicit ValueStorage(Kind kind) : kind(kind) {} ~ValueStorage() { - if (context != nullptr && !JS_IsUninitialized(value)) { + if (kind == Kind::QuickJS && context != nullptr && !JS_IsUninitialized(value)) { JS_FreeValue(context, value); } } @@ -177,6 +179,14 @@ inline std::string valueToUtf8(JSContext* context, JSValueConst value) { return result; } +inline std::string currentExceptionMessage(JSContext* context) { + JSValue exception = JS_GetException(context); + std::string message = valueToUtf8(context, exception); + JS_FreeValue(context, exception); + return message.empty() ? std::string("QuickJS function call failed.") + : message; +} + inline std::string atomToUtf8(JSContext* context, JSAtom atom) { const char* cString = JS_AtomToCString(context, atom); if (cString == nullptr) { @@ -193,14 +203,14 @@ inline JSValue throwError(JSContext* context, const std::exception& error) { void ensureClasses(Runtime& runtime); -} // namespace quickjsdirect +} // namespace quickjsengine class Runtime { public: - explicit Runtime(JSContext* context) : state_(quickjsdirect::stateForContext(context)) {} - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + explicit Runtime(JSContext* context) : state_(quickjsengine::stateForContext(context)) {} + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} JSContext* context() const { return state_->context; } - std::shared_ptr state() const { return state_; } + std::shared_ptr state() const { return state_; } Object global(); Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); void drainMicrotasks() { @@ -212,7 +222,7 @@ class Runtime { } private: - std::shared_ptr state_; + std::shared_ptr state_; }; class String { @@ -235,125 +245,155 @@ class String { private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class Value { public: - Value() - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::Undefined)) {} - Value(bool value) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::Bool)) { - storage_->boolValue = value; - } - Value(double value) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::Number)) { - storage_->numberValue = value; - } + Value() : kind_(quickjsengine::ValueStorage::Kind::Undefined) {} + + Value(bool value) : kind_(quickjsengine::ValueStorage::Kind::Bool), boolValue_(value) {} + + Value(double value) : kind_(quickjsengine::ValueStorage::Kind::Number), numberValue_(value) {} + Value(int value) : Value(static_cast(value)) {} Value(uint32_t value) : Value(static_cast(value)) {} - Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} - Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed) { + // Promote borrowed to owned + storage_ = std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS); + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value.borrowedValue_); + kind_ = quickjsengine::ValueStorage::Kind::QuickJS; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedContext_ = value.borrowedContext_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedContext_(value.borrowedContext_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value) : storage_(value.storage_) { + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; + } Value(Runtime& runtime, const Object& object); Value(Runtime& runtime, const Function& function); Value(Runtime& runtime, const Array& array); Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); Value(Runtime& runtime, const BigInt& bigint); Value(Runtime& runtime, JSValue value) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { + : kind_(quickjsengine::ValueStorage::Kind::QuickJS), + storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { storage_->context = runtime.context(); storage_->value = JS_DupValue(runtime.context(), value); } + static Value borrowed(Runtime& runtime, JSValueConst value) { + Value result; + result.kind_ = quickjsengine::ValueStorage::Kind::QuickJSBorrowed; + result.borrowedContext_ = runtime.context(); + result.borrowedValue_ = value; + return result; + } + static Value undefined() { return Value(); } static Value null() { Value value; - value.storage_ = - std::make_shared(quickjsdirect::ValueStorage::Kind::Null); + value.kind_ = quickjsengine::ValueStorage::Kind::Null; return value; } bool isUndefined() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::Undefined || - (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsUndefined(storage_->value)); + if (kind_ == quickjsengine::ValueStorage::Kind::Undefined) { + return true; + } + return isQuickJS() && JS_IsUndefined(jsValue()); } bool isNull() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::Null || - (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsNull(storage_->value)); + if (kind_ == quickjsengine::ValueStorage::Kind::Null) { + return true; + } + return isQuickJS() && JS_IsNull(jsValue()); } bool isBool() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::Bool || - (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsBool(storage_->value)); + if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { + return true; + } + return isQuickJS() && JS_IsBool(jsValue()); } bool getBool() const { - if (storage_->kind == quickjsdirect::ValueStorage::Kind::Bool) { - return storage_->boolValue; + if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { + return boolValue_; } - if (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS) { - return JS_ToBool(storage_->context, storage_->value) != 0; + if (isQuickJS()) { + return JS_ToBool(jsContext(), jsValue()) != 0; } return false; } bool isNumber() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::Number || - (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsNumber(storage_->value)); + if (kind_ == quickjsengine::ValueStorage::Kind::Number) { + return true; + } + return isQuickJS() && JS_IsNumber(jsValue()); } double getNumber() const { - if (storage_->kind == quickjsdirect::ValueStorage::Kind::Number) { - return storage_->numberValue; + if (kind_ == quickjsengine::ValueStorage::Kind::Number) { + return numberValue_; } - if (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS) { + if (isQuickJS()) { double value = 0; - JS_ToFloat64(storage_->context, &value, storage_->value); + JS_ToFloat64(jsContext(), &value, jsValue()); return value; } return 0; } - bool isObject() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsObject(storage_->value); - } - bool isString() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsString(storage_->value); - } - bool isBigInt() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsBigInt(storage_->context, storage_->value); - } - bool isSymbol() const { - return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && - JS_IsSymbol(storage_->value); - } + bool isObject() const { return isQuickJS() && JS_IsObject(jsValue()); } + bool isString() const { return isQuickJS() && JS_IsString(jsValue()); } + bool isBigInt() const { return isQuickJS() && JS_IsBigInt(jsContext(), jsValue()); } + bool isSymbol() const { return isQuickJS() && JS_IsSymbol(jsValue()); } Object asObject(Runtime& runtime) const; String asString(Runtime& runtime) const; BigInt getBigInt(Runtime& runtime) const; JSValue local(Runtime& runtime) const { - switch (storage_->kind) { - case quickjsdirect::ValueStorage::Kind::Undefined: + switch (kind_) { + case quickjsengine::ValueStorage::Kind::Undefined: return JS_UNDEFINED; - case quickjsdirect::ValueStorage::Kind::Null: + case quickjsengine::ValueStorage::Kind::Null: return JS_NULL; - case quickjsdirect::ValueStorage::Kind::Bool: - return JS_NewBool(runtime.context(), storage_->boolValue); - case quickjsdirect::ValueStorage::Kind::Number: - return JS_NewFloat64(runtime.context(), storage_->numberValue); - case quickjsdirect::ValueStorage::Kind::QuickJS: - return JS_DupValue(runtime.context(), storage_->value); + case quickjsengine::ValueStorage::Kind::Bool: + return JS_NewBool(runtime.context(), boolValue_); + case quickjsengine::ValueStorage::Kind::Number: + return JS_NewFloat64(runtime.context(), numberValue_); + case quickjsengine::ValueStorage::Kind::QuickJS: + case quickjsengine::ValueStorage::Kind::QuickJSBorrowed: + return JS_DupValue(runtime.context(), jsValue()); } } + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.storage_ = std::move(s); + return v; + } + private: friend class Runtime; friend class Object; @@ -362,19 +402,38 @@ class Value { friend class ArrayBuffer; friend class Function; friend class Array; - std::shared_ptr storage_; + + bool isQuickJS() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJS || + kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed; + } + JSContext* jsContext() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedContext_ + : storage_->context; + } + JSValue jsValue() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedValue_ + : storage_->value; + } + + quickjsengine::ValueStorage::Kind kind_ = quickjsengine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + JSContext* borrowedContext_ = nullptr; + JSValue borrowedValue_ = JS_UNINITIALIZED; + std::shared_ptr storage_; }; class Object { public: Object() = default; explicit Object(Runtime& runtime) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { + : storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { storage_->context = runtime.context(); storage_->value = JS_NewObject(runtime.context()); } - static Object fromValueStorage(std::shared_ptr storage) { + static Object fromValueStorage(std::shared_ptr storage) { Object object; object.storage_ = std::move(storage); return object; @@ -383,7 +442,7 @@ class Object { static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { auto baseHost = std::static_pointer_cast(std::move(host)); return createFromHostObjectWithToken(runtime, std::move(baseHost), - quickjsdirect::hostObjectTypeToken()); + quickjsengine::hostObjectTypeToken()); } Value getProperty(Runtime& runtime, const char* name) const { @@ -501,22 +560,18 @@ class Object { template bool isHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == quickjsdirect::hostObjectTypeToken(); + return holder != nullptr && holder->typeToken == quickjsengine::hostObjectTypeToken(); } template std::shared_ptr getHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != quickjsdirect::hostObjectTypeToken()) { + if (holder == nullptr || holder->typeToken != quickjsengine::hostObjectTypeToken()) { return nullptr; } return std::static_pointer_cast(holder->hostObject); } JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } - operator Value() const { - Value value; - value.storage_ = storage_; - return value; - } + operator Value() const { return Value::fromStorage(storage_); } protected: friend class Value; @@ -524,12 +579,12 @@ class Object { friend class Function; friend class Array; friend class ArrayBuffer; - explicit Object(std::shared_ptr storage) + explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken); - quickjsdirect::HostObjectHolder* hostObjectHolder(Runtime& runtime) const; - std::shared_ptr storage_; + quickjsengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const; + std::shared_ptr storage_; }; class Function : public Object { @@ -554,7 +609,7 @@ class Function : public Object { JS_FreeValue(runtime.context(), global); JS_FreeValue(runtime.context(), function); if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS function call failed."); + throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); } Value value(runtime, result); JS_FreeValue(runtime.context(), result); @@ -592,7 +647,7 @@ class Function : public Object { JS_FreeValue(runtime.context(), thisValue); JS_FreeValue(runtime.context(), function); if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS function call failed."); + throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); } Value value(runtime, result); JS_FreeValue(runtime.context(), result); @@ -635,8 +690,8 @@ class Function : public Object { class Array : public Object { public: explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { + : Object(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { storage_->context = runtime.context(); storage_->value = JS_NewArray(runtime.context()); JS_SetPropertyStr(runtime.context(), storage_->value, "length", @@ -677,8 +732,8 @@ class BigInt { public: BigInt() = default; BigInt(Runtime& runtime, JSValue value) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { + : storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { storage_->context = runtime.context(); storage_->value = JS_DupValue(runtime.context(), value); } @@ -696,28 +751,24 @@ class BigInt { } String toString(Runtime& runtime, int) const; JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } - operator Value() const { - Value value; - value.storage_ = storage_; - return value; - } + operator Value() const { return Value::fromStorage(storage_); } private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class ArrayBuffer : public Object { public: ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { - auto* holder = new quickjsdirect::ArrayBufferHolder(std::move(buffer)); + : Object(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + auto* holder = new quickjsengine::ArrayBufferHolder(std::move(buffer)); storage_->context = runtime.context(); storage_->value = JS_NewArrayBuffer( runtime.context(), holder->buffer->data(), holder->buffer->size(), [](JSRuntime*, void* opaque, void*) { - delete static_cast(opaque); + delete static_cast(opaque); }, holder, false); } @@ -737,8 +788,8 @@ class ArrayBuffer : public Object { return data; } }; -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm index 6a64b8e57..d38eb3ac6 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm @@ -2,8 +2,8 @@ #ifdef TARGET_ENGINE_QUICKJS -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { String BigInt::toString(Runtime& runtime, int) const { JSValue value = local(runtime); @@ -34,7 +34,7 @@ return value; } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntimeSupport.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntimeSupport.mm new file mode 100644 index 000000000..c755e5b97 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntimeSupport.mm @@ -0,0 +1,100 @@ +// Included by NativeApiQuickJS.mm inside the NativeScript anonymous namespace. + +static JSValue NativeApiLazyGlobalGetter(JSContext* context, JSValueConst, int, + JSValueConst*, int, JSValueConst* data) { + JSValue global = JS_GetGlobalObject(context); + JSValue resolver = JS_GetPropertyStr(context, global, "__nativeScriptResolveNativeApiLazyGlobal"); + if (!JS_IsFunction(context, resolver)) { + JS_FreeValue(context, resolver); + JS_FreeValue(context, global); + return JS_UNDEFINED; + } + + JSValueConst args[] = {data[0], data[1]}; + JSValue result = JS_Call(context, resolver, global, 2, args); + JS_FreeValue(context, resolver); + if (JS_IsException(result)) { + JS_FreeValue(context, global); + return result; + } + + JSAtom atom = JS_ValueToAtom(context, data[0]); + if (atom != JS_ATOM_NULL) { + JS_DefinePropertyValue(context, global, atom, JS_DupValue(context, result), + JS_PROP_CONFIGURABLE); + JS_FreeAtom(context, atom); + } + JS_FreeValue(context, global); + return result; +} + +bool InstallNativeApiLazyGlobal(Runtime& runtime, std::shared_ptr, + const std::string& name, const std::string& kind, + bool force) { + if (name.empty() || kind.empty()) { + return false; + } + + JSContext* context = runtime.context(); + JSValue global = JS_GetGlobalObject(context); + JSAtom atom = JS_NewAtomLen(context, name.data(), name.size()); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(context, global); + return false; + } + + int hasProperty = JS_HasProperty(context, global, atom); + if (!force && hasProperty > 0) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + if (hasProperty < 0) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + JSValue data[] = { + JS_NewStringLen(context, name.data(), name.size()), + JS_NewStringLen(context, kind.data(), kind.size()), + }; + if (JS_IsException(data[0]) || JS_IsException(data[1])) { + JS_FreeValue(context, data[0]); + JS_FreeValue(context, data[1]); + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + JSValue getter = JS_NewCFunctionData(context, NativeApiLazyGlobalGetter, 0, 0, 2, data); + JS_FreeValue(context, data[0]); + JS_FreeValue(context, data[1]); + if (JS_IsException(getter)) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + int status = + JS_DefinePropertyGetSet(context, global, atom, getter, JS_UNDEFINED, JS_PROP_CONFIGURABLE); + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return status >= 0; +} + +void SetNativeApiObjectPrototype(Runtime& runtime, Object& object, + const Object& prototype) { + JSValue objectValue = object.local(runtime); + JSValue prototypeValue = prototype.local(runtime); + int status = JS_SetPrototype(runtime.context(), objectValue, prototypeValue); + JS_FreeValue(runtime.context(), prototypeValue); + JS_FreeValue(runtime.context(), objectValue); + if (status < 0) { + throw JSError(runtime, "QuickJS prototype assignment failed."); + } +} + +std::shared_ptr retainNativeApiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSSelectorGroups.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSSelectorGroups.mm new file mode 100644 index 000000000..26d0f1415 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSSelectorGroups.mm @@ -0,0 +1,464 @@ +// Included by NativeApiQuickJS.mm inside the NativeScript anonymous namespace. + +struct NativeApiSelectorGroupData { + NativeApiSelectorGroupData( + std::shared_ptr state, + std::shared_ptr bridge, Class lookupClass, + bool receiverIsClass, + std::shared_ptr> + selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver = {}, + std::shared_ptr boundReceiverState = + nullptr) + : state(state), + bridge(std::move(bridge)), + lookupClass(lookupClass), + receiverIsClass(receiverIsClass), + selectors(std::move(selectors)), + preparedInvocations(std::move(preparedInvocations)), + boundReceiver(std::move(boundReceiver)), + boundReceiverState(std::move(boundReceiverState)), + runtime(state) {} + + std::shared_ptr state; + std::shared_ptr bridge; + Class lookupClass = Nil; + bool receiverIsClass = false; + std::shared_ptr> selectors; + std::shared_ptr< + std::vector>> + preparedInvocations; + std::weak_ptr boundReceiver; + std::shared_ptr boundReceiverState; + Runtime runtime; + Class cachedReceiverClass = Nil; + Class cachedDispatchClass = Nil; +}; + +#include "NativeApiQuickJSMarshalling.mm" + +#include "NativeApiQuickJSGsd.mm" + + +void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { + return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); +} + +bool tryCallGeneratedEngineObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass, Value* result) { + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount) { + return false; + } + + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, + runtime.context(), nullptr, + prepared.signature.returnType}; + ctx.valueArguments = args; + ctx.materializeValueResult = true; + if (!invoker(ctx)) { + return false; + } + *result = std::move(ctx.valueResult); + return true; +} + +JSValue setQuickJSEnginePreparedObjCResult( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const std::shared_ptr& receiverHostObject, + const std::optional& initializerClassWrapper, + size_t providedCount, JSValueConst arguments[], + Class dispatchSuperClass) { + const NativeApiSignature& signature = prepared.signature; + if (receiver == nil || signature.variadic || + unsupportedEngineType(signature.returnType)) { + throw JSError(runtime, + "Objective-C selector is not supported by QuickJS engine: " + + prepared.selectorName); + } + + const bool isNSErrorOutMethod = prepared.isNSErrorOutMethod; + if (isNSErrorOutMethod) { + size_t expected = signature.argumentTypes.size(); + if (providedCount > expected || providedCount + 1 < expected) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + std::to_string(expected) + "\"."); + } + } else if (providedCount != signature.argumentTypes.size()) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + + std::to_string(signature.argumentTypes.size()) + "\"."); + } + + // GSD fast path: the generated invoker reads args directly from the QuickJS + // arguments, calls objc_msgSend with a typed cast, and produces the JS + // return value — bypassing all generic marshalling. + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod) { + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector, + runtime.context(), arguments, signature.returnType}; + if (invoker(ctx)) { + return ctx.result; + } + } + + if (dispatchSuperClass == Nil && !initializerClassWrapper && + providedCount <= 2) { + Value fastArgs[2]; + for (size_t i = 0; i < providedCount; i++) { + fastArgs[i] = Value::borrowed(runtime, arguments[i]); + } + Value fastResult; + if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared, + fastArgs, providedCount, Nil, + &fastResult)) { + return fastResult.local(runtime); + } + } + + NativeApiArgumentFrame frame(signature.argumentTypes.size()); + for (size_t i = 0; i < providedCount; i++) { + if (!prepareQuickJSEngineArgument(runtime, bridge, + signature.argumentTypes[i], + arguments[i], frame, i)) { + throw JSError(runtime, + "Objective-C argument is not supported by QuickJS engine: " + + prepared.selectorName); + } + } + + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && providedCount + 1 == signature.argumentTypes.size(); + NSError* implicitNSError = nil; + if (hasImplicitNSErrorOutArg) { + size_t outArgIndex = signature.argumentTypes.size() - 1; + void* target = frame.storageAt(outArgIndex, sizeof(NSError**)); + NSError** implicitNSErrorOutArg = &implicitNSError; + *static_cast(target) = implicitNSErrorOutArg; + } + + NativeApiPointerFrame values(signature.argumentTypes.size() + 2); + size_t valueIndex = 0; + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.set(valueIndex++, &superReceiverPtr); + } else { + values.set(valueIndex++, &receiver); + } + values.set(valueIndex++, const_cast(&prepared.selector)); + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + values.set(valueIndex++, frame.values()[i]); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature.returnType)); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (prepared.preparedInvoker != nullptr && dispatchSuperClass == Nil) { + prepared.preparedInvoker(reinterpret_cast(objc_msgSend), + values.data(), returnStorage.data()); + } else { +#if defined(__x86_64__) + bool isStret = signature.returnType.ffiType->size > 16 && + signature.returnType.ffiType->type == FFI_TYPE_STRUCT; + void (*target)(void) = + dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend)); + ffi_call(const_cast(&signature.cif), target, + returnStorage.data(), values.data()); +#else + ffi_call(const_cast(&signature.cif), + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#endif + } + }); + + NativeApiType returnType = signature.returnType; + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + if (initializerClassWrapper) { + id resultObject = nil; + if (isObjectiveCObjectType(returnType)) { + resultObject = *static_cast(returnStorage.data()); + } + if (receiverHostObject != nullptr && resultObject != receiver) { + receiverHostObject->disownObject(receiver); + } + if (resultObject != nil) { + bridge->setObjectExpando(runtime, resultObject, + "__nativeApiClassWrapper", + Value(runtime, *initializerClassWrapper)); + } + } + return setQuickJSEngineReturnValue(runtime, bridge, returnType, + returnStorage.data(), + prepared.selectorName); +} + +static JSClassID gNativeApiSelectorGroupDataClassId = 0; + +void NativeApiSelectorGroupFinalize(JSRuntime*, JSValue value) { + auto* data = static_cast( + JS_GetOpaque(value, gNativeApiSelectorGroupDataClassId)); + delete data; +} + +void EnsureNativeApiSelectorGroupClass(Runtime& runtime) { + JSRuntime* jsRuntime = JS_GetRuntime(runtime.context()); + if (gNativeApiSelectorGroupDataClassId == 0) { + JS_NewClassID(jsRuntime, &gNativeApiSelectorGroupDataClassId); + } + + auto state = runtime.state(); + if (!state->selectorGroupDataClassRegistered) { + JSClassDef definition = {}; + definition.class_name = "NativeScriptEngineSelectorGroupData"; + definition.finalizer = NativeApiSelectorGroupFinalize; + JS_NewClass(jsRuntime, gNativeApiSelectorGroupDataClassId, + &definition); + state->selectorGroupDataClassRegistered = true; + } +} + +JSValue NativeApiSelectorGroupCall(JSContext* context, JSValue thisValue, + int argc, JSValue* argv, int, + JSValue* dataValues) { + auto* data = static_cast( + JS_GetOpaque(dataValues[0], gNativeApiSelectorGroupDataClassId)); + if (data == nullptr || data->selectors == nullptr || + data->preparedInvocations == nullptr) { + return JS_UNDEFINED; + } + + Runtime& runtime = data->runtime; + try { + NativeApiRoundTripCacheFrameGuard roundTripFrame(data->bridge); + size_t count = argc > 0 ? static_cast(argc) : 0; + if (count >= data->selectors->size() || + (*data->selectors)[count].selectorName.empty()) { + throw JSError(runtime, + "Objective-C selector is not available for the provided arguments " + "count."); + } + + NativeApiSelectorGroupEntry& entry = (*data->selectors)[count]; + auto& prepared = (*data->preparedInvocations)[count]; + Class selectorLookupClass = data->lookupClass; + id receiver = data->receiverIsClass ? static_cast(data->lookupClass) : nil; + std::shared_ptr receiverHostObject; + if (!data->receiverIsClass) { + if (data->boundReceiverState != nullptr) { + receiver = data->boundReceiverState->object(); + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + } else { + if (auto* rawHost = + quickJSHostObjectRaw(runtime, + thisValue)) { + receiver = rawHost->object(); + } + } + } + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + + const bool propertyGetterCall = + entry.hasMember && entry.member.property && count == 0; + const std::string* selectorNamePtr = &entry.selectorName; + const NativeApiMember* selectedMember = + entry.hasMember ? &entry.member : nullptr; + bool callTargetCanPrepare = true; + if (prepared == nullptr || propertyGetterCall) { + NativeApiSelectorGroupCallTarget callTarget = + selectorGroupCallTargetForEntry(receiver, selectorLookupClass, + data->receiverIsClass, entry, count); + selectorNamePtr = callTarget.selectorName; + selectedMember = callTarget.member; + callTargetCanPrepare = callTarget.canPrepare; + if (prepared != nullptr && prepared->selectorName != *selectorNamePtr) { + prepared = nullptr; + } + } + const std::string& selectorName = + prepared != nullptr && !propertyGetterCall ? prepared->selectorName + : *selectorNamePtr; + + if (data->receiverIsClass) { + Class methodClass = prepared != nullptr ? prepared->receiverClass : Nil; + if (methodClass == Nil) { + SEL selector = sel_registerName(selectorName.c_str()); + methodClass = + NativeApiClassHostObject::classRespondingToClassSelector( + data->lookupClass, selector); + } + if (methodClass == Nil) { + throw JSError(runtime, + "Objective-C selector is not available: " + + entry.selectorName); + } + selectorLookupClass = methodClass; + receiver = static_cast(methodClass); + } + if (propertyGetterCall && !callTargetCanPrepare) { + return callObjCSelector(runtime, data->bridge, receiver, + data->receiverIsClass, selectorName, + selectedMember, nullptr, 0) + .local(runtime); + } + + if (prepared == nullptr) { + if (!data->receiverIsClass) { + SEL selector = sel_registerName(selectorName.c_str()); + if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { + Class receiverClass = object_getClass(receiver); + if (class_getInstanceMethod(receiverClass, selector) != nullptr) { + selectorLookupClass = receiverClass; + } + } + } + prepared = prepareNativeApiObjCInvocation( + runtime, data->bridge, selectorLookupClass, data->receiverIsClass, + selectorName, selectedMember); + // Look up the engine-neutral GSD invoker for this signature. + if (prepared->engineInvoker == nullptr) { + uint64_t dispatchId = dispatchIdForEngineSignature( + prepared->signature, SignatureCallKind::ObjCMethod); + if (auto gsdInvoker = lookupObjCGsdInvoker(dispatchId)) { + prepared->engineInvoker = reinterpret_cast(gsdInvoker); + configureGeneratedEngineObjCInvocation(*prepared); + } + } + } + + std::optional initializerClassWrapper; + if (!data->receiverIsClass && prepared->isInitMethod) { + if (!receiverHostObject) { + if (data->boundReceiverState != nullptr) { + if (auto boundReceiver = data->boundReceiver.lock()) { + receiverHostObject = std::move(boundReceiver); + } + } else { + receiverHostObject = + quickJSHostObject(runtime, thisValue); + } + } + Value classWrapperValue = data->bridge->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + initializerClassWrapper.emplace(classWrapperValue.asObject(runtime)); + } + data->bridge->forgetRoundTripValue(receiver); + data->bridge->forgetObjectExpandos(receiver); + } + + Class dispatchClass = Nil; + if (!data->receiverIsClass) { + Class receiverClass = object_getClass(receiver); + if (receiverClass == data->cachedReceiverClass) { + dispatchClass = data->cachedDispatchClass; + } else { + dispatchClass = dispatchSuperclassForEngineDerivedReceiver( + receiver, data->lookupClass); + data->cachedReceiverClass = receiverClass; + data->cachedDispatchClass = dispatchClass; + } + } + return setQuickJSEnginePreparedObjCResult( + runtime, data->bridge, receiver, *prepared, receiverHostObject, + initializerClassWrapper, count, argv, dispatchClass); + } catch (const std::exception& error) { + return engine::quickjsengine::throwError(context, error); + } +} + +Function CreateNativeApiSelectorGroupFunctionImpl( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver, + std::shared_ptr boundReceiverState = + nullptr) { + EnsureNativeApiSelectorGroupClass(runtime); + auto* data = new NativeApiSelectorGroupData( + runtime.state(), std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), + std::move(boundReceiver), std::move(boundReceiverState)); + + JSValue dataObject = + JS_NewObjectClass(runtime.context(), + gNativeApiSelectorGroupDataClassId); + if (JS_IsException(dataObject)) { + delete data; + throw JSError(runtime, "QuickJS selector group allocation failed."); + } + JS_SetOpaque(dataObject, data); + + JSValue function = + JS_NewCFunctionData(runtime.context(), NativeApiSelectorGroupCall, + 0, 0, 1, &dataObject); + JS_FreeValue(runtime.context(), dataObject); + if (JS_IsException(function)) { + throw JSError(runtime, "QuickJS selector group function allocation failed."); + } + + JSValue nameValue = JS_NewStringLen(runtime.context(), "__nativeSelectorGroup", + std::strlen("__nativeSelectorGroup")); + JS_DefinePropertyValueStr(runtime.context(), function, "name", nameValue, + JS_PROP_CONFIGURABLE); + Value functionValue(runtime, function); + Function result = functionValue.asObject(runtime).asFunction(runtime); + JS_FreeValue(runtime.context(), function); + return result; +} + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), {}, nullptr); +} + +Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, false, std::move(selectors), + std::move(preparedInvocations), receiverHostObject, + receiverHostObject != nullptr ? receiverHostObject->lifetimeState() + : nullptr); +} diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm index 94ae05e0e..bb6852019 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm @@ -2,38 +2,61 @@ #ifdef TARGET_ENGINE_QUICKJS -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } -void HostObject::set(Runtime&, const PropNameID&, const Value&) {} +bool HostObject::set(Runtime&, const PropNameID&, const Value&) { return true; } std::vector HostObject::getPropertyNames(Runtime&) { return {}; } String::String(Runtime& runtime, JSValue value) - : storage_(std::make_shared( - quickjsdirect::ValueStorage::Kind::QuickJS)) { + : storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { storage_->context = runtime.context(); storage_->value = JS_DupValue(runtime.context(), value); } std::string String::utf8(Runtime& runtime) const { JSValue value = local(runtime); - std::string result = quickjsdirect::valueToUtf8(runtime.context(), value); + std::string result = quickjsengine::valueToUtf8(runtime.context(), value); JS_FreeValue(runtime.context(), value); return result; } JSValue String::local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } -String::operator Value() const { - Value value; - value.storage_ = storage_; - return value; -} -Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} -Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} -Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} -Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} -Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} -Object Value::asObject(Runtime&) const { return Object::fromValueStorage(storage_); } +String::operator Value() const { return Value::fromStorage(storage_); } +Value::Value(Runtime&, const Object& object) { + storage_ = object.storage_; + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Function& function) { + storage_ = function.storage_; + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Array& array) { + storage_ = array.storage_; + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) { + storage_ = arrayBuffer.storage_; + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const BigInt& bigint) { + storage_ = bigint.storage_; + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; +} +Object Value::asObject(Runtime& runtime) const { + if (storage_) { + return Object::fromValueStorage(storage_); + } + // Promote to owned storage for Object. + auto s = std::make_shared(kind_); + if (kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed) { + s->kind = quickjsengine::ValueStorage::Kind::QuickJS; + s->context = borrowedContext_; + s->value = JS_DupValue(borrowedContext_, borrowedValue_); + } + return Object::fromValueStorage(std::move(s)); +} String Value::asString(Runtime& runtime) const { JSValue value = local(runtime); String result(runtime, value); @@ -82,7 +105,7 @@ setProperty(runtime, name, Value(runtime, value)); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/SignatureDispatch.h b/NativeScript/ffi/quickjs/SignatureDispatch.h new file mode 100644 index 000000000..a42fe6faf --- /dev/null +++ b/NativeScript/ffi/quickjs/SignatureDispatch.h @@ -0,0 +1,14 @@ +#ifndef NATIVESCRIPT_FFI_QUICKJS_SIGNATURE_DISPATCH_H +#define NATIVESCRIPT_FFI_QUICKJS_SIGNATURE_DISPATCH_H + +#include "ffi/shared/SignatureDispatchCore.h" + +#if defined(__has_include) +#if __has_include("GeneratedSignatureDispatch.inc") +#include "GeneratedSignatureDispatch.inc" +#endif +#endif + +#include "ffi/shared/PreparedSignatureDispatch.h" + +#endif // NATIVESCRIPT_FFI_QUICKJS_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/shared/direct/EmbeddedMetadata.mm b/NativeScript/ffi/shared/MetadataState.mm similarity index 100% rename from NativeScript/ffi/shared/direct/EmbeddedMetadata.mm rename to NativeScript/ffi/shared/MetadataState.mm diff --git a/NativeScript/ffi/shared/direct/NativeApiDirect.h b/NativeScript/ffi/shared/NativeApiBackendConfig.h similarity index 56% rename from NativeScript/ffi/shared/direct/NativeApiDirect.h rename to NativeScript/ffi/shared/NativeApiBackendConfig.h index ef55cda4f..a6c2044e8 100644 --- a/NativeScript/ffi/shared/direct/NativeApiDirect.h +++ b/NativeScript/ffi/shared/NativeApiBackendConfig.h @@ -1,30 +1,32 @@ -#ifndef NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H -#define NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H +#ifndef NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H +#define NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H #include #include namespace nativescript { -class NativeApiDirectScheduler { +class NativeApiBackendScheduler { public: - virtual ~NativeApiDirectScheduler() = default; + virtual ~NativeApiBackendScheduler() = default; virtual void invokeOnJS(std::function task) = 0; virtual void invokeOnUI(std::function task) = 0; }; -struct NativeApiDirectConfig { +struct NativeApiBackendConfig { const char* metadataPath = nullptr; const void* metadataPtr = nullptr; const char* globalName = "__nativeScriptNativeApi"; - std::shared_ptr scheduler = nullptr; + std::shared_ptr scheduler = nullptr; std::function)> nativeInvocationInvoker = nullptr; std::function)> nativeCallbackInvoker = nullptr; + std::function)> runtimeCallbackInvoker = nullptr; std::function)> jsThreadCallbackInvoker = nullptr; + std::function)> jsThreadAsyncCallbackInvoker = nullptr; bool invokeCallbacksOnNativeCallerThread = false; bool installGlobalSymbols = false; }; } // namespace nativescript -#endif // NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H +#endif // NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H diff --git a/NativeScript/ffi/shared/PreparedSignatureDispatch.h b/NativeScript/ffi/shared/PreparedSignatureDispatch.h new file mode 100644 index 000000000..c941006fe --- /dev/null +++ b/NativeScript/ffi/shared/PreparedSignatureDispatch.h @@ -0,0 +1,80 @@ +#ifndef NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H +#define NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H + +#include "SignatureDispatchCore.h" + +#ifndef NS_GSD_BACKEND_PREPARED +#define NS_GSD_BACKEND_PREPARED 0 +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#define NS_GSD_BACKEND_HERMES 0 +#endif + +#ifndef NS_GSD_BACKEND_NAPI +#define NS_GSD_BACKEND_NAPI 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 +#endif + +#define NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH \ + (NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_NAPI || NS_GSD_BACKEND_PREPARED) + +#if NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH && \ + !NS_HAS_GENERATED_SIGNATURE_DISPATCH +#error GeneratedSignatureDispatch.inc did not enable this generated signature dispatch backend. +#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 + +namespace nativescript { + +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 bool isPreparedGeneratedDispatchRequired() { +#if NS_HAS_GENERATED_SIGNATURE_DISPATCH && \ + (NS_GSD_BACKEND_PREPARED || NS_GSD_BACKEND_HERMES) + return isGeneratedDispatchEnabled(); +#else + return false; +#endif +} + +} // namespace nativescript + +#endif // NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/shared/SignatureDispatchCore.h b/NativeScript/ffi/shared/SignatureDispatchCore.h new file mode 100644 index 000000000..229347a74 --- /dev/null +++ b/NativeScript/ffi/shared/SignatureDispatchCore.h @@ -0,0 +1,298 @@ +#ifndef NS_FFI_SHARED_SIGNATURE_DISPATCH_CORE_H +#define NS_FFI_SHARED_SIGNATURE_DISPATCH_CORE_H + +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.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); + +struct ObjCDispatchEntry { + uint64_t dispatchId; + ObjCPreparedInvoker invoker; +}; + +struct CFunctionDispatchEntry { + uint64_t dispatchId; + CFunctionPreparedInvoker invoker; +}; + +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + +inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; +inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; +inline constexpr metagen::MDSectionOffset kNullMetadataSectionOffset = + static_cast(0xFFFFFFFFu >> 1); + +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); +} + +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"); + return disableFlag == nullptr || disableFlag[0] == '\0' || + (disableFlag[0] == '0' && disableFlag[1] == '\0'); + }(); + return enabled; +} + +namespace signature_dispatch_detail { + +inline metagen::MDTypeKind canonicalizeSignatureTypeKind( + metagen::MDTypeKind kind) { + switch (kind) { + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + return metagen::mdTypeAnyObject; + default: + return kind; + } +} + +template +inline void appendIntegralToHash(uint64_t* hash, 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); + } +} + +inline metagen::MDTypeKind stripMetadataTypeFlags(metagen::MDTypeKind kind) { + uint8_t raw = static_cast(kind); + raw &= ~(metagen::mdTypeFlagNext | metagen::mdTypeFlagVariadic); + return static_cast(raw); +} + +inline bool appendMetadataSignatureHash( + metagen::MDMetadataReader* reader, metagen::MDSectionOffset signatureOffset, + std::unordered_set* activeSignatures, + uint64_t* hash); + +inline bool appendMetadataTypeHash( + metagen::MDMetadataReader* reader, metagen::MDSectionOffset* offset, + std::unordered_set* activeSignatures, + uint64_t* hash) { + if (reader == nullptr || offset == nullptr || hash == nullptr || + activeSignatures == nullptr) { + return false; + } + + const metagen::MDTypeKind kindWithFlags = reader->getTypeKind(*offset); + *offset += sizeof(metagen::MDTypeKind); + const metagen::MDTypeKind rawKind = stripMetadataTypeFlags(kindWithFlags); + + appendIntegralToHash(hash, 0xB0); + appendIntegralToHash( + hash, static_cast(canonicalizeSignatureTypeKind(rawKind))); + + switch (rawKind) { + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: { + const auto arraySize = reader->getArraySize(*offset); + *offset += sizeof(uint16_t); + appendIntegralToHash(hash, arraySize); + if (!appendMetadataTypeHash(reader, offset, activeSignatures, hash)) { + return false; + } + break; + } + + case metagen::mdTypeStruct: { + const auto structOffset = reader->getOffset(*offset); + *offset += sizeof(metagen::MDSectionOffset); + appendIntegralToHash(hash, structOffset); + break; + } + + case metagen::mdTypeClassObject: { + auto classOffset = reader->getOffset(*offset); + *offset += sizeof(metagen::MDSectionOffset); + bool hasNext = (classOffset & metagen::mdSectionOffsetNext) != 0; + while (hasNext) { + auto protocolOffset = reader->getOffset(*offset); + *offset += sizeof(metagen::MDSectionOffset); + hasNext = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + + case metagen::mdTypeProtocolObject: { + bool hasNext = true; + while (hasNext) { + auto protocolOffset = reader->getOffset(*offset); + *offset += sizeof(metagen::MDSectionOffset); + hasNext = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + + case metagen::mdTypePointer: + if (!appendMetadataTypeHash(reader, offset, activeSignatures, hash)) { + return false; + } + break; + + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + const auto nestedSignatureOffset = reader->getOffset(*offset); + *offset += sizeof(metagen::MDSectionOffset); + if (nestedSignatureOffset != kNullMetadataSectionOffset) { + const auto nestedAbsoluteOffset = + reader->signaturesOffset + nestedSignatureOffset; + if (!appendMetadataSignatureHash(reader, nestedAbsoluteOffset, + activeSignatures, hash)) { + return false; + } + } + break; + } + + default: + break; + } + + appendIntegralToHash(hash, 0xBF); + return true; +} + +inline bool appendMetadataSignatureHash( + metagen::MDMetadataReader* reader, metagen::MDSectionOffset signatureOffset, + std::unordered_set* activeSignatures, + uint64_t* hash) { + if (reader == nullptr || hash == nullptr || activeSignatures == nullptr) { + return false; + } + + if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { + appendIntegralToHash(hash, 0xEE); + return true; + } + activeSignatures->insert(signatureOffset); + + metagen::MDSectionOffset offset = signatureOffset; + const metagen::MDTypeKind returnTypeKind = reader->getTypeKind(offset); + bool next = + (static_cast(returnTypeKind) & metagen::mdTypeFlagNext) != 0; + const bool isVariadic = + (static_cast(returnTypeKind) & metagen::mdTypeFlagVariadic) != 0; + + appendIntegralToHash(hash, 0xA0); + appendIntegralToHash(hash, isVariadic ? 1 : 0); + + if (!appendMetadataTypeHash(reader, &offset, activeSignatures, hash)) { + activeSignatures->erase(signatureOffset); + return false; + } + + uint32_t argCount = 0; + while (next) { + const metagen::MDTypeKind argTypeKind = reader->getTypeKind(offset); + next = + (static_cast(argTypeKind) & metagen::mdTypeFlagNext) != 0; + if (!appendMetadataTypeHash(reader, &offset, activeSignatures, hash)) { + activeSignatures->erase(signatureOffset); + return false; + } + argCount++; + } + + appendIntegralToHash(hash, argCount); + appendIntegralToHash(hash, 0xAF); + + activeSignatures->erase(signatureOffset); + return true; +} + +} // namespace signature_dispatch_detail + +inline uint64_t metadataSignatureHash( + metagen::MDMetadataReader* reader, + metagen::MDSectionOffset signatureOffset) { + if (reader == nullptr || signatureOffset == kNullMetadataSectionOffset) { + return 0; + } + + uint64_t hash = kSignatureHashOffsetBasis; + std::unordered_set activeSignatures; + if (!signature_dispatch_detail::appendMetadataSignatureHash( + reader, signatureOffset, &activeSignatures, &hash)) { + return 0; + } + return hash; +} + +} // namespace nativescript + +#endif // NS_FFI_SHARED_SIGNATURE_DISPATCH_CORE_H diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h b/NativeScript/ffi/shared/bridge/Callbacks.mm similarity index 64% rename from NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h rename to NativeScript/ffi/shared/bridge/Callbacks.mm index 7df14ed01..9f9d2c0cd 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h +++ b/NativeScript/ffi/shared/bridge/Callbacks.mm @@ -1,4 +1,4 @@ -bool isObjectiveCObjectType(const NativeApiJsiType& type) { +bool isObjectiveCObjectType(const NativeApiType& type) { switch (type.kind) { case metagen::mdTypeAnyObject: case metagen::mdTypeProtocolObject: @@ -13,56 +13,58 @@ bool isObjectiveCObjectType(const NativeApiJsiType& type) { } #ifndef NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { +std::shared_ptr retainNativeApiRuntime(Runtime& runtime) { return std::shared_ptr(&runtime, [](Runtime*) {}); } #endif #ifndef NATIVESCRIPT_NATIVE_API_RUNTIME_SCOPE -class NativeApiJsiRuntimeScope final { +class NativeApiRuntimeScope final { public: - explicit NativeApiJsiRuntimeScope(Runtime&) {} + explicit NativeApiRuntimeScope(Runtime&) {} }; #endif -struct NativeApiJsiSignature { +struct NativeApiSignature { ffi_cif cif = {}; - NativeApiJsiType returnType; - std::vector argumentTypes; + NativeApiType returnType; + std::vector argumentTypes; std::vector ffiTypes; std::string selectorName; + uint64_t signatureHash = 0; + uint8_t dispatchFlags = 0; bool variadic = false; bool prepared = false; unsigned int implicitArgumentCount = 0; }; -enum class NativeApiJsiCallbackThreadPolicy { +enum class NativeApiCallbackThreadPolicy { Default, - UI, JS, + Runtime, }; -NativeApiJsiCallbackThreadPolicy readJsiCallbackThreadPolicy( +NativeApiCallbackThreadPolicy readEngineCallbackThreadPolicy( Runtime& runtime, Object& functionObject) { constexpr const char* propertyName = "__nativeScriptCallbackThread"; try { if (!functionObject.hasProperty(runtime, propertyName)) { - return NativeApiJsiCallbackThreadPolicy::Default; + return NativeApiCallbackThreadPolicy::Default; } Value policyValue = functionObject.getProperty(runtime, propertyName); if (!policyValue.isString()) { - return NativeApiJsiCallbackThreadPolicy::Default; + return NativeApiCallbackThreadPolicy::Default; } std::string policy = policyValue.asString(runtime).utf8(runtime); - if (policy == "ui") { - return NativeApiJsiCallbackThreadPolicy::UI; - } if (policy == "js") { - return NativeApiJsiCallbackThreadPolicy::JS; + return NativeApiCallbackThreadPolicy::JS; + } + if (policy == "runtime" || policy == "worklet") { + return NativeApiCallbackThreadPolicy::Runtime; } } catch (const std::exception&) { } - return NativeApiJsiCallbackThreadPolicy::Default; + return NativeApiCallbackThreadPolicy::Default; } bool selectorEndsWithNSErrorParam(const std::string& selectorName) { @@ -73,7 +75,7 @@ bool selectorEndsWithNSErrorParam(const std::string& selectorName) { suffix) == 0; } -bool isNSErrorOutJsiMethodSignature(const NativeApiJsiSignature& signature) { +bool isNSErrorOutEngineMethodSignature(const NativeApiSignature& signature) { if (signature.argumentTypes.empty() || signature.variadic || !selectorEndsWithNSErrorParam(signature.selectorName)) { return false; @@ -82,17 +84,22 @@ bool isNSErrorOutJsiMethodSignature(const NativeApiJsiSignature& signature) { return signature.argumentTypes.back().kind == metagen::mdTypePointer; } -bool isNSErrorOutJsiMethodCallback(const NativeApiJsiSignature& signature) { +bool isNSErrorOutEngineMethodCallback(const NativeApiSignature& signature) { return signature.returnType.kind == metagen::mdTypeBool && signature.implicitArgumentCount >= 2 && - isNSErrorOutJsiMethodSignature(signature); + isNSErrorOutEngineMethodSignature(signature); } -class NativeApiJsiArgumentFrame { +class NativeApiArgumentFrame { public: - explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(count) {} + explicit NativeApiArgumentFrame(size_t count) : count_(count) { + if (count_ > kInlineArgumentCount) { + heapStorage_.resize(count_); + heapValues_.resize(count_); + } + } - ~NativeApiJsiArgumentFrame() { + ~NativeApiArgumentFrame() { for (char* string : ownedCStrings_) { free(string); } @@ -103,17 +110,34 @@ class NativeApiJsiArgumentFrame { [object release]; } for (const auto& entry : temporaryRoundTripValues_) { - if (entry.first != nullptr) { - entry.first->forgetRoundTripValue(entry.second); + if (entry.bridge != nullptr && entry.runtime != nullptr) { + entry.bridge->forgetRoundTripValue(*entry.runtime, entry.native); } } ownedLifetimes_.clear(); } 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]; + if (index >= count_) { + throw std::out_of_range("Native argument index out of range."); + } + + size = std::max(size, sizeof(void*)); + if (count_ <= kInlineArgumentCount && size <= kInlineStorageSize) { + std::memset(inlineStorage_[index], 0, kInlineStorageSize); + inlineValues_[index] = inlineStorage_[index]; + return inlineValues_[index]; + } + + if (count_ <= kInlineArgumentCount) { + overflowStorage_.emplace_back(size, 0); + inlineValues_[index] = overflowStorage_.back().data(); + return inlineValues_[index]; + } + + heapStorage_[index].assign(size, 0); + heapValues_[index] = heapStorage_[index].data(); + return heapValues_[index]; } void addCString(char* value) { ownedCStrings_.push_back(value); } @@ -126,31 +150,54 @@ class NativeApiJsiArgumentFrame { return buffer; } void addObject(id value) { ownedObjects_.push_back(value); } + void retainObject(id value) { + if (value != nil) { + [value retain]; + ownedObjects_.push_back(value); + } + } void addLifetime(std::shared_ptr value) { if (value != nullptr) { ownedLifetimes_.push_back(std::move(value)); } } void rememberRoundTripValue( - const std::shared_ptr& bridge, Runtime& runtime, + const std::shared_ptr& bridge, Runtime& runtime, const void* native, const Value& value) { if (bridge == nullptr || native == nullptr) { return; } bridge->rememberRoundTripValue(runtime, native, value); - temporaryRoundTripValues_.push_back({bridge, native}); + temporaryRoundTripValues_.push_back({bridge, &runtime, native}); + } + void** values() { + if (count_ == 0) { + return nullptr; + } + return count_ <= kInlineArgumentCount ? inlineValues_ : heapValues_.data(); } - void** values() { return values_.empty() ? nullptr : values_.data(); } private: - std::vector> storage_; - std::vector values_; + static constexpr size_t kInlineArgumentCount = 8; + static constexpr size_t kInlineStorageSize = 32; + + size_t count_ = 0; + alignas(void*) unsigned char + inlineStorage_[kInlineArgumentCount][kInlineStorageSize] = {}; + void* inlineValues_[kInlineArgumentCount] = {}; + std::vector> heapStorage_; + std::vector heapValues_; + std::vector> overflowStorage_; std::vector ownedCStrings_; std::vector ownedBuffers_; std::vector ownedObjects_; std::vector> ownedLifetimes_; - std::vector, const void*>> - temporaryRoundTripValues_; + struct TemporaryRoundTripValue { + std::shared_ptr bridge; + Runtime* runtime = nullptr; + const void* native = nullptr; + }; + std::vector temporaryRoundTripValues_; }; class NativeApiMutableBuffer final : public MutableBuffer { @@ -169,24 +216,24 @@ class NativeApiMutableBuffer final : public MutableBuffer { std::vector data_; }; -void convertJsiArgument(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, +void convertEngineArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, - NativeApiJsiArgumentFrame& frame); + NativeApiArgumentFrame& frame); Value convertNativeReturnValue(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, void* value); + const std::shared_ptr& bridge, + const NativeApiType& type, void* value); Value wrapNativeFunctionPointer(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, void* pointer, + const std::shared_ptr& bridge, + const NativeApiType& type, void* pointer, bool block); -bool isObjectiveCObjectType(const NativeApiJsiType& type); +bool isObjectiveCObjectType(const NativeApiType& type); -struct NativeApiJsiBlockDescriptor { +struct NativeApiBlockDescriptor { unsigned long reserved = 0; unsigned long size = 0; void (*copyHelper)(void*, void*) = nullptr; @@ -194,29 +241,29 @@ struct NativeApiJsiBlockDescriptor { const char* signature = nullptr; }; -struct NativeApiJsiBlockLiteral { +struct NativeApiBlockLiteral { void* isa = nullptr; int flags = 0; int reserved = 0; void* invoke = nullptr; - NativeApiJsiBlockDescriptor* descriptor = nullptr; + NativeApiBlockDescriptor* 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); +constexpr int kNativeApiBlockNeedsFree = (1 << 24); +constexpr int kNativeApiBlockHasCopyDispose = (1 << 25); +constexpr int kNativeApiBlockRefCountOne = (1 << 1); +constexpr int kNativeApiBlockHasSignature = (1 << 30); -void* nativeApiJsiStackBlockIsa() { - static void* isa = dlsym(RTLD_DEFAULT, "_NSConcreteStackBlock"); +void* nativeApiEngineMallocBlockIsa() { + static void* isa = dlsym(RTLD_DEFAULT, "_NSConcreteMallocBlock"); return isa; } -void nativeApiJsiBlockCopy(void* dst, void* src); -void nativeApiJsiBlockDispose(void* src); +void nativeApiEngineBlockCopy(void* dst, void* src); +void nativeApiEngineBlockDispose(void* src); -std::string objcEncodingForJsiType(const NativeApiJsiType& type) { +std::string objcEncodingForEngineType(const NativeApiType& type) { switch (type.kind) { case metagen::mdTypeVoid: return "v"; @@ -266,7 +313,7 @@ std::string objcEncodingForJsiType(const NativeApiJsiType& type) { case metagen::mdTypeOpaquePointer: if (type.elementType != nullptr && type.elementType->kind != metagen::mdTypeVoid) { - return "^" + objcEncodingForJsiType(*type.elementType); + return "^" + objcEncodingForEngineType(*type.elementType); } return "^v"; case metagen::mdTypeStruct: @@ -276,111 +323,137 @@ std::string objcEncodingForJsiType(const NativeApiJsiType& type) { "=}"; case metagen::mdTypeArray: return "[" + std::to_string(type.arraySize) + - (type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + (type.elementType != nullptr ? objcEncodingForEngineType(*type.elementType) : std::string("?")) + "]"; case metagen::mdTypeVector: case metagen::mdTypeExtVector: case metagen::mdTypeComplex: - return type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + return type.elementType != nullptr ? objcEncodingForEngineType(*type.elementType) : "?"; default: return "?"; } } -std::string objcBlockSignatureForJsiSignature( - const NativeApiJsiSignature& signature) { - std::string encoding = objcEncodingForJsiType(signature.returnType); +std::string objcBlockSignatureForEngineSignature( + const NativeApiSignature& signature) { + std::string encoding = objcEncodingForEngineType(signature.returnType); encoding += "@?"; for (const auto& argType : signature.argumentTypes) { - encoding += objcEncodingForJsiType(argType); + encoding += objcEncodingForEngineType(argType); } return encoding; } -std::string objcMethodSignatureForJsiSignature( - const NativeApiJsiSignature& signature) { - std::string encoding = objcEncodingForJsiType(signature.returnType); +std::string objcMethodSignatureForEngineSignature( + const NativeApiSignature& signature) { + std::string encoding = objcEncodingForEngineType(signature.returnType); encoding += "@:"; for (const auto& argType : signature.argumentTypes) { - encoding += objcEncodingForJsiType(argType); + encoding += objcEncodingForEngineType(argType); } return encoding; } -[[noreturn]] void throwNativeApiJsiCallbackException( +[[noreturn]] void throwNativeApiCallbackException( const std::string& message) { NSString* reason = [NSString stringWithUTF8String:message.c_str()]; - @throw [NSException exceptionWithName:@"NativeScriptJSICallbackException" + @throw [NSException exceptionWithName:@"NativeScriptEngineCallbackException" reason:reason userInfo:nil]; } -class NativeApiJsiCallback; +class NativeApiCallback; -void nativeApiJsiCallbackTrampoline(ffi_cif* cif, void* ret, void* args[], +void nativeApiEngineCallbackTrampoline(ffi_cif* cif, void* ret, void* args[], void* data); -std::atomic gActiveNativeThreadJsiCallbacks{0}; +std::atomic gActiveNativeThreadEngineCallbacks{0}; + +// A callback can outlive the scope in which its function argument was created +// (e.g. a block invoked asynchronously). Round-trip the function through the +// engine value copy constructor so any scope-bound/borrowed handle is promoted +// to a persistent one before it is stored. +Function persistentEngineFunction(Runtime& runtime, const Function& function) { + Value shared(runtime, function); + Value persistent(runtime, shared); + return persistent.asObject(runtime).asFunction(runtime); +} -class NativeApiJsiCallback final - : public std::enable_shared_from_this { +class NativeApiCallback final + : public std::enable_shared_from_this { public: - NativeApiJsiCallback(Runtime& runtime, - std::shared_ptr bridge, - std::shared_ptr signature, + NativeApiCallback(Runtime& runtime, + std::shared_ptr bridge, + std::shared_ptr signature, Function function, bool block, - NativeApiJsiCallbackThreadPolicy threadPolicy = - NativeApiJsiCallbackThreadPolicy::Default, - bool bindThis = false) - : runtimeOwner_(retainNativeApiJsiRuntime(runtime)), + NativeApiCallbackThreadPolicy threadPolicy = + NativeApiCallbackThreadPolicy::Default, + bool bindThis = false, + uintptr_t roundTripValidationKey = 0) + : runtimeOwner_(retainNativeApiRuntime(runtime)), runtime_(runtimeOwner_.get()), bridge_(std::move(bridge)), signature_(std::move(signature)), - function_(std::make_shared(std::move(function))), + function_(std::make_shared( + persistentEngineFunction(runtime, function))), block_(block), threadPolicy_(threadPolicy), - bindThis_(bindThis) { + bindThis_(bindThis), + roundTripValidationKey_(roundTripValidationKey) { 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."); + throw JSError(runtime, + "Unable to allocate native callback."); } ffi_status status = ffi_prep_closure_loc( - closure_, &signature_->cif, nativeApiJsiCallbackTrampoline, this, + closure_, &signature_->cif, nativeApiEngineCallbackTrampoline, 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."); + throw JSError(runtime, + "Unable to prepare native callback."); } if (block_) { - blockSignature_ = objcBlockSignatureForJsiSignature(*signature_); - descriptor_ = std::make_unique(); + blockSignature_ = objcBlockSignatureForEngineSignature(*signature_); + descriptor_ = std::make_unique(); descriptor_->reserved = 0; - descriptor_->size = sizeof(NativeApiJsiBlockLiteral); - descriptor_->copyHelper = nativeApiJsiBlockCopy; - descriptor_->disposeHelper = nativeApiJsiBlockDispose; + descriptor_->size = sizeof(NativeApiBlockLiteral); + descriptor_->copyHelper = nativeApiEngineBlockCopy; + descriptor_->disposeHelper = nativeApiEngineBlockDispose; descriptor_->signature = blockSignature_.c_str(); - blockLiteral_ = std::make_unique(); - blockLiteral_->isa = nativeApiJsiStackBlockIsa(); - blockLiteral_->flags = kNativeApiJsiBlockHasCopyDispose | - kNativeApiJsiBlockHasSignature; + blockLiteral_ = static_cast( + calloc(1, sizeof(NativeApiBlockLiteral))); + if (blockLiteral_ == nullptr) { + throw JSError(runtime, "Unable to allocate native block callback."); + } + void* blockIsa = nativeApiEngineMallocBlockIsa(); + if (blockIsa == nullptr) { + free(blockLiteral_); + blockLiteral_ = nullptr; + throw JSError(runtime, + "Objective-C malloc block runtime is unavailable."); + } + blockLiteral_->isa = blockIsa; + blockLiteral_->flags = kNativeApiBlockNeedsFree | + kNativeApiBlockHasCopyDispose | + kNativeApiBlockRefCountOne | + kNativeApiBlockHasSignature; blockLiteral_->invoke = executable_; blockLiteral_->descriptor = descriptor_.get(); blockLiteral_->callback = this; } } - ~NativeApiJsiCallback() { + ~NativeApiCallback() { if (closure_ != nullptr) { ffi_closure_free(closure_); closure_ = nullptr; @@ -390,11 +463,18 @@ class NativeApiJsiCallback final void* functionPointer() const { return block_ && blockLiteral_ != nullptr - ? static_cast(blockLiteral_.get()) + ? static_cast(blockLiteral_) : executable_; } - const NativeApiJsiSignature& signature() const { return *signature_; } + const NativeApiSignature& signature() const { return *signature_; } + + void retainInitialBlockLifetime( + std::shared_ptr lifetime) { + if (block_) { + initialBlockLifetime_ = std::move(lifetime); + } + } void retainBlockCopy(const void* blockPointer) { if (!block_) { @@ -404,7 +484,8 @@ class NativeApiJsiCallback final if (bridge_ != nullptr && runtime_ != nullptr && function_ != nullptr && blockPointer != nullptr) { bridge_->rememberRoundTripValue(*runtime_, blockPointer, - Value(*runtime_, *function_)); + Value(*runtime_, *function_), false, + roundTripValidationKey_); } std::lock_guard lock(retainedBlockCopiesMutex_); retainedBlockCopies_.push_back({blockPointer, std::move(self)}); @@ -414,66 +495,107 @@ class NativeApiJsiCallback final if (!block_) { return false; } - std::shared_ptr keepAlive; - try { - keepAlive = shared_from_this(); - } catch (const std::bad_weak_ptr&) { + + bool canRelease = false; + { + std::lock_guard lock(retainedBlockCopiesMutex_); + auto it = retainedBlockCopies_.end(); + if (blockPointer != nullptr) { + it = std::find_if( + retainedBlockCopies_.begin(), retainedBlockCopies_.end(), + [blockPointer](const RetainedBlockCopy& retained) { + return retained.blockPointer == blockPointer; + }); + } + canRelease = + it != retainedBlockCopies_.end() || blockPointer == blockLiteral_; + } + // Forgetting the round-trip value touches the JS engine global/context. + // Block disposal can run during an autorelease-pool drain on an arbitrary + // thread (e.g. an NSOperationQueue worker). Keep the retained block entry + // in place until the JS-thread task runs so the callback and its engine + // function are also destroyed on the JS thread. + if (!canRelease) { return false; } - std::lock_guard lock(retainedBlockCopiesMutex_); - auto it = retainedBlockCopies_.end(); - if (blockPointer != nullptr) { - it = std::find_if( - retainedBlockCopies_.begin(), retainedBlockCopies_.end(), - [blockPointer](const RetainedBlockCopy& retained) { - return retained.blockPointer == blockPointer; - }); - } - if (it != retainedBlockCopies_.end()) { - if (bridge_ != nullptr && it->blockPointer != nullptr) { - bridge_->forgetRoundTripValue(it->blockPointer); + + auto bridge = bridge_; + auto* runtime = runtime_; + auto runtimeOwner = runtimeOwner_; + auto releaseOnJS = [this, bridge, runtime, runtimeOwner, blockPointer]() { + std::shared_ptr keepAlive; + try { + keepAlive = shared_from_this(); + } catch (const std::bad_weak_ptr&) { + return; } - retainedBlockCopies_.erase(it); - return true; + + const void* pointerToForget = nullptr; + { + std::lock_guard lock(retainedBlockCopiesMutex_); + auto it = retainedBlockCopies_.end(); + if (blockPointer != nullptr) { + it = std::find_if( + retainedBlockCopies_.begin(), retainedBlockCopies_.end(), + [blockPointer](const RetainedBlockCopy& retained) { + return retained.blockPointer == blockPointer; + }); + } + if (it != retainedBlockCopies_.end()) { + pointerToForget = it->blockPointer; + retainedBlockCopies_.erase(it); + } else if (blockPointer == blockLiteral_) { + pointerToForget = blockPointer; + blockLiteral_ = nullptr; + initialBlockLifetime_.reset(); + } + } + + if (bridge != nullptr && runtime != nullptr && + pointerToForget != nullptr) { + NativeApiRuntimeScope runtimeScope(*runtime); + bridge->forgetRoundTripValue(*runtime, pointerToForget); + } + }; + + if (bridge == nullptr) { + releaseOnJS(); + } else if (const auto& asyncInvoker = + bridge->jsThreadAsyncCallbackInvoker()) { + asyncInvoker(std::move(releaseOnJS)); + } else if (auto scheduler = bridge->scheduler()) { + scheduler->invokeOnJS(std::move(releaseOnJS)); + } else if (std::this_thread::get_id() == bridge->jsThreadId()) { + releaseOnJS(); + } else if (const auto& invoker = bridge->jsThreadCallbackInvoker()) { + invoker(std::move(releaseOnJS)); + } else { + releaseOnJS(); } - return false; + return true; } void invoke(void* ret, void* args[]) { if (runtime_ == nullptr || function_ == nullptr || signature_ == nullptr) { - throwNativeApiJsiCallbackException("Invalid JSI callback."); + throwNativeApiCallbackException("Invalid callback."); } std::string error; auto call = [&]() { invokeOnCurrentThread(ret, args, &error); }; const auto& nativeCallbackInvoker = bridge_->nativeCallbackInvoker(); + const auto& runtimeCallbackInvoker = bridge_->runtimeCallbackInvoker(); const auto& jsThreadCallbackInvoker = bridge_->jsThreadCallbackInvoker(); bool currentThreadIsJs = std::this_thread::get_id() == bridge_->jsThreadId(); auto callOnNativeCallerThread = [&]() { - ScopedNativeCallerThreadJsiCallback callbackScope; + ScopedNativeCallerThreadEngineCallback callbackScope; if (nativeCallbackInvoker) { nativeCallbackInvoker(call); } else { call(); } }; - auto callOnUIThread = [&]() { - auto runOnUIThread = [&]() { - bool previous = gExecutingDispatchedUINativeCall; - gExecutingDispatchedUINativeCall = true; - callOnNativeCallerThread(); - gExecutingDispatchedUINativeCall = previous; - }; - if ([NSThread isMainThread]) { - runOnUIThread(); - } else { - dispatch_sync(dispatch_get_main_queue(), ^{ - runOnUIThread(); - }); - } - }; auto callOnJSThread = [&]() { if (currentThreadIsJs) { call(); @@ -494,73 +616,103 @@ class NativeApiJsiCallback final } error = "Native callback was invoked off the JS thread without a JS scheduler."; }; + auto callOnRuntimeThread = [&]() { + if (currentThreadIsJs) { + call(); + return; + } + if (runtimeCallbackInvoker) { + runtimeCallbackInvoker(call); + return; + } + error = "Native callback was invoked off its owning runtime thread without a runtime scheduler."; + }; - if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::UI) { - callOnUIThread(); + if (threadPolicy_ == NativeApiCallbackThreadPolicy::JS) { + callOnJSThread(); if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } return; } - if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::JS) { - callOnJSThread(); + if (threadPolicy_ == NativeApiCallbackThreadPolicy::Runtime) { + callOnRuntimeThread(); if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } return; } bool returnsVoid = signature_->returnType.kind == metagen::mdTypeVoid; - bool activeSynchronousNativeInvocation = - gActiveSynchronousNativeInvocationDepth.load( - std::memory_order_acquire) > 0; bool nativeCallerThreadCallbacks = bridge_->invokeCallbacksOnNativeCallerThread(); - bool nativeCallerThreadCallback = - nativeCallerThreadCallbacks && !currentThreadIsJs && - (block_ || bindThis_ || - (activeSynchronousNativeInvocation && !returnsVoid)); bool direct = currentThreadIsJs || - gExecutingDispatchedUINativeCall || - gSynchronousNativeInvocationDepth > 0 || - nativeCallerThreadCallback || - (nativeCallerThreadCallbacks && !nativeCallbackInvoker && - activeSynchronousNativeInvocation); + gSynchronousNativeInvocationDepth > 0; bool waitForNativeThreadCallback = currentThreadIsJs && nativeCallbackInvoker && - gActiveNativeThreadJsiCallbacks.load(std::memory_order_acquire) > 0; - if (direct && !waitForNativeThreadCallback) { - if (nativeCallerThreadCallback) { - callOnNativeCallerThread(); - } else { - call(); + gActiveNativeThreadEngineCallbacks.load(std::memory_order_acquire) > 0; + auto dispatchZeroArgVoidBlockAsync = [&]() -> bool { + if (currentThreadIsJs || !returnsVoid || !block_ || + !signature_->argumentTypes.empty()) { + return false; + } + + std::shared_ptr keepAlive; + try { + keepAlive = shared_from_this(); + } catch (const std::bad_weak_ptr&) { + return false; + } + + auto asyncCall = [keepAlive = std::move(keepAlive)]() mutable { + std::string asyncError; + keepAlive->invokeOnCurrentThread(nullptr, nullptr, &asyncError); + if (!asyncError.empty()) { + recordNativeCallbackException(asyncError); + } + }; + + const auto& asyncInvoker = bridge_->jsThreadAsyncCallbackInvoker(); + if (asyncInvoker) { + asyncInvoker(std::move(asyncCall)); + return true; } - } else if (!currentThreadIsJs && !nativeCallerThreadCallbacks) { + if (auto scheduler = bridge_->scheduler()) { + scheduler->invokeOnJS(std::move(asyncCall)); + return true; + } + return false; + }; + + if (nativeCallerThreadCallbacks && !currentThreadIsJs) { + callOnNativeCallerThread(); + } else if (dispatchZeroArgVoidBlockAsync()) { + return; + } else if (direct && !waitForNativeThreadCallback) { + call(); + } else if (!currentThreadIsJs) { callOnJSThread(); - } else if (!currentThreadIsJs && returnsVoid && block_ && - jsThreadCallbackInvoker) { - jsThreadCallbackInvoker(call); } else if (nativeCallbackInvoker) { bool nativeThreadCallback = !currentThreadIsJs; if (nativeThreadCallback) { - gActiveNativeThreadJsiCallbacks.fetch_add(1, + gActiveNativeThreadEngineCallbacks.fetch_add(1, std::memory_order_acq_rel); } try { nativeCallbackInvoker(call); } catch (...) { if (nativeThreadCallback) { - gActiveNativeThreadJsiCallbacks.fetch_sub( + gActiveNativeThreadEngineCallbacks.fetch_sub( 1, std::memory_order_acq_rel); } throw; } if (nativeThreadCallback) { - gActiveNativeThreadJsiCallbacks.fetch_sub(1, + gActiveNativeThreadEngineCallbacks.fetch_sub(1, std::memory_order_acq_rel); } } else if (auto scheduler = bridge_->scheduler()) { @@ -576,7 +728,7 @@ class NativeApiJsiCallback final if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } } @@ -584,7 +736,7 @@ class NativeApiJsiCallback final private: void invokeOnCurrentThread(void* ret, void* args[], std::string* error) { try { - NativeApiJsiRuntimeScope runtimeScope(*runtime_); + NativeApiRuntimeScope runtimeScope(*runtime_); size_t nativeArgOffset = signature_->implicitArgumentCount; std::vector jsArgs; jsArgs.reserve(signature_->argumentTypes.size()); @@ -618,11 +770,12 @@ class NativeApiJsiCallback final static_cast(jsArgs.size())); } storeReturnValue(result, ret); - if (std::this_thread::get_id() == bridge_->jsThreadId()) { + if (std::this_thread::get_id() == bridge_->jsThreadId() && + gSynchronousNativeInvocationDepth == 0) { runtime_->drainMicrotasks(); } } catch (const std::exception& exception) { - if (isNSErrorOutJsiMethodCallback(*signature_)) { + if (isNSErrorOutEngineMethodCallback(*signature_)) { zeroReturnValue(ret); populateNSErrorOutArgument(args, exception.what()); return; @@ -632,13 +785,13 @@ class NativeApiJsiCallback final } zeroReturnValue(ret); } catch (...) { - if (isNSErrorOutJsiMethodCallback(*signature_)) { + if (isNSErrorOutEngineMethodCallback(*signature_)) { zeroReturnValue(ret); - populateNSErrorOutArgument(args, "Unknown exception in native JSI callback."); + populateNSErrorOutArgument(args, "Unknown exception in native callback."); return; } if (error != nullptr) { - *error = "Unknown exception in native JSI callback."; + *error = "Unknown exception in native callback."; } zeroReturnValue(ret); } @@ -706,8 +859,8 @@ class NativeApiJsiCallback final return; } - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(*runtime_, bridge_, returnType, result, ret, frame); + NativeApiArgumentFrame frame(1); + convertEngineArgument(*runtime_, bridge_, returnType, result, ret, frame); if (isObjectiveCObjectType(returnType)) { id object = *static_cast(ret); if (object != nil) { @@ -719,53 +872,55 @@ class NativeApiJsiCallback final std::shared_ptr runtimeOwner_; Runtime* runtime_ = nullptr; - std::shared_ptr bridge_; - std::shared_ptr signature_; + std::shared_ptr bridge_; + std::shared_ptr signature_; std::shared_ptr function_; bool block_ = false; - NativeApiJsiCallbackThreadPolicy threadPolicy_ = - NativeApiJsiCallbackThreadPolicy::Default; + NativeApiCallbackThreadPolicy threadPolicy_ = + NativeApiCallbackThreadPolicy::Default; bool bindThis_ = false; + uintptr_t roundTripValidationKey_ = 0; ffi_closure* closure_ = nullptr; void* executable_ = nullptr; std::string blockSignature_; - std::unique_ptr descriptor_; - std::unique_ptr blockLiteral_; + std::unique_ptr descriptor_; + NativeApiBlockLiteral* blockLiteral_ = nullptr; + std::shared_ptr initialBlockLifetime_; struct RetainedBlockCopy { const void* blockPointer = nullptr; - std::shared_ptr lifetime; + std::shared_ptr lifetime; }; std::mutex retainedBlockCopiesMutex_; std::vector retainedBlockCopies_; }; -void nativeApiJsiBlockCopy(void* dst, void* src) { - auto* dstBlock = static_cast(dst); - auto* srcBlock = static_cast(src); +void nativeApiEngineBlockCopy(void* dst, void* src) { + auto* dstBlock = static_cast(dst); + auto* srcBlock = static_cast(src); if (dstBlock == nullptr || srcBlock == nullptr || srcBlock->callback == nullptr) { return; } dstBlock->callback = srcBlock->callback; - static_cast(srcBlock->callback) + static_cast(srcBlock->callback) ->retainBlockCopy(dstBlock); } -void nativeApiJsiBlockDispose(void* src) { - auto* block = static_cast(src); +void nativeApiEngineBlockDispose(void* src) { + auto* block = static_cast(src); if (block == nullptr || block->callback == nullptr) { return; } bool released = - static_cast(block->callback)->releaseBlockCopy(block); + static_cast(block->callback)->releaseBlockCopy(block); if (released) { block->callback = nullptr; } } -void nativeApiJsiCallbackTrampoline(ffi_cif*, void* ret, void* args[], +void nativeApiEngineCallbackTrampoline(ffi_cif*, void* ret, void* args[], void* data) { - auto callback = static_cast(data); + auto callback = static_cast(data); if (callback == nullptr) { return; } @@ -776,14 +931,14 @@ void nativeApiJsiCallbackTrampoline(ffi_cif*, void* ret, void* args[], exception.description != nil ? exception.description.UTF8String : nullptr; std::string message = description != nullptr ? description - : "Objective-C exception in native JSI callback."; + : "Objective-C exception in native callback."; if (!recordNativeCallbackException(message)) { @throw; } } } -size_t nativeSizeForType(const NativeApiJsiType& type) { +size_t nativeSizeForType(const NativeApiType& type) { switch (type.kind) { case metagen::mdTypeStruct: if (type.aggregateInfo != nullptr) { @@ -818,7 +973,7 @@ size_t nativeSizeForType(const NativeApiJsiType& type) { return sizeof(void*); } -Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { +Value signedInteger64ToEngineValue(Runtime& runtime, int64_t value) { constexpr int64_t maxSafeInteger = 9007199254740991LL; constexpr int64_t minSafeInteger = -9007199254740991LL; if (value >= minSafeInteger && value <= maxSafeInteger) { @@ -827,7 +982,7 @@ Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { return BigInt::fromInt64(runtime, value); } -Value unsignedInteger64ToJsiValue(Runtime& runtime, uint64_t value) { +Value unsignedInteger64ToEngineValue(Runtime& runtime, uint64_t value) { constexpr uint64_t maxSafeInteger = 9007199254740991ULL; if (value <= maxSafeInteger) { return static_cast(value); @@ -873,7 +1028,7 @@ bool parseBigIntToUintptr(Runtime& runtime, const BigInt& bigint, address); } -bool readJsiBuffer(Runtime& runtime, const Object& object, const uint8_t** data, +bool readEngineBuffer(Runtime& runtime, const Object& object, const uint8_t** data, size_t* byteLength) { if (data == nullptr || byteLength == nullptr) { return false; @@ -936,7 +1091,7 @@ size_t alignUp(size_t value, size_t alignment) { return ((value + alignment - 1) / alignment) * alignment; } -ffi_type* ffiTypeForJsiKind(MDTypeKind kind) { +ffi_type* ffiTypeForEngineKind(MDTypeKind kind) { switch (kind) { case metagen::mdTypeChar: return &ffi_type_sint8; @@ -983,23 +1138,23 @@ ffi_type* ffiTypeForJsiKind(MDTypeKind kind) { } } -bool isSupportedJsiKind(MDTypeKind kind) { +bool isSupportedEngineKind(MDTypeKind kind) { switch (kind) { default: - return ffiTypeForJsiKind(kind) != nullptr; + return ffiTypeForEngineKind(kind) != nullptr; } } -void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, +void skipMetadataEngineTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, MDTypeKind kind); -void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset) { +void skipMetadataEngineType(MDMetadataReader* metadata, MDSectionOffset* offset) { MDTypeKind kind = stripTypeFlags(metadata->getTypeKind(*offset)); *offset += sizeof(MDTypeKind); - skipMetadataJsiTypePayload(metadata, offset, kind); + skipMetadataEngineTypePayload(metadata, offset, kind); } -void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, +void skipMetadataEngineTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, MDTypeKind kind) { switch (kind) { case metagen::mdTypeClassObject: { @@ -1027,13 +1182,13 @@ void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* off case metagen::mdTypeExtVector: case metagen::mdTypeComplex: *offset += sizeof(uint16_t); - skipMetadataJsiType(metadata, offset); + skipMetadataEngineType(metadata, offset); break; case metagen::mdTypeStruct: *offset += sizeof(MDSectionOffset); break; case metagen::mdTypePointer: - skipMetadataJsiType(metadata, offset); + skipMetadataEngineType(metadata, offset); break; case metagen::mdTypeBlock: case metagen::mdTypeFunctionPointer: @@ -1044,14 +1199,14 @@ void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* off } } -NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, +NativeApiType parseMetadataEngineType(MDMetadataReader* metadata, MDSectionOffset* offset, - NativeApiJsiBridge* bridge) { + NativeApiBridge* bridge) { MDTypeKind rawKind = metadata->getTypeKind(*offset); MDTypeKind kind = stripTypeFlags(rawKind); *offset += sizeof(MDTypeKind); - NativeApiJsiType type; + NativeApiType type; type.kind = kind; switch (kind) { @@ -1059,9 +1214,9 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, type.arraySize = metadata->getArraySize(*offset); *offset += sizeof(uint16_t); type.elementType = - std::make_shared( - parseMetadataJsiType(metadata, offset, bridge)); - auto ffiOwner = std::make_shared(); + std::make_shared( + parseMetadataEngineType(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 @@ -1081,9 +1236,9 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, type.arraySize = metadata->getArraySize(*offset); *offset += sizeof(uint16_t); type.elementType = - std::make_shared( - parseMetadataJsiType(metadata, offset, bridge)); - auto ffiOwner = std::make_shared(); + std::make_shared( + parseMetadataEngineType(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; @@ -1143,8 +1298,8 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, } case metagen::mdTypePointer: type.elementType = - std::make_shared( - parseMetadataJsiType(metadata, offset, bridge)); + std::make_shared( + parseMetadataEngineType(metadata, offset, bridge)); type.ffiType = &ffi_type_pointer; type.supported = true; return type; @@ -1179,12 +1334,12 @@ NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, break; } - type.ffiType = ffiTypeForJsiKind(kind); - type.supported = type.ffiType != nullptr && isSupportedJsiKind(kind); + type.ffiType = ffiTypeForEngineKind(kind); + type.supported = type.ffiType != nullptr && isSupportedEngineKind(kind); return type; } -std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( +std::shared_ptr NativeApiBridge::aggregateInfoFor( MDSectionOffset aggregateOffset, bool isUnion) { if (metadata_ == nullptr || aggregateOffset == MD_SECTION_OFFSET_NULL) { return nullptr; @@ -1195,14 +1350,14 @@ std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( return cached->second; } - auto info = std::make_shared(); + 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(); + auto ffiOwner = std::make_shared(); ffiOwner->elements.push_back(&ffi_type_pointer); ffiOwner->finalize(); info->ffi = ffiOwner; @@ -1228,18 +1383,18 @@ std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( break; } - NativeApiJsiAggregateField field; + NativeApiAggregateField 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); + field.type = parseMetadataEngineType(metadata_.get(), &offset, this); info->fields.push_back(std::move(field)); } - auto ffiOwner = std::make_shared(); + auto ffiOwner = std::make_shared(); if (isUnion) { ffi_type* largest = &ffi_type_uint8; size_t largestSize = 0; @@ -1267,7 +1422,7 @@ std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( return info; } -ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { +ffi_type* ffiTypeForEngineArgument(const NativeApiType& type) { switch (type.kind) { case metagen::mdTypeArray: return &ffi_type_pointer; @@ -1276,16 +1431,20 @@ ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { } } -std::optional parseMetadataJsiSignature( +std::optional parseMetadataEngineSignature( MDMetadataReader* metadata, MDSectionOffset signatureOffset, - unsigned int implicitArgumentCount, NativeApiJsiBridge* bridge, + unsigned int implicitArgumentCount, NativeApiBridge* bridge, bool returnOwned = false) { if (metadata == nullptr || signatureOffset == MD_SECTION_OFFSET_NULL) { return std::nullopt; } - NativeApiJsiSignature signature; + NativeApiSignature signature; signature.implicitArgumentCount = implicitArgumentCount; + signature.signatureHash = isPreparedGeneratedDispatchRequired() + ? metadataSignatureHash(metadata, signatureOffset) + : 0; + signature.dispatchFlags = returnOwned ? 1 : 0; MDSectionOffset offset = signatureOffset; MDTypeKind returnKind = metadata->getTypeKind(offset); @@ -1294,14 +1453,14 @@ std::optional parseMetadataJsiSignature( (returnKindRaw & static_cast(metagen::mdTypeFlagNext)) != 0; signature.variadic = (returnKindRaw & static_cast(metagen::mdTypeFlagVariadic)) != 0; - signature.returnType = parseMetadataJsiType(metadata, &offset, bridge); + signature.returnType = parseMetadataEngineType(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.argumentTypes.push_back(parseMetadataEngineType(metadata, &offset, bridge)); } signature.ffiTypes.reserve(signature.argumentTypes.size() + @@ -1310,7 +1469,7 @@ std::optional parseMetadataJsiSignature( signature.ffiTypes.push_back(&ffi_type_pointer); } for (const auto& argType : signature.argumentTypes) { - signature.ffiTypes.push_back(ffiTypeForJsiArgument(argType)); + signature.ffiTypes.push_back(ffiTypeForEngineArgument(argType)); } ffi_status status = ffi_prep_cif( @@ -1323,6 +1482,31 @@ std::optional parseMetadataJsiSignature( return signature; } +bool prepareEngineCallbackSignature(NativeApiSignature* signature) { + if (signature == nullptr) { + return false; + } + + signature->ffiTypes.clear(); + signature->ffiTypes.reserve(signature->argumentTypes.size() + + signature->implicitArgumentCount); + for (unsigned int i = 0; i < signature->implicitArgumentCount; i++) { + signature->ffiTypes.push_back(&ffi_type_pointer); + } + for (const auto& argType : signature->argumentTypes) { + signature->ffiTypes.push_back(ffiTypeForEngineArgument(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->prepared; +} + const char* skipObjCTypeQualifiers(const char* encoding) { while (encoding != nullptr && *encoding != '\0' && std::strchr("rnNoORV", *encoding) != nullptr) { @@ -1331,6 +1515,13 @@ const char* skipObjCTypeQualifiers(const char* encoding) { return encoding; } +const char* skipObjCTypeFrameOffset(const char* encoding) { + while (encoding != nullptr && *encoding >= '0' && *encoding <= '9') { + encoding++; + } + return encoding; +} + const char* skipObjCTypeFieldName(const char* encoding, std::string* name) { if (encoding == nullptr || *encoding != '"') { return encoding; @@ -1386,7 +1577,7 @@ std::vector knownObjCAggregateFieldNames( } const NativeApiSymbol* findObjCAggregateSymbol( - NativeApiJsiBridge* bridge, const std::string& name, bool isUnion) { + NativeApiBridge* bridge, const std::string& name, bool isUnion) { if (bridge == nullptr || name.empty()) { return nullptr; } @@ -1424,7 +1615,7 @@ const NativeApiSymbol* findObjCAggregateSymbol( } void applyObjCEncodingSizeAndAlignment(const char* encoding, - NativeApiJsiFfiType* ffiType, + NativeApiFfiType* ffiType, uint16_t* sizeOut = nullptr) { if (encoding == nullptr || ffiType == nullptr) { return; @@ -1445,15 +1636,15 @@ void applyObjCEncodingSizeAndAlignment(const char* encoding, } } -NativeApiJsiType parseObjCEncodedJsiType( - const char* encoding, NativeApiJsiBridge* bridge = nullptr, +NativeApiType parseObjCEncodedEngineType( + const char* encoding, NativeApiBridge* bridge = nullptr, const char** endEncoding = nullptr); -bool unsupportedJsiType(const NativeApiJsiType& type); +bool unsupportedEngineType(const NativeApiType& type); -NativeApiJsiType parseObjCEncodedAggregateJsiType( - const char* encoding, NativeApiJsiBridge* bridge, const char** endEncoding) { - NativeApiJsiType type; +NativeApiType parseObjCEncodedAggregateEngineType( + const char* encoding, NativeApiBridge* bridge, const char** endEncoding) { + NativeApiType type; type.kind = metagen::mdTypeStruct; const bool isUnion = *encoding == '('; @@ -1491,7 +1682,7 @@ NativeApiJsiType parseObjCEncodedAggregateJsiType( return type; } - auto info = std::make_shared(); + auto info = std::make_shared(); info->name = aggregateName; info->isUnion = isUnion; info->offset = MD_SECTION_OFFSET_NULL; @@ -1504,13 +1695,13 @@ NativeApiJsiType parseObjCEncodedAggregateJsiType( size_t maxFieldSize = 0; size_t fieldIndex = 0; while (*cursor != '\0' && *cursor != close) { - NativeApiJsiAggregateField field; + NativeApiAggregateField field; std::string encodedFieldName; cursor = skipObjCTypeFieldName(cursor, &encodedFieldName); const char* fieldStart = cursor; const char* fieldEnd = cursor; - field.type = parseObjCEncodedJsiType(cursor, bridge, &fieldEnd); - if (fieldEnd == fieldStart || unsupportedJsiType(field.type)) { + field.type = parseObjCEncodedEngineType(cursor, bridge, &fieldEnd); + if (fieldEnd == fieldStart || unsupportedEngineType(field.type)) { type.supported = false; type.ffiType = nullptr; if (endEncoding != nullptr) { @@ -1559,7 +1750,7 @@ NativeApiJsiType parseObjCEncodedAggregateJsiType( info->fields[i].name = knownNames[i]; } - auto ffiOwner = std::make_shared(); + auto ffiOwner = std::make_shared(); if (isUnion) { ffi_type* largest = &ffi_type_uint8; size_t largestSize = 0; @@ -1599,9 +1790,9 @@ NativeApiJsiType parseObjCEncodedAggregateJsiType( return type; } -NativeApiJsiType parseObjCEncodedArrayJsiType( - const char* encoding, NativeApiJsiBridge* bridge, const char** endEncoding) { - NativeApiJsiType type; +NativeApiType parseObjCEncodedArrayEngineType( + const char* encoding, NativeApiBridge* bridge, const char** endEncoding) { + NativeApiType type; type.kind = metagen::mdTypeArray; const char* cursor = encoding + 1; @@ -1615,8 +1806,8 @@ NativeApiJsiType parseObjCEncodedArrayJsiType( type.arraySize = count; const char* elementEnd = cursor; - type.elementType = std::make_shared( - parseObjCEncodedJsiType(cursor, bridge, &elementEnd)); + type.elementType = std::make_shared( + parseObjCEncodedEngineType(cursor, bridge, &elementEnd)); cursor = elementEnd; if (*cursor == ']') { cursor++; @@ -1625,7 +1816,7 @@ NativeApiJsiType parseObjCEncodedArrayJsiType( *endEncoding = cursor; } - auto ffiOwner = std::make_shared(); + auto ffiOwner = std::make_shared(); ffi_type* elementFfiType = type.elementType != nullptr && type.elementType->ffiType != nullptr ? type.elementType->ffiType @@ -1645,10 +1836,10 @@ NativeApiJsiType parseObjCEncodedArrayJsiType( return type; } -NativeApiJsiType parseObjCEncodedJsiType( - const char* encoding, NativeApiJsiBridge* bridge, const char** endEncoding) { +NativeApiType parseObjCEncodedEngineType( + const char* encoding, NativeApiBridge* bridge, const char** endEncoding) { encoding = skipObjCTypeQualifiers(encoding); - NativeApiJsiType type; + NativeApiType type; if (encoding == nullptr || *encoding == '\0') { type.kind = metagen::mdTypePointer; @@ -1660,7 +1851,7 @@ NativeApiJsiType parseObjCEncodedJsiType( } auto finishPrimitive = [&](const char* end) { - type.ffiType = ffiTypeForJsiKind(type.kind); + type.ffiType = ffiTypeForEngineKind(type.kind); type.supported = type.ffiType != nullptr; if (endEncoding != nullptr) { *endEncoding = end; @@ -1745,8 +1936,8 @@ NativeApiJsiType parseObjCEncodedJsiType( type.kind = metagen::mdTypePointer; { const char* elementEnd = encoding + 1; - type.elementType = std::make_shared( - parseObjCEncodedJsiType(encoding + 1, bridge, &elementEnd)); + type.elementType = std::make_shared( + parseObjCEncodedEngineType(encoding + 1, bridge, &elementEnd)); type.ffiType = &ffi_type_pointer; type.supported = true; if (elementEnd == encoding + 1 && encoding[1] != '\0') { @@ -1759,9 +1950,9 @@ NativeApiJsiType parseObjCEncodedJsiType( return type; case '{': case '(': - return parseObjCEncodedAggregateJsiType(encoding, bridge, endEncoding); + return parseObjCEncodedAggregateEngineType(encoding, bridge, endEncoding); case '[': - return parseObjCEncodedArrayJsiType(encoding, bridge, endEncoding); + return parseObjCEncodedArrayEngineType(encoding, bridge, endEncoding); case 'b': { type.kind = metagen::mdTypeUInt; const char* cursor = encoding + 1; @@ -1781,17 +1972,59 @@ NativeApiJsiType parseObjCEncodedJsiType( return finishPrimitive(encoding + 1); } -std::optional parseObjCMethodJsiSignature( - Method method, NativeApiJsiBridge* bridge = nullptr) { +std::optional parseObjCCallbackEngineSignature( + const std::string& encodingString, bool block, NativeApiBridge* bridge) { + const char* cursor = skipObjCTypeQualifiers(encodingString.c_str()); + if (cursor == nullptr || *cursor == '\0') { + return std::nullopt; + } + + NativeApiSignature signature; + signature.implicitArgumentCount = block ? 1 : 0; + + const char* returnEnd = cursor; + signature.returnType = parseObjCEncodedEngineType(cursor, bridge, &returnEnd); + if (returnEnd == cursor) { + return std::nullopt; + } + cursor = skipObjCTypeFrameOffset(returnEnd); + + if (block) { + const char* blockSelf = skipObjCTypeQualifiers(cursor); + if (blockSelf != nullptr && blockSelf[0] == '@' && blockSelf[1] == '?') { + cursor = skipObjCTypeFrameOffset(blockSelf + 2); + } + } + + while (cursor != nullptr && *cursor != '\0') { + const char* argStart = skipObjCTypeQualifiers(cursor); + if (argStart == nullptr || *argStart == '\0') { + break; + } + const char* argEnd = argStart; + NativeApiType argType = parseObjCEncodedEngineType(argStart, bridge, &argEnd); + if (argEnd == argStart) { + return std::nullopt; + } + signature.argumentTypes.push_back(std::move(argType)); + cursor = skipObjCTypeFrameOffset(argEnd); + } + + prepareEngineCallbackSignature(&signature); + return signature; +} + +std::optional parseObjCMethodEngineSignature( + Method method, NativeApiBridge* bridge = nullptr) { if (method == nullptr) { return std::nullopt; } - NativeApiJsiSignature signature; + NativeApiSignature signature; signature.implicitArgumentCount = 2; char* returnEncoding = method_copyReturnType(method); - signature.returnType = parseObjCEncodedJsiType(returnEncoding, bridge); + signature.returnType = parseObjCEncodedEngineType(returnEncoding, bridge); if (returnEncoding != nullptr) { free(returnEncoding); } @@ -1799,7 +2032,7 @@ std::optional parseObjCMethodJsiSignature( 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, bridge)); + signature.argumentTypes.push_back(parseObjCEncodedEngineType(argEncoding, bridge)); if (argEncoding != nullptr) { free(argEncoding); } @@ -1809,7 +2042,7 @@ std::optional parseObjCMethodJsiSignature( 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)); + signature.ffiTypes.push_back(ffiTypeForEngineArgument(argType)); } ffi_status status = ffi_prep_cif( @@ -1822,7 +2055,7 @@ std::optional parseObjCMethodJsiSignature( return signature; } -bool prepareJsiMethodSignature(NativeApiJsiSignature* signature) { +bool prepareEngineMethodSignature(NativeApiSignature* signature) { if (signature == nullptr) { return false; } @@ -1832,7 +2065,7 @@ bool prepareJsiMethodSignature(NativeApiJsiSignature* signature) { 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); + ffi_type* ffiType = ffiTypeForEngineArgument(argType); if (ffiType == nullptr) { signature->prepared = false; return false; @@ -1849,30 +2082,65 @@ bool prepareJsiMethodSignature(NativeApiJsiSignature* signature) { return signature->prepared; } -bool reconcileObjCMethodRuntimeSignature(NativeApiJsiSignature* signature, - const NativeApiJsiSignature& runtime) { +bool isRuntimeAggregateType(const NativeApiType& type) { + switch (type.kind) { + case metagen::mdTypeStruct: + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + return true; + default: + return false; + } +} + +bool reconcileObjCMethodRuntimeType(NativeApiType* metadataType, + const NativeApiType& runtimeType, + bool* abiChanged) { + if (metadataType == nullptr || unsupportedEngineType(runtimeType)) { + return false; + } + + if (runtimeType.kind == metagen::mdTypeBlock && + metadataType->kind == metagen::mdTypeFunctionPointer) { + metadataType->kind = metagen::mdTypeBlock; + metadataType->ffiType = runtimeType.ffiType; + metadataType->supported = runtimeType.supported; + return true; + } + + // Do not overwrite aggregate (struct/union) metadata types with the + // anonymous ObjC runtime encoding: the metadata type carries the real + // field names and layout that the runtime encoding (e.g. "{?=qqq}") lacks. + (void)abiChanged; + return false; +} + +bool reconcileObjCMethodRuntimeSignature(NativeApiSignature* signature, + const NativeApiSignature& runtime) { if (signature == nullptr || signature->argumentTypes.size() != runtime.argumentTypes.size()) { return false; } bool changed = false; + bool abiChanged = false; + changed |= reconcileObjCMethodRuntimeType(&signature->returnType, + runtime.returnType, &abiChanged); for (size_t i = 0; i < signature->argumentTypes.size(); i++) { - NativeApiJsiType& metadataType = signature->argumentTypes[i]; - const NativeApiJsiType& runtimeType = runtime.argumentTypes[i]; - if (runtimeType.kind == metagen::mdTypeBlock && - metadataType.kind == metagen::mdTypeFunctionPointer) { - metadataType.kind = metagen::mdTypeBlock; - metadataType.ffiType = runtimeType.ffiType; - metadataType.supported = runtimeType.supported; - changed = true; - } + changed |= reconcileObjCMethodRuntimeType(&signature->argumentTypes[i], + runtime.argumentTypes[i], + &abiChanged); } - return !changed || prepareJsiMethodSignature(signature); + if (abiChanged) { + signature->signatureHash = 0; + } + return !changed || prepareEngineMethodSignature(signature); } -bool unsupportedJsiType(const NativeApiJsiType& type) { +bool unsupportedEngineType(const NativeApiType& type) { if (type.kind == metagen::mdTypeStruct && type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr) { return false; @@ -1880,93 +2148,127 @@ bool unsupportedJsiType(const NativeApiJsiType& type) { return !type.supported || type.ffiType == nullptr; } -bool signatureSupportedForJsiCallback(const NativeApiJsiSignature& signature) { +bool signatureSupportedForEngineCallback(const NativeApiSignature& signature) { if (!signature.prepared || signature.variadic || - unsupportedJsiType(signature.returnType)) { + unsupportedEngineType(signature.returnType)) { return false; } for (const auto& argType : signature.argumentTypes) { - if (unsupportedJsiType(argType)) { + if (unsupportedEngineType(argType)) { return false; } } return true; } -std::shared_ptr createJsiCallback( - Runtime& runtime, const std::shared_ptr& bridge, - const NativeApiJsiType& type, Function function, bool block, - NativeApiJsiCallbackThreadPolicy threadPolicy = - NativeApiJsiCallbackThreadPolicy::Default) { +std::shared_ptr createEngineCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, Function function, bool block, + NativeApiCallbackThreadPolicy threadPolicy = + NativeApiCallbackThreadPolicy::Default) { if (bridge == nullptr || bridge->metadata() == nullptr || type.signatureOffset == MD_SECTION_OFFSET_NULL) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Native callback metadata is unavailable."); } - auto parsed = parseMetadataJsiSignature( + auto parsed = parseMetadataEngineSignature( 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."); + if (!parsed || !signatureSupportedForEngineCallback(*parsed)) { + throw JSError( + runtime, "Native callback signature is not supported by backend."); + } + + auto signature = + std::make_shared(std::move(*parsed)); + uintptr_t roundTripValidationKey = + NativeApiBridge::callbackRoundTripValidationKey(type); + auto callback = std::make_shared( + runtime, bridge, std::move(signature), std::move(function), block, + threadPolicy, false, roundTripValidationKey); + if (block) { + callback->retainInitialBlockLifetime(callback); + } else { + bridge->retainEngineLifetime(callback); + } + return callback; +} + +std::shared_ptr createEngineCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& objcSignatureEncoding, Function function, bool block, + NativeApiCallbackThreadPolicy threadPolicy = + NativeApiCallbackThreadPolicy::Default, + uintptr_t roundTripValidationKey = 0) { + if (bridge == nullptr || objcSignatureEncoding.empty()) { + throw JSError(runtime, "Native callback encoding is unavailable."); + } + + auto parsed = parseObjCCallbackEngineSignature( + objcSignatureEncoding, block, bridge.get()); + if (!parsed || !signatureSupportedForEngineCallback(*parsed)) { + throw JSError( + runtime, "Native callback signature is not supported by backend."); } auto signature = - std::make_shared(std::move(*parsed)); - auto callback = std::make_shared( + std::make_shared(std::move(*parsed)); + auto callback = std::make_shared( runtime, bridge, std::move(signature), std::move(function), block, - threadPolicy); - if (!block) { - bridge->retainJsiLifetime(callback); + threadPolicy, false, roundTripValidationKey); + if (block) { + callback->retainInitialBlockLifetime(callback); + } else { + bridge->retainEngineLifetime(callback); } return callback; } -std::shared_ptr createJsiMethodCallback( - Runtime& runtime, const std::shared_ptr& bridge, +std::shared_ptr createEngineMethodCallback( + 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( + throw JSError( runtime, "Native method callback metadata is unavailable."); } - auto parsed = parseMetadataJsiSignature( + auto parsed = parseMetadataEngineSignature( 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."); + if (!parsed || !signatureSupportedForEngineCallback(*parsed)) { + throw JSError( + runtime, "Native method callback signature is not supported by backend."); } parsed->selectorName = selectorName; auto signature = - std::make_shared(std::move(*parsed)); - auto threadPolicy = readJsiCallbackThreadPolicy(runtime, function); - auto callback = std::make_shared( + std::make_shared(std::move(*parsed)); + auto threadPolicy = readEngineCallbackThreadPolicy(runtime, function); + auto callback = std::make_shared( runtime, bridge, std::move(signature), std::move(function), false, threadPolicy, true); - bridge->retainJsiLifetime(callback); + bridge->retainEngineLifetime(callback); return callback; } -std::shared_ptr createJsiMethodCallback( - Runtime& runtime, const std::shared_ptr& bridge, - const std::string& selectorName, NativeApiJsiSignature signature, +std::shared_ptr createEngineMethodCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, NativeApiSignature 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."); + prepareEngineMethodSignature(&signature); + if (!signatureSupportedForEngineCallback(signature)) { + throw JSError( + runtime, "Native method callback signature is not supported by backend."); } auto sharedSignature = - std::make_shared(std::move(signature)); - auto threadPolicy = readJsiCallbackThreadPolicy(runtime, function); - auto callback = std::make_shared( + std::make_shared(std::move(signature)); + auto threadPolicy = readEngineCallbackThreadPolicy(runtime, function); + auto callback = std::make_shared( runtime, bridge, std::move(sharedSignature), std::move(function), false, threadPolicy, true); - bridge->retainJsiLifetime(callback); + bridge->retainEngineLifetime(callback); return callback; } diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h b/NativeScript/ffi/shared/bridge/ClassBuilder.mm similarity index 76% rename from NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h rename to NativeScript/ffi/shared/bridge/ClassBuilder.mm index bf4d3b2fc..9c816f6e4 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h +++ b/NativeScript/ffi/shared/bridge/ClassBuilder.mm @@ -7,72 +7,72 @@ std::string readOptionalStringProperty(Runtime& runtime, const Object& object, return value.isString() ? value.asString(runtime).utf8(runtime) : ""; } -struct NativeApiJsiClassBuilderRegistration { +struct NativeApiClassBuilderRegistration { std::shared_ptr runtimeOwner; Runtime* runtime = nullptr; - std::shared_ptr bridge; + std::shared_ptr bridge; }; -std::mutex gNativeApiJsiClassBuilderMutex; -std::unordered_map - gNativeApiJsiClassBuilders; -struct NativeApiJsiKnownExposedMethod { +std::mutex gNativeApiClassBuilderMutex; +std::unordered_map + gNativeApiClassBuilders; +struct NativeApiKnownExposedMethod { std::string selectorName; - NativeApiJsiSignature signature; + NativeApiSignature signature; }; -std::mutex gNativeApiJsiKnownExposedMethodsMutex; -std::unordered_map - gNativeApiJsiKnownExposedMethods; +std::mutex gNativeApiKnownExposedMethodsMutex; +std::unordered_map + gNativeApiKnownExposedMethods; -void rememberNativeApiJsiClassBuilder( - Runtime& runtime, const std::shared_ptr& bridge, +void rememberNativeApiClassBuilder( + Runtime& runtime, const std::shared_ptr& bridge, Class cls) { if (cls == Nil) { return; } - std::lock_guard lock(gNativeApiJsiClassBuilderMutex); - auto runtimeOwner = retainNativeApiJsiRuntime(runtime); - gNativeApiJsiClassBuilders[cls] = NativeApiJsiClassBuilderRegistration{ + std::lock_guard lock(gNativeApiClassBuilderMutex); + auto runtimeOwner = retainNativeApiRuntime(runtime); + gNativeApiClassBuilders[cls] = NativeApiClassBuilderRegistration{ .runtimeOwner = runtimeOwner, .runtime = runtimeOwner.get(), .bridge = bridge, }; } -void rememberNativeApiJsiKnownExposedMethod( - const std::string& selectorName, const NativeApiJsiSignature& signature) { +void rememberNativeApiKnownExposedMethod( + const std::string& selectorName, const NativeApiSignature& signature) { if (selectorName.empty()) { return; } - NativeApiJsiKnownExposedMethod method{ + NativeApiKnownExposedMethod method{ .selectorName = selectorName, .signature = signature, }; - std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); - gNativeApiJsiKnownExposedMethods[selectorName] = method; - gNativeApiJsiKnownExposedMethods[jsifySelector(selectorName.c_str())] = + std::lock_guard lock(gNativeApiKnownExposedMethodsMutex); + gNativeApiKnownExposedMethods[selectorName] = method; + gNativeApiKnownExposedMethods[jsifySelector(selectorName.c_str())] = std::move(method); } -std::optional knownNativeApiJsiExposedMethod( +std::optional knownNativeApiExposedMethod( const std::string& name) { - std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); - auto it = gNativeApiJsiKnownExposedMethods.find(name); - if (it == gNativeApiJsiKnownExposedMethods.end()) { + std::lock_guard lock(gNativeApiKnownExposedMethodsMutex); + auto it = gNativeApiKnownExposedMethods.find(name); + if (it == gNativeApiKnownExposedMethods.end()) { return std::nullopt; } - NativeApiJsiKnownExposedMethod method = it->second; - prepareJsiMethodSignature(&method.signature); + NativeApiKnownExposedMethod method = it->second; + prepareEngineMethodSignature(&method.signature); return method; } -std::optional -findNativeApiJsiClassBuilder(id object) { +std::optional +findNativeApiClassBuilder(id object) { Class cls = object != nil ? object_getClass(object) : Nil; - std::lock_guard lock(gNativeApiJsiClassBuilderMutex); + std::lock_guard lock(gNativeApiClassBuilderMutex); while (cls != Nil) { - auto it = gNativeApiJsiClassBuilders.find(cls); - if (it != gNativeApiJsiClassBuilders.end()) { + auto it = gNativeApiClassBuilders.find(cls); + if (it != gNativeApiClassBuilders.end()) { return it->second; } cls = class_getSuperclass(cls); @@ -80,7 +80,7 @@ findNativeApiJsiClassBuilder(id object) { return std::nullopt; } -const char* nativeApiJsiFastEnumerationEncoding() { +const char* nativeApiEngineFastEnumerationEncoding() { static const char* encoding = nullptr; if (encoding == nullptr) { struct objc_method_description desc = protocol_getMethodDescription( @@ -91,21 +91,21 @@ const char* nativeApiJsiFastEnumerationEncoding() { return encoding; } -NSUInteger nativeApiJsiSymbolIteratorCountByEnumerating( +NSUInteger nativeApiEngineSymbolIteratorCountByEnumerating( id self, SEL, NSFastEnumerationState* state, id __unsafe_unretained stackbuf[], NSUInteger len) { if (len == 0 || state == nullptr || stackbuf == nullptr) { return 0; } - auto registration = findNativeApiJsiClassBuilder(self); + auto registration = findNativeApiClassBuilder(self); if (!registration || registration->runtime == nullptr || registration->bridge == nullptr) { return 0; } Runtime& runtime = *registration->runtime; - NativeApiJsiRuntimeScope runtimeScope(runtime); + NativeApiRuntimeScope runtimeScope(runtime); auto bridge = registration->bridge; try { Value receiver = makeNativeObjectValue(runtime, bridge, self, false); @@ -170,8 +170,8 @@ NSUInteger nativeApiJsiSymbolIteratorCountByEnumerating( } Value value = nextObject.getProperty(runtime, "value"); - NativeApiJsiArgumentFrame frame(1); - id nativeValue = objectFromJsiValue(runtime, bridge, value, frame, false); + NativeApiArgumentFrame frame(1); + id nativeValue = objectFromEngineValue(runtime, bridge, value, frame, false); if (nativeValue != nil) { [nativeValue retain]; [nativeValue autorelease]; @@ -190,7 +190,7 @@ NSUInteger nativeApiJsiSymbolIteratorCountByEnumerating( } NativeApiSymbol runtimeSymbolForClass( - const std::shared_ptr& bridge, Class cls) { + const std::shared_ptr& bridge, Class cls) { if (bridge != nullptr) { if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { return *symbol; @@ -206,7 +206,7 @@ NativeApiSymbol runtimeSymbolForClass( }; } -std::string nextAvailableJsiClassName(const std::string& requestedName) { +std::string nextAvailableEngineClassName(const std::string& requestedName) { if (requestedName.empty()) { return ""; } @@ -241,23 +241,23 @@ std::vector methodOverridesForName( const NativeApiMember* propertyOverrideForName( const std::vector& members, const std::string& name) { - const NativeApiMember* fallback = nullptr; + const NativeApiMember* propertyMember = nullptr; for (const auto& member : members) { if (member.property && member.name == name && (member.flags & metagen::mdMemberStatic) == 0) { - if (fallback == nullptr) { - fallback = &member; + if (propertyMember == nullptr) { + propertyMember = &member; } if (!member.readonly && !member.setterSelectorName.empty()) { return &member; } } } - return fallback; + return propertyMember; } -void addJsiOverrideMethod(Runtime& runtime, - const std::shared_ptr& bridge, +void addEngineOverrideMethod(Runtime& runtime, + const std::shared_ptr& bridge, Class nativeClass, Class baseClass, const std::string& selectorName, MDSectionOffset signatureOffset, @@ -266,12 +266,12 @@ void addJsiOverrideMethod(Runtime& runtime, return; } - auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + auto callback = createEngineMethodCallback(runtime, bridge, selectorName, signatureOffset, std::move(function), returnOwned); SEL selector = sel_registerName(selectorName.c_str()); std::string metadataEncoding = - objcMethodSignatureForJsiSignature(callback->signature()); + objcMethodSignatureForEngineSignature(callback->signature()); class_replaceMethod(nativeClass, selector, reinterpret_cast(callback->functionPointer()), metadataEncoding.c_str()); @@ -284,7 +284,8 @@ Value getObjectPropertyOrUndefined(Runtime& runtime, const Object& object, : Value::undefined(); } -Class dispatchSuperclassForJsiDerivedReceiver(id receiver, Class fallback) { +Class dispatchSuperclassForEngineDerivedReceiver(id receiver, + Class defaultSuperclass) { if (receiver == nil) { return Nil; } @@ -292,12 +293,12 @@ Class dispatchSuperclassForJsiDerivedReceiver(id receiver, Class fallback) { Class receiverClass = object_getClass(receiver); if (receiverClass == Nil || !class_conformsToProtocol(receiverClass, - @protocol(NativeApiJsiClassBuilderProtocol))) { + @protocol(NativeApiClassBuilderProtocol))) { return Nil; } Class superclass = class_getSuperclass(receiverClass); - return superclass != Nil ? superclass : fallback; + return superclass != Nil ? superclass : defaultSuperclass; } std::optional functionForSelector(Runtime& runtime, @@ -316,8 +317,8 @@ std::optional functionForSelector(Runtime& runtime, return value.asObject(runtime).asFunction(runtime); } -std::optional readExposedType( - Runtime& runtime, const std::shared_ptr& bridge, +std::optional readExposedType( + Runtime& runtime, const std::shared_ptr& bridge, const Object& descriptor, const char* propertyName) { if (!descriptor.hasProperty(runtime, propertyName)) { return std::nullopt; @@ -326,10 +327,10 @@ std::optional readExposedType( descriptor.getProperty(runtime, propertyName)); } -std::optional exposedMethodSignature( - Runtime& runtime, const std::shared_ptr& bridge, +std::optional exposedMethodSignature( + Runtime& runtime, const std::shared_ptr& bridge, const std::string& selectorName, const Object& descriptor) { - NativeApiJsiSignature signature; + NativeApiSignature signature; if (auto returnType = readExposedType(runtime, bridge, descriptor, "returns")) { signature.returnType = *returnType; } else { @@ -339,7 +340,7 @@ std::optional exposedMethodSignature( Value paramsValue = getObjectPropertyOrUndefined(runtime, descriptor, "params"); if (!paramsValue.isUndefined() && !paramsValue.isNull()) { if (!paramsValue.isObject() || !paramsValue.asObject(runtime).isArray(runtime)) { - throw facebook::jsi::JSError( + throw JSError( runtime, "exposedMethods params must be an array."); } Array params = paramsValue.asObject(runtime).getArray(runtime); @@ -347,7 +348,7 @@ std::optional exposedMethodSignature( Value typeValue = params.getValueAtIndex(runtime, i); auto type = interopTypeFromValue(runtime, bridge, typeValue); if (!type) { - throw facebook::jsi::JSError( + throw JSError( runtime, "exposedMethods contains an unsupported parameter type."); } signature.argumentTypes.push_back(*type); @@ -355,15 +356,15 @@ std::optional exposedMethodSignature( } if (selectorArgumentCount(selectorName) != signature.argumentTypes.size()) { - throw facebook::jsi::JSError( + throw JSError( runtime, "exposedMethods selector argument count does not match params."); } - prepareJsiMethodSignature(&signature); + prepareEngineMethodSignature(&signature); return signature; } -std::optional runtimeProtocolMethodSignature( +std::optional runtimeProtocolMethodSignature( const char* types) { if (types == nullptr) { return std::nullopt; @@ -375,27 +376,27 @@ std::optional runtimeProtocolMethodSignature( return std::nullopt; } - NativeApiJsiSignature signature; + NativeApiSignature signature; signature.implicitArgumentCount = 2; signature.returnType = - parseObjCEncodedJsiType(methodSignature.methodReturnType); + parseObjCEncodedEngineType(methodSignature.methodReturnType); for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) { signature.argumentTypes.push_back( - parseObjCEncodedJsiType([methodSignature getArgumentTypeAtIndex:i])); + parseObjCEncodedEngineType([methodSignature getArgumentTypeAtIndex:i])); } - if (unsupportedJsiType(signature.returnType)) { + if (unsupportedEngineType(signature.returnType)) { return std::nullopt; } for (const auto& argumentType : signature.argumentTypes) { - if (unsupportedJsiType(argumentType)) { + if (unsupportedEngineType(argumentType)) { return std::nullopt; } } return signature; } -std::optional protocolSymbolFromJsiValue( - Runtime& runtime, const std::shared_ptr& bridge, +std::optional protocolSymbolFromEngineValue( + Runtime& runtime, const std::shared_ptr& bridge, const Value& value) { if (value.isString()) { std::string name = value.asString(runtime).utf8(runtime); @@ -434,23 +435,23 @@ std::optional protocolSymbolFromJsiValue( return std::nullopt; } -void addJsiExposedMethod(Runtime& runtime, - const std::shared_ptr& bridge, +void addEngineExposedMethod(Runtime& runtime, + const std::shared_ptr& bridge, Class nativeClass, const std::string& selectorName, - NativeApiJsiSignature signature, Function function) { + NativeApiSignature signature, Function function) { if (selectorName.empty()) { return; } - auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + auto callback = createEngineMethodCallback(runtime, bridge, selectorName, std::move(signature), std::move(function)); - std::string encoding = objcMethodSignatureForJsiSignature(callback->signature()); + std::string encoding = objcMethodSignatureForEngineSignature(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, + Runtime& runtime, const std::shared_ptr& bridge, Class nativeClass, const std::vector& protocols, const std::string& propertyName, Function function) { std::unordered_set visited; @@ -487,7 +488,7 @@ bool addRuntimeProtocolOverrideForName( } auto signature = runtimeProtocolMethodSignature(descriptions[i].types); if (signature) { - addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + addEngineExposedMethod(runtime, bridge, nativeClass, selectorName, std::move(*signature), std::move(function)); free(descriptions); return true; @@ -519,22 +520,22 @@ Object getOwnPropertyDescriptor(Runtime& runtime, const Object& object, : Object(runtime); } -Value extendNativeApiJsiClass( - Runtime& runtime, const std::shared_ptr& bridge, +Value extendNativeApiClass( + 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( + throw JSError( runtime, "extendClass expects a native class and method object."); } - Class baseClass = classFromJsiValue(runtime, args[0]); + Class baseClass = classFromEngineValue(runtime, args[0]); if (baseClass == Nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "extendClass can only extend native class constructors."); } if (class_conformsToProtocol(baseClass, - @protocol(NativeApiJsiClassBuilderProtocol))) { - throw facebook::jsi::JSError(runtime, + @protocol(NativeApiClassBuilderProtocol))) { + throw JSError(runtime, "Cannot extend an already extended class."); } @@ -549,15 +550,15 @@ Value extendNativeApiJsiClass( "_Extended_" + std::to_string(rand()); } - std::string className = nextAvailableJsiClassName(requestedName); + std::string className = nextAvailableEngineClassName(requestedName); Class nativeClass = objc_allocateClassPair(baseClass, className.c_str(), 0); if (nativeClass == Nil) { - throw facebook::jsi::JSError(runtime, "Failed to allocate Objective-C class."); + throw JSError(runtime, "Failed to allocate Objective-C class."); } - markNativeApiJsiExtendedClass(nativeClass); - class_addProtocol(nativeClass, @protocol(NativeApiJsiClassBuilderProtocol)); - rememberNativeApiJsiClassBuilder(runtime, bridge, nativeClass); + markNativeApiExtendedClass(nativeClass); + class_addProtocol(nativeClass, @protocol(NativeApiClassBuilderProtocol)); + rememberNativeApiClassBuilder(runtime, bridge, nativeClass); NativeApiSymbol baseSymbol = runtimeSymbolForClass(bridge, baseClass); std::vector extensionMembers = @@ -569,9 +570,9 @@ Value extendNativeApiJsiClass( 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); + Protocol* protocol = protocolFromEngineValue(runtime, protocolValue); std::optional protocolSymbol = - protocolSymbolFromJsiValue(runtime, bridge, protocolValue); + protocolSymbolFromEngineValue(runtime, bridge, protocolValue); if (protocol != nullptr) { optionProtocols.push_back(protocol); class_addProtocol(nativeClass, protocol); @@ -611,7 +612,7 @@ Value extendNativeApiJsiClass( member.signatureOffset == 0) { continue; } - addJsiOverrideMethod( + addEngineOverrideMethod( runtime, bridge, nativeClass, baseClass, member.selectorName, member.signatureOffset, (member.flags & metagen::mdMemberReturnOwned) != 0, @@ -623,8 +624,8 @@ Value extendNativeApiJsiClass( runtime, bridge, nativeClass, optionProtocols, propertyName, value.asObject(runtime).asFunction(runtime)); if (!addedRuntimeProtocolOverride) { - if (auto known = knownNativeApiJsiExposedMethod(propertyName)) { - addJsiExposedMethod(runtime, bridge, nativeClass, + if (auto known = knownNativeApiExposedMethod(propertyName)) { + addEngineExposedMethod(runtime, bridge, nativeClass, known->selectorName, std::move(known->signature), value.asObject(runtime).asFunction(runtime)); @@ -639,7 +640,7 @@ Value extendNativeApiJsiClass( Value getter = descriptor.getProperty(runtime, "get"); if (propertyMember != nullptr && getter.isObject() && getter.asObject(runtime).isFunction(runtime)) { - addJsiOverrideMethod( + addEngineOverrideMethod( runtime, bridge, nativeClass, baseClass, propertyMember->selectorName, propertyMember->signatureOffset, (propertyMember->flags & metagen::mdMemberReturnOwned) != 0, @@ -651,7 +652,7 @@ Value extendNativeApiJsiClass( if (selectorArgumentCount(member.selectorName) != 0) { continue; } - addJsiOverrideMethod( + addEngineOverrideMethod( runtime, bridge, nativeClass, baseClass, member.selectorName, member.signatureOffset, (member.flags & metagen::mdMemberReturnOwned) != 0, @@ -663,7 +664,7 @@ Value extendNativeApiJsiClass( if (propertyMember != nullptr && setter.isObject() && setter.asObject(runtime).isFunction(runtime) && !propertyMember->setterSelectorName.empty()) { - addJsiOverrideMethod(runtime, bridge, nativeClass, baseClass, + addEngineOverrideMethod(runtime, bridge, nativeClass, baseClass, propertyMember->setterSelectorName, propertyMember->setterSignatureOffset, false, setter.asObject(runtime).asFunction(runtime)); @@ -697,8 +698,8 @@ Value extendNativeApiJsiClass( auto signature = exposedMethodSignature( runtime, bridge, selectorName, descriptorValue.asObject(runtime)); if (signature) { - rememberNativeApiJsiKnownExposedMethod(selectorName, *signature); - addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + rememberNativeApiKnownExposedMethod(selectorName, *signature); + addEngineExposedMethod(runtime, bridge, nativeClass, selectorName, std::move(*signature), std::move(*function)); } } @@ -708,11 +709,11 @@ Value extendNativeApiJsiClass( getObjectPropertyOrUndefined(runtime, options, "__hasIterator"); if (hasIteratorValue.isBool() && hasIteratorValue.getBool()) { class_addProtocol(nativeClass, @protocol(NSFastEnumeration)); - if (const char* encoding = nativeApiJsiFastEnumerationEncoding()) { + if (const char* encoding = nativeApiEngineFastEnumerationEncoding()) { class_replaceMethod( nativeClass, @selector(countByEnumeratingWithState:objects:count:), - reinterpret_cast(nativeApiJsiSymbolIteratorCountByEnumerating), + reinterpret_cast(nativeApiEngineSymbolIteratorCountByEnumerating), encoding); } } @@ -726,27 +727,28 @@ Value extendNativeApiJsiClass( return makeNativeClassValue(runtime, bridge, std::move(newSymbol)); } -Value invokeNativeApiJsiBaseMethod( - Runtime& runtime, const std::shared_ptr& bridge, +Value invokeNativeApiBaseMethod( + 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( + throw JSError( runtime, "__invokeBase expects base class, receiver, and member name."); } - Class baseClass = classFromJsiValue(runtime, args[0]); + Class baseClass = classFromEngineValue(runtime, args[0]); if (baseClass == Nil) { - throw facebook::jsi::JSError(runtime, "__invokeBase base class is invalid."); + throw 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."); + throw JSError(runtime, "__invokeBase receiver is not native."); } - id receiver = - receiverObject.getHostObject(runtime)->object(); + auto receiverHostObject = + receiverObject.getHostObject(runtime); + id receiver = receiverHostObject->object(); std::string memberName = args[2].asString(runtime).utf8(runtime); size_t actualArgc = count - 3; @@ -759,31 +761,32 @@ Value invokeNativeApiJsiBaseMethod( selectWritablePropertyMember(members, memberName, false)) { if (actualArgc == 0) { Class dispatchClass = - dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); - return callObjCSelector(runtime, bridge, receiver, false, - propertyMember->selectorName, propertyMember, - nullptr, 0, dispatchClass); + dispatchSuperclassForEngineDerivedReceiver(receiver, baseClass); + return receiverHostObject->callObjectSelector( + runtime, propertyMember->selectorName, propertyMember, nullptr, 0, + dispatchClass); } if (actualArgc == 1 && !propertyMember->setterSelectorName.empty() && !propertyMember->readonly) { Class dispatchClass = - dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + dispatchSuperclassForEngineDerivedReceiver(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); + return receiverHostObject->callObjectSelector( + runtime, setterMember.selectorName, &setterMember, args + 3, + actualArgc, dispatchClass); } } } if (member == nullptr) { - throw facebook::jsi::JSError( + throw 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); + dispatchSuperclassForEngineDerivedReceiver(receiver, baseClass); + return receiverHostObject->callObjectSelector(runtime, member->selectorName, + member, args + 3, actualArgc, + dispatchClass); } diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h b/NativeScript/ffi/shared/bridge/HostObject.mm similarity index 71% rename from NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h rename to NativeScript/ffi/shared/bridge/HostObject.mm index 06b82774d..2e671ed32 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h +++ b/NativeScript/ffi/shared/bridge/HostObject.mm @@ -1,23 +1,43 @@ +#ifndef NATIVESCRIPT_NATIVE_API_BACKEND_NAME +#error Engine backends must define NATIVESCRIPT_NATIVE_API_BACKEND_NAME. +#endif + +#ifndef NATIVESCRIPT_NATIVE_API_RUNTIME_NAME +#define NATIVESCRIPT_NATIVE_API_RUNTIME_NAME NATIVESCRIPT_NATIVE_API_BACKEND_NAME +#endif + #ifndef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS -inline bool InstallNativeApiEngineLazyGlobal( - Runtime&, std::shared_ptr, const std::string&, +inline bool InstallNativeApiLazyGlobal( + Runtime&, std::shared_ptr, const std::string&, const std::string&, bool) { return false; } #endif +#ifndef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_SELECTOR_GROUP_FUNCTION +#error Engine backends must provide an engine selector group function. +#endif + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations); + class NativeApiHostObject final : public HostObject { public: - explicit NativeApiHostObject(std::shared_ptr bridge) + 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"); + return makeString(runtime, NATIVESCRIPT_NATIVE_API_RUNTIME_NAME); } if (property == "backend") { - return makeString(runtime, "hermes"); + return makeString(runtime, NATIVESCRIPT_NATIVE_API_BACKEND_NAME); } if (property == "metadata") { return metadataObject(runtime); @@ -38,7 +58,7 @@ class NativeApiHostObject final : public HostObject { std::string name = readStringArg(runtime, args, count, 0, "name"); std::string kind = readStringArg(runtime, args, count, 1, "kind"); bool force = count > 2 && args[2].isBool() && args[2].getBool(); - return InstallNativeApiEngineLazyGlobal(runtime, bridge, name, kind, + return InstallNativeApiLazyGlobal(runtime, bridge, name, kind, force); }); } @@ -50,16 +70,16 @@ class NativeApiHostObject final : public HostObject { [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 1 || !args[0].isObject()) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Fast enumeration expects a native object."); } id object = NativeApiObjectHostObject::nativeObjectFromValue(runtime, args[0]); if (object == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Fast enumeration expects a native object."); } if (![object conformsToProtocol:@protocol(NSFastEnumeration)]) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Object does not conform to NSFastEnumeration."); } return Object::createFromHostObject( @@ -68,85 +88,6 @@ class NativeApiHostObject final : public HostObject { 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, @@ -162,7 +103,7 @@ class NativeApiHostObject final : public HostObject { NSBundle* bundle = [NSBundle bundleWithPath:[NSString stringWithUTF8String:frameworkPath.c_str()]]; if (bundle == nil || ![bundle load]) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Could not load bundle: " + frameworkPath); } return true; @@ -216,7 +157,7 @@ class NativeApiHostObject final : public HostObject { runtime, PropNameID::forAscii(runtime, "__extendClass"), 2, [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { - return extendNativeApiJsiClass(runtime, bridge, args, count); + return extendNativeApiClass(runtime, bridge, args, count); }); } if (property == "__invokeBase") { @@ -225,7 +166,100 @@ class NativeApiHostObject final : public HostObject { runtime, PropNameID::forAscii(runtime, "__invokeBase"), 3, [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { - return invokeNativeApiJsiBaseMethod(runtime, bridge, args, count); + return invokeNativeApiBaseMethod(runtime, bridge, args, count); + }); + } + if (property == "__makeSelectorGroupFunction") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__makeSelectorGroupFunction"), + 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 3 || !args[1].isBool() || !args[2].isObject() || + !args[2].asObject(runtime).isArray(runtime)) { + throw JSError( + runtime, + "__makeSelectorGroupFunction expects class, receiver kind, " + "and selector table."); + } + + Class lookupClass = classFromEngineValue(runtime, args[0]); + if (lookupClass == Nil) { + throw JSError(runtime, + "__makeSelectorGroupFunction class is invalid."); + } + + bool receiverIsClass = args[1].getBool(); + Array selectorTable = args[2].asObject(runtime).getArray(runtime); + size_t selectorCount = selectorTable.size(runtime); + auto selectors = + std::make_shared< + std::vector>( + selectorCount); + for (size_t i = 0; i < selectorCount; i++) { + Value selectorValue = selectorTable.getValueAtIndex(runtime, i); + if (selectorValue.isString()) { + (*selectors)[i].selectorName = + selectorValue.asString(runtime).utf8(runtime); + } else if (selectorValue.isObject()) { + Object descriptor = selectorValue.asObject(runtime); + Value selectorNameValue = + descriptor.getProperty(runtime, "selectorName"); + if (!selectorNameValue.isString()) { + continue; + } + NativeApiMember member; + member.selectorName = + selectorNameValue.asString(runtime).utf8(runtime); + Value nameValue = descriptor.getProperty(runtime, "name"); + if (nameValue.isString()) { + member.name = nameValue.asString(runtime).utf8(runtime); + } + Value setterSelectorNameValue = + descriptor.getProperty(runtime, "setterSelectorName"); + if (setterSelectorNameValue.isString()) { + member.setterSelectorName = + setterSelectorNameValue.asString(runtime).utf8(runtime); + } + Value signatureOffsetValue = + descriptor.getProperty(runtime, "signatureOffset"); + if (signatureOffsetValue.isNumber()) { + member.signatureOffset = static_cast( + signatureOffsetValue.getNumber()); + } + Value setterSignatureOffsetValue = + descriptor.getProperty(runtime, "setterSignatureOffset"); + if (setterSignatureOffsetValue.isNumber()) { + member.setterSignatureOffset = static_cast( + setterSignatureOffsetValue.getNumber()); + } + Value flagsValue = descriptor.getProperty(runtime, "flags"); + if (flagsValue.isNumber()) { + member.flags = static_cast( + static_cast(flagsValue.getNumber())); + } + Value propertyValue = descriptor.getProperty(runtime, "property"); + if (propertyValue.isBool()) { + member.property = propertyValue.getBool(); + } + Value readonlyValue = descriptor.getProperty(runtime, "readonly"); + if (readonlyValue.isBool()) { + member.readonly = readonlyValue.getBool(); + } + (*selectors)[i].selectorName = member.selectorName; + (*selectors)[i].member = std::move(member); + (*selectors)[i].hasMember = true; + } + } + + auto preparedInvocations = std::make_shared>>( + selectors->size()); + + return CreateNativeApiSelectorGroupFunction( + runtime, bridge, lookupClass, receiverIsClass, selectors, + preparedInvocations); }); } if (property == "__rememberClassWrapper") { @@ -237,7 +271,7 @@ class NativeApiHostObject final : public HostObject { if (count < 2) { return Value::undefined(); } - Class cls = classFromJsiValue(runtime, args[0]); + Class cls = classFromEngineValue(runtime, args[0]); if (cls == Nil) { return Value::undefined(); } @@ -263,8 +297,30 @@ class NativeApiHostObject final : public HostObject { if (object == nil) { return Value::undefined(); } + // A factory/class method may return an instance of a different + // class (e.g. +[TNSSwiftLikeFactory create] returns a TNSSwiftLike). + // Only label the object with this wrapper when it actually is an + // instance of the wrapper's class, so `constructor` resolves to the + // object's real class instead of the calling class. + if (args[1].isObject()) { + Class wrapperClass = classFromEngineValue(runtime, args[1]); + if (wrapperClass != Nil && ![object isKindOfClass:wrapperClass]) { + return Value::undefined(); + } + } bridge->setObjectExpando(runtime, object, "__nativeApiClassWrapper", args[1]); + if (args[1].isObject()) { + Object classWrapper = args[1].asObject(runtime); + Value prototypeValue = + classWrapper.getProperty(runtime, "prototype"); + if (prototypeValue.isObject()) { + Object instanceObject = args[0].asObject(runtime); + Object prototype = prototypeValue.asObject(runtime); + SetNativeApiObjectPrototype(runtime, instanceObject, + prototype); + } + } return Value::undefined(); }); } @@ -275,7 +331,7 @@ class NativeApiHostObject final : public HostObject { [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 3 || !args[1].isNumber()) { - throw facebook::jsi::JSError( + throw JSError( runtime, "CC_SHA256 expects data, length, and output."); } void* commonCrypto = @@ -288,12 +344,13 @@ class NativeApiHostObject final : public HostObject { symbol = dlsym(commonCrypto, "_CC_SHA256"); } if (symbol == nullptr) { - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, "CC_SHA256 is not available."); } - NativeApiJsiArgumentFrame frame(3); - void* data = pointerFromJsiValue(runtime, args[0], frame); - void* output = pointerFromJsiValue(runtime, args[2], frame); + NativeApiArgumentFrame frame(3); + void* data = pointerFromEngineValue(runtime, bridge, args[0], frame); + void* output = + pointerFromEngineValue(runtime, bridge, args[2], frame); using CC_SHA256_Fn = unsigned char* (*)(const void*, unsigned long, unsigned char*); auto fn = reinterpret_cast(symbol); @@ -315,12 +372,15 @@ class NativeApiHostObject final : public HostObject { if (symbol == nullptr) { return Value::null(); } + auto prepared = + std::make_shared(); + prepared->symbol = *symbol; 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); + [bridge, prepared](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, prepared, args, count); }); function.setProperty(runtime, "kind", makeString(runtime, "function")); function.setProperty(runtime, "nativeName", @@ -412,13 +472,16 @@ class NativeApiHostObject final : public HostObject { } if (const NativeApiSymbol* functionSymbol = bridge_->findFunction(property)) { + auto prepared = + std::make_shared(); + prepared->symbol = *functionSymbol; 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); + [bridge, prepared](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, prepared, args, count); }); function.setProperty(runtime, "kind", makeString(runtime, "function")); function.setProperty(runtime, "nativeName", @@ -462,12 +525,12 @@ class NativeApiHostObject final : public HostObject { #ifdef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS addPropertyName(runtime, names, "__defineLazyGlobal"); #endif - 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, "__makeSelectorGroupFunction"); addPropertyName(runtime, names, "__rememberClassWrapper"); addPropertyName(runtime, names, "__rememberObjectClassWrapper"); addPropertyName(runtime, names, "getFunction"); @@ -556,5 +619,5 @@ class NativeApiHostObject final : public HostObject { return metadata; } - std::shared_ptr bridge_; + std::shared_ptr bridge_; }; diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h b/NativeScript/ffi/shared/bridge/HostObjects.mm similarity index 55% rename from NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h rename to NativeScript/ffi/shared/bridge/HostObjects.mm index a1dbf289d..36c3b3d91 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h +++ b/NativeScript/ffi/shared/bridge/HostObjects.mm @@ -1,14 +1,61 @@ +// HostObject::set returns bool on engines whose interceptors can defer an +// unhandled set to the JS prototype chain. JSI's HostObject::set is void, so +// the Hermes backend defines NATIVESCRIPT_NATIVE_API_HOST_SET_VOID and the +// set overrides below collapse their return type/values accordingly. +#ifdef NATIVESCRIPT_NATIVE_API_HOST_SET_VOID +using NativeApiHostSetResult = void; +#define NATIVE_API_SET_RETURN(handled) return +#else +using NativeApiHostSetResult = bool; +#define NATIVE_API_SET_RETURN(handled) return (handled) +#endif + +// Engine-neutral factory for native object instance wrappers. V8 uses its +// kNonMasking native instance template (fast prototype-based property access); +// every other engine uses its standard host-object creation. Selected at +// compile time so the shared bridge code stays engine-agnostic. +template +Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { +#ifdef TARGET_ENGINE_V8 + return Object::createNativeInstanceHostObject(runtime, std::move(host)); +#else + return Object::createFromHostObject(runtime, std::move(host)); +#endif +} + +class NativeApiObjectLifetimeState final { + public: + explicit NativeApiObjectLifetimeState(id object) + : object_(reinterpret_cast(object)) {} + + id object() const { + return reinterpret_cast(object_.load(std::memory_order_relaxed)); + } + + void setObject(id object) { + object_.store(reinterpret_cast(object), std::memory_order_relaxed); + } + + void clear() { object_.store(nullptr, std::memory_order_relaxed); } + + private: + std::atomic object_{nullptr}; +}; + + class NativeApiPointerHostObject final : public HostObject, public std::enable_shared_from_this { public: - NativeApiPointerHostObject(std::shared_ptr bridge, + NativeApiPointerHostObject(std::shared_ptr bridge, void* pointer, std::string kind = "pointer", - bool adopted = false) + bool adopted = false, + std::shared_ptr backingValue = nullptr) : bridge_(std::move(bridge)), pointer_(pointer), kind_(std::move(kind)), - adopted_(adopted) {} + adopted_(adopted), + backingValue_(std::move(backingValue)) {} ~NativeApiPointerHostObject() override { if (adopted_ && pointer_ != nullptr) { @@ -21,6 +68,10 @@ class NativeApiPointerHostObject final } void* pointer() const { return pointer_; } + std::shared_ptr backingValue() const { return backingValue_; } + void setBackingValue(Runtime& runtime, const Value& value) { + backingValue_ = std::make_shared(runtime, value); + } bool adopted() const { return adopted_; } void adopt() { adopted_ = true; } void clearWithoutFree() { @@ -29,6 +80,7 @@ class NativeApiPointerHostObject final } pointer_ = nullptr; adopted_ = false; + backingValue_.reset(); } Value get(Runtime& runtime, const PropNameID& name) override { @@ -51,12 +103,13 @@ class NativeApiPointerHostObject final 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."); + throw JSError(runtime, "Unmanaged value has already been consumed."); } id object = static_cast(self->pointer_); self->consumed_ = true; self->pointer_ = nullptr; self->adopted_ = false; + self->backingValue_.reset(); return makeNativeObjectValue(runtime, self->bridge_, object, retained); }); } @@ -69,7 +122,7 @@ class NativeApiPointerHostObject final [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."); + throw JSError(runtime, "Pointer offset must be a number."); } intptr_t offset = static_cast(args[0].getNumber()); intptr_t base = reinterpret_cast(pointer); @@ -128,7 +181,7 @@ class NativeApiPointerHostObject final return makeString(runtime, ""); } - return makeString(runtime, "[NativeApiJsi " + kind + " " + + return makeString(runtime, "[NativeApi " + kind + " " + std::string(address) + "]"); }); } @@ -154,17 +207,18 @@ class NativeApiPointerHostObject final } private: - std::shared_ptr bridge_; + std::shared_ptr bridge_; void* pointer_ = nullptr; std::string kind_; bool adopted_ = false; bool consumed_ = false; + std::shared_ptr backingValue_; }; class NativeApiReferenceHostObject final : public HostObject { public: - NativeApiReferenceHostObject(std::shared_ptr bridge, - NativeApiJsiType type, void* data, bool ownsData, + NativeApiReferenceHostObject(std::shared_ptr bridge, + NativeApiType type, void* data, bool ownsData, size_t byteLength = 0, std::shared_ptr pendingValue = nullptr, std::shared_ptr backingValue = nullptr) @@ -184,12 +238,13 @@ class NativeApiReferenceHostObject final : public HostObject { } void* data() const { return data_; } - const NativeApiJsiType& type() const { return type_; } - void ensureStorage(Runtime& runtime, NativeApiJsiType type, - NativeApiJsiArgumentFrame& frame, size_t elements = 1); + const NativeApiType& type() const { return type_; } + std::shared_ptr backingValue() const { return backingValue_; } + void ensureStorage(Runtime& runtime, NativeApiType type, + NativeApiArgumentFrame& frame, size_t elements = 1); Value get(Runtime& runtime, const PropNameID& name) override; - void set(Runtime& runtime, const PropNameID& name, const Value& value) override; + NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override; std::vector getPropertyNames(Runtime& runtime) override { std::vector names; addPropertyName(runtime, names, "kind"); @@ -200,8 +255,8 @@ class NativeApiReferenceHostObject final : public HostObject { } private: - std::shared_ptr bridge_; - NativeApiJsiType type_; + std::shared_ptr bridge_; + NativeApiType type_; void* data_ = nullptr; bool ownsData_ = false; size_t byteLength_ = 0; @@ -212,8 +267,8 @@ class NativeApiReferenceHostObject final : public HostObject { class NativeApiStructObjectHostObject final : public HostObject { public: NativeApiStructObjectHostObject( - std::shared_ptr bridge, - std::shared_ptr info, + 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) @@ -238,19 +293,19 @@ class NativeApiStructObjectHostObject final : public HostObject { } void* data() const { return data_; } - std::shared_ptr info() const { return info_; } + 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; + NativeApiHostSetResult 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 bridge_; + std::shared_ptr info_; std::shared_ptr> ownedData_; std::shared_ptr backingValue_; void* data_ = nullptr; @@ -260,7 +315,7 @@ class NativeApiStructObjectHostObject final : public HostObject { class NativeApiFastEnumerationIteratorHostObject final : public HostObject { public: NativeApiFastEnumerationIteratorHostObject( - std::shared_ptr bridge, id collection) + std::shared_ptr bridge, id collection) : bridge_(std::move(bridge)), collection_(collection) { [(id)collection_ retain]; } @@ -309,14 +364,14 @@ class NativeApiFastEnumerationIteratorHostObject final : public HostObject { } id value = state_.itemsPtr[stackIndex_++]; - NativeApiJsiType valueType = nativeObjectReturnTypeForClass(object_getClass(value)); + NativeApiType 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_; + std::shared_ptr bridge_; id collection_ = nil; NSFastEnumerationState state_ = {}; id __unsafe_unretained stack_[16] = {}; @@ -326,7 +381,7 @@ class NativeApiFastEnumerationIteratorHostObject final : public HostObject { }; NativeApiSymbol nativeApiSymbolForRuntimeClass( - const std::shared_ptr& bridge, Class cls) { + 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)) { @@ -350,9 +405,76 @@ NativeApiSymbol nativeApiSymbolForRuntimeClass( }; } +std::optional runtimeWritablePropertySetter(id object, + const std::string& property) { + if (object == nil || property.empty()) { + return std::nullopt; + } + + Class current = object_getClass(object); + while (current != Nil) { + objc_property_t prop = class_getProperty(current, property.c_str()); + if (prop != nullptr) { + if (char* readonly = property_copyAttributeValue(prop, "R")) { + free(readonly); + return std::nullopt; + } + + std::string setter = setterSelectorForProperty(property); + if (char* customSetter = property_copyAttributeValue(prop, "S")) { + setter = customSetter; + free(customSetter); + } + + SEL selector = sel_getUid(setter.c_str()); + if ([object respondsToSelector:selector]) { + return setter; + } + } + + current = class_getSuperclass(current); + } + + std::string setter = setterSelectorForProperty(property); + SEL selector = sel_getUid(setter.c_str()); + if ([object respondsToSelector:selector]) { + return setter; + } + + return std::nullopt; +} + +std::optional runtimeReadablePropertyGetter(id object, + const std::string& property) { + if (object == nil || property.empty()) { + return std::nullopt; + } + + Class current = object_getClass(object); + while (current != Nil) { + objc_property_t prop = class_getProperty(current, property.c_str()); + if (prop != nullptr) { + std::string getter = property; + if (char* customGetter = property_copyAttributeValue(prop, "G")) { + getter = customGetter; + free(customGetter); + } + + if (auto selector = + respondingPropertyGetterSelector(object, property, getter)) { + return selector; + } + } + + current = class_getSuperclass(current); + } + + return respondingPropertyGetterSelector(object, property, property); +} + class NativeApiSuperHostObject final : public HostObject { public: - NativeApiSuperHostObject(std::shared_ptr bridge, + NativeApiSuperHostObject(std::shared_ptr bridge, id receiver, Class dispatchClass) : bridge_(std::move(bridge)), receiver_(receiver), @@ -378,7 +500,7 @@ class NativeApiSuperHostObject final : public HostObject { return Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "toString"), 0, [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return makeString(runtime, "[NativeApiJsiSuper]"); + return makeString(runtime, "[NativeApiSuper]"); }); } if (receiver_ == nil || dispatchClass_ == Nil) { @@ -398,7 +520,7 @@ class NativeApiSuperHostObject final : public HostObject { } } - if (selectMethodMember(members, property, false, 0) != nullptr) { + if (hasMethodMember(members, property, false)) { auto bridge = bridge_; id receiver = receiver_; Class dispatchClass = dispatchClass_; @@ -411,13 +533,13 @@ class NativeApiSuperHostObject final : public HostObject { const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(dispatchClass); if (symbol == nullptr) { - throw facebook::jsi::JSError( + throw 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( + throw JSError( runtime, "Objective-C super selector is not available: " + memberName); } @@ -428,29 +550,13 @@ class NativeApiSuperHostObject final : public HostObject { } } - 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 { + NativeApiHostSetResult 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."); + throw JSError(runtime, "Cannot set property on nil super."); } if (const NativeApiSymbol* symbol = @@ -460,7 +566,7 @@ class NativeApiSuperHostObject final : public HostObject { selectWritablePropertyMember(members, property, false)) { if (propertyMember->readonly || propertyMember->setterSelectorName.empty()) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Attempted to assign to readonly property."); } NativeApiMember setterMember = *propertyMember; @@ -470,7 +576,7 @@ class NativeApiSuperHostObject final : public HostObject { callObjCSelector(runtime, bridge_, receiver_, false, setterMember.selectorName, &setterMember, args, 1, dispatchClass_); - return; + NATIVE_API_SET_RETURN(true); } } @@ -480,10 +586,10 @@ class NativeApiSuperHostObject final : public HostObject { Value args[] = {Value(runtime, value)}; callObjCSelector(runtime, bridge_, receiver_, false, setterSelectorName, nullptr, args, 1, dispatchClass_); - return; + NATIVE_API_SET_RETURN(true); } - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, "No writable native super property: " + property); } @@ -496,25 +602,205 @@ class NativeApiSuperHostObject final : public HostObject { } private: - std::shared_ptr bridge_; + std::shared_ptr bridge_; id receiver_ = nil; Class dispatchClass_ = Nil; }; +struct NativeApiRuntimeMember { + std::string name; + std::string selectorName; + size_t argumentCount = 0; +}; + +using NativeApiRuntimeMembers = std::vector; + +struct NativeApiRuntimeMemberIndex { + NativeApiRuntimeMembers members; + std::unordered_set memberNames; + std::unordered_map> + selectorsByNameAndCount; +}; + +struct NativeApiRuntimeMembersCacheKey { + Class cls = Nil; + bool staticMembers = false; + + bool operator==(const NativeApiRuntimeMembersCacheKey& other) const { + return cls == other.cls && staticMembers == other.staticMembers; + } +}; + +struct NativeApiRuntimeMembersCacheKeyHash { + size_t operator()(const NativeApiRuntimeMembersCacheKey& key) const { + size_t classHash = std::hash{}(reinterpret_cast(key.cls)); + return classHash ^ (key.staticMembers ? 0x9e3779b97f4a7c15ULL : 0); + } +}; + +std::mutex& runtimeMembersCacheMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map, + NativeApiRuntimeMembersCacheKeyHash>& +runtimeMembersCache() { + static std::unordered_map, + NativeApiRuntimeMembersCacheKeyHash> + cache; + return cache; +} + +std::shared_ptr emptyRuntimeMembers() { + static auto empty = std::make_shared(); + return empty; +} + +NativeApiRuntimeMemberIndex buildRuntimeMembersForClass(Class cls, + bool staticMembers) { + NativeApiRuntimeMemberIndex index; + if (cls == Nil) { + return index; + } + + std::unordered_set seen; + Class current = staticMembers ? object_getClass(cls) : cls; + while (current != Nil) { + unsigned int methodCount = 0; + Method* methods = class_copyMethodList(current, &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 || selectorName[0] == '\0') { + continue; + } + + std::string selectorString(selectorName); + std::string name = jsifySelector(selectorString.c_str()); + if (name.empty()) { + continue; + } + + size_t argumentCount = selectorArgumentCount(selectorString); + std::string key = name + "\x1f" + std::to_string(argumentCount); + if (!seen.insert(key).second) { + continue; + } + + index.memberNames.insert(name); + index.selectorsByNameAndCount[name].emplace(argumentCount, selectorString); + index.members.push_back(NativeApiRuntimeMember{ + .name = std::move(name), + .selectorName = std::move(selectorString), + .argumentCount = argumentCount, + }); + } + if (methods != nullptr) { + free(methods); + } + current = class_getSuperclass(current); + } + + return index; +} + +std::shared_ptr runtimeMembersForClass( + Class cls, bool staticMembers) { + if (cls == Nil) { + return emptyRuntimeMembers(); + } + + NativeApiRuntimeMembersCacheKey key{.cls = cls, + .staticMembers = staticMembers}; + + { + std::lock_guard lock(runtimeMembersCacheMutex()); + auto& cache = runtimeMembersCache(); + auto cached = cache.find(key); + if (cached != cache.end()) { + return cached->second; + } + } + + auto members = + std::make_shared( + buildRuntimeMembersForClass(cls, staticMembers)); + + { + std::lock_guard lock(runtimeMembersCacheMutex()); + auto& cache = runtimeMembersCache(); + auto [cached, inserted] = cache.emplace(key, members); + return inserted ? members : cached->second; + } +} + +bool hasRuntimeMemberForName(Class cls, bool staticMembers, + const std::string& name) { + auto index = runtimeMembersForClass(cls, staticMembers); + return index->memberNames.find(name) != index->memberNames.end(); +} + +std::optional selectRuntimeSelectorForName( + Class cls, bool staticMembers, const std::string& name, size_t count) { + auto index = runtimeMembersForClass(cls, staticMembers); + auto selectorsForName = index->selectorsByNameAndCount.find(name); + if (selectorsForName == index->selectorsByNameAndCount.end()) { + return std::nullopt; + } + auto selector = selectorsForName->second.find(count); + if (selector == selectorsForName->second.end()) { + return std::nullopt; + } + return selector->second; +} + +Array runtimeMembersArray(Runtime& runtime, Class cls, bool staticMembers) { + auto index = runtimeMembersForClass(cls, staticMembers); + Array result(runtime, index->members.size()); + for (size_t i = 0; i < index->members.size(); i++) { + const auto& member = index->members[i]; + 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(member.argumentCount)); + descriptor.setProperty(runtime, "property", false); + descriptor.setProperty(runtime, "readonly", false); + descriptor.setProperty(runtime, "setterSelectorName", makeString(runtime, "")); + result.setValueAtIndex(runtime, i, descriptor); + } + return result; +} + class NativeApiObjectHostObject final : public HostObject, public std::enable_shared_from_this { public: - NativeApiObjectHostObject(std::shared_ptr bridge, + NativeApiObjectHostObject(std::shared_ptr bridge, id object, bool ownsObject) - : bridge_(std::move(bridge)), object_(object), ownsObject_(ownsObject) { + : bridge_(std::move(bridge)), + object_(object), + ownsObject_(ownsObject), + lifetimeState_(std::make_shared(object)) { if (object_ != nil && !ownsObject_) { [object_ retain]; ownsObject_ = true; + wrapperRetainedObject_ = true; } } ~NativeApiObjectHostObject() override { + if (bridge_ != nullptr && object_ != nil) { + bridge_->forgetRoundTripValue(object_); + bridge_->forgetObjectExpandos(object_); + } + if (lifetimeState_ != nullptr) { + lifetimeState_->clear(); + } if (ownsObject_ && object_ != nil) { [object_ release]; object_ = nil; @@ -522,11 +808,32 @@ class NativeApiObjectHostObject final } id object() const { return object_; } + std::shared_ptr lifetimeState() const { + return lifetimeState_; + } + + // Store a JS-owned property as a bridge expando (read back by get()). Used by + // engine adapters whose exotic property storage doesn't fall back to own + // properties when the host set handler defers. + void storeOwnExpando(Runtime& runtime, const std::string& property, + const Value& value) { + if (object_ != nil) { + bridge_->setObjectExpando(runtime, object_, property, value); + } + } void disownObject(id expected) { if (object_ == expected) { + if (bridge_ != nullptr && expected != nil) { + bridge_->forgetRoundTripValue(expected); + bridge_->forgetObjectExpandos(expected); + } ownsObject_ = false; + wrapperRetainedObject_ = false; object_ = nil; + if (lifetimeState_ != nullptr) { + lifetimeState_->clear(); + } } } @@ -545,12 +852,22 @@ class NativeApiObjectHostObject final return object.getHostObject(runtime)->object(); } + static Value descriptionString(Runtime& runtime, id object) { + NSString* description = nil; + performDirectObjCInvocation(runtime, [&]() { + description = [(object != nil ? [object description] : @"") copy]; + }); + std::string text = description.UTF8String ?: ""; + [description release]; + return makeString(runtime, text); + } + 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, + throw JSError(runtime, "Cannot send Objective-C selector to nil."); } @@ -562,7 +879,7 @@ class NativeApiObjectHostObject final if (classWrapperValue.isObject()) { classWrapper.emplace(classWrapperValue.asObject(runtime)); } - bridge_->forgetRoundTripValue(receiver); + bridge_->forgetRoundTripValue(runtime, receiver); bridge_->forgetObjectExpandos(receiver); } @@ -570,18 +887,292 @@ class NativeApiObjectHostObject final 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)); + id resultObject = nativeObjectFromValue(runtime, result); + disownObject(receiver); + if (resultObject != nil) { + // Re-adopt the init result on this host object so that JS overrides + // returning `this` still have a valid native object. + object_ = resultObject; + ownsObject_ = true; + wrapperRetainedObject_ = true; + if (lifetimeState_ != nullptr) { + lifetimeState_->setObject(object_); + } + [object_ retain]; + if (classWrapper) { + bridge_->setObjectExpando(runtime, resultObject, + "__nativeApiClassWrapper", + Value(runtime, *classWrapper)); + if (result.isObject()) { + Value prototypeValue = classWrapper->getProperty(runtime, "prototype"); + if (prototypeValue.isObject()) { + Object resultValue = result.asObject(runtime); + Object prototype = prototypeValue.asObject(runtime); + SetNativeApiObjectPrototype(runtime, resultValue, prototype); + } + } + } + } + } + return result; + } + + Value callPreparedObjectSelector( + Runtime& runtime, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass = Nil) { + id receiver = object_; + if (receiver == nil) { + throw JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + const bool initializer = preparedObjCInvocationIsInit(prepared); + std::optional classWrapper; + if (initializer) { + Value classWrapperValue = bridge_->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + classWrapper.emplace(classWrapperValue.asObject(runtime)); + } + bridge_->forgetRoundTripValue(runtime, receiver); + bridge_->forgetObjectExpandos(receiver); + } + + Value result = callPreparedObjCSelector( + runtime, bridge_, receiver, false, prepared, args, count, + dispatchSuperClass); + if (initializer) { + id resultObject = nativeObjectFromValue(runtime, result); + disownObject(receiver); + if (resultObject != nil) { + // Re-adopt the init result on this host object so that JS overrides + // returning `this` still have a valid native object. + object_ = resultObject; + ownsObject_ = true; + wrapperRetainedObject_ = true; + if (lifetimeState_ != nullptr) { + lifetimeState_->setObject(object_); + } + [object_ retain]; + if (classWrapper) { + bridge_->setObjectExpando(runtime, resultObject, + "__nativeApiClassWrapper", + Value(runtime, *classWrapper)); + if (result.isObject()) { + Value prototypeValue = classWrapper->getProperty(runtime, "prototype"); + if (prototypeValue.isObject()) { + Object resultValue = result.asObject(runtime); + Object prototype = prototypeValue.asObject(runtime); + SetNativeApiObjectPrototype(runtime, resultValue, prototype); + } + } + } } } return result; } + Value prototypeFunctionForProperty(Runtime& runtime, + const std::string& property) { + if (object_ == nil || property.empty()) { + return Value::undefined(); + } + + Value classWrapperValue = bridge_->findObjectExpando( + runtime, object_, "__nativeApiClassWrapper"); + if (!classWrapperValue.isObject()) { + classWrapperValue = bridge_->findClassValue(runtime, object_getClass(object_)); + } + if (!classWrapperValue.isObject()) { + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { + classWrapperValue = bridge_->findClassValue( + runtime, objc_lookUpClass(symbol->runtimeName.c_str())); + } + } + if (!classWrapperValue.isObject()) { + return Value::undefined(); + } + + Object classWrapper = classWrapperValue.asObject(runtime); + Value prototypeValue = classWrapper.getProperty(runtime, "prototype"); + if (!prototypeValue.isObject()) { + return Value::undefined(); + } + + Object objectConstructor = + runtime.global().getPropertyAsObject(runtime, "Object"); + Function getOwnPropertyDescriptor = + objectConstructor.getPropertyAsFunction(runtime, + "getOwnPropertyDescriptor"); + Function getPrototypeOf = + objectConstructor.getPropertyAsFunction(runtime, "getPrototypeOf"); + Value propertyName = makeString(runtime, property); + Value currentValue(runtime, prototypeValue); + + for (size_t depth = 0; depth < 64 && currentValue.isObject(); depth++) { + Object current = currentValue.asObject(runtime); + Value descriptorValue = + getOwnPropertyDescriptor.call(runtime, Value(runtime, current), + propertyName); + if (descriptorValue.isObject()) { + Value functionValue = + descriptorValue.asObject(runtime).getProperty(runtime, "value"); + if (functionValue.isObject() && + functionValue.asObject(runtime).isFunction(runtime)) { + bridge_->setObjectExpando(runtime, object_, property, functionValue); + return functionValue; + } + return Value::undefined(); + } + currentValue = + getPrototypeOf.call(runtime, Value(runtime, current)); + } + + return Value::undefined(); + } + + // Invoke a JS-prototype getter accessor with this instance as the receiver. + // Sets *found and returns the resolved value. + Value resolveEnginePrototypeGetter(Runtime& runtime, + const std::string& property, bool* found) { + *found = false; + if (object_ == nil || property.empty()) { + return Value::undefined(); + } + Value classWrapperValue = + bridge_->findObjectExpando(runtime, object_, "__nativeApiClassWrapper"); + if (!classWrapperValue.isObject()) { + classWrapperValue = + bridge_->findClassValue(runtime, object_getClass(object_)); + } + if (!classWrapperValue.isObject()) { + return Value::undefined(); + } + Value prototypeValue = + classWrapperValue.asObject(runtime).getProperty(runtime, "prototype"); + if (!prototypeValue.isObject()) { + return Value::undefined(); + } + Object objectConstructor = + runtime.global().getPropertyAsObject(runtime, "Object"); + Function getOwnPropertyDescriptor = + objectConstructor.getPropertyAsFunction(runtime, "getOwnPropertyDescriptor"); + Function getPrototypeOf = + objectConstructor.getPropertyAsFunction(runtime, "getPrototypeOf"); + Value propertyName = makeString(runtime, property); + Value currentValue(runtime, prototypeValue); + for (size_t depth = 0; depth < 64 && currentValue.isObject(); depth++) { + Object current = currentValue.asObject(runtime); + Value descriptorValue = getOwnPropertyDescriptor.call( + runtime, Value(runtime, current), propertyName); + if (descriptorValue.isObject()) { + Object descriptor = descriptorValue.asObject(runtime); + Value getterValue = descriptor.getProperty(runtime, "get"); + if (getterValue.isObject() && + getterValue.asObject(runtime).isFunction(runtime)) { + Value thisValue = bridge_->findRoundTripValue(runtime, object_, + nullptr, true); + if (thisValue.isObject()) { + *found = true; + return getterValue.asObject(runtime).asFunction(runtime).callWithThis( + runtime, thisValue.asObject(runtime), + static_cast(nullptr), static_cast(0)); + } + } + Value dataValue = descriptor.getProperty(runtime, "value"); + if (!dataValue.isUndefined()) { + *found = true; + return dataValue; + } + return Value::undefined(); + } + currentValue = getPrototypeOf.call(runtime, Value(runtime, current)); + } + return Value::undefined(); + } + + // Invoke a JS-prototype setter accessor with this instance as the receiver. + // Returns true when a setter was found and invoked. + bool invokeEnginePrototypeSetter(Runtime& runtime, const std::string& property, + const Value& value) { + if (object_ == nil || property.empty()) { + return false; + } + Value classWrapperValue = + bridge_->findObjectExpando(runtime, object_, "__nativeApiClassWrapper"); + if (!classWrapperValue.isObject()) { + classWrapperValue = + bridge_->findClassValue(runtime, object_getClass(object_)); + } + if (!classWrapperValue.isObject()) { + return false; + } + Value prototypeValue = + classWrapperValue.asObject(runtime).getProperty(runtime, "prototype"); + if (!prototypeValue.isObject()) { + return false; + } + Object objectConstructor = + runtime.global().getPropertyAsObject(runtime, "Object"); + Function getOwnPropertyDescriptor = + objectConstructor.getPropertyAsFunction(runtime, "getOwnPropertyDescriptor"); + Function getPrototypeOf = + objectConstructor.getPropertyAsFunction(runtime, "getPrototypeOf"); + Value propertyName = makeString(runtime, property); + Value currentValue(runtime, prototypeValue); + for (size_t depth = 0; depth < 64 && currentValue.isObject(); depth++) { + Object current = currentValue.asObject(runtime); + Value descriptorValue = getOwnPropertyDescriptor.call( + runtime, Value(runtime, current), propertyName); + if (descriptorValue.isObject()) { + Value setterValue = + descriptorValue.asObject(runtime).getProperty(runtime, "set"); + if (setterValue.isObject() && + setterValue.asObject(runtime).isFunction(runtime)) { + Value thisValue = bridge_->findRoundTripValue(runtime, object_, + nullptr, true); + if (thisValue.isObject()) { + Value args[] = {Value(runtime, value)}; + setterValue.asObject(runtime).asFunction(runtime).callWithThis( + runtime, thisValue.asObject(runtime), + static_cast(args), static_cast(1)); + return true; + } + } + return false; + } + currentValue = getPrototypeOf.call(runtime, Value(runtime, current)); + } + return false; + } + Value get(Runtime& runtime, const PropNameID& name) override { std::string property = name.utf8(runtime); + + // Fast path: check expando cache first (hot path for method calls). + Value expando = bridge_->findObjectExpando(runtime, object_, property); + if (!expando.isUndefined()) { + return expando; + } + + // Fast path: cached metadata property-getter resolution. Skips the + // special-name chain + per-access metadata discovery for hot getters + // (hash/length/count/...). Only populated for genuine non-extended + // metadata property members below, so a hit is always safe to serve. + if (object_ != nil) { + if (const auto* cached = bridge_->findCachedPropertyGetter( + object_getClass(object_), property)) { + if (cached->preparedInvocation != nullptr) { + return callPreparedObjectSelector(runtime, + *cached->preparedInvocation, + nullptr, 0); + } + return callObjectSelector(runtime, cached->selectorName, cached->member, + nullptr, 0); + } + } + if (property == "kind") { return makeString(runtime, "object"); } @@ -617,13 +1208,58 @@ class NativeApiObjectHostObject final if (object_ == nil) { return Value::undefined(); } + // Check class wrapper expando first (set during class setup). Value classWrapper = bridge_->findObjectExpando( runtime, object_, "__nativeApiClassWrapper"); if (classWrapper.isObject()) { return classWrapper; } + // Try cached class value. + Class objClass = object_getClass(object_); + Value cached = bridge_->findClassValue(runtime, objClass); + if (!cached.isUndefined()) { + return cached; + } + // Resolve through metadata and global. NativeApiSymbol symbol = - nativeApiSymbolForRuntimeClass(bridge_, object_getClass(object_)); + nativeApiSymbolForRuntimeClass(bridge_, objClass); + // Try the global by the symbol's name (which may be the JS-friendly name + // from metadata, different from the ObjC runtime name for Swift classes). + if (!symbol.name.empty()) { + Object global = runtime.global(); + if (global.hasProperty(runtime, symbol.name.c_str())) { + Value globalClass = global.getProperty(runtime, symbol.name.c_str()); + if (!globalClass.isUndefined() && !globalClass.isNull()) { + return globalClass; + } + } + // Also try the runtime name if different. + if (symbol.runtimeName != symbol.name && + global.hasProperty(runtime, symbol.runtimeName.c_str())) { + Value globalClass = global.getProperty(runtime, symbol.runtimeName.c_str()); + if (!globalClass.isUndefined() && !globalClass.isNull()) { + return globalClass; + } + } + } + // For Swift classes: try findClass by runtime name which checks + // classSymbolsByRuntimeName_ and may return a different JS-friendly name. + if (bridge_ != nullptr) { + const char* runtimeName = class_getName(objClass); + if (runtimeName != nullptr) { + if (const NativeApiSymbol* found = bridge_->findClass(runtimeName)) { + if (found->name != symbol.name) { + Object global = runtime.global(); + if (global.hasProperty(runtime, found->name.c_str())) { + Value globalClass = global.getProperty(runtime, found->name.c_str()); + if (!globalClass.isUndefined() && !globalClass.isNull()) { + return globalClass; + } + } + } + } + } + } return makeNativeClassValue(runtime, bridge_, std::move(symbol)); } if (property == "superclass") { @@ -634,6 +1270,22 @@ class NativeApiObjectHostObject final if (superclass == Nil) { return Value::null(); } + // Try cached class value. + Value cached = bridge_->findClassValue(runtime, superclass); + if (!cached.isUndefined()) { + return cached; + } + // Try global lookup by class name. + const char* name = class_getName(superclass); + if (name != nullptr && name[0] != '\0') { + Object global = runtime.global(); + if (global.hasProperty(runtime, name)) { + Value globalClass = global.getProperty(runtime, name); + if (!globalClass.isUndefined()) { + return globalClass; + } + } + } NativeApiSymbol symbol = nativeApiSymbolForRuntimeClass(bridge_, superclass); return makeNativeClassValue(runtime, bridge_, std::move(symbol)); } @@ -660,7 +1312,7 @@ class NativeApiObjectHostObject final return self->callObjectSelector(runtime, selectorName, nullptr, args + 1, count - 1); } - return callObjCSelector(runtime, bridge, object, false, selectorName, + return callObjCSelector(runtime, bridge, object, false, selectorName, nullptr, args + 1, count - 1); }); } @@ -673,20 +1325,38 @@ class NativeApiObjectHostObject final 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."); + throw JSError(runtime, "Unmanaged value has already been consumed."); } id object = self->object_; + bool ownsObject = self->ownsObject_; + bool wrapperRetainedObject = self->wrapperRetainedObject_; if (self->bridge_ != nullptr) { - self->bridge_->forgetRoundTripValue(object); - } - if (self->ownsObject_) { - [object release]; + self->bridge_->forgetRoundTripValue(runtime, object); + self->bridge_->forgetObjectExpandos(object); } self->object_ = nil; self->ownsObject_ = false; + self->wrapperRetainedObject_ = false; + if (self->lifetimeState_ != nullptr) { + self->lifetimeState_->clear(); + } self->consumed_ = true; - return makeNativeObjectValue(runtime, self->bridge_, object, retained); + const bool releasePreviousOwnership = + ownsObject && (!retained || wrapperRetainedObject); + try { + Value result = + makeNativeObjectValue(runtime, self->bridge_, object, retained); + if (releasePreviousOwnership) { + [object release]; + } + return result; + } catch (...) { + if (releasePreviousOwnership) { + [object release]; + } + throw; + } }); } if (property == "toString") { @@ -694,10 +1364,11 @@ class NativeApiObjectHostObject final 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 ?: ""); - }); + return NativeApiObjectHostObject::descriptionString(runtime, object); + }); + } + if (property == "description") { + return descriptionString(runtime, object_); } if (property == "URL" && object_ != nil && [object_ respondsToSelector:@selector(URL)]) { @@ -714,7 +1385,7 @@ class NativeApiObjectHostObject final size_t) -> Value { if (object == nil || ![object conformsToProtocol:@protocol(NSFastEnumeration)]) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Object does not conform to NSFastEnumeration."); } return Object::createFromHostObject( @@ -747,7 +1418,7 @@ class NativeApiObjectHostObject final selectorName, nullptr, args, count); } } - throw facebook::jsi::JSError( + throw JSError( runtime, "NSColor RGB initializer is not available."); }); } @@ -764,7 +1435,7 @@ class NativeApiObjectHostObject final [bridge, timerClass](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 6) { - throw facebook::jsi::JSError( + throw JSError( runtime, "NSTimer initializer expects six arguments."); } return callObjCSelector( @@ -775,57 +1446,6 @@ class NativeApiObjectHostObject final } } - 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") { @@ -836,117 +1456,158 @@ class NativeApiObjectHostObject final return Value::undefined(); } id element = [array objectAtIndex:*index]; - NativeApiJsiType elementType = nativeObjectReturnType(); + NativeApiType elementType = nativeObjectReturnType(); return convertNativeReturnValue(runtime, bridge_, elementType, &element); } } - if (object_ != nil) { + if (object_ != nil && property == "length" && + ![object_ respondsToSelector:@selector(length)]) { + return Value::undefined(); + } + if (object_ != nil && property == "count" && + ![object_ respondsToSelector:@selector(count)]) { + return Value::undefined(); + } + + // For JS-extended instances, metadata property accessors live on the + // prototype chain (native accessors plus any JS overrides), so defer to the + // engine instead of reading the native property here and shadowing a JS + // override. + bool isEngineExtendedInstance = + object_ != nil && + class_conformsToProtocol(object_getClass(object_), + @protocol(NativeApiClassBuilderProtocol)); + + if (object_ != nil && !isEngineExtendedInstance) { 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 ([object_ respondsToSelector:selector]) { - return callObjectSelector(runtime, propertyMember->selectorName, - propertyMember, nullptr, 0); - } - std::string booleanSelectorName = - booleanGetterSelectorForProperty(property); - if (booleanSelectorName != propertyMember->selectorName) { - SEL booleanSelector = sel_getUid(booleanSelectorName.c_str()); - if ([object_ respondsToSelector:booleanSelector]) { - NativeApiMember getterMember = *propertyMember; - getterMember.selectorName = booleanSelectorName; - return callObjectSelector(runtime, getterMember.selectorName, - &getterMember, nullptr, 0); + if (auto getter = respondingPropertyGetterSelector( + object_, property, propertyMember->selectorName)) { + NativeApiMember getterMember = *propertyMember; + getterMember.selectorName = *getter; + std::shared_ptr preparedGetter; + try { + preparedGetter = prepareNativeApiObjCInvocation( + runtime, bridge_, object_getClass(object_), false, + getterMember.selectorName, &getterMember); + } catch (const std::exception&) { } + bridge_->cachePropertyGetter(object_getClass(object_), property, + propertyMember, + getterMember.selectorName, + preparedGetter); + if (preparedGetter != nullptr) { + return callPreparedObjectSelector(runtime, *preparedGetter, + nullptr, 0); + } + return callObjectSelector(runtime, getterMember.selectorName, + &getterMember, 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); - }); + // Resolve metadata methods to a bound selector-group function. The + // bound receiver keeps method-call semantics correct even on engines + // whose host-object interceptor does not preserve `this`, while the + // engine backend can still use its direct selector-group/GSD path. + if (hasMethodMember(members, property, false)) { + auto selectors = + selectorGroupEntriesForMethod(members, property, false); + if (selectors != nullptr) { + auto preparedInvocations = std::make_shared>>( + selectors->size()); + Value methodFunction = CreateNativeApiBoundSelectorGroupFunction( + runtime, bridge_, object_getClass(object_), shared_from_this(), + selectors, preparedInvocations); + // Cache the resolved host function so repeated method access does + // not reallocate it on every call (hot path). + bridge_->setObjectExpando(runtime, object_, property, + methodFunction); + return methodFunction; + } } } + } - 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); - } + Value prototypeFunction = prototypeFunctionForProperty(runtime, property); + if (!prototypeFunction.isUndefined()) { + return prototypeFunction; + } - 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); - }); + // JS-subclassed instances own their members in JS (prototype accessors and + // methods); defer so the engine resolves them instead of the bridge + // returning a registered getter IMP as a raw callable. + if (isEngineExtendedInstance) { +#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE + // Engines whose exotic property handler invokes prototype accessors with + // the wrong receiver need the JS-prototype getter resolved here with this + // instance as the receiver. + bool found = false; + Value resolved = resolveEnginePrototypeGetter(runtime, property, &found); + if (found) { + return resolved; } +#endif + if (auto selector = + runtimeReadablePropertyGetter(object_, property)) { + return callObjectSelector(runtime, *selector, nullptr, nullptr, 0); + } + return Value::undefined(); + } - 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); - } + if (object_ != nil) { + // A runtime ObjC property (e.g. from a protocol the concrete, non-metadata + // class adopts) must be invoked as a getter, not returned as a callable. + if (objc_property_t prop = + class_getProperty(object_getClass(object_), property.c_str())) { + std::string getter = property; + if (char* customGetter = property_copyAttributeValue(prop, "G")) { + getter = customGetter; + free(customGetter); + } + if (auto selector = + respondingPropertyGetterSelector(object_, property, getter)) { + return callObjectSelector(runtime, *selector, nullptr, nullptr, 0); } } } + if (object_ != nil && + hasRuntimeMemberForName(object_getClass(object_), false, property)) { + std::weak_ptr weakSelf = shared_from_this(); + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, memberName](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + auto self = weakSelf.lock(); + if (!self || self->object_ == nil) { + throw JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + auto selectorName = selectRuntimeSelectorForName( + object_getClass(self->object_), false, memberName, count); + if (!selectorName) { + throw JSError(runtime, + "Objective-C selector is not available: " + + memberName); + } + return self->callObjectSelector(runtime, *selectorName, nullptr, + args, count); + }); + } + return Value::undefined(); } - void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + NativeApiHostSetResult 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."); + throw JSError(runtime, "Cannot set property on nil object."); } if (const NativeApiSymbol* symbol = @@ -955,7 +1616,7 @@ class NativeApiObjectHostObject final if (const NativeApiMember* propertyMember = selectWritablePropertyMember(members, property, false)) { if (propertyMember->readonly) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Attempted to assign to readonly property."); } NativeApiMember setterMember = *propertyMember; @@ -964,20 +1625,38 @@ class NativeApiObjectHostObject final Value args[] = {Value(runtime, value)}; callObjCSelector(runtime, bridge_, object_, false, setterMember.selectorName, &setterMember, args, 1); - return; + NATIVE_API_SET_RETURN(true); } } - std::string setterSelectorName = setterSelectorForProperty(property); - SEL selector = sel_getUid(setterSelectorName.c_str()); - if ([object_ respondsToSelector:selector]) { + if (auto setterSelectorName = + runtimeWritablePropertySetter(object_, property)) { Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, object_, false, setterSelectorName, - nullptr, args, 1); - return; + callObjCSelector(runtime, bridge_, object_, false, + *setterSelectorName, nullptr, args, 1); + NATIVE_API_SET_RETURN(true); + } + + // For JS-subclassed instances, an unknown property is owned by the JS + // prototype (e.g. a JS-defined accessor); defer so the engine runs it instead of + // shadowing it with a bridge expando. + if (class_conformsToProtocol(object_getClass(object_), + @protocol(NativeApiClassBuilderProtocol))) { +#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE + // Engines whose exotic property storage doesn't fall back to own + // properties need the JS-owned set resolved here: invoke a JS-prototype + // setter if present, otherwise store the value as a bridge expando. + if (!invokeEnginePrototypeSetter(runtime, property, value)) { + storeOwnExpando(runtime, property, value); + } + NATIVE_API_SET_RETURN(true); +#else + NATIVE_API_SET_RETURN(false); +#endif } bridge_->setObjectExpando(runtime, object_, property, value); + NATIVE_API_SET_RETURN(true); } std::vector getPropertyNames(Runtime& runtime) override { @@ -998,15 +1677,17 @@ class NativeApiObjectHostObject final } private: - std::shared_ptr bridge_; + std::shared_ptr bridge_; id object_ = nil; bool ownsObject_ = false; + bool wrapperRetainedObject_ = false; bool consumed_ = false; + std::shared_ptr lifetimeState_; }; class NativeApiClassHostObject final : public HostObject { public: - NativeApiClassHostObject(std::shared_ptr bridge, + NativeApiClassHostObject(std::shared_ptr bridge, NativeApiSymbol symbol) : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} @@ -1014,6 +1695,16 @@ class NativeApiClassHostObject final : public HostObject { return objc_lookUpClass(symbol_.runtimeName.c_str()); } + static Class classRespondingToClassSelector(Class cls, SEL selector) { + for (Class current = cls; current != Nil; + current = class_getSuperclass(current)) { + if (class_getClassMethod(current, selector) != nullptr) { + return current; + } + } + return Nil; + } + Value get(Runtime& runtime, const PropNameID& name) override { std::string property = name.utf8(runtime); if (property == "kind") { @@ -1042,6 +1733,11 @@ class NativeApiClassHostObject final : public HostObject { } return makeNativeClassValue(runtime, bridge_, *superclass); } + if (property == "__runtimeStaticMembers" || + property == "__runtimeInstanceMembers") { + return runtimeMembersArray(runtime, nativeClass(), + property == "__runtimeStaticMembers"); + } if (property == "__staticMembers" || property == "__instanceMembers") { bool staticMembers = property == "__staticMembers"; const auto& members = bridge_->surfaceMembersForClass(symbol_); @@ -1062,6 +1758,13 @@ class NativeApiClassHostObject final : public HostObject { static_cast(selectorArgumentCount(member.selectorName))); descriptor.setProperty(runtime, "property", member.property); descriptor.setProperty(runtime, "readonly", member.readonly); + descriptor.setProperty(runtime, "signatureOffset", + static_cast(member.signatureOffset)); + descriptor.setProperty( + runtime, "setterSignatureOffset", + static_cast(member.setterSignatureOffset)); + descriptor.setProperty(runtime, "flags", + static_cast(member.flags)); descriptor.setProperty(runtime, "setterSelectorName", makeString(runtime, member.setterSelectorName)); result.setValueAtIndex(runtime, index++, descriptor); @@ -1078,7 +1781,7 @@ class NativeApiClassHostObject final : public HostObject { [symbol = symbol_](Runtime& runtime, const Value&, const Value*, size_t) -> Value { return makeString(runtime, - "[NativeApiJsiClass " + symbol.name + "]"); + "[NativeApiClass " + symbol.name + "]"); }); } if (property == "construct" || property == "alloc" || property == "new") { @@ -1090,7 +1793,7 @@ class NativeApiClassHostObject final : public HostObject { const Value* args, size_t count) -> Value { Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); if (cls == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Objective-C class is not available: " + symbol.name); } @@ -1103,16 +1806,38 @@ class NativeApiClassHostObject final : public HostObject { } else if (args[0].isObject()) { Object object = args[0].asObject(runtime); if (object.isHostObject(runtime)) { - pointer = object - .getHostObject( - runtime) - ->pointer(); + auto pointerHost = + object.getHostObject( + runtime); + pointer = pointerHost->pointer(); + if (pointerHost->backingValue() != nullptr) { + Value backingValue(runtime, *pointerHost->backingValue()); + id backingObject = + NativeApiObjectHostObject::nativeObjectFromValue( + runtime, backingValue); + if (backingObject == static_cast(pointer) && + backingObject != nil && + [backingObject isKindOfClass:cls]) { + return backingValue; + } + } } else if (object.isHostObject( runtime)) { - pointer = object - .getHostObject( - runtime) - ->data(); + auto referenceHost = + object.getHostObject( + runtime); + pointer = referenceHost->data(); + if (referenceHost->backingValue() != nullptr) { + Value backingValue(runtime, *referenceHost->backingValue()); + id backingObject = + NativeApiObjectHostObject::nativeObjectFromValue( + runtime, backingValue); + if (backingObject == static_cast(pointer) && + backingObject != nil && + [backingObject isKindOfClass:cls]) { + return backingValue; + } + } } else if (object.isHostObject( runtime)) { pointer = object @@ -1127,18 +1852,20 @@ class NativeApiClassHostObject final : public HostObject { if (property == "new") { if (count != 0) { - throw facebook::jsi::JSError( + throw JSError( runtime, "new does not take arguments; use invoke for an " "explicit Objective-C selector."); } - result = [[cls alloc] init]; + performDirectObjCInvocation(runtime, + [&]() { result = [[cls alloc] init]; }); } else { if (count != 0) { - throw facebook::jsi::JSError( + throw JSError( runtime, "alloc does not take arguments; call invoke on the " "allocated object for an explicit init selector."); } - result = [cls alloc]; + performDirectObjCInvocation(runtime, + [&]() { result = [cls alloc]; }); } return makeNativeObjectValue(runtime, bridge, result, true); @@ -1155,7 +1882,7 @@ class NativeApiClassHostObject final : public HostObject { readStringArg(runtime, args, count, 0, "selector"); Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); if (cls == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Objective-C class is not available: " + symbol.name); } return callObjCSelector(runtime, bridge, static_cast(cls), true, @@ -1164,6 +1891,14 @@ class NativeApiClassHostObject final : public HostObject { }); } + Class cls = nativeClass(); + if (cls != Nil) { + Value expando = bridge_->findObjectExpando(runtime, cls, property); + if (!expando.isUndefined()) { + return expando; + } + } + const auto& members = bridge_->membersForClass(symbol_); if (const NativeApiMember* propertyMember = selectWritablePropertyMember(members, property, true)) { @@ -1171,75 +1906,40 @@ class NativeApiClassHostObject final : public HostObject { auto symbol = symbol_; Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); if (cls == nil) { - throw facebook::jsi::JSError( + throw 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 callObjCSelector(runtime, bridge, static_cast(cls), true, + Class dispatchClass = classRespondingToClassSelector(cls, selector); + if (dispatchClass != Nil) { + return callObjCSelector(runtime, bridge, static_cast(dispatchClass), 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); - }); + auto selectors = selectorGroupEntriesForMethod(members, property, true); + if (selectors != nullptr) { + if (cls == Nil) { + throw JSError( + runtime, "Objective-C class is not available: " + symbol_.name); } + auto preparedInvocations = std::make_shared>>(selectors->size()); + Value methodFunction = CreateNativeApiSelectorGroupFunction( + runtime, bridge_, cls, true, selectors, preparedInvocations); + bridge_->setObjectExpando(runtime, cls, property, methodFunction); + return methodFunction; } return Value::undefined(); } - void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + NativeApiHostSetResult 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( + throw JSError( runtime, "Objective-C class is not available: " + symbol_.name); } @@ -1247,28 +1947,26 @@ class NativeApiClassHostObject final : public HostObject { if (const NativeApiMember* propertyMember = selectPropertyMember(members, property, true)) { if (propertyMember->readonly) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Attempted to assign to readonly property."); } NativeApiMember setterMember = *propertyMember; setterMember.selectorName = propertyMember->setterSelectorName; setterMember.signatureOffset = propertyMember->setterSignatureOffset; + SEL selector = sel_getUid(setterMember.selectorName.c_str()); + Class dispatchClass = classRespondingToClassSelector(cls, selector); + if (dispatchClass == Nil) { + throw JSError(runtime, + "Objective-C selector is not available: " + + setterMember.selectorName); + } Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, static_cast(cls), true, + callObjCSelector(runtime, bridge_, static_cast(dispatchClass), true, setterMember.selectorName, &setterMember, args, 1); - return; + NATIVE_API_SET_RETURN(true); } - 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, + throw JSError(runtime, "No writable native property: " + property); } @@ -1290,29 +1988,63 @@ class NativeApiClassHostObject final : public HostObject { } private: - std::shared_ptr bridge_; + std::shared_ptr bridge_; NativeApiSymbol symbol_; }; Value makeNativeObjectValue(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, id object, bool ownsObject) { if (object == nil) { return Value::null(); } - Value cached = bridge->findRoundTripValue(runtime, object); + Value cached = bridge->findRoundTripValue(runtime, object, nullptr, true); if (!cached.isUndefined()) { - if (ownsObject) { - [object release]; + // A consumed wrapper (e.g. an alloc'd placeholder singleton already passed + // to an initializer) must not be reused: drop the stale entry and re-wrap. + auto cachedHost = + cached.isObject() + ? cached.asObject(runtime).getHostObject(runtime) + : nullptr; + if (cachedHost != nullptr && cachedHost->object() != nil) { + if (ownsObject) { + [object release]; + } + return cached; } - return cached; + bridge->forgetRoundTripValue(runtime, object); } - Object result = Object::createFromHostObject( + Object result = createNativeInstanceHostObject( runtime, std::make_shared(bridge, object, ownsObject)); - bridge->rememberRoundTripValue(runtime, object, Value(runtime, result)); + Value prototypeValue = Value::undefined(); + Value classWrapperValue = + bridge->findObjectExpando(runtime, object, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + Object classWrapper = classWrapperValue.asObject(runtime); + prototypeValue = classWrapper.getProperty(runtime, "prototype"); + } + if (!prototypeValue.isObject()) { + prototypeValue = bridge->findClassPrototype(runtime, object_getClass(object)); + } + if (!prototypeValue.isObject()) { + Value classWrapper = makeNativeClassValue( + runtime, bridge, + nativeApiSymbolForRuntimeClass(bridge, object_getClass(object))); + if (classWrapper.isObject()) { + prototypeValue = + classWrapper.asObject(runtime).getProperty(runtime, "prototype"); + } + } + if (prototypeValue.isObject()) { + Object prototype = prototypeValue.asObject(runtime); + SetNativeApiObjectPrototype(runtime, result, prototype); + } + bridge->rememberScopedRoundTripValue( + runtime, object, Value(runtime, result), + nativeObjectIsStringLike(object)); return result; } @@ -1424,7 +2156,7 @@ Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, } Value makeNativeClassValue(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, NativeApiSymbol symbol) { Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); Value cachedClass = bridge->findClassValue(runtime, cls); @@ -1457,7 +2189,7 @@ Protocol* lookupProtocolByNativeName(const std::string& name) { class NativeApiProtocolHostObject final : public HostObject { public: - NativeApiProtocolHostObject(std::shared_ptr bridge, + NativeApiProtocolHostObject(std::shared_ptr bridge, NativeApiSymbol symbol) : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} @@ -1514,7 +2246,7 @@ class NativeApiProtocolHostObject final : public HostObject { runtime, PropNameID::forAscii(runtime, "toString"), 0, [symbol](Runtime& runtime, const Value&, const Value*, size_t) -> Value { return makeString(runtime, - "[NativeApiJsiProtocol " + symbol.name + "]"); + "[NativeApiProtocol " + symbol.name + "]"); }); } const auto& members = bridge_->membersForProtocol(symbol_); @@ -1526,13 +2258,23 @@ class NativeApiProtocolHostObject final : public HostObject { selectPropertyMember(members, property, false)) { return makeProtocolPropertyGetter(runtime, *propertyMember, true); } - if (const NativeApiMember* methodMember = - selectMethodMember(members, property, true, 0)) { - return makeProtocolMemberFunction(runtime, *methodMember, true); + for (const auto& member : members) { + if (member.property || member.name != property) { + continue; + } + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic) { + return makeProtocolMemberFunction(runtime, member, true); + } } - if (const NativeApiMember* methodMember = - selectMethodMember(members, property, false, 0)) { - return makeProtocolMemberFunction(runtime, *methodMember, true); + for (const auto& member : members) { + if (member.property || member.name != property) { + continue; + } + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (!memberIsStatic) { + return makeProtocolMemberFunction(runtime, member, true); + } } return Value::undefined(); } @@ -1625,7 +2367,7 @@ class NativeApiProtocolHostObject final : public HostObject { } if (receiver == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Protocol member requires a native receiver."); } return callObjCSelector(runtime, bridge, receiver, receiverIsClass, @@ -1654,11 +2396,17 @@ class NativeApiProtocolHostObject final : public HostObject { } if (receiver == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Protocol property requires a native receiver."); } + NativeApiMember getterMember = member; + if (auto selector = respondingPropertyGetterSelector( + receiver, member.name, member.selectorName)) { + getterMember.selectorName = *selector; + } return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - member.selectorName, &member, nullptr, 0); + getterMember.selectorName, &getterMember, + nullptr, 0); }); } @@ -1685,11 +2433,11 @@ class NativeApiProtocolHostObject final : public HostObject { } if (receiver == nil) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Protocol property requires a native receiver."); } if (count < 1) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Protocol property setter expects a value."); } @@ -1726,12 +2474,12 @@ class NativeApiProtocolHostObject final : public HostObject { } } - std::shared_ptr bridge_; + std::shared_ptr bridge_; NativeApiSymbol symbol_; }; Value makeNativeProtocolValue(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, NativeApiSymbol symbol) { Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); if (!globalValue.isUndefined()) { @@ -1742,7 +2490,7 @@ Value makeNativeProtocolValue(Runtime& runtime, std::make_shared(bridge, std::move(symbol))); } -Class nativeClassFromJsiObject(Runtime& runtime, const Object& object) { +Class nativeClassFromEngineObject(Runtime& runtime, const Object& object) { if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->nativeClass(); } diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h b/NativeScript/ffi/shared/bridge/Install.mm similarity index 77% rename from NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h rename to NativeScript/ffi/shared/bridge/Install.mm index 6e711d5be..de8716fd9 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h +++ b/NativeScript/ffi/shared/bridge/Install.mm @@ -1,19 +1,18 @@ -Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { - auto bridge = std::make_shared(config); - return Object::createFromHostObject( - runtime, std::make_shared(std::move(bridge))); +Object CreateNativeApi(Runtime& runtime, const NativeApiConfig& config) { + auto bridge = std::make_shared(config); + return Object::createFromHostObject(runtime, + std::make_shared(std::move(bridge))); } -void NativeApiJsiWriteSmokeStage(const char* stage) { +void NativeApiWriteSmokeStage(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 : ""]; + 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]; } @@ -88,9 +87,9 @@ std::string jsStringLiteral(const char* value) { return result; } -void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) { - NativeApiJsiWriteSmokeStage("jsi:globals:before-eval"); - static const char* GlobalInstaller = R"JSI_GLOBALS( +void InstallNativeApiGlobalSymbols(Runtime& runtime, const char* globalName) { + NativeApiWriteSmokeStage("engine:globals:before-eval"); + static const char* GlobalInstaller = R"Engine_GLOBALS( (function(nativeApiGlobalName) { 'use strict'; var api = globalThis[nativeApiGlobalName]; @@ -238,28 +237,6 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) 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, @@ -320,29 +297,54 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } function setDescriptorValue(target, property, receiver, value) { - var descriptor = Object.getOwnPropertyDescriptor(target, property); - if (!descriptor) { + for (var current = target; current; current = Object.getPrototypeOf(current)) { + var descriptor = Object.getOwnPropertyDescriptor(current, property); + if (!descriptor) { + continue; + } + if (typeof descriptor.set === 'function') { + descriptor.set.call(receiver, value); + return true; + } + if (descriptor.writable) { + if (receiver && receiver !== current) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + } else { + current[property] = value; + } + return true; + } 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; - } + return false; + } + + function setInheritedNativeClassValue(target, property, value) { + for (var current = Object.getPrototypeOf(target); + current && current !== Function.prototype; + current = Object.getPrototypeOf(current)) { + var nativeClassValue; + try { + nativeClassValue = current.__nativeApiClass; + } catch (_) { + nativeClassValue = null; + } + if (!nativeClassValue) { + continue; + } + try { + nativeClassValue[property] = value; + return true; + } catch (_) { + } + } + return false; + } function isConstructorOptions(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) { @@ -452,7 +454,9 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } function initializerMembers(nativeClass, argumentCount) { - var members = nativeClass.__instanceMembers || []; + var metadataMembers = nativeClass.__instanceMembers || []; + var runtimeMembers = nativeClass.__runtimeInstanceMembers || []; + var members = metadataMembers.concat(runtimeMembers); var result = []; for (var i = 0; i < members.length; i++) { var member = members[i]; @@ -518,7 +522,7 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) /Objective-C selector is not available/.test(String(error.message || error)); } - function constructNativeInstance(nativeClass, args) { + function constructNativeInstance(nativeClass, args, rememberInstance) { if (args.length === 1 && args[0] && typeof args[0] === 'object' && @@ -554,6 +558,9 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) throw new Error('Native class cannot be allocated'); } var instance = nativeClass.alloc(); + if (typeof rememberInstance === 'function') { + instance = rememberInstance(instance); + } if (initializer.selectorName === 'init') { if (typeof instance.init !== 'function') { throw new Error('No initializer found that matches constructor invocation.'); @@ -562,11 +569,13 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } try { if (initializer.name && typeof instance[initializer.name] === 'function') { - return instance[initializer.name].apply(instance, actualArgs); + return instance[initializer.name](...actualArgs); } var invokeArgs = [initializer.selectorName]; - Array.prototype.push.apply(invokeArgs, actualArgs); - return instance.invoke.apply(instance, invokeArgs); + for (var invokeArgIndex = 0; invokeArgIndex < actualArgs.length; invokeArgIndex++) { + invokeArgs.push(actualArgs[invokeArgIndex]); + } + return instance.invoke(...invokeArgs); } catch (error) { if (unavailableInitializerError(error)) { throw new Error('No initializer found that matches constructor invocation.'); @@ -595,49 +604,45 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) 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 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 === 'function') { + return rememberClassOnInstance( + redirectedWrapper.call(this, ...args), + redirectConstructor + ); + } } - var instance = nativeClass.alloc(); - if (instance && typeof instance.init === 'function') { - return rememberInstanceClass(instance.init()); + if (args.length > 0) { + return rememberInstanceClass(constructNativeInstance(nativeClass, args, rememberInstanceClass)); } - 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', { + if (typeof nativeClass.new !== 'function') { + throw new Error('Native class cannot be initialized'); + } + return rememberInstanceClass(nativeClass.new()); + }; + 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, @@ -695,7 +700,10 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) enumerable: false, writable: true, value: function() { - return rememberInstanceClass(nativeClass.alloc.apply(nativeClass, arguments)); + if (arguments.length !== 0) { + throw new Error('alloc does not take arguments; use invoke for an explicit Objective-C selector.'); + } + return rememberInstanceClass(nativeClass.alloc()); } }); } catch (_) { @@ -709,14 +717,10 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) if (arguments.length !== 0) { throw new Error('new does not take arguments; use invoke for an explicit Objective-C selector.'); } - if (typeof nativeClass.alloc !== 'function') { - throw new Error('Native class cannot be allocated'); + if (typeof nativeClass.new !== 'function') { + throw new Error('Native class cannot be initialized'); } - var instance = nativeClass.alloc(); - if (instance && typeof instance.init === 'function') { - return rememberInstanceClass(instance.init()); - } - return rememberInstanceClass(instance); + return rememberInstanceClass(nativeClass.new()); } }); } catch (_) { @@ -741,17 +745,182 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } var basePrototypeTarget = {}; var classMembersInstalled = false; - function installClassMembers(target, members, receiverIsClass) { - if (!target || !members || typeof members.length !== 'number') { + function selectorArgumentCount(selectorName) { + var count = 0; + if (typeof selectorName !== 'string') { + return count; + } + for (var i = 0; i < selectorName.length; i++) { + if (selectorName.charCodeAt(i) === 58) { + count++; + } + } + return count; + } + function selectorDescriptor(member, selectorName, signatureOffset, argumentCount, runtimeOnly) { + return { + name: member.name || '', + selectorName: selectorName || '', + setterSelectorName: member.setterSelectorName || '', + signatureOffset: typeof signatureOffset === 'number' ? signatureOffset : 0, + setterSignatureOffset: typeof member.setterSignatureOffset === 'number' + ? member.setterSignatureOffset + : 0, + flags: typeof member.flags === 'number' ? member.flags : 0, + property: !!member.property, + readonly: !!member.readonly, + argumentCount: typeof argumentCount === 'number' + ? argumentCount + : selectorArgumentCount(selectorName), + runtimeOnly: !!runtimeOnly + }; + } + function addSelectorGroups(groups, members, runtimeOnly) { + if (!groups || !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)) { + if (!member || member.property || !member.name || !member.selectorName) { continue; } - try { - if (member.property) { + // Skip methods that need special interceptor handling with kNonMasking. + if (member.name === 'superclass' || member.name === 'class' || + member.name === 'constructor' || member.name === 'className') { + continue; + } + var argumentCount = typeof member.argumentCount === 'number' + ? member.argumentCount + : 0; + var group = groups[member.name]; + if (!group) { + group = []; + groups[member.name] = group; + } + if (group[argumentCount] === undefined) { + group[argumentCount] = selectorDescriptor( + member, + member.selectorName, + member.signatureOffset, + argumentCount, + runtimeOnly + ); + } + // Methods with a trailing NSError** out-parameter (selector ending in + // "error:") may be called with the error argument omitted, so register + // the error-omitted arity too. + if (argumentCount > 0 && + /error:$/.test(member.selectorName) && + group[argumentCount - 1] === undefined) { + group[argumentCount - 1] = selectorDescriptor( + member, + member.selectorName, + member.signatureOffset, + argumentCount - 1, + runtimeOnly + ); + } + } + } + function installSelectorGroups(target, groups, receiverIsClass) { + if (!target || !groups) { + return; + } + for (var name in groups) { + if (!Object.prototype.hasOwnProperty.call(groups, name) || + Object.prototype.hasOwnProperty.call(target, name)) { + continue; + } + var selectors = groups[name]; + if (!selectors || !selectors.length) { + continue; + } + var hasMetadataSelector = false; + for (var selectorIndex = 0; selectorIndex < selectors.length; selectorIndex++) { + if (selectors[selectorIndex] && !selectors[selectorIndex].runtimeOnly) { + hasMetadataSelector = true; + break; + } + } + if (!hasMetadataSelector && receiverIsClass && name in target) { + continue; + } + var selectorFunction = + api.__makeSelectorGroupFunction(nativeClass, !!receiverIsClass, selectors); + Object.defineProperty(target, name, { + configurable: true, + enumerable: false, + writable: true, + value: receiverIsClass + ? (function(fn, memberName) { + return function() { + if (this && typeof this === 'object' && this.kind === 'object') { + var baseArgs = [nativeClass, this, memberName]; + for (var baseArgIndex = 0; baseArgIndex < arguments.length; baseArgIndex++) { + baseArgs.push(arguments[baseArgIndex]); + } + return api.__invokeBase(...baseArgs); + } + var args = []; + for (var argIndex = 0; argIndex < arguments.length; argIndex++) { + args.push(arguments[argIndex]); + } + return rememberInstanceClass(fn(...args)); + }; + })(selectorFunction, name) + : selectorFunction + }); + } + } + function installClassMembers(target, members, receiverIsClass, runtimeMembers) { + var hasMetadataMembers = members && typeof members.length === 'number'; + var hasRuntimeMembers = runtimeMembers && typeof runtimeMembers.length === 'number'; + if (!target || (!hasMetadataMembers && !hasRuntimeMembers)) { + return; + } + var selectorGroups = Object.create(null); + addSelectorGroups(selectorGroups, members, false); + for (var i = 0; hasMetadataMembers && i < members.length; i++) { + var member = members[i]; + if (!member || !member.name) { + continue; + } + if (member.property) { + // Skip properties that need special interceptor handling (they + // return wrapped class constructors, not raw native values). + if (member.name === 'superclass' || member.name === 'class' || + member.name === 'constructor' || member.name === 'debugDescription' || + member.name === 'className') { + continue; + } + var existingDescriptor = Object.getOwnPropertyDescriptor(target, member.name); + if (existingDescriptor && + (typeof existingDescriptor.get === 'function' || + typeof existingDescriptor.set === 'function')) { + continue; + } + var getterFunction = member.selectorName + ? api.__makeSelectorGroupFunction( + nativeClass, + !!receiverIsClass, + [selectorDescriptor(member, member.selectorName, member.signatureOffset, 0)] + ) + : undefined; + var setterFunction = !member.readonly && member.setterSelectorName + ? api.__makeSelectorGroupFunction( + nativeClass, + !!receiverIsClass, + [ + null, + selectorDescriptor( + member, + member.setterSelectorName, + member.setterSignatureOffset, + 1 + ) + ] + ) + : undefined; var descriptor = { configurable: true, enumerable: false, @@ -763,11 +932,11 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) : nativeClass[name]; }; })(member.name, member.selectorName) - : (function(name) { + : (getterFunction || (function(name) { return function() { return api.__invokeBase(nativeClass, this, name); }; - })(member.name) + })(member.name)) }; if (!member.readonly) { descriptor.set = receiverIsClass @@ -779,49 +948,36 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) nativeClass[name] = value; }; })(member.name, member.setterSelectorName) - : (function(name) { + : (setterFunction || (function(name) { return function(value) { return api.__invokeBase(nativeClass, this, name, value); }; - })(member.name); + })(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 (_) { + } else { + continue; } } + installSelectorGroups(target, selectorGroups, receiverIsClass); } function installNativeClassMembersIfNeeded() { if (classMembersInstalled) { return; } classMembersInstalled = true; - installClassMembers(constructable, nativeClass.__staticMembers, true); - installClassMembers(basePrototypeTarget, nativeClass.__instanceMembers, false); + installClassMembers( + constructable, + nativeClass.__staticMembers, + true, + nativeClass.__runtimeStaticMembers + ); + installClassMembers( + basePrototypeTarget, + nativeClass.__instanceMembers, + false, + nativeClass.__runtimeInstanceMembers + ); try { delete constructable.__nativeApiInstallMembers; } catch (_) { @@ -870,56 +1026,7 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } } catch (_) { } - constructable.prototype = typeof Proxy === 'function' - ? new Proxy(basePrototypeTarget, { - get: function(target, property, receiver) { - installNativeClassMembersIfNeeded(); - 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) { - installNativeClassMembersIfNeeded(); - return property in target; - }, - ownKeys: function(target) { - installNativeClassMembersIfNeeded(); - return Reflect.ownKeys(target); - }, - getOwnPropertyDescriptor: function(target, property) { - installNativeClassMembersIfNeeded(); - return Reflect.getOwnPropertyDescriptor(target, property); - } - }) - : basePrototypeTarget; + constructable.prototype = basePrototypeTarget; try { Object.defineProperty(constructable, Symbol.hasInstance, { configurable: true, @@ -971,6 +1078,10 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) return String(nativeClass); }; } + if (property === 'prototype') { + installNativeClassMembersIfNeeded(); + return Reflect.get(target, property, receiver); + } if (property === 'hasOwnProperty') { return function(key) { installNativeClassMembersIfNeeded(); @@ -1024,9 +1135,13 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) target[property] = value; return true; } + installNativeClassMembersIfNeeded(); if (setDescriptorValue(target, property, receiver, value)) { return true; } + if (setInheritedNativeClassValue(target, property, value)) { + return true; + } try { nativeClass[property] = value; return true; @@ -1071,6 +1186,9 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) if (superclassWrapper && superclassWrapper !== wrapper && typeof Object.setPrototypeOf === 'function') { Object.setPrototypeOf(wrapper, superclassWrapper); + if (superclassWrapper.prototype) { + Object.setPrototypeOf(constructable.prototype, superclassWrapper.prototype); + } } } } catch (_) { @@ -1104,6 +1222,9 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) function rememberClassOnInstance(instance, classWrapper) { if (instance && typeof instance === 'object' && classWrapper) { try { + if (typeof classWrapper.__nativeApiInstallMembers === 'function') { + classWrapper.__nativeApiInstallMembers(); + } if (typeof api.__rememberObjectClassWrapper === 'function') { api.__rememberObjectClassWrapper(instance, classWrapper); } else { @@ -1239,7 +1360,11 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) if (typeof member !== 'function') { throw new TypeError(String(name) + ' is not a function'); } - var result = member.apply(wrapper, arguments); + var memberArgs = []; + for (var memberArgIndex = 0; memberArgIndex < arguments.length; memberArgIndex++) { + memberArgs.push(arguments[memberArgIndex]); + } + var result = wrapper[name](...memberArgs); if (name === 'alloc' || name === 'new' || name === 'construct') { return rememberClassOnInstance(result, constructor); } @@ -1361,7 +1486,9 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) var protocols = Array.prototype.slice.call(arguments); return function(constructor) { if (constructor.ObjCProtocols) { - Array.prototype.push.apply(constructor.ObjCProtocols, protocols); + for (var protocolIndex = 0; protocolIndex < protocols.length; protocolIndex++) { + constructor.ObjCProtocols.push(protocols[protocolIndex]); + } } else { constructor.ObjCProtocols = protocols; } @@ -1378,7 +1505,11 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) return nativeFactory; } var constructable = function NativeScriptInteropValue() { - return nativeFactory.apply(undefined, arguments); + var factoryArgs = []; + for (var factoryArgIndex = 0; factoryArgIndex < arguments.length; factoryArgIndex++) { + factoryArgs.push(arguments[factoryArgIndex]); + } + return nativeFactory(...factoryArgs); }; try { if (nativeFactory.prototype) { @@ -1431,6 +1562,7 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } interop.Pointer = wrapInteropFactory(interop.Pointer, { kind: 'pointer', sizeof: pointerSize }); interop.Reference = wrapInteropFactory(interop.Reference, { kind: 'reference', sizeof: pointerSize }); + interop.Block = wrapInteropFactory(interop.Block, { kind: 'block', sizeof: pointerSize }); interop.FunctionReference = wrapInteropFactory( interop.FunctionReference, { kind: 'functionReference', sizeof: pointerSize } @@ -1638,38 +1770,38 @@ void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) } catch (_) { } }) -)JSI_GLOBALS"; +)Engine_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"); + "NativeApiGlobals.js"); + NativeApiWriteSmokeStage("engine:globals:after-eval"); } -void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { +void InstallNativeApi(Runtime& runtime, const NativeApiConfig& config) { const char* globalName = config.globalName != nullptr && config.globalName[0] != '\0' ? config.globalName : "__nativeScriptNativeApi"; - NativeApiJsiWriteSmokeStage("jsi:create-api"); - Object api = CreateNativeApiJSI(runtime, config); + NativeApiWriteSmokeStage("engine:create-api"); + Object api = CreateNativeApi(runtime, config); Object global = runtime.global(); - NativeApiJsiWriteSmokeStage("jsi:set-global"); + NativeApiWriteSmokeStage("engine:set-global"); global.setProperty(runtime, globalName, api); - NativeApiJsiWriteSmokeStage("jsi:set-interop"); + NativeApiWriteSmokeStage("engine: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); + NativeApiWriteSmokeStage("engine:install-globals"); + InstallNativeApiGlobalSymbols(runtime, globalName); } else { - NativeApiJsiWriteSmokeStage("jsi:install-aggregate-globals"); + NativeApiWriteSmokeStage("engine:install-aggregate-globals"); InstallAggregateGlobals(runtime, api, "protocolNames"); } - NativeApiJsiWriteSmokeStage("jsi:installed"); + NativeApiWriteSmokeStage("engine:installed"); } diff --git a/NativeScript/ffi/shared/bridge/Invocation.mm b/NativeScript/ffi/shared/bridge/Invocation.mm new file mode 100644 index 000000000..af32e2d3e --- /dev/null +++ b/NativeScript/ffi/shared/bridge/Invocation.mm @@ -0,0 +1,1764 @@ +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", "Enumeration", + "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"; +} + +class NativeApiReturnStorage { + public: + explicit NativeApiReturnStorage(size_t size) + : size_(std::max(size, sizeof(void*))) { + if (size_ > kInlineSize) { + heap_.assign(size_, 0); + } else { + std::memset(inline_, 0, kInlineSize); + } + } + + void* data() { return heap_.empty() ? inline_ : heap_.data(); } + unsigned char* bytes() { return static_cast(data()); } + + private: + static constexpr size_t kInlineSize = 64; + + size_t size_ = 0; + alignas(std::max_align_t) unsigned char inline_[kInlineSize] = {}; + std::vector heap_; +}; + +class NativeApiPointerFrame { + public: + explicit NativeApiPointerFrame(size_t count) : count_(count) { + if (count_ > kInlineCount) { + heap_.resize(count_); + } + } + + void set(size_t index, void* value) { + if (index >= count_) { + throw std::out_of_range("Native invocation argument index out of range."); + } + if (count_ <= kInlineCount) { + inline_[index] = value; + } else { + heap_[index] = value; + } + } + + void** data() { + if (count_ == 0) { + return nullptr; + } + return count_ <= kInlineCount ? inline_ : heap_.data(); + } + + private: + static constexpr size_t kInlineCount = 10; + + size_t count_ = 0; + void* inline_[kInlineCount] = {}; + std::vector heap_; +}; + +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(); + } + + NativeApiType 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; + NativeApiType type = parseMetadataEngineType(metadata, &typeOffset, bridge.get()); + if (unsupportedEngineType(type)) { + throw JSError( + runtime, "Native constant type is not supported by backend: " + + symbol.name); + } + + void* symbolPtr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (symbolPtr == nullptr) { + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge, type, symbolPtr); +} + +void prepareEngineArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiType& type, const Value& arg, + size_t index, NativeApiArgumentFrame& frame) { + ffi_type* ffiType = ffiTypeForEngineArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + convertEngineFfiArgument(runtime, bridge, type, arg, target, frame); +} + +void prepareEngineArguments(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSignature& signature, + const Value* args, size_t count, + NativeApiArgumentFrame& frame) { + if (count != signature.argumentTypes.size()) { + throw 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++) { + prepareEngineArgument(runtime, bridge, signature.argumentTypes[i], args[i], i, + frame); + } +} + +inline uint64_t dispatchIdForEngineSignature( + const NativeApiSignature& signature, SignatureCallKind kind) { + if (signature.signatureHash == 0) { + return 0; + } + return composeSignatureDispatchId(signature.signatureHash, kind, + signature.dispatchFlags); +} + +struct NativeApiPreparedCFunctionInvocation { + NativeApiSymbol symbol; + bool initialized = false; + void* function = nullptr; + NativeApiSignature signature; + CFunctionPreparedInvoker preparedInvoker = nullptr; +}; + +bool tryCallFastEngineCFunction( + Runtime& runtime, const std::shared_ptr& bridge, + void* function, const NativeApiSignature& signature, const Value* args, + size_t count, Value* result); + +Value callNativeFunctionPointer( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, void* pointer, bool block, const Value* args, + size_t count) { + if (pointer == nullptr) { + throw JSError(runtime, "Native function pointer is null."); + } + if (bridge == nullptr || bridge->metadata() == nullptr || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + throw JSError( + runtime, "Native function pointer metadata is unavailable."); + } + + auto signature = parseMetadataEngineSignature( + bridge->metadata(), type.signatureOffset, block ? 1 : 0, bridge.get()); + if (!signature || !signature->prepared || signature->variadic || + unsupportedEngineType(signature->returnType)) { + throw JSError( + runtime, + "Native function pointer signature is not supported by backend."); + } + + NativeApiArgumentFrame frame(signature->argumentTypes.size()); + prepareEngineArguments(runtime, bridge, *signature, args, count, frame); + + NativeApiPointerFrame values(signature->argumentTypes.size() + 1); + if (block) { + values.set(0, &pointer); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.set(i + 1, frame.values()[i]); + } + } + + void* callable = pointer; + if (block) { + auto literal = static_cast(pointer); + if (literal == nullptr || literal->invoke == nullptr) { + throw JSError(runtime, "Native block invoke pointer is null."); + } + callable = literal->invoke; + } + + if (!block) { + Value fastResult; + if (tryCallFastEngineCFunction(runtime, bridge, callable, *signature, args, + count, &fastResult)) { + return fastResult; + } + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature->returnType)); + BlockPreparedInvoker blockPreparedInvoker = nullptr; + CFunctionPreparedInvoker functionPreparedInvoker = nullptr; + if (block) { + blockPreparedInvoker = lookupBlockPreparedInvoker(dispatchIdForEngineSignature( + *signature, SignatureCallKind::BlockInvoke)); + } else { + functionPreparedInvoker = lookupCFunctionPreparedInvoker( + dispatchIdForEngineSignature(*signature, SignatureCallKind::CFunction)); + } + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (block) { + if (blockPreparedInvoker != nullptr) { + blockPreparedInvoker(callable, values.data(), returnStorage.data()); + return; + } + } else { + if (functionPreparedInvoker != nullptr) { + functionPreparedInvoker(callable, frame.values(), returnStorage.data()); + return; + } + } + 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 NativeApiType& type, void* pointer, + bool block) { + const char* functionName = block ? "NativeApiBlock" : "NativeApiFunctionPointer"; + 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("[NativeApi ") + + (block ? "Block " : "FunctionPointer ") + + address + "]"); + })); + return function; +} + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const std::shared_ptr& prepared, + const Value* args, + size_t count) { + if (prepared == nullptr) { + throw JSError(runtime, "Native function state is unavailable."); + } + NativeApiRoundTripCacheFrameGuard roundTripFrame(bridge); + + MDMetadataReader* metadata = bridge->metadata(); + if (metadata == nullptr) { + throw JSError(runtime, "Native metadata is not loaded."); + } + + if (!prepared->initialized) { + void* fnptr = dlsym(bridge->selfDl(), prepared->symbol.name.c_str()); + if (fnptr == nullptr) { + throw JSError(runtime, + "Native function is not available: " + + prepared->symbol.name); + } + + MDSectionOffset signatureOffset = + metadata->signaturesOffset + + metadata->getOffset(prepared->symbol.offset + sizeof(MDSectionOffset)); + auto signature = parseMetadataEngineSignature( + metadata, signatureOffset, 0, bridge.get(), + (metadata->getFunctionFlag( + prepared->symbol.offset + sizeof(MDSectionOffset) * 2) & + metagen::mdFunctionReturnOwned) != 0); + if (!signature || !signature->prepared || signature->variadic || + unsupportedEngineType(signature->returnType)) { + throw JSError( + runtime, "Native function signature is not supported by backend: " + + prepared->symbol.name); + } + + prepared->function = fnptr; + prepared->signature = std::move(*signature); + prepared->preparedInvoker = lookupCFunctionPreparedInvoker( + dispatchIdForEngineSignature(prepared->signature, + SignatureCallKind::CFunction)); + prepared->initialized = true; + } + + NativeApiSignature& signature = prepared->signature; + Value fastResult; + if (tryCallFastEngineCFunction(runtime, bridge, prepared->function, signature, + args, count, &fastResult)) { + return fastResult; + } + + NativeApiArgumentFrame frame(signature.argumentTypes.size()); + prepareEngineArguments(runtime, bridge, signature, args, count, frame); + + if (prepared->symbol.name == "NSApplicationMain" || + prepared->symbol.name == "UIApplicationMain") { + runtime.drainMicrotasks(); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature.returnType)); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (prepared->preparedInvoker != nullptr) { + prepared->preparedInvoker(prepared->function, frame.values(), + returnStorage.data()); + } else { + ffi_call(&signature.cif, FFI_FN(prepared->function), returnStorage.data(), + frame.values()); + } + }); + + NativeApiType returnType = signature.returnType; + if (prepared->symbol.name == "CFBagContainsValue" && + (returnType.kind == metagen::mdTypeChar || + returnType.kind == metagen::mdTypeUChar || + returnType.kind == metagen::mdTypeUInt8)) { + return *returnStorage.bytes() != 0; + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol, const Value* args, + size_t count) { + auto prepared = std::make_shared(); + prepared->symbol = symbol; + return callCFunction(runtime, bridge, prepared, args, count); +} + +bool signatureSupportedForEngineInvocation( + const std::optional& signature) { + if (!signature || !signature->prepared || signature->variadic || + unsupportedEngineType(signature->returnType)) { + return false; + } + for (const auto& argType : signature->argumentTypes) { + if (unsupportedEngineType(argType)) { + return false; + } + } + return true; +} + +bool signatureSupportedForEngineInvocation( + const NativeApiSignature& signature) { + if (!signature.prepared || signature.variadic || + unsupportedEngineType(signature.returnType)) { + return false; + } + for (const auto& argType : signature.argumentTypes) { + if (unsupportedEngineType(argType)) { + return false; + } + } + return true; +} + +struct NativeApiPreparedObjCInvocation { + SEL selector = nullptr; + Class receiverClass = Nil; + std::string selectorName; + NativeApiSignature signature; + ObjCPreparedInvoker preparedInvoker = nullptr; + void* engineInvoker = nullptr; // Engine-neutral GSD invoker (ObjCGsdInvoker) + bool isNSErrorOutMethod = false; // Cached: avoids per-call selector scan. + bool isInitMethod = false; // Cached: avoids per-call "init" rfind. + bool gsdEngineCallable = false; + uint8_t gsdEngineArgumentCount = 0; + bool fastEngineCallable = false; + uint8_t fastEngineArgumentCount = 0; + uint8_t fastEngineFirstArgKind = 0; + uint8_t fastEngineSecondArgKind = 0; +}; + +bool preparedObjCInvocationIsInit( + const NativeApiPreparedObjCInvocation& prepared) { + return prepared.isInitMethod; +} + +bool isFastEngineObjectType(const NativeApiType& 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; + } +} + +bool isFastEngineSignedIntegerType(const NativeApiType& type) { + switch (type.kind) { + case metagen::mdTypeChar: + case metagen::mdTypeSShort: + case metagen::mdTypeSInt: + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return true; + default: + return false; + } +} + +bool isFastEngineUnsignedIntegerType(const NativeApiType& type) { + switch (type.kind) { + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + case metagen::mdTypeUInt: + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return true; + default: + return false; + } +} + +enum class NativeApiFastEngineArgKind : uint8_t { + Bool, + SignedInteger, + UnsignedInteger, + Float, + Double, + Object, + Class, + Selector, +}; + +std::optional fastEngineArgKind( + const NativeApiType& type) { + if (isFastEngineObjectType(type)) { + return NativeApiFastEngineArgKind::Object; + } + if (isFastEngineSignedIntegerType(type)) { + return NativeApiFastEngineArgKind::SignedInteger; + } + if (isFastEngineUnsignedIntegerType(type)) { + return NativeApiFastEngineArgKind::UnsignedInteger; + } + switch (type.kind) { + case metagen::mdTypeBool: + return NativeApiFastEngineArgKind::Bool; + case metagen::mdTypeFloat: + return NativeApiFastEngineArgKind::Float; + case metagen::mdTypeDouble: + return NativeApiFastEngineArgKind::Double; + case metagen::mdTypeClass: + return NativeApiFastEngineArgKind::Class; + case metagen::mdTypeSelector: + return NativeApiFastEngineArgKind::Selector; + default: + return std::nullopt; + } +} + +bool readFastEngineBoolArgument(Runtime& runtime, const Value& value, + BOOL* result) { + if (result == nullptr || !value.isBool()) { + return false; + } + *result = value.getBool() ? YES : NO; + return true; +} + +bool readFastEngineSignedIntegerArgument(Runtime& runtime, const Value& value, + NSInteger* result) { + if (result == nullptr) { + return false; + } + if (value.isNumber()) { + *result = static_cast(value.getNumber()); + return true; + } + return false; +} + +bool readFastEngineUnsignedIntegerArgument(Runtime& runtime, const Value& value, + NSUInteger* result) { + if (result == nullptr) { + return false; + } + if (value.isNumber()) { + *result = static_cast(value.getNumber()); + return true; + } + return false; +} + +bool readFastEngineFloatArgument(Runtime&, const Value& value, float* result) { + if (result == nullptr || !value.isNumber()) { + return false; + } + *result = static_cast(value.getNumber()); + return true; +} + +bool readFastEngineDoubleArgument(Runtime&, const Value& value, double* result) { + if (result == nullptr || !value.isNumber()) { + return false; + } + *result = value.getNumber(); + return true; +} + +bool readFastEngineObjectArgument( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, + NativeApiArgumentFrame& frame, id* result) { + if (result == nullptr) { + return false; + } + *result = objectFromEngineValue( + runtime, bridge, value, frame, + type.kind == metagen::mdTypeNSMutableStringObject); + if (valueIsNativeObjectHostObject(runtime, value)) { + frame.retainObject(*result); + } + return true; +} + +class NativeApiScopedObjCObjectRetain { + public: + explicit NativeApiScopedObjCObjectRetain(id object) : object_(object) { + if (object_ != nil) { + [object_ retain]; + } + } + + ~NativeApiScopedObjCObjectRetain() { + if (object_ != nil) { + [object_ release]; + } + } + + private: + id object_ = nil; +}; + +bool readFastEngineClassArgument(Runtime& runtime, const Value& value, + Class* result) { + if (result == nullptr) { + return false; + } + *result = classFromEngineValue(runtime, value); + return *result != Nil; +} + +bool readFastEngineSelectorArgument(Runtime& runtime, const Value& value, + SEL* result) { + if (result == nullptr) { + return false; + } + if (value.isNull() || value.isUndefined()) { + *result = nullptr; + return true; + } + if (!value.isString()) { + return false; + } + std::string selectorName = value.asString(runtime).utf8(runtime); + *result = sel_registerName(selectorName.c_str()); + return true; +} + +template +Value callFastEngineCFunctionWithReturn( + Runtime& runtime, const std::shared_ptr& bridge, + void* function, NativeApiType returnType, Args... nativeArgs) { + auto finalizeObjectReturn = [&](id object) -> Value { + return convertNativeReturnValue(runtime, bridge, returnType, &object); + }; + + switch (returnType.kind) { + case metagen::mdTypeVoid: { + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = void (*)(Args...); + reinterpret_cast(function)(nativeArgs...); + }); + return Value::undefined(); + } + case metagen::mdTypeBool: { + BOOL nativeResult = NO; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = BOOL (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + uint8_t storage = nativeResult ? 1 : 0; + return convertNativeReturnValue(runtime, bridge, returnType, &storage); + } + case metagen::mdTypeFloat: { + float nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = float (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + case metagen::mdTypeDouble: { + double nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = double (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + default: + break; + } + + if (isFastEngineObjectType(returnType) || + returnType.kind == metagen::mdTypeClass) { + id nativeResult = nil; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = id (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return finalizeObjectReturn(nativeResult); + } + + if (returnType.kind == metagen::mdTypeSelector) { + SEL nativeResult = nullptr; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = SEL (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + + if (isFastEngineSignedIntegerType(returnType)) { + int64_t nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = int64_t (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + + if (isFastEngineUnsignedIntegerType(returnType)) { + uint64_t nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = uint64_t (*)(Args...); + nativeResult = reinterpret_cast(function)(nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + + throw JSError(runtime, "C function return type is not engine fast-callable."); +} + +bool isFastEngineCallableReturnType(const NativeApiType& returnType) { + return isFastEngineObjectType(returnType) || + returnType.kind == metagen::mdTypeVoid || + returnType.kind == metagen::mdTypeBool || + returnType.kind == metagen::mdTypeFloat || + returnType.kind == metagen::mdTypeDouble || + returnType.kind == metagen::mdTypeClass || + returnType.kind == metagen::mdTypeSelector || + isFastEngineSignedIntegerType(returnType) || + isFastEngineUnsignedIntegerType(returnType); +} + +bool tryCallFastEngineCFunction( + Runtime& runtime, const std::shared_ptr& bridge, + void* function, const NativeApiSignature& signature, const Value* args, + size_t count, Value* result) { + if (result == nullptr || function == nullptr || signature.variadic || + count != signature.argumentTypes.size() || count > 2 || + unsupportedEngineType(signature.returnType) || + !isFastEngineCallableReturnType(signature.returnType)) { + return false; + } + + std::optional firstArgKind; + std::optional secondArgKind; + if (count > 0) { + firstArgKind = fastEngineArgKind(signature.argumentTypes[0]); + if (!firstArgKind) { + return false; + } + } + if (count > 1) { + secondArgKind = fastEngineArgKind(signature.argumentTypes[1]); + if (!secondArgKind) { + return false; + } + } + + if (count == 0) { + *result = callFastEngineCFunctionWithReturn( + runtime, bridge, function, signature.returnType); + return true; + } + + NativeApiArgumentFrame frame(count); + auto callOne = [&](auto nativeArg0) -> Value { + return callFastEngineCFunctionWithReturn(runtime, bridge, function, + signature.returnType, nativeArg0); + }; + auto callTwo = [&](auto nativeArg0, auto nativeArg1) -> Value { + return callFastEngineCFunctionWithReturn(runtime, bridge, function, + signature.returnType, nativeArg0, + nativeArg1); + }; + + auto callWithSecondArg = [&](auto nativeArg0) -> bool { + switch (*secondArgKind) { + case NativeApiFastEngineArgKind::Bool: { + BOOL arg1 = NO; + if (!readFastEngineBoolArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::SignedInteger: { + NSInteger arg1 = 0; + if (!readFastEngineSignedIntegerArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::UnsignedInteger: { + NSUInteger arg1 = 0; + if (!readFastEngineUnsignedIntegerArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Float: { + float arg1 = 0; + if (!readFastEngineFloatArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Double: { + double arg1 = 0; + if (!readFastEngineDoubleArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Object: { + id arg1 = nil; + if (!readFastEngineObjectArgument( + runtime, bridge, signature.argumentTypes[1], args[1], frame, + &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Class: { + Class arg1 = Nil; + if (!readFastEngineClassArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Selector: { + SEL arg1 = nullptr; + if (!readFastEngineSelectorArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + } + return false; + }; + + switch (*firstArgKind) { + case NativeApiFastEngineArgKind::Bool: { + BOOL arg0 = NO; + if (!readFastEngineBoolArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::SignedInteger: { + NSInteger arg0 = 0; + if (!readFastEngineSignedIntegerArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::UnsignedInteger: { + NSUInteger arg0 = 0; + if (!readFastEngineUnsignedIntegerArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Float: { + float arg0 = 0; + if (!readFastEngineFloatArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Double: { + double arg0 = 0; + if (!readFastEngineDoubleArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Object: { + id arg0 = nil; + if (!readFastEngineObjectArgument(runtime, bridge, + signature.argumentTypes[0], args[0], + frame, &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Class: { + Class arg0 = Nil; + if (!readFastEngineClassArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Selector: { + SEL arg0 = nullptr; + if (!readFastEngineSelectorArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + } + + return false; +} + +template +Value callFastEngineObjCWithReturn( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, SEL selector, NativeApiType returnType, + const std::string& selectorName, Args... nativeArgs) { + auto finalizeObjectReturn = [&](id object) -> Value { + NativeApiType effectiveReturnType = returnType; + if ((selectorName == "valueForKey:" || selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(effectiveReturnType)) { + effectiveReturnType.kind = metagen::mdTypeAnyObject; + } + if (startsWith(selectorName, "init") && + isObjectiveCObjectType(effectiveReturnType)) { + effectiveReturnType.kind = metagen::mdTypeInstanceObject; + } + return convertNativeReturnValue(runtime, bridge, effectiveReturnType, + &object); + }; + + switch (returnType.kind) { + case metagen::mdTypeVoid: { + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = void (*)(id, SEL, Args...); + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return Value::undefined(); + } + case metagen::mdTypeBool: { + BOOL nativeResult = NO; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = BOOL (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + uint8_t storage = nativeResult ? 1 : 0; + return convertNativeReturnValue(runtime, bridge, returnType, &storage); + } + case metagen::mdTypeFloat: { + float nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = float (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + case metagen::mdTypeDouble: { + double nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = double (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + default: + break; + } + + if (isFastEngineObjectType(returnType) || returnType.kind == metagen::mdTypeClass) { + id nativeResult = nil; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = id (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return finalizeObjectReturn(nativeResult); + } + + if (isFastEngineSignedIntegerType(returnType)) { + int64_t nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = int64_t (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + + if (isFastEngineUnsignedIntegerType(returnType)) { + uint64_t nativeResult = 0; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + using Fn = uint64_t (*)(id, SEL, Args...); + nativeResult = + reinterpret_cast(objc_msgSend)(receiver, selector, nativeArgs...); + }); + return convertNativeReturnValue(runtime, bridge, returnType, + &nativeResult); + } + + throw JSError(runtime, "Objective-C return type is not engine fast-callable."); +} + +template +Value callFastEngineObjC1( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, SEL selector, const NativeApiType& returnType, + const std::string& selectorName, A0 arg0) { + return callFastEngineObjCWithReturn(runtime, bridge, receiver, selector, + returnType, selectorName, arg0); +} + +template +Value callFastEngineObjC2( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, SEL selector, const NativeApiType& returnType, + const std::string& selectorName, A0 arg0, A1 arg1) { + return callFastEngineObjCWithReturn(runtime, bridge, receiver, selector, + returnType, selectorName, arg0, arg1); +} + +bool tryCallFastEngineObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass, Value* result) { + if (result == nullptr || receiver == nil || dispatchSuperClass != Nil) { + return false; + } + + const NativeApiSignature& signature = prepared.signature; + if (!prepared.fastEngineCallable || + count != prepared.fastEngineArgumentCount) { + return false; + } + + NativeApiFastEngineArgKind firstArgKind = + static_cast(prepared.fastEngineFirstArgKind); + NativeApiFastEngineArgKind secondArgKind = + static_cast(prepared.fastEngineSecondArgKind); + + SEL selector = prepared.selector; + if (count == 0) { + *result = callFastEngineObjCWithReturn( + runtime, bridge, receiver, selector, signature.returnType, + prepared.selectorName); + return true; + } + + NativeApiArgumentFrame frame(count); + auto callOne = [&](auto nativeArg0) -> Value { + return callFastEngineObjC1(runtime, bridge, receiver, selector, + signature.returnType, prepared.selectorName, + nativeArg0); + }; + auto callTwo = [&](auto nativeArg0, auto nativeArg1) -> Value { + return callFastEngineObjC2(runtime, bridge, receiver, selector, + signature.returnType, prepared.selectorName, + nativeArg0, nativeArg1); + }; + auto callWithSecondArg = [&](auto nativeArg0) -> bool { + switch (secondArgKind) { + case NativeApiFastEngineArgKind::Bool: { + BOOL arg1 = NO; + if (!readFastEngineBoolArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::SignedInteger: { + NSInteger arg1 = 0; + if (!readFastEngineSignedIntegerArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::UnsignedInteger: { + NSUInteger arg1 = 0; + if (!readFastEngineUnsignedIntegerArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Float: { + float arg1 = 0; + if (!readFastEngineFloatArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Double: { + double arg1 = 0; + if (!readFastEngineDoubleArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Object: { + id arg1 = nil; + if (!readFastEngineObjectArgument( + runtime, bridge, signature.argumentTypes[1], args[1], frame, + &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Class: { + Class arg1 = Nil; + if (!readFastEngineClassArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + case NativeApiFastEngineArgKind::Selector: { + SEL arg1 = nullptr; + if (!readFastEngineSelectorArgument(runtime, args[1], &arg1)) { + return false; + } + *result = callTwo(nativeArg0, arg1); + return true; + } + } + return false; + }; + + switch (firstArgKind) { + case NativeApiFastEngineArgKind::Bool: { + BOOL arg0 = NO; + if (!readFastEngineBoolArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::SignedInteger: { + NSInteger arg0 = 0; + if (!readFastEngineSignedIntegerArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::UnsignedInteger: { + NSUInteger arg0 = 0; + if (!readFastEngineUnsignedIntegerArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Float: { + float arg0 = 0; + if (!readFastEngineFloatArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Double: { + double arg0 = 0; + if (!readFastEngineDoubleArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Object: { + id arg0 = nil; + if (!readFastEngineObjectArgument(runtime, bridge, signature.argumentTypes[0], + args[0], frame, &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Class: { + Class arg0 = Nil; + if (!readFastEngineClassArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + case NativeApiFastEngineArgKind::Selector: { + SEL arg0 = nullptr; + if (!readFastEngineSelectorArgument(runtime, args[0], &arg0)) { + return false; + } + if (count == 1) { + *result = callOne(arg0); + return true; + } + return callWithSecondArg(arg0); + } + } + + return false; +} + +bool isFastEngineObjCReturnType(const NativeApiType& returnType) { + return !unsupportedEngineType(returnType) && + (isFastEngineObjectType(returnType) || + returnType.kind == metagen::mdTypeVoid || + returnType.kind == metagen::mdTypeBool || + returnType.kind == metagen::mdTypeFloat || + returnType.kind == metagen::mdTypeDouble || + returnType.kind == metagen::mdTypeClass || + isFastEngineSignedIntegerType(returnType) || + isFastEngineUnsignedIntegerType(returnType)); +} + +void configureFastEngineObjCInvocation( + NativeApiPreparedObjCInvocation& prepared) { + prepared.fastEngineCallable = false; + prepared.fastEngineArgumentCount = 0; + prepared.fastEngineFirstArgKind = 0; + prepared.fastEngineSecondArgKind = 0; + + const NativeApiSignature& signature = prepared.signature; + if (signature.variadic || prepared.isNSErrorOutMethod || + signature.argumentTypes.size() > 2 || + !isFastEngineObjCReturnType(signature.returnType)) { + return; + } + + if (!signature.argumentTypes.empty()) { + std::optional firstArgKind = + fastEngineArgKind(signature.argumentTypes[0]); + if (!firstArgKind) { + return; + } + prepared.fastEngineFirstArgKind = static_cast(*firstArgKind); + } + if (signature.argumentTypes.size() > 1) { + std::optional secondArgKind = + fastEngineArgKind(signature.argumentTypes[1]); + if (!secondArgKind) { + return; + } + prepared.fastEngineSecondArgKind = static_cast(*secondArgKind); + } + + prepared.fastEngineArgumentCount = + static_cast(signature.argumentTypes.size()); + prepared.fastEngineCallable = true; +} + +void configureGeneratedEngineObjCInvocation( + NativeApiPreparedObjCInvocation& prepared) { + prepared.gsdEngineCallable = false; + prepared.gsdEngineArgumentCount = 0; + + const NativeApiSignature& signature = prepared.signature; + if (prepared.engineInvoker == nullptr || signature.variadic || + prepared.isNSErrorOutMethod || signature.argumentTypes.size() > 255) { + return; + } + + prepared.gsdEngineArgumentCount = + static_cast(signature.argumentTypes.size()); + prepared.gsdEngineCallable = true; +} + +std::shared_ptr +prepareNativeApiObjCInvocation( + Runtime& runtime, const std::shared_ptr& bridge, + Class lookupClass, bool receiverIsClass, const std::string& selectorName, + const NativeApiMember* member) { + if (lookupClass == Nil) { + throw JSError(runtime, + "Objective-C class is not available for selector: " + + selectorName); + } + + SEL selector = sel_registerName(selectorName.c_str()); + Method method = receiverIsClass ? class_getClassMethod(lookupClass, selector) + : class_getInstanceMethod(lookupClass, selector); + if (method == nullptr) { + throw JSError(runtime, + "Objective-C selector is not available: " + selectorName); + } + + std::optional signature; + std::optional runtimeSignature; + if (member != nullptr && + member->signatureOffset != MD_SECTION_OFFSET_NULL && + member->signatureOffset != 0) { + signature = parseMetadataEngineSignature( + bridge->metadata(), member->signatureOffset, 2, bridge.get(), + (member->flags & metagen::mdMemberReturnOwned) != 0); + } + if (method != nullptr) { + runtimeSignature = parseObjCMethodEngineSignature(method, bridge.get()); + } + if (signatureSupportedForEngineInvocation(signature) && + signatureSupportedForEngineInvocation(runtimeSignature)) { + reconcileObjCMethodRuntimeSignature(&*signature, *runtimeSignature); + } + if (!signatureSupportedForEngineInvocation(signature) && runtimeSignature) { + signature = std::move(runtimeSignature); + } + + if (!signatureSupportedForEngineInvocation(signature)) { + throw JSError( + runtime, "Objective-C signature is not supported by backend: " + + selectorName); + } + signature->selectorName = selectorName; + + auto prepared = std::make_shared(); + prepared->selector = selector; + prepared->receiverClass = receiverIsClass ? lookupClass : Nil; + prepared->selectorName = selectorName; + prepared->signature = std::move(*signature); + prepared->preparedInvoker = lookupObjCPreparedInvoker( + dispatchIdForEngineSignature(prepared->signature, + SignatureCallKind::ObjCMethod)); + prepared->engineInvoker = lookupGeneratedEngineObjCGsdInvoker( + dispatchIdForEngineSignature(prepared->signature, + SignatureCallKind::ObjCMethod)); + prepared->isNSErrorOutMethod = + isNSErrorOutEngineMethodSignature(prepared->signature); + prepared->isInitMethod = prepared->selectorName.rfind("init", 0) == 0; + configureGeneratedEngineObjCInvocation(*prepared); + configureFastEngineObjCInvocation(*prepared); + return prepared; +} + +Value callPreparedObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const NativeApiPreparedObjCInvocation& prepared, const Value* args, + size_t count, Class dispatchSuperClass) { + if (receiver == nil) { + throw JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + NativeApiRoundTripCacheFrameGuard roundTripFrame(bridge); + + const NativeApiSignature& signature = prepared.signature; + Value fastResult; + if (tryCallGeneratedEngineObjCSelector(runtime, bridge, receiver, prepared, + args, count, dispatchSuperClass, + &fastResult)) { + return fastResult; + } + if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared, args, + count, dispatchSuperClass, &fastResult)) { + return fastResult; + } + + NativeApiArgumentFrame frame(signature.argumentTypes.size()); + frame.retainObject(receiver); + const bool isNSErrorOutMethod = prepared.isNSErrorOutMethod; + if (isNSErrorOutMethod) { + size_t expected = signature.argumentTypes.size(); + if (count > expected || count + 1 < expected) { + throw 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++) { + prepareEngineArgument(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 { + prepareEngineArguments(runtime, bridge, signature, args, count, frame); + } + + NativeApiPointerFrame values(signature.argumentTypes.size() + 2); + size_t valueIndex = 0; + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.set(valueIndex++, &superReceiverPtr); + } else { + values.set(valueIndex++, &receiver); + } + values.set(valueIndex++, const_cast(&prepared.selector)); + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + values.set(valueIndex++, frame.values()[i]); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature.returnType)); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (prepared.preparedInvoker != nullptr && dispatchSuperClass == Nil) { + prepared.preparedInvoker(reinterpret_cast(objc_msgSend), + values.data(), returnStorage.data()); + } else { +#if defined(__x86_64__) + bool isStret = signature.returnType.ffiType->size > 16 && + signature.returnType.ffiType->type == FFI_TYPE_STRUCT; + void (*target)(void) = + dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend)); + ffi_call(const_cast(&signature.cif), target, + returnStorage.data(), values.data()); +#else + ffi_call(const_cast(&signature.cif), + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#endif + } + }); + + NativeApiType returnType = signature.returnType; + if ((prepared.selectorName == "valueForKey:" || + prepared.selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeAnyObject; + } + if (prepared.isInitMethod && + isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeInstanceObject; + } + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + 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 JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + NativeApiRoundTripCacheFrameGuard roundTripFrame(bridge); + + 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 && + (dispatchSuperClass != Nil || ![receiver respondsToSelector:selector])) { + throw JSError(runtime, + "Objective-C selector is not available: " + + selectorName); + } + + std::optional signature; + std::optional runtimeSignature; + if (member != nullptr && + member->signatureOffset != MD_SECTION_OFFSET_NULL && + member->signatureOffset != 0) { + signature = parseMetadataEngineSignature( + bridge->metadata(), member->signatureOffset, 2, bridge.get(), + (member->flags & metagen::mdMemberReturnOwned) != 0); + } + if (method != nullptr) { + runtimeSignature = parseObjCMethodEngineSignature(method, bridge.get()); + } + if (signatureSupportedForEngineInvocation(signature) && + signatureSupportedForEngineInvocation(runtimeSignature)) { + reconcileObjCMethodRuntimeSignature(&*signature, *runtimeSignature); + } + if (!signatureSupportedForEngineInvocation(signature) && runtimeSignature) { + signature = std::move(runtimeSignature); + } + + if (!signatureSupportedForEngineInvocation(signature)) { + throw JSError( + runtime, "Objective-C signature is not supported by backend: " + + selectorName); + } + signature->selectorName = selectorName; + + NativeApiPreparedObjCInvocation engineInvocation; + engineInvocation.selector = selector; + engineInvocation.selectorName = selectorName; + engineInvocation.signature = *signature; + engineInvocation.engineInvoker = lookupGeneratedEngineObjCGsdInvoker( + dispatchIdForEngineSignature(*signature, SignatureCallKind::ObjCMethod)); + engineInvocation.isNSErrorOutMethod = + isNSErrorOutEngineMethodSignature(*signature); + engineInvocation.isInitMethod = selectorName.rfind("init", 0) == 0; + configureGeneratedEngineObjCInvocation(engineInvocation); + configureFastEngineObjCInvocation(engineInvocation); + Value fastResult; + if (tryCallGeneratedEngineObjCSelector(runtime, bridge, receiver, + engineInvocation, args, count, + dispatchSuperClass, &fastResult)) { + return fastResult; + } + if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, + engineInvocation, args, count, + dispatchSuperClass, &fastResult)) { + return fastResult; + } + + NativeApiArgumentFrame frame(signature->argumentTypes.size()); + const bool isNSErrorOutMethod = engineInvocation.isNSErrorOutMethod; + if (isNSErrorOutMethod) { + size_t expected = signature->argumentTypes.size(); + if (count > expected || count + 1 < expected) { + throw 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++) { + prepareEngineArgument(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 { + prepareEngineArguments(runtime, bridge, *signature, args, count, frame); + } + + NativeApiPointerFrame values(signature->argumentTypes.size() + 2); + size_t valueIndex = 0; + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.set(valueIndex++, &superReceiverPtr); + } else { + values.set(valueIndex++, &receiver); + } + values.set(valueIndex++, &selector); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.set(valueIndex++, frame.values()[i]); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature->returnType)); + auto preparedInvoker = + dispatchSuperClass == Nil + ? lookupObjCPreparedInvoker(dispatchIdForEngineSignature( + *signature, SignatureCallKind::ObjCMethod)) + : nullptr; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (preparedInvoker != nullptr) { + preparedInvoker(reinterpret_cast(objc_msgSend), values.data(), + returnStorage.data()); + } else { +#if defined(__x86_64__) + bool isStret = signature->returnType.ffiType->size > 16 && + signature->returnType.ffiType->type == FFI_TYPE_STRUCT; + void (*target)(void) = + 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 + } + }); + + NativeApiType returnType = signature->returnType; + if ((selectorName == "valueForKey:" || selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeAnyObject; + } + if (engineInvocation.isInitMethod && isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeInstanceObject; + } + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h b/NativeScript/ffi/shared/bridge/ObjCBridge.mm similarity index 58% rename from NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h rename to NativeScript/ffi/shared/bridge/ObjCBridge.mm index d543b1ec3..0f7c0e764 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h +++ b/NativeScript/ffi/shared/bridge/ObjCBridge.mm @@ -1,23 +1,21 @@ -thread_local bool gDispatchNativeCallsToUI = false; -thread_local bool gExecutingDispatchedUINativeCall = false; thread_local int gSynchronousNativeInvocationDepth = 0; -thread_local int gNativeCallerThreadJsiCallbackDepth = 0; +thread_local int gNativeCallerThreadEngineCallbackDepth = 0; thread_local std::vector gNativeCallbackExceptionCaptureStack; std::atomic gActiveSynchronousNativeInvocationDepth{0}; -static char gNativeApiJsiExtendedClassKey; +static char gNativeApiExtendedClassKey; -void markNativeApiJsiExtendedClass(Class cls) { +void markNativeApiExtendedClass(Class cls) { if (cls == Nil) { return; } - objc_setAssociatedObject(cls, &gNativeApiJsiExtendedClassKey, @YES, + objc_setAssociatedObject(cls, &gNativeApiExtendedClassKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -bool isNativeApiJsiExtendedClass(Class cls) { +bool isNativeApiExtendedClass(Class cls) { Class current = cls; while (current != Nil) { - if (objc_getAssociatedObject(current, &gNativeApiJsiExtendedClassKey) != nil) { + if (objc_getAssociatedObject(current, &gNativeApiExtendedClassKey) != nil) { return true; } current = class_getSuperclass(current); @@ -25,25 +23,6 @@ bool isNativeApiJsiExtendedClass(Class cls) { return false; } -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() { @@ -59,20 +38,20 @@ class ScopedNativeApiSynchronousInvocation final { } }; -class ScopedNativeCallerThreadJsiCallback final { +class ScopedNativeCallerThreadEngineCallback final { public: - ScopedNativeCallerThreadJsiCallback() { - gNativeCallerThreadJsiCallbackDepth += 1; + ScopedNativeCallerThreadEngineCallback() { + gNativeCallerThreadEngineCallbackDepth += 1; } - ~ScopedNativeCallerThreadJsiCallback() { - gNativeCallerThreadJsiCallbackDepth -= 1; + ~ScopedNativeCallerThreadEngineCallback() { + gNativeCallerThreadEngineCallbackDepth -= 1; } - ScopedNativeCallerThreadJsiCallback( - const ScopedNativeCallerThreadJsiCallback&) = delete; - ScopedNativeCallerThreadJsiCallback& operator=( - const ScopedNativeCallerThreadJsiCallback&) = delete; + ScopedNativeCallerThreadEngineCallback( + const ScopedNativeCallerThreadEngineCallback&) = delete; + ScopedNativeCallerThreadEngineCallback& operator=( + const ScopedNativeCallerThreadEngineCallback&) = delete; }; class ScopedNativeCallbackExceptionCapture final { @@ -132,19 +111,8 @@ void performNativeInvocation(Runtime& runtime, } }; - 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) { + bool skipInvoker = gNativeCallerThreadEngineCallbackDepth > 0; + if (invoker && !skipInvoker) { invoker(run); } else { run(); @@ -153,10 +121,30 @@ void performNativeInvocation(Runtime& runtime, if (exceptionDescription != nil) { std::string message = exceptionDescription.UTF8String ?: ""; [exceptionDescription release]; - throw facebook::jsi::JSError(runtime, message); + throw JSError(runtime, message); } if (!callbackException.empty()) { - throw facebook::jsi::JSError(runtime, callbackException); + throw JSError(runtime, callbackException); + } +} + +template +void performDirectObjCInvocation(Runtime& runtime, Invocation&& invocation) { + NSString* exceptionDescription = nil; + auto run = [&]() { + @try { + invocation(); + } @catch (NSException* exception) { + exceptionDescription = [exception.description copy]; + } + }; + + run(); + + if (exceptionDescription != nil) { + std::string message = exceptionDescription.UTF8String ?: ""; + [exceptionDescription release]; + throw JSError(runtime, message); } } @@ -189,13 +177,30 @@ struct NativeApiMember { bool readonly = false; }; -struct NativeApiJsiAggregateInfo; +struct NativeApiSelectorGroupEntry { + std::string selectorName; + NativeApiMember member; + bool hasMember = false; + bool propertyGetterResolved = false; + bool propertyGetterCanPrepare = true; + bool propertyGetterHasAdjustedMember = false; + std::string propertyGetterSelectorName; + NativeApiMember propertyGetterMember; +}; + +struct NativeApiSelectorGroupCallTarget { + const std::string* selectorName = nullptr; + const NativeApiMember* member = nullptr; + bool canPrepare = true; +}; + +struct NativeApiAggregateInfo; -struct NativeApiJsiFfiType { +struct NativeApiFfiType { ffi_type type = {}; std::vector elements; - NativeApiJsiFfiType() { + NativeApiFfiType() { type.type = FFI_TYPE_STRUCT; type.size = 0; type.alignment = 0; @@ -208,7 +213,7 @@ struct NativeApiJsiFfiType { } }; -struct NativeApiJsiType { +struct NativeApiType { MDTypeKind kind = metagen::mdTypeVoid; ffi_type* ffiType = &ffi_type_void; bool supported = true; @@ -217,24 +222,24 @@ struct NativeApiJsiType { 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; + std::shared_ptr elementType; + std::shared_ptr aggregateInfo; + std::shared_ptr ownedFfiType; }; -struct NativeApiJsiAggregateField { +struct NativeApiAggregateField { std::string name; uint16_t offset = 0; - NativeApiJsiType type; + NativeApiType type; }; -struct NativeApiJsiAggregateInfo { +struct NativeApiAggregateInfo { std::string name; uint16_t size = 0; bool isUnion = false; MDSectionOffset offset = MD_SECTION_OFFSET_NULL; - std::vector fields; - std::shared_ptr ffi; + std::vector fields; + std::shared_ptr ffi; }; std::string jsifySelector(const char* selector) { @@ -264,76 +269,29 @@ std::string booleanGetterSelectorForProperty(const std::string& property) { return selector; } -std::optional runtimeBooleanGetterSelectorForProperty( - Class cls, bool staticMethod, const std::string& property) { - if (cls == nil || property.empty()) { +std::optional respondingPropertyGetterSelector( + id receiver, const std::string& property, + const std::string& preferredSelector) { + if (receiver == nil) { return std::nullopt; } - std::string selectorName = booleanGetterSelectorForProperty(property); - SEL selector = sel_getUid(selectorName.c_str()); - if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || - (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { - return selectorName; - } - return std::nullopt; -} + auto respondsToSelectorName = [receiver](const std::string& selectorName) { + return !selectorName.empty() && + [receiver respondsToSelector:sel_getUid(selectorName.c_str())]; + }; -std::optional runtimeSelectorNameForProperty( - Class cls, bool staticMethod, const std::string& property) { - if (cls == nil || property.empty()) { - return std::nullopt; + if (respondsToSelectorName(preferredSelector)) { + return preferredSelector; } - -#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); - } - } + if (preferredSelector != property && respondsToSelectorName(property)) { + return property; } -#endif - if (auto selectorName = - runtimeBooleanGetterSelectorForProperty(cls, staticMethod, property)) { - return selectorName; - } - - 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); + std::string booleanSelector = booleanGetterSelectorForProperty(property); + if (booleanSelector != preferredSelector && booleanSelector != property && + respondsToSelectorName(booleanSelector)) { + return booleanSelector; } return std::nullopt; @@ -351,18 +309,6 @@ std::string setterSelectorForProperty(const std::string& property) { 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(), ':')); @@ -371,7 +317,6 @@ size_t selectorArgumentCount(const std::string& selector) { 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; @@ -382,14 +327,152 @@ const NativeApiMember* selectMethodMember( continue; } - if (fallback == nullptr) { - fallback = &member; - } if (selectorArgumentCount(member.selectorName) == argumentCount) { return &member; } } - return fallback; + return nullptr; +} + +bool hasMethodMember(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 true; + } + } + return false; +} + +std::shared_ptr> +selectorGroupEntriesForMethod(const std::vector& members, + const std::string& property, bool staticMethod) { + auto selectors = std::make_shared>(); + for (const auto& member : members) { + if (member.property || member.name != property || member.selectorName.empty()) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + size_t argumentCount = selectorArgumentCount(member.selectorName); + if (selectors->size() <= argumentCount) { + selectors->resize(argumentCount + 1); + } + if ((*selectors)[argumentCount].selectorName.empty()) { + (*selectors)[argumentCount].selectorName = member.selectorName; + (*selectors)[argumentCount].member = member; + (*selectors)[argumentCount].hasMember = true; + } + + if (argumentCount > 0 && member.selectorName.size() >= 6 && + member.selectorName.compare(member.selectorName.size() - 6, 6, + "error:") == 0) { + size_t omittedErrorCount = argumentCount - 1; + if (selectors->size() <= omittedErrorCount) { + selectors->resize(omittedErrorCount + 1); + } + if ((*selectors)[omittedErrorCount].selectorName.empty()) { + (*selectors)[omittedErrorCount].selectorName = member.selectorName; + (*selectors)[omittedErrorCount].member = member; + (*selectors)[omittedErrorCount].hasMember = true; + } + } + } + return selectors->empty() ? nullptr : selectors; +} + +bool selectorGroupCanPrepareSelector(id receiver, Class lookupClass, + bool receiverIsClass, + const std::string& selectorName) { + if (selectorName.empty()) { + return false; + } + SEL selector = sel_registerName(selectorName.c_str()); + if (receiverIsClass) { + return lookupClass != Nil && + class_getClassMethod(lookupClass, selector) != nullptr; + } + if (lookupClass != Nil && + class_getInstanceMethod(lookupClass, selector) != nullptr) { + return true; + } + return receiver != nil && + class_getInstanceMethod(object_getClass(receiver), selector) != nullptr; +} + +std::string selectorGroupPropertyGetterSelector( + id receiver, Class lookupClass, bool receiverIsClass, + const NativeApiMember& member) { + if (selectorGroupCanPrepareSelector(receiver, lookupClass, receiverIsClass, + member.selectorName)) { + return member.selectorName; + } + if (member.selectorName != member.name && + selectorGroupCanPrepareSelector(receiver, lookupClass, receiverIsClass, + member.name)) { + return member.name; + } + + std::string booleanSelector = booleanGetterSelectorForProperty(member.name); + if (booleanSelector != member.selectorName && booleanSelector != member.name && + selectorGroupCanPrepareSelector(receiver, lookupClass, receiverIsClass, + booleanSelector)) { + return booleanSelector; + } + + if (auto responding = respondingPropertyGetterSelector( + receiver, member.name, member.selectorName)) { + return *responding; + } + + return member.selectorName != member.name ? member.name : member.selectorName; +} + +NativeApiSelectorGroupCallTarget selectorGroupMemberForCall( + id receiver, Class lookupClass, bool receiverIsClass, + NativeApiSelectorGroupEntry& entry, size_t count) { + if (!entry.hasMember) { + return {&entry.selectorName, nullptr, true}; + } + if (count == 0 && entry.member.property) { + if (!entry.propertyGetterResolved) { + entry.propertyGetterSelectorName = selectorGroupPropertyGetterSelector( + receiver, lookupClass, receiverIsClass, entry.member); + entry.propertyGetterCanPrepare = selectorGroupCanPrepareSelector( + receiver, lookupClass, receiverIsClass, + entry.propertyGetterSelectorName); + if (entry.propertyGetterSelectorName != entry.member.selectorName) { + entry.propertyGetterMember = entry.member; + entry.propertyGetterMember.selectorName = + entry.propertyGetterSelectorName; + entry.propertyGetterHasAdjustedMember = true; + } + entry.propertyGetterResolved = true; + } + return {&entry.propertyGetterSelectorName, + entry.propertyGetterHasAdjustedMember ? &entry.propertyGetterMember + : &entry.member, + entry.propertyGetterCanPrepare}; + } + return {&entry.selectorName, &entry.member, true}; +} + +inline NativeApiSelectorGroupCallTarget selectorGroupCallTargetForEntry( + id receiver, Class lookupClass, bool receiverIsClass, + NativeApiSelectorGroupEntry& entry, size_t count) { + if (entry.hasMember && (!entry.member.property || count != 0)) { + return {&entry.selectorName, &entry.member, true}; + } + return selectorGroupMemberForCall(receiver, lookupClass, receiverIsClass, + entry, count); } const NativeApiMember* selectPropertyMember( @@ -411,7 +494,7 @@ const NativeApiMember* selectPropertyMember( const NativeApiMember* selectWritablePropertyMember( const std::vector& members, const std::string& property, bool staticMethod) { - const NativeApiMember* fallback = nullptr; + const NativeApiMember* propertyMember = nullptr; for (const auto& member : members) { if (!member.property || member.name != property) { continue; @@ -422,18 +505,21 @@ const NativeApiMember* selectWritablePropertyMember( continue; } - if (fallback == nullptr) { - fallback = &member; + if (propertyMember == nullptr) { + propertyMember = &member; } if (!member.readonly && !member.setterSelectorName.empty()) { return &member; } } - return fallback; + return propertyMember; } -void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); +void skipMetadataEngineType(MDMetadataReader* metadata, MDSectionOffset* offset); Protocol* lookupProtocolByNativeName(const std::string& name); +struct NativeApiPreparedObjCInvocation; +bool preparedObjCInvocationIsInit( + const NativeApiPreparedObjCInvocation& prepared); inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { #if INTPTR_MAX == INT64_MAX @@ -443,21 +529,37 @@ inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { #endif } -class NativeApiJsiBridge { +class NativeApiBridge { + struct NativeApiRoundTripValue { + std::shared_ptr value; + bool stringLikeNative = false; + bool persistBeyondFrame = true; + uintptr_t validationKey = 0; + }; + using NativeApiRoundTripReleaseList = + std::vector; + using NativeApiRoundTripFrame = + std::unordered_map; + using NativeApiRoundTripFrameStack = std::vector; + + static constexpr size_t kRecentRoundTripValueLimit = 2; + public: - explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) + explicit NativeApiBridge(const NativeApiConfig& config) : metadata_(loadMetadata(config)), scheduler_(config.scheduler), nativeInvocationInvoker_(config.nativeInvocationInvoker), nativeCallbackInvoker_(config.nativeCallbackInvoker), + runtimeCallbackInvoker_(config.runtimeCallbackInvoker), jsThreadCallbackInvoker_(config.jsThreadCallbackInvoker), + jsThreadAsyncCallbackInvoker_(config.jsThreadAsyncCallbackInvoker), invokeCallbacksOnNativeCallerThread_( config.invokeCallbacksOnNativeCallerThread) { selfDl_ = dlopen(nullptr, RTLD_NOW); buildSymbolIndexes(); } - ~NativeApiJsiBridge() { + ~NativeApiBridge() { if (selfDl_ != nullptr) { dlclose(selfDl_); } @@ -472,21 +574,21 @@ class NativeApiJsiBridge { 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* 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* findClassByOffset(MDSectionOffset offset) const { + auto it = classSymbolsByOffset_.find(offset); + return it != classSymbolsByOffset_.end() ? &it->second : nullptr; + } - const NativeApiSymbol* findClassForRuntimeClass(Class cls) const { + const NativeApiSymbol* findClassForRuntimeClass(Class cls) const { Class current = cls; while (current != Nil) { const char* name = class_getName(current); @@ -525,37 +627,286 @@ class NativeApiJsiBridge { return it != functionSymbolsByName_.end() ? &it->second : nullptr; } + static uintptr_t callbackRoundTripValidationKey( + const NativeApiType& type) { + if (type.signatureOffset == 0 || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + return 0; + } + return (static_cast(type.signatureOffset) << 8) | + (static_cast(type.kind) & 0xff); + } + void rememberRoundTripValue(Runtime& runtime, const void* native, - const Value& value) { + const Value& value, + bool stringLikeNative = false, + uintptr_t validationKey = 0) { if (native == nullptr) { return; } - std::lock_guard lock(roundTripValuesMutex_); - roundTripValues_[normalizeRuntimePointer( - reinterpret_cast(native))] = - std::make_shared(runtime, value); + uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); + NativeApiRoundTripReleaseList releaseAfterUnlock; + { + std::lock_guard lock(roundTripValuesMutex_); + storeRoundTripEntry( + roundTripValues_, key, + NativeApiRoundTripValue{ + std::make_shared(runtime, value), stringLikeNative, true, + validationKey}, + releaseAfterUnlock); + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } +#ifdef TARGET_ENGINE_HERMES + rootRoundTripValue(runtime, key, value); +#endif } - Value findRoundTripValue(Runtime& runtime, const void* native) const { + void rememberScopedRoundTripValue(Runtime& runtime, const void* native, + const Value& value, + bool stringLikeNative = false, + bool persistBeyondFrame = true) { + rememberScopedRoundTripValueWithValidationKey( + runtime, native, value, stringLikeNative, persistBeyondFrame, + nativeObjectClassKey(native)); + } + + void rememberScopedRawRoundTripValue(Runtime& runtime, const void* native, + const Value& value, + bool stringLikeNative = false, + bool persistBeyondFrame = true) { + rememberScopedRoundTripValueWithValidationKey(runtime, native, value, + stringLikeNative, + persistBeyondFrame, 0); + } + + void rememberScopedRoundTripValueWithValidationKey(Runtime& runtime, + const void* native, + const Value& value, + bool stringLikeNative, + bool persistBeyondFrame, + uintptr_t validationKey) { if (native == nullptr) { - return Value::undefined(); + return; } - std::lock_guard lock(roundTripValuesMutex_); - auto it = roundTripValues_.find( - normalizeRuntimePointer(reinterpret_cast(native))); - if (it == roundTripValues_.end() || it->second == nullptr) { + uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); + NativeApiRoundTripValue entry{ + std::make_shared(runtime, value), stringLikeNative, + persistBeyondFrame, validationKey}; + NativeApiRoundTripReleaseList releaseAfterUnlock; + { + std::lock_guard lock(roundTripValuesMutex_); + auto framesIt = + roundTripCacheFramesByThread_.find(std::this_thread::get_id()); + if (framesIt != roundTripCacheFramesByThread_.end() && + !framesIt->second.empty()) { + storeRoundTripEntry(framesIt->second.back(), key, std::move(entry), + releaseAfterUnlock); + } else if (persistBeyondFrame) { + rememberRecentRoundTripValue(key, std::move(entry), + releaseAfterUnlock); + } + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } + } + + Value findRoundTripValue(Runtime& runtime, const void* native, + bool* stringLikeNative = nullptr, + bool nativeIsObject = false, + uintptr_t validationKey = 0) { + if (stringLikeNative != nullptr) { + *stringLikeNative = false; + } + if (native == nullptr) { return Value::undefined(); } - return Value(runtime, *it->second); + uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); + const uintptr_t expectedValidationKey = + validationKey != 0 + ? validationKey + : (nativeIsObject ? nativeObjectClassKey(native) : 0); + struct RoundTripCacheEntry { + const NativeApiBridge* bridge = nullptr; + uintptr_t key = 0; + uint64_t generation = 0; + std::weak_ptr value; + bool miss = false; + bool stringLikeNative = false; + uintptr_t validationKey = 0; + }; + static thread_local RoundTripCacheEntry cache[4]; + const uint64_t generation = + roundTripValuesGeneration_.load(std::memory_order_acquire); + const size_t firstSlot = (key >> 4) & 3; + for (size_t i = 0; i < 4; i++) { + RoundTripCacheEntry& entry = cache[(firstSlot + i) & 3]; + if (entry.bridge == this && entry.key == key && + entry.generation == generation) { + if (entry.validationKey != expectedValidationKey) { + break; + } + if (entry.miss) { + return Value::undefined(); + } + if (auto cached = entry.value.lock()) { + if (roundTripValuesGeneration_.load(std::memory_order_acquire) == + generation) { + if (stringLikeNative != nullptr) { + *stringLikeNative = entry.stringLikeNative; + } + return Value(runtime, *cached); + } + } + break; + } + } + + std::shared_ptr storedValue; + bool cachedStringLike = false; + { + std::lock_guard lock(roundTripValuesMutex_); + auto findEntry = [&](const auto& map) -> const NativeApiRoundTripValue* { + auto it = map.find(key); + if (it == map.end() || it->second.value == nullptr) { + return nullptr; + } + if (it->second.validationKey != expectedValidationKey) { + return nullptr; + } + return &it->second; + }; + + const NativeApiRoundTripValue* entry = findEntry(roundTripValues_); + if (entry == nullptr) { + auto framesIt = + roundTripCacheFramesByThread_.find(std::this_thread::get_id()); + if (framesIt != roundTripCacheFramesByThread_.end()) { + for (auto frame = framesIt->second.rbegin(); + frame != framesIt->second.rend(); ++frame) { + entry = findEntry(*frame); + if (entry != nullptr) { + break; + } + } + } + } + if (entry == nullptr) { + entry = findEntry(recentRoundTripValues_); + } + if (entry == nullptr) { + cache[firstSlot] = RoundTripCacheEntry{ + this, key, generation, {}, true, false, expectedValidationKey}; + return Value::undefined(); + } + storedValue = entry->value; + cachedStringLike = entry->stringLikeNative; + cache[firstSlot] = RoundTripCacheEntry{ + this, key, generation, storedValue, false, cachedStringLike, + entry->validationKey}; + } + if (stringLikeNative != nullptr) { + *stringLikeNative = cachedStringLike; + } + return Value(runtime, *storedValue); + } + + void forgetRoundTripValue(Runtime& runtime, const void* native) { + if (native == nullptr) { + return; + } + uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); +#ifdef TARGET_ENGINE_HERMES + bool rooted = false; + NativeApiRoundTripReleaseList releaseAfterUnlock; + { + std::lock_guard lock(roundTripValuesMutex_); + eraseRoundTripMapKey(roundTripValues_, key, releaseAfterUnlock); + eraseRoundTripKeyFromScopedCaches(key, releaseAfterUnlock); + rooted = rootedRoundTripValues_.erase(key) > 0; + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } + if (rooted) { + unrootRoundTripValue(runtime, key); + } +#else + forgetRoundTripKey(key); +#endif + } + + void forgetRoundTripKey(uintptr_t key) { + if (key == 0) { + return; + } + NativeApiRoundTripReleaseList releaseAfterUnlock; + { + std::lock_guard lock(roundTripValuesMutex_); + eraseRoundTripMapKey(roundTripValues_, key, releaseAfterUnlock); + eraseRoundTripKeyFromScopedCaches(key, releaseAfterUnlock); + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } } void forgetRoundTripValue(const void* native) { if (native == nullptr) { return; } + uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); + NativeApiRoundTripReleaseList releaseAfterUnlock; + { + std::lock_guard lock(roundTripValuesMutex_); + eraseRoundTripMapKey(roundTripValues_, key, releaseAfterUnlock); + eraseRoundTripKeyFromScopedCaches(key, releaseAfterUnlock); + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } + } + + uint64_t roundTripValuesGeneration() const { + return roundTripValuesGeneration_.load(std::memory_order_acquire); + } + + void beginRoundTripCacheFrame() { std::lock_guard lock(roundTripValuesMutex_); - roundTripValues_.erase( - normalizeRuntimePointer(reinterpret_cast(native))); + roundTripCacheFramesByThread_[std::this_thread::get_id()].emplace_back(); + } + + void endRoundTripCacheFrame() { + NativeApiRoundTripReleaseList releaseAfterUnlock; + NativeApiRoundTripFrame frame; + { + std::lock_guard lock(roundTripValuesMutex_); + auto framesIt = + roundTripCacheFramesByThread_.find(std::this_thread::get_id()); + if (framesIt == roundTripCacheFramesByThread_.end() || + framesIt->second.empty()) { + return; + } + + auto& frames = framesIt->second; + frame = std::move(frames.back()); + frames.pop_back(); + if (!frames.empty()) { + auto& parent = frames.back(); + for (auto& entry : frame) { + storeRoundTripEntry(parent, entry.first, std::move(entry.second), + releaseAfterUnlock); + } + } else { + roundTripCacheFramesByThread_.erase(framesIt); + for (auto& entry : frame) { + if (entry.second.persistBeyondFrame) { + rememberRecentRoundTripValue(entry.first, std::move(entry.second), + releaseAfterUnlock); + } else { + releaseAfterUnlock.push_back(std::move(entry.second)); + } + } + } + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } } void rememberClassValue(Runtime& runtime, Class cls, const Value& value) { @@ -605,6 +956,7 @@ class NativeApiJsiBridge { } objectExpandos_[normalizeRuntimePointer(reinterpret_cast(native))] [property] = std::make_shared(runtime, value); + objectExpandosGeneration_.fetch_add(1, std::memory_order_release); } Value findObjectExpando(Runtime& runtime, const void* native, @@ -612,15 +964,49 @@ class NativeApiJsiBridge { if (native == nullptr || property.empty()) { return Value::undefined(); } - auto objectIt = objectExpandos_.find( - normalizeRuntimePointer(reinterpret_cast(native))); + struct ObjectExpandoCacheEntry { + const NativeApiBridge* bridge = nullptr; + uintptr_t key = 0; + uint64_t generation = 0; + std::string property; + std::weak_ptr value; + bool miss = false; + }; + static thread_local ObjectExpandoCacheEntry cache[8]; + static thread_local size_t nextSlot = 0; + + const uintptr_t key = + normalizeRuntimePointer(reinterpret_cast(native)); + const uint64_t generation = + objectExpandosGeneration_.load(std::memory_order_acquire); + for (auto& entry : cache) { + if (entry.bridge == this && entry.key == key && + entry.generation == generation && entry.property == property) { + if (entry.miss) { + return Value::undefined(); + } + if (auto cached = entry.value.lock()) { + return Value(runtime, *cached); + } + break; + } + } + + auto objectIt = objectExpandos_.find(key); + const size_t slot = nextSlot++ & 7; if (objectIt == objectExpandos_.end()) { + cache[slot] = + ObjectExpandoCacheEntry{this, key, generation, property, {}, true}; return Value::undefined(); } auto propertyIt = objectIt->second.find(property); if (propertyIt == objectIt->second.end() || propertyIt->second == nullptr) { + cache[slot] = + ObjectExpandoCacheEntry{this, key, generation, property, {}, true}; return Value::undefined(); } + cache[slot] = ObjectExpandoCacheEntry{ + this, key, generation, property, propertyIt->second, false}; return Value(runtime, *propertyIt->second); } @@ -630,6 +1016,72 @@ class NativeApiJsiBridge { } objectExpandos_.erase( normalizeRuntimePointer(reinterpret_cast(native))); + objectExpandosGeneration_.fetch_add(1, std::memory_order_release); + } + + // Per-class cache of resolved metadata property-getter members. Lets the + // instance property interceptor skip the special-name chain + metadata + // discovery on every `object.prop` access (the engines without V8's + // kNonMasking prototype fast path otherwise re-resolve on each access). + // membersByClassOffset_ vectors are permanent, so the member pointer is + // stable for the bridge's lifetime. + struct CachedPropertyGetter { + const NativeApiMember* member; + std::string selectorName; + std::shared_ptr preparedInvocation; + }; + const CachedPropertyGetter* findCachedPropertyGetter( + Class cls, const std::string& property) const { + if (cls == Nil || property.empty()) { + return nullptr; + } + struct PropertyGetterCacheEntry { + const NativeApiBridge* bridge = nullptr; + Class cls = Nil; + uint64_t generation = 0; + std::string property; + const CachedPropertyGetter* getter = nullptr; + bool miss = false; + }; + static thread_local PropertyGetterCacheEntry cache[8]; + static thread_local size_t nextSlot = 0; + + const uint64_t generation = + propertyGetterCacheGeneration_.load(std::memory_order_acquire); + for (auto& entry : cache) { + if (entry.bridge == this && entry.cls == cls && + entry.generation == generation && entry.property == property) { + return entry.miss ? nullptr : entry.getter; + } + } + + auto classIt = propertyGetterCache_.find(cls); + const size_t slot = nextSlot++ & 7; + if (classIt == propertyGetterCache_.end()) { + cache[slot] = PropertyGetterCacheEntry{ + this, cls, generation, property, nullptr, true}; + return nullptr; + } + auto propIt = classIt->second.find(property); + if (propIt == classIt->second.end()) { + cache[slot] = PropertyGetterCacheEntry{ + this, cls, generation, property, nullptr, true}; + return nullptr; + } + cache[slot] = + PropertyGetterCacheEntry{this, cls, generation, property, + &propIt->second, false}; + return &propIt->second; + } + void cachePropertyGetter(Class cls, const std::string& property, + const NativeApiMember* member, + const std::string& selectorName, + std::shared_ptr + preparedInvocation = nullptr) { + propertyGetterCache_[cls][property] = + CachedPropertyGetter{member, selectorName, + std::move(preparedInvocation)}; + propertyGetterCacheGeneration_.fetch_add(1, std::memory_order_release); } void rememberPointerValue(Runtime& runtime, const void* native, @@ -705,7 +1157,7 @@ class NativeApiJsiBridge { 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::shared_ptr scheduler() const { return scheduler_; } const std::function)>& nativeInvocationInvoker() const { return nativeInvocationInvoker_; @@ -714,16 +1166,25 @@ class NativeApiJsiBridge { const { return nativeCallbackInvoker_; } + const std::function)>& runtimeCallbackInvoker() + const { + return runtimeCallbackInvoker_; + } const std::function)>& jsThreadCallbackInvoker() const { return jsThreadCallbackInvoker_; } + const std::function)>& + jsThreadAsyncCallbackInvoker() const { + return jsThreadAsyncCallbackInvoker_; + } bool invokeCallbacksOnNativeCallerThread() const { return invokeCallbacksOnNativeCallerThread_; } + std::thread::id jsThreadId() const { return jsThreadId_; } - void retainJsiLifetime(std::shared_ptr lifetime) { + void retainEngineLifetime(std::shared_ptr lifetime) { if (lifetime == nullptr) { return; } @@ -731,6 +1192,115 @@ class NativeApiJsiBridge { retainedLifetimes_.push_back(std::move(lifetime)); } +#ifdef TARGET_ENGINE_HERMES + std::string roundTripRootKey(uintptr_t key) const { + char buffer[32] = {}; + snprintf(buffer, sizeof(buffer), "p%llx", + static_cast(key)); + return buffer; + } + + Object roundTripRootObject(Runtime& runtime) { + if (roundTripRootCache_) { + return roundTripRootCache_->asObject(runtime); + } + static constexpr const char* kRootName = + "__nativeScriptNativeApiRoundTripValues"; + Object global = runtime.global(); + if (global.hasProperty(runtime, kRootName)) { + Value existing = global.getProperty(runtime, kRootName); + if (existing.isObject()) { + Object root = existing.asObject(runtime); + roundTripRootCache_ = std::make_shared(runtime, root); + return root; + } + } + + Object root(runtime); + global.setProperty(runtime, kRootName, root); + roundTripRootCache_ = std::make_shared(runtime, root); + return root; + } + + void rootRoundTripValue(Runtime& runtime, uintptr_t key, + const Value& value) { + roundTripRootObject(runtime) + .setProperty(runtime, roundTripRootKey(key).c_str(), value); + std::lock_guard lock(roundTripValuesMutex_); + rootedRoundTripValues_.insert(key); + } + + void unrootRoundTripValue(Runtime& runtime, uintptr_t key) { + roundTripRootObject(runtime) + .setProperty(runtime, roundTripRootKey(key).c_str(), + Value::undefined()); + } +#endif + + uintptr_t nativeObjectClassKey(const void* native) const { + if (native == nullptr) { + return 0; + } + return normalizeRuntimePointer( + reinterpret_cast(object_getClass(static_cast(native)))); + } + + void storeRoundTripEntry( + std::unordered_map& map, + uintptr_t key, NativeApiRoundTripValue&& entry, + NativeApiRoundTripReleaseList& releaseAfterUnlock) { + auto it = map.find(key); + if (it == map.end()) { + map.emplace(key, std::move(entry)); + return; + } + + releaseAfterUnlock.push_back(std::move(it->second)); + it->second = std::move(entry); + } + + void eraseRoundTripMapKey( + std::unordered_map& map, + uintptr_t key, NativeApiRoundTripReleaseList& releaseAfterUnlock) { + auto it = map.find(key); + if (it == map.end()) { + return; + } + + releaseAfterUnlock.push_back(std::move(it->second)); + map.erase(it); + } + + void eraseRoundTripKeyFromScopedCaches( + uintptr_t key, NativeApiRoundTripReleaseList& releaseAfterUnlock) { + eraseRoundTripMapKey(recentRoundTripValues_, key, releaseAfterUnlock); + recentRoundTripValueOrder_.erase( + std::remove(recentRoundTripValueOrder_.begin(), + recentRoundTripValueOrder_.end(), key), + recentRoundTripValueOrder_.end()); + for (auto& stackEntry : roundTripCacheFramesByThread_) { + for (auto& frame : stackEntry.second) { + eraseRoundTripMapKey(frame, key, releaseAfterUnlock); + } + } + } + + void rememberRecentRoundTripValue(uintptr_t key, + NativeApiRoundTripValue&& entry, + NativeApiRoundTripReleaseList& releaseAfterUnlock) { + if (recentRoundTripValues_.find(key) == recentRoundTripValues_.end()) { + recentRoundTripValueOrder_.push_back(key); + } + storeRoundTripEntry(recentRoundTripValues_, key, std::move(entry), + releaseAfterUnlock); + while (recentRoundTripValueOrder_.size() > kRecentRoundTripValueLimit) { + uintptr_t evicted = recentRoundTripValueOrder_.front(); + recentRoundTripValueOrder_.erase(recentRoundTripValueOrder_.begin()); + eraseRoundTripMapKey(recentRoundTripValues_, evicted, + releaseAfterUnlock); + } + } + const std::vector& membersForClass( const NativeApiSymbol& symbol) const { auto cached = membersByClassOffset_.find(symbol.offset); @@ -767,10 +1337,10 @@ class NativeApiJsiBridge { return inserted.first->second; } - std::shared_ptr aggregateInfoFor( + std::shared_ptr aggregateInfoFor( MDSectionOffset aggregateOffset, bool isUnion); - std::shared_ptr aggregateInfoFor( + std::shared_ptr aggregateInfoFor( const NativeApiSymbol& symbol) { return aggregateInfoFor(symbol.offset, symbol.kind == NativeApiSymbolKind::Union); @@ -810,7 +1380,7 @@ class NativeApiJsiBridge { } static std::unique_ptr loadMetadata( - const NativeApiJsiConfig& config) { + const NativeApiConfig& config) { if (config.metadataPtr != nullptr && *static_cast(config.metadataPtr) != '\0') { #ifdef EMBED_METADATA_SIZE @@ -979,7 +1549,7 @@ class NativeApiJsiBridge { metagen::MDVariableEvalKind evalKind) { switch (evalKind) { case metagen::mdEvalNone: - skipMetadataJsiType(metadata, &offset); + skipMetadataEngineType(metadata, &offset); break; case metagen::mdEvalInt64: offset += sizeof(int64_t); @@ -1124,7 +1694,7 @@ class NativeApiJsiBridge { if (!isUnion) { offset += sizeof(uint16_t); } - skipMetadataJsiType(metadata_.get(), &offset); + skipMetadataEngineType(metadata_.get(), &offset); } } @@ -1535,13 +2105,27 @@ class NativeApiJsiBridge { std::unordered_map classSymbolsByRuntimePointer_; std::unordered_map protocolSymbolsByRuntimePointer_; mutable std::mutex roundTripValuesMutex_; - std::unordered_map> roundTripValues_; + std::unordered_map roundTripValues_; + std::unordered_map + roundTripCacheFramesByThread_; + std::unordered_map + recentRoundTripValues_; + std::vector recentRoundTripValueOrder_; +#ifdef TARGET_ENGINE_HERMES + std::unordered_set rootedRoundTripValues_; + std::shared_ptr roundTripRootCache_; +#endif + std::atomic roundTripValuesGeneration_{1}; std::unordered_map> classValues_; std::unordered_map> classPrototypes_; std::unordered_map> pointerValues_; std::unordered_map>> objectExpandos_; + std::atomic objectExpandosGeneration_{1}; + std::unordered_map> + propertyGetterCache_; + std::atomic propertyGetterCacheGeneration_{1}; std::unordered_map classSymbolsByOffset_; std::unordered_map protocolSymbolsByOffset_; std::vector classNames_; @@ -1551,10 +2135,12 @@ class NativeApiJsiBridge { std::vector enumNames_; std::vector structNames_; std::vector unionNames_; - std::shared_ptr scheduler_; + std::shared_ptr scheduler_; std::function)> nativeInvocationInvoker_; std::function)> nativeCallbackInvoker_; + std::function)> runtimeCallbackInvoker_; std::function)> jsThreadCallbackInvoker_; + std::function)> jsThreadAsyncCallbackInvoker_; bool invokeCallbacksOnNativeCallerThread_ = false; mutable std::unordered_map> membersByClassOffset_; @@ -1564,7 +2150,7 @@ class NativeApiJsiBridge { membersByProtocolOffset_; std::unordered_map structSymbolsByOffset_; std::unordered_map unionSymbolsByOffset_; - std::unordered_map> + std::unordered_map> aggregateInfoByOffset_; std::unordered_set aggregateInfoInProgress_; std::thread::id jsThreadId_ = std::this_thread::get_id(); @@ -1572,6 +2158,89 @@ class NativeApiJsiBridge { std::vector> retainedLifetimes_; }; +class NativeApiRoundTripCacheFrameGuard final { + public: + explicit NativeApiRoundTripCacheFrameGuard( + const std::shared_ptr& bridge) + : bridge_(bridge) { + if (bridge_ != nullptr) { + bridge_->beginRoundTripCacheFrame(); + } + } + + ~NativeApiRoundTripCacheFrameGuard() { + if (bridge_ != nullptr) { + bridge_->endRoundTripCacheFrame(); + } + } + + NativeApiRoundTripCacheFrameGuard( + const NativeApiRoundTripCacheFrameGuard&) = delete; + NativeApiRoundTripCacheFrameGuard& operator=( + const NativeApiRoundTripCacheFrameGuard&) = delete; + + private: + std::shared_ptr bridge_; +}; + +template +void performGeneratedObjCInvocation( + Runtime& runtime, const std::shared_ptr& bridge, + Invocation&& invocation) { + const auto& invoker = bridge->nativeInvocationInvoker(); + if (invoker || bridge->invokeCallbacksOnNativeCallerThread()) { + performNativeInvocation(runtime, invoker, [&]() { invocation(); }); + } else { + invocation(); + } +} + +bool nativeObjectReturnMayCoerceToString(const NativeApiType& type) { + return type.kind == metagen::mdTypeAnyObject || + type.kind == metagen::mdTypeNSStringObject; +} + +bool nativeObjectIsStringLike(id object) { + if (object == nil) { + return false; + } + Class cls = object_getClass(object); + struct StringLikeClassCacheEntry { + Class cls = Nil; + bool stringLike = false; + }; + static thread_local StringLikeClassCacheEntry cache[4]; + const size_t firstSlot = (reinterpret_cast(cls) >> 4) & 3; + for (size_t i = 0; i < 4; i++) { + const auto& entry = cache[(firstSlot + i) & 3]; + if (entry.cls == cls) { + return entry.stringLike; + } + } + bool stringLike = [object isKindOfClass:[NSString class]]; + cache[firstSlot] = StringLikeClassCacheEntry{cls, stringLike}; + return stringLike; +} + +Value findCachedNativeObjectReturn(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiType& type, id object) { + bool roundTripStringLike = false; + const bool stringReturnCandidate = nativeObjectReturnMayCoerceToString(type); + // AnyObject/NSString returns intentionally coerce string-like native objects + // to JS strings, so cached identity is only valid for non-string wrappers. + if (stringReturnCandidate && nativeObjectIsStringLike(object)) { + return Value::undefined(); + } + Value roundTrip = bridge->findRoundTripValue( + runtime, object, stringReturnCandidate ? &roundTripStringLike : nullptr, + true); + if (!roundTrip.isUndefined() && !roundTripStringLike) { + return roundTrip; + } + return Value::undefined(); +} + Value makeString(Runtime& runtime, const std::string& value) { return String::createFromUtf8(runtime, value); } @@ -1579,7 +2248,7 @@ Value makeString(Runtime& runtime, const std::string& 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( + throw JSError( runtime, std::string(argumentName) + " must be a string."); } return args[index].asString(runtime).utf8(runtime); @@ -1622,27 +2291,67 @@ class NativeApiPointerHostObject; class NativeApiObjectHostObject; class NativeApiClassHostObject; class NativeApiProtocolHostObject; -class NativeApiJsiArgumentFrame; +class NativeApiArgumentFrame; +struct NativeApiPreparedCFunctionInvocation; +struct NativeApiPreparedObjCInvocation; Value callCFunction(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, const NativeApiSymbol& symbol, const Value* args, size_t count); +Value callCFunction( + Runtime& runtime, const std::shared_ptr& bridge, + const std::shared_ptr& prepared, + const Value* args, size_t count); Value callObjCSelector(Runtime& runtime, - const std::shared_ptr& bridge, + 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); +std::shared_ptr +prepareNativeApiObjCInvocation( + Runtime& runtime, const std::shared_ptr& bridge, + Class lookupClass, bool receiverIsClass, const std::string& selectorName, + const NativeApiMember* member); + +Value callPreparedObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const NativeApiPreparedObjCInvocation& prepared, const Value* args, + size_t count, Class dispatchSuperClass = Nil); + +void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId); +bool tryCallGeneratedEngineObjCSelector( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const Value* args, size_t count, Class dispatchSuperClass, Value* result); + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations); + +Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations); + Value makeNativeObjectValue(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, id object, bool ownsObject); Value makeNativeClassValue(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, NativeApiSymbol symbol); Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { @@ -1670,19 +2379,19 @@ Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { return result; } -size_t nativeSizeForType(const NativeApiJsiType& type); +size_t nativeSizeForType(const NativeApiType& type); std::optional parseArrayIndexProperty(const std::string& property); -NativeApiJsiType nativeObjectReturnType( +NativeApiType nativeObjectReturnType( MDTypeKind kind = metagen::mdTypeAnyObject) { - NativeApiJsiType type; + NativeApiType type; type.kind = kind; type.ffiType = &ffi_type_pointer; type.supported = true; return type; } -NativeApiJsiType nativeObjectReturnTypeForClass(Class cls) { +NativeApiType nativeObjectReturnTypeForClass(Class cls) { if (cls != Nil) { const char* name = class_getName(cls); if (name != nullptr && std::strcmp(name, "NSString") == 0) { @@ -1696,10 +2405,11 @@ NativeApiJsiType nativeObjectReturnTypeForClass(Class cls) { } Value convertNativeReturnValue(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, void* value); + const std::shared_ptr& bridge, + const NativeApiType& type, void* value); Object createPointer(Runtime& runtime, - const std::shared_ptr& bridge, - void* pointer, bool adopted = false); + const std::shared_ptr& bridge, + void* pointer, bool adopted = false, + std::shared_ptr backingValue = nullptr); -NativeApiJsiType primitiveInteropType(MDTypeKind kind); +NativeApiType primitiveInteropType(MDTypeKind kind); diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h b/NativeScript/ffi/shared/bridge/TypeConv.mm similarity index 57% rename from NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h rename to NativeScript/ffi/shared/bridge/TypeConv.mm index 9e37ec0a9..00249778d 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h +++ b/NativeScript/ffi/shared/bridge/TypeConv.mm @@ -1,23 +1,19 @@ -std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, - const char* name); +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) { +id objectFromEngineValue(Runtime& runtime, const std::shared_ptr& bridge, + const Value& value, NativeApiArgumentFrame& 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]; + 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; } @@ -32,24 +28,21 @@ id objectFromJsiValue(Runtime& runtime, if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->object(); } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { + if (Class cls = nativeClassFromEngineObject(runtime, object)) { return static_cast(cls); } if (object.isHostObject(runtime)) { return static_cast( - object.getHostObject(runtime) - ->nativeProtocol()); + 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()); + return static_cast(object.getHostObject(runtime)->pointer()); } if (object.isHostObject(runtime)) { - return static_cast( - object.getHostObject(runtime)->data()); + return static_cast(object.getHostObject(runtime)->data()); } if (object.isHostObject(runtime)) { return static_cast( @@ -58,52 +51,43 @@ id objectFromJsiValue(Runtime& runtime, 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 (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); + bridge->rememberScopedRoundTripValue(runtime, date, value, false, true); 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); + 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 objectFromEngineValue(runtime, bridge, primitiveValue, frame, mutableString); } } const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (readEngineBuffer(runtime, object, &bytes, &byteLength)) { NSData* data = [NSData dataWithBytes:bytes length:byteLength]; - bridge->rememberRoundTripValue(runtime, data, value); + bridge->rememberScopedRoundTripValue(runtime, data, value, false, false); return data; } if (object.isArray(runtime)) { Array array = object.getArray(runtime); - NSMutableArray* nativeArray = - [NSMutableArray arrayWithCapacity:array.size(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); + id element = + objectFromEngineValue(runtime, bridge, array.getValueAtIndex(runtime, i), frame, false); [nativeArray addObject:element != nil ? element : [NSNull null]]; } - bridge->rememberRoundTripValue(runtime, nativeArray, value); + bridge->rememberScopedRoundTripValue(runtime, nativeArray, value, false, false); return nativeArray; } @@ -114,27 +98,24 @@ id objectFromJsiValue(Runtime& runtime, 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); + id element = objectFromEngineValue(runtime, bridge, + object.getProperty(runtime, key.c_str()), frame, false); [nativeArray addObject:element != nil ? element : [NSNull null]]; } - bridge->rememberRoundTripValue(runtime, nativeArray, value); + bridge->rememberScopedRoundTripValue(runtime, nativeArray, value, false, false); 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) && + 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 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); @@ -142,26 +123,22 @@ id objectFromJsiValue(Runtime& runtime, [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)) { + 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); + id key = objectFromEngineValue(runtime, bridge, pair.getValueAtIndex(runtime, 0), frame, + false); + id nativeValue = objectFromEngineValue(runtime, bridge, pair.getValueAtIndex(runtime, 1), + frame, false); if (key != nil) { - [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] - forKey:key]; + [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] forKey:key]; } } - bridge->rememberRoundTripValue(runtime, nativeMap, value); + bridge->rememberScopedRoundTripValue(runtime, nativeMap, value, false, false); return nativeMap; } } @@ -178,19 +155,16 @@ id objectFromJsiValue(Runtime& runtime, if (propertyValue.isUndefined()) { continue; } - id nativeValue = - objectFromJsiValue(runtime, bridge, propertyValue, frame, false); + id nativeValue = objectFromEngineValue(runtime, bridge, propertyValue, frame, false); NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; if (nativeKey != nil) { - [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] - forKey:nativeKey]; + [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] forKey:nativeKey]; } } - bridge->rememberRoundTripValue(runtime, dictionary, value); + bridge->rememberScopedRoundTripValue(runtime, dictionary, value, false, false); return dictionary; } - throw facebook::jsi::JSError(runtime, - "Value cannot be converted to Objective-C object."); + throw JSError(runtime, "Value cannot be converted to Objective-C object."); } std::string utf8StringFromNSString(NSString* string) { @@ -215,45 +189,53 @@ std::string utf8StringFromNSString(NSString* string) { return result; } -bool readNativePointerProperty(Runtime& runtime, const Object& object, - void** pointer) { +char* copyCStringForReference(const char* string, size_t* byteLength = nullptr) { + size_t length = string != nullptr ? std::strlen(string) + 1 : 1; + char* copy = static_cast(malloc(length)); + if (copy == nullptr) { + throw std::bad_alloc(); + } + if (string != nullptr) { + std::memcpy(copy, string, length); + } else { + copy[0] = '\0'; + } + if (byteLength != nullptr) { + *byteLength = length; + } + return copy; +} + +bool readNativePointerProperty(Runtime& runtime, const Object& object, void** pointer) { if (pointer == nullptr) { return false; } - Value nativePointerObjectValue = - object.getProperty(runtime, "__nativeApiPointerObject"); + Value nativePointerObjectValue = object.getProperty(runtime, "__nativeApiPointerObject"); if (nativePointerObjectValue.isObject()) { Object nativePointerObject = nativePointerObjectValue.asObject(runtime); - if (nativePointerObject.isHostObject( - runtime)) { - *pointer = nativePointerObject - .getHostObject(runtime) - ->pointer(); + if (nativePointerObject.isHostObject(runtime)) { + *pointer = nativePointerObject.getHostObject(runtime)->pointer(); return true; } } - Value nativePointerValue = - object.getProperty(runtime, "__nativeApiPointer"); + Value nativePointerValue = object.getProperty(runtime, "__nativeApiPointer"); if (nativePointerValue.isNumber()) { - *pointer = reinterpret_cast( - static_cast(nativePointerValue.getNumber())); + *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())); + *pointer = reinterpret_cast(static_cast(nativeAddressValue.getNumber())); return true; } return false; } -std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, - const char* name) { +std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, const char* name) { if (name == nullptr || !object.hasProperty(runtime, name)) { return ""; } @@ -261,6 +243,62 @@ std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, return value.isString() ? value.asString(runtime).utf8(runtime) : ""; } +constexpr const char* kNativeApiCallbackEncodingProperty = "__nativeApiCallbackEncoding"; + +Function interopCallbackFromArguments(Runtime& runtime, const char* constructorName, + const char* kind, const Value* args, size_t count) { + if (count < 1) { + throw JSError(runtime, std::string(constructorName) + " expects a function."); + } + + std::optional callbackObject; + bool hasCallback = false; + std::string encoding; + + for (size_t i = 0; i < count; i++) { + const Value& arg = args[i]; + if (arg.isUndefined() || arg.isNull()) { + continue; + } + if (arg.isString()) { + if (!encoding.empty()) { + throw JSError(runtime, std::string(constructorName) + + " expects only one Objective-C encoding string."); + } + encoding = arg.asString(runtime).utf8(runtime); + continue; + } + if (arg.isObject()) { + Object object = arg.asObject(runtime); + if (object.isFunction(runtime)) { + if (hasCallback) { + throw JSError(runtime, std::string(constructorName) + " expects only one function."); + } + callbackObject.emplace(std::move(object)); + hasCallback = true; + continue; + } + } + + throw JSError(runtime, std::string(constructorName) + + " expects a function and an optional Objective-C encoding string."); + } + + if (!hasCallback) { + throw JSError(runtime, std::string(constructorName) + " expects a function."); + } + + Function function = callbackObject->asFunction(runtime); + function.setProperty(runtime, "kind", makeString(runtime, kind)); + function.setProperty(runtime, "sizeof", static_cast(sizeof(void*))); + if (!encoding.empty()) { + function.setProperty(runtime, kNativeApiCallbackEncodingProperty, + makeString(runtime, encoding)); + } + + return function; +} + void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object) { std::string kind = stringPropertyOrEmpty(runtime, object, "kind"); if (kind != "class" && kind != "protocol") { @@ -281,8 +319,8 @@ void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object) { return lookupProtocolByNativeName(runtimeName); } -void* pointerFromJsiValue(Runtime& runtime, const Value& value, - NativeApiJsiArgumentFrame& frame) { +void* pointerFromEngineValue(Runtime& runtime, const std::shared_ptr& bridge, + const Value& value, NativeApiArgumentFrame& frame) { if (value.isNull() || value.isUndefined()) { return nullptr; } @@ -297,19 +335,17 @@ void* pointerFromJsiValue(Runtime& runtime, const Value& value, if (object.isHostObject(runtime)) { return object.getHostObject(runtime)->object(); } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { + if (Class cls = nativeClassFromEngineObject(runtime, object)) { return cls; } if (object.isHostObject(runtime)) { - return object.getHostObject(runtime) - ->nativeProtocol(); + return object.getHostObject(runtime)->nativeProtocol(); } if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { return symbolPointer; } if (object.isHostObject(runtime)) { - auto reference = - object.getHostObject(runtime); + auto reference = object.getHostObject(runtime); if (reference->data() == nullptr) { reference->ensureStorage(runtime, reference->type(), frame); } @@ -324,16 +360,23 @@ void* pointerFromJsiValue(Runtime& runtime, const Value& value, } const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (readEngineBuffer(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()); + if (string == nullptr) { + throw std::bad_alloc(); + } + if (bridge != nullptr) { + bridge->rememberScopedRawRoundTripValue(runtime, string, value, true, false); + } + frame.addCString(string); return string; } - throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); + throw JSError(runtime, "Value cannot be converted to pointer."); } bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) { @@ -357,13 +400,12 @@ bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) *pointer = object.getHostObject(runtime)->object(); return true; } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { + if (Class cls = nativeClassFromEngineObject(runtime, object)) { *pointer = cls; return true; } if (object.isHostObject(runtime)) { - *pointer = - object.getHostObject(runtime)->nativeProtocol(); + *pointer = object.getHostObject(runtime)->nativeProtocol(); return true; } if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { @@ -381,37 +423,51 @@ void writeNumericArgument(Runtime& runtime, const Value& value, void* target, 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); + 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."); + throw JSError(runtime, std::string("Expected numeric ") + typeName + " argument."); } - double number = numericValue->isBool() ? (numericValue->getBool() ? 1.0 : 0.0) - : numericValue->getNumber(); + 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); +void convertEngineArgument(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, + NativeApiArgumentFrame& frame); -Value convertNativeReturnValue(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, void* value); +Value convertNativeReturnValue(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, void* value); -Class classFromJsiValue(Runtime& runtime, const Value& value); -Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value); +Class classFromEngineValue(Runtime& runtime, const Value& value); +Protocol* protocolFromEngineValue(Runtime& runtime, const Value& value); + +bool valueIsNativeObjectHostObject(Runtime& runtime, const Value& value) { + if (!value.isObject()) { + return false; + } + return value.asObject(runtime).isHostObject(runtime); +} + +bool nativeTypeStoresObjectiveCObject(const NativeApiType& 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; + } +} std::optional parseArrayIndexProperty(const std::string& property) { if (property.empty()) { @@ -431,15 +487,13 @@ std::optional parseArrayIndexProperty(const std::string& property) { return index; } -size_t referenceElementStride(const NativeApiJsiType& type) { +size_t referenceElementStride(const NativeApiType& 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) { +void convertAggregateArgument(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, + NativeApiArgumentFrame& frame) { size_t size = nativeSizeForType(type); if (size == 0) { return; @@ -477,7 +531,7 @@ void convertAggregateArgument(Runtime& runtime, const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (readEngineBuffer(runtime, object, &bytes, &byteLength)) { if (bytes != nullptr) { std::memcpy(target, bytes, std::min(byteLength, size)); } @@ -486,10 +540,10 @@ void convertAggregateArgument(Runtime& runtime, } if (type.aggregateInfo == nullptr) { - throw facebook::jsi::JSError(runtime, "Missing native struct metadata."); + throw JSError(runtime, "Missing native struct metadata."); } if (!value.isObject()) { - throw facebook::jsi::JSError(runtime, "Expected struct descriptor object."); + throw JSError(runtime, "Expected struct descriptor object."); } Object object = value.asObject(runtime); @@ -500,16 +554,14 @@ void convertAggregateArgument(Runtime& runtime, } Value fieldValue = object.getProperty(runtime, field.name.c_str()); void* fieldTarget = static_cast(target) + field.offset; - convertJsiArgument(runtime, bridge, field.type, fieldValue, fieldTarget, - frame); + convertEngineArgument(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) { + const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, + NativeApiArgumentFrame& frame) { size_t size = nativeSizeForType(type); std::memset(target, 0, size); if (value.isNull() || value.isUndefined()) { @@ -518,7 +570,7 @@ void convertIndexedAggregateArgument(Runtime& runtime, if (value.isObject()) { const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, value.asObject(runtime), &bytes, &byteLength)) { + if (readEngineBuffer(runtime, value.asObject(runtime), &bytes, &byteLength)) { if (bytes != nullptr) { std::memcpy(target, bytes, std::min(byteLength, size)); } @@ -526,28 +578,27 @@ void convertIndexedAggregateArgument(Runtime& runtime, } } if (!value.isObject() || !value.asObject(runtime).isArray(runtime)) { - throw facebook::jsi::JSError(runtime, "Expected array, ArrayBuffer, or typed array."); + throw 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."); + throw 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); + convertEngineArgument(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) { +void convertEngineFfiArgument(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, + NativeApiArgumentFrame& frame) { if (type.kind != metagen::mdTypeArray) { - convertJsiArgument(runtime, bridge, type, value, target, frame); + convertEngineArgument(runtime, bridge, type, value, target, frame); return; } @@ -558,7 +609,7 @@ void convertJsiFfiArgument(Runtime& runtime, if (!readPointerLikeValue(runtime, value, &pointer)) { const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (readEngineBuffer(runtime, object, &bytes, &byteLength)) { pointer = const_cast(bytes); } } @@ -567,8 +618,7 @@ void convertJsiFfiArgument(Runtime& runtime, if (pointer == nullptr) { size_t byteLength = nativeSizeForType(type); void* buffer = frame.addBuffer(byteLength); - convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, - frame); + convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, frame); pointer = buffer; } } @@ -576,26 +626,22 @@ void convertJsiFfiArgument(Runtime& runtime, *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."); +void convertEngineArgument(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, + NativeApiArgumentFrame& frame) { + if (unsupportedEngineType(type)) { + throw JSError(runtime, "This native signature is not supported by " + "the engine bridge yet."); } switch (type.kind) { case metagen::mdTypeBool: if (!value.isNumber() && !value.isBool()) { - throw facebook::jsi::JSError(runtime, - "Expected boolean or numeric argument."); + throw JSError(runtime, "Expected boolean or numeric argument."); } - *static_cast(target) = - value.isBool() ? static_cast(value.getBool()) - : static_cast(value.getNumber() != 0); + *static_cast(target) = value.isBool() + ? static_cast(value.getBool()) + : static_cast(value.getNumber() != 0); break; case metagen::mdTypeChar: writeNumericArgument(runtime, value, target, "int8"); @@ -611,8 +657,7 @@ void convertJsiArgument(Runtime& runtime, 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."); + throw JSError(runtime, "Expected a single-character string."); } *static_cast(target) = static_cast(static_cast(text[0])); @@ -649,35 +694,52 @@ void convertJsiArgument(Runtime& runtime, Object object = value.asObject(runtime); void* pointer = nullptr; if (readPointerLikeValue(runtime, value, &pointer)) { + if (bridge != nullptr) { + bridge->rememberScopedRawRoundTripValue(runtime, pointer, value, false, true); + } *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)); + if (readEngineBuffer(runtime, object, &bytes, &byteLength)) { + if (bridge != nullptr) { + bridge->rememberScopedRawRoundTripValue(runtime, bytes, value, false, true); + } + *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 (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()); + if (string == nullptr) { + throw std::bad_alloc(); + } + if (bridge != nullptr) { + bridge->rememberScopedRawRoundTripValue(runtime, string, value, true, false); + } + frame.addCString(string); *static_cast(target) = string; break; } } } if (!value.isString()) { - throw facebook::jsi::JSError(runtime, "Expected string argument."); + throw JSError(runtime, "Expected string argument."); } std::string utf8 = value.asString(runtime).utf8(runtime); char* string = strdup(utf8.c_str()); + if (string == nullptr) { + throw std::bad_alloc(); + } + if (bridge != nullptr) { + bridge->rememberScopedRawRoundTripValue(runtime, string, value, true, false); + } + frame.addCString(string); *static_cast(target) = string; break; } @@ -687,14 +749,16 @@ void convertJsiArgument(Runtime& runtime, case metagen::mdTypeInstanceObject: case metagen::mdTypeNSStringObject: case metagen::mdTypeNSMutableStringObject: { - id object = objectFromJsiValue( - runtime, bridge, value, frame, - type.kind == metagen::mdTypeNSMutableStringObject); + id object = objectFromEngineValue(runtime, bridge, value, frame, + type.kind == metagen::mdTypeNSMutableStringObject); + if (valueIsNativeObjectHostObject(runtime, value)) { + frame.retainObject(object); + } *static_cast(target) = object; break; } case metagen::mdTypeClass: { - *static_cast(target) = classFromJsiValue(runtime, value); + *static_cast(target) = classFromEngineValue(runtime, value); break; } case metagen::mdTypeSelector: { @@ -703,7 +767,7 @@ void convertJsiArgument(Runtime& runtime, break; } if (!value.isString()) { - throw facebook::jsi::JSError(runtime, "Expected selector string."); + throw JSError(runtime, "Expected selector string."); } std::string selectorName = value.asString(runtime).utf8(runtime); *static_cast(target) = sel_registerName(selectorName.c_str()); @@ -725,29 +789,31 @@ void convertJsiArgument(Runtime& runtime, break; } if (object.isHostObject(runtime)) { - void* pointer = - object.getHostObject(runtime) - ->data(); + void* pointer = object.getHostObject(runtime)->data(); frame.rememberRoundTripValue(bridge, runtime, pointer, value); *static_cast(target) = pointer; break; } const uint8_t* bytes = nullptr; size_t byteLength = 0; - if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (readEngineBuffer(runtime, object, &bytes, &byteLength)) { void* pointer = const_cast(bytes); frame.rememberRoundTripValue(bridge, runtime, pointer, value); *static_cast(target) = pointer; break; } } - *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + *static_cast(target) = pointerFromEngineValue(runtime, bridge, value, frame); break; case metagen::mdTypeOpaquePointer: - *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + *static_cast(target) = pointerFromEngineValue(runtime, bridge, value, frame); break; case metagen::mdTypeBlock: case metagen::mdTypeFunctionPointer: { + if (value.isNull() || value.isUndefined()) { + *static_cast(target) = nullptr; + break; + } if (value.isObject()) { Object object = value.asObject(runtime); void* nativePointer = nullptr; @@ -761,30 +827,37 @@ void convertJsiArgument(Runtime& runtime, } } - auto threadPolicy = readJsiCallbackThreadPolicy(runtime, object); + uintptr_t roundTripValidationKey = NativeApiBridge::callbackRoundTripValidationKey(type); + auto threadPolicy = readEngineCallbackThreadPolicy(runtime, object); + std::string callbackEncoding = + stringPropertyOrEmpty(runtime, object, kNativeApiCallbackEncodingProperty); auto callback = - createJsiCallback(runtime, bridge, type, object.asFunction(runtime), - type.kind == metagen::mdTypeBlock, threadPolicy); + callbackEncoding.empty() + ? createEngineCallback(runtime, bridge, type, object.asFunction(runtime), + type.kind == metagen::mdTypeBlock, threadPolicy) + : createEngineCallback( + runtime, bridge, callbackEncoding, object.asFunction(runtime), + type.kind == metagen::mdTypeBlock, threadPolicy, roundTripValidationKey); void* pointer = callback->functionPointer(); if (type.kind == metagen::mdTypeBlock) { + frame.addObject(static_cast(pointer)); frame.addLifetime(callback); - frame.rememberRoundTripValue(bridge, runtime, pointer, value); + bridge->rememberRoundTripValue(runtime, pointer, value, false, roundTripValidationKey); } else { - bridge->rememberRoundTripValue(runtime, pointer, value); + bridge->rememberRoundTripValue(runtime, pointer, value, false, roundTripValidationKey); } try { object.setProperty(runtime, "__nativeApiPointerObject", createPointer(runtime, bridge, pointer)); - object.setProperty( - runtime, "__nativeApiPointer", - static_cast(reinterpret_cast(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); + *static_cast(target) = pointerFromEngineValue(runtime, bridge, value, frame); break; } case metagen::mdTypeStruct: @@ -794,21 +867,18 @@ void convertJsiArgument(Runtime& runtime, case metagen::mdTypeVector: case metagen::mdTypeExtVector: case metagen::mdTypeComplex: - convertIndexedAggregateArgument(runtime, bridge, type, value, target, - frame); + convertIndexedAggregateArgument(runtime, bridge, type, value, target, frame); break; default: - throw facebook::jsi::JSError(runtime, "Unsupported JSI argument type."); + throw JSError(runtime, "Unsupported Engine 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."); +Value convertNativeReturnValue(Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, void* value) { + if (unsupportedEngineType(type)) { + throw JSError(runtime, "This native return type is not supported by " + "the engine bridge yet."); } switch (type.kind) { @@ -837,11 +907,10 @@ Value convertNativeReturnValue(Runtime& runtime, return static_cast(*static_cast(value)); case metagen::mdTypeSLong: case metagen::mdTypeSInt64: - return signedInteger64ToJsiValue(runtime, *static_cast(value)); + return signedInteger64ToEngineValue(runtime, *static_cast(value)); case metagen::mdTypeULong: case metagen::mdTypeUInt64: - return unsignedInteger64ToJsiValue(runtime, - *static_cast(value)); + return unsignedInteger64ToEngineValue(runtime, *static_cast(value)); case metagen::mdTypeFloat: return static_cast(*static_cast(value)); case metagen::mdTypeDouble: @@ -851,11 +920,26 @@ Value convertNativeReturnValue(Runtime& runtime, if (string == nullptr) { return Value::null(); } - NativeApiJsiType cStringType = - primitiveInteropType(metagen::mdTypeChar); + NativeApiType cStringType = primitiveInteropType(metagen::mdTypeChar); + std::shared_ptr backingValue; + bool stringLikeNative = false; + if (bridge != nullptr) { + Value roundTrip = bridge->findRoundTripValue(runtime, string, &stringLikeNative); + if (!roundTrip.isUndefined()) { + backingValue = std::make_shared(runtime, roundTrip); + } + } + if (stringLikeNative) { + size_t byteLength = 0; + char* copy = copyCStringForReference(string, &byteLength); + return Object::createFromHostObject( + runtime, std::make_shared(bridge, cStringType, copy, true, + byteLength)); + } return Object::createFromHostObject( runtime, std::make_shared( - bridge, cStringType, const_cast(string), false)); + bridge, cStringType, const_cast(string), false, 0, nullptr, + std::move(backingValue))); } case metagen::mdTypeClass: { Class cls = *static_cast(value); @@ -884,47 +968,40 @@ Value convertNativeReturnValue(Runtime& runtime, if (object == nil) { return Value::null(); } - if ([object isKindOfClass:[NSNull class]]) { + Value roundTrip = findCachedNativeObjectReturn(runtime, bridge, type, object); + if (!roundTrip.isUndefined()) { if (type.returnOwned) { [object release]; } - return Value::null(); + return roundTrip; } - 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 (nativeObjectReturnMayCoerceToString(type) && nativeObjectIsStringLike(object)) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; } + return makeString(runtime, utf8); + } + if ([object isKindOfClass:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + return Value::null(); } 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]); + 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)) { + if (const NativeApiSymbol* classSymbol = bridge->findClassForRuntimePointer((void*)object)) { return makeNativeClassValue(runtime, bridge, *classSymbol); } if (const NativeApiSymbol* protocolSymbol = @@ -936,8 +1013,7 @@ Value convertNativeReturnValue(Runtime& runtime, 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(); + return selectorName != nullptr ? makeString(runtime, selectorName) : Value::null(); } case metagen::mdTypePointer: case metagen::mdTypeOpaquePointer: { @@ -945,24 +1021,29 @@ Value convertNativeReturnValue(Runtime& runtime, if (pointer == nullptr) { return Value::null(); } - if (const NativeApiSymbol* classSymbol = - bridge->findClassForRuntimePointer(pointer)) { + if (const NativeApiSymbol* classSymbol = bridge->findClassForRuntimePointer(pointer)) { return makeNativeClassValue(runtime, bridge, *classSymbol); } - if (const NativeApiSymbol* protocolSymbol = - bridge->findProtocolForRuntimePointer(pointer)) { + if (const NativeApiSymbol* protocolSymbol = bridge->findProtocolForRuntimePointer(pointer)) { return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); } if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { std::shared_ptr backingValue; - Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + bool stringLikeNative = false; + Value roundTrip = bridge->findRoundTripValue(runtime, pointer, &stringLikeNative); + if (stringLikeNative) { + size_t byteLength = 0; + char* copy = copyCStringForReference(static_cast(pointer), &byteLength); + return Object::createFromHostObject( + runtime, std::make_shared(bridge, *type.elementType, + copy, true, byteLength)); + } if (!roundTrip.isUndefined()) { backingValue = std::make_shared(runtime, roundTrip); } - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, *type.elementType, pointer, false, 0, nullptr, - std::move(backingValue))); + return Object::createFromHostObject(runtime, std::make_shared( + bridge, *type.elementType, pointer, false, + 0, nullptr, std::move(backingValue))); } return createPointer(runtime, bridge, pointer); } @@ -972,7 +1053,8 @@ Value convertNativeReturnValue(Runtime& runtime, if (pointer == nullptr) { return Value::null(); } - Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + Value roundTrip = bridge->findRoundTripValue( + runtime, pointer, nullptr, false, NativeApiBridge::callbackRoundTripValidationKey(type)); if (!roundTrip.isUndefined()) { return roundTrip; } @@ -982,12 +1064,11 @@ Value convertNativeReturnValue(Runtime& runtime, case metagen::mdTypeStruct: if (type.aggregateInfo == nullptr) { return ArrayBuffer( - runtime, std::make_shared( - value, nativeSizeForType(type))); + runtime, std::make_shared(value, nativeSizeForType(type))); } return Object::createFromHostObject( - runtime, std::make_shared( - bridge, type.aggregateInfo, value, true)); + runtime, std::make_shared(bridge, type.aggregateInfo, + value, true)); case metagen::mdTypeArray: case metagen::mdTypeVector: case metagen::mdTypeExtVector: @@ -1007,15 +1088,14 @@ Value convertNativeReturnValue(Runtime& runtime, return result; } default: - throw facebook::jsi::JSError(runtime, "Unsupported JSI return type."); + throw JSError(runtime, "Unsupported Engine return type."); } } -void NativeApiReferenceHostObject::ensureStorage( - Runtime& runtime, NativeApiJsiType type, NativeApiJsiArgumentFrame& frame, - size_t elements) { +void NativeApiReferenceHostObject::ensureStorage(Runtime& runtime, NativeApiType type, + NativeApiArgumentFrame& frame, size_t elements) { size_t elementCount = std::max(elements, 1); - NativeApiJsiType storageType = std::move(type); + NativeApiType 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); @@ -1029,21 +1109,19 @@ void NativeApiReferenceHostObject::ensureStorage( if (expanded == nullptr) { throw std::bad_alloc(); } - std::memset(static_cast(expanded) + byteLength_, 0, - required - byteLength_); + 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); + convertEngineArgument(runtime, bridge_, type_, pending, data_, frame); pendingValue_.reset(); } } -Value NativeApiReferenceHostObject::get(Runtime& runtime, - const PropNameID& name) { +Value NativeApiReferenceHostObject::get(Runtime& runtime, const PropNameID& name) { std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, "reference"); @@ -1058,14 +1136,16 @@ Value NativeApiReferenceHostObject::get(Runtime& runtime, } return Value::undefined(); } + if (backingValue_ != nullptr && nativeTypeStoresObjectiveCObject(type_)) { + return Value(runtime, *backingValue_); + } 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_)); + void* slot = static_cast(data_) + (*index * referenceElementStride(type_)); return convertNativeReturnValue(runtime, bridge_, type_, slot); } if (property == "toString") { @@ -1075,38 +1155,36 @@ Value NativeApiReferenceHostObject::get(Runtime& runtime, [data](Runtime& runtime, const Value&, const Value*, size_t) -> Value { char address[32] = {}; snprintf(address, sizeof(address), "%p", data); - return makeString(runtime, - ""); + return makeString(runtime, ""); }); } return Value::undefined(); } -void NativeApiReferenceHostObject::set(Runtime& runtime, - const PropNameID& name, - const Value& value) { +NativeApiHostSetResult 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; + NATIVE_API_SET_RETURN(true); } size_t slotIndex = index.value_or(0); - NativeApiJsiArgumentFrame frame(1); + NativeApiArgumentFrame frame(1); if (data_ == nullptr) { if (slotIndex == 0) { pendingValue_ = std::make_shared(runtime, value); - return; + NATIVE_API_SET_RETURN(true); } ensureStorage(runtime, type_, frame, slotIndex + 1); } pendingValue_.reset(); - void* slot = static_cast(data_) + - (slotIndex * referenceElementStride(type_)); - convertJsiArgument(runtime, bridge_, type_, value, slot, frame); + backingValue_.reset(); + void* slot = static_cast(data_) + (slotIndex * referenceElementStride(type_)); + convertEngineArgument(runtime, bridge_, type_, value, slot, frame); + NATIVE_API_SET_RETURN(true); } -Value NativeApiStructObjectHostObject::get(Runtime& runtime, - const PropNameID& name) { +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"); @@ -1125,10 +1203,9 @@ Value NativeApiStructObjectHostObject::get(Runtime& runtime, 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 : "") + "]"); + return makeString(runtime, std::string("[NativeApi ") + + (info != nullptr && info->isUnion ? "Union " : "Struct ") + + (info != nullptr ? info->name : "") + "]"); }); } @@ -1138,12 +1215,11 @@ Value NativeApiStructObjectHostObject::get(Runtime& runtime, continue; } void* fieldData = static_cast(data_) + field.offset; - if (field.type.kind == metagen::mdTypeStruct && - field.type.aggregateInfo != nullptr) { + 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_)); + runtime, + std::make_shared( + bridge_, field.type.aggregateInfo, fieldData, false, ownedData_, backingValue_)); } return convertNativeReturnValue(runtime, bridge_, field.type, fieldData); } @@ -1151,27 +1227,26 @@ Value NativeApiStructObjectHostObject::get(Runtime& runtime, return Value::undefined(); } -void NativeApiStructObjectHostObject::set(Runtime& runtime, - const PropNameID& name, - const Value& value) { +NativeApiHostSetResult 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."); + throw 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; + NativeApiArgumentFrame frame(1); + convertEngineArgument(runtime, bridge_, field.type, value, + static_cast(data_) + field.offset, frame); + NATIVE_API_SET_RETURN(true); } - throw facebook::jsi::JSError(runtime, "No native struct field: " + property); + throw JSError(runtime, "No native struct field: " + property); } -std::vector NativeApiStructObjectHostObject::getPropertyNames( - Runtime& runtime) { +std::vector NativeApiStructObjectHostObject::getPropertyNames(Runtime& runtime) { std::vector names; addPropertyName(runtime, names, "kind"); addPropertyName(runtime, names, "name"); @@ -1186,15 +1261,15 @@ std::vector NativeApiStructObjectHostObject::getPropertyNames( return names; } -NativeApiJsiType primitiveInteropType(MDTypeKind kind) { - NativeApiJsiType type; +NativeApiType primitiveInteropType(MDTypeKind kind) { + NativeApiType type; type.kind = kind; - type.ffiType = ffiTypeForJsiKind(kind); + type.ffiType = ffiTypeForEngineKind(kind); type.supported = type.ffiType != nullptr; return type; } -std::optional primitiveInteropTypeFromCode(int32_t code) { +std::optional primitiveInteropTypeFromCode(int32_t code) { MDTypeKind kind = static_cast(code); switch (kind) { case metagen::mdTypeVoid: @@ -1227,9 +1302,9 @@ std::optional primitiveInteropTypeFromCode(int32_t code) { } } -std::optional interopTypeFromValue( - Runtime& runtime, const std::shared_ptr& bridge, - const Value& value) { +std::optional interopTypeFromValue(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value) { if (value.isNumber()) { return primitiveInteropTypeFromCode(static_cast(value.getNumber())); } @@ -1241,26 +1316,20 @@ std::optional interopTypeFromValue( Object object = value.asObject(runtime); Value typeCodeValue = object.getProperty(runtime, "__nativeApiTypeCode"); if (typeCodeValue.isNumber()) { - return primitiveInteropTypeFromCode( - static_cast(typeCodeValue.getNumber())); + 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 (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())); + 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)); + Class descriptorClass = nativeClassFromEngineObject(runtime, object); + if (descriptorClass == Nil && stringPropertyOrEmpty(runtime, object, "kind") == "class") { + descriptorClass = static_cast(pointerFromSymbolLikeObject(runtime, object)); } if (descriptorClass != Nil) { return nativeObjectReturnTypeForClass(descriptorClass); @@ -1268,14 +1337,12 @@ std::optional interopTypeFromValue( if (object.isHostObject(runtime)) { auto structObject = object.getHostObject(runtime); - NativeApiJsiType type; + NativeApiType 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.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; @@ -1316,9 +1383,9 @@ std::optional interopTypeFromValue( 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; + auto info = + bridge->aggregateInfoFor(static_cast(offsetValue.getNumber()), isUnion); + NativeApiType type; type.kind = metagen::mdTypeStruct; type.aggregateInfo = info; type.aggregateOffset = info != nullptr ? info->offset : MD_SECTION_OFFSET_NULL; @@ -1332,8 +1399,7 @@ std::optional interopTypeFromValue( return std::nullopt; } -Value makeAggregateConstructor(Runtime& runtime, - const std::shared_ptr& bridge, +Value makeAggregateConstructor(Runtime& runtime, const std::shared_ptr& bridge, const NativeApiSymbol& symbol) { auto info = bridge->aggregateInfoFor(symbol); auto constructor = Function::createFromHostFunction( @@ -1341,12 +1407,10 @@ Value makeAggregateConstructor(Runtime& runtime, [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); + throw JSError(runtime, "Native aggregate metadata is unavailable: " + symbol.name); } - NativeApiJsiType type; + NativeApiType type; type.kind = metagen::mdTypeStruct; type.aggregateInfo = info; type.aggregateOffset = info->offset; @@ -1357,43 +1421,39 @@ Value makeAggregateConstructor(Runtime& runtime, 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]))); + 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); + NativeApiArgumentFrame frame(1); + convertAggregateArgument(runtime, bridge, type, args[0], storage.data(), frame); } return Object::createFromHostObject( - runtime, std::make_shared( - bridge, info, storage.data(), true)); + runtime, + std::make_shared(bridge, info, storage.data(), true)); }); - constructor.setProperty(runtime, "kind", - makeString(runtime, symbol.kind == NativeApiSymbolKind::Union - ? "union" - : "struct")); + 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, "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 { + [bridge, info](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (info == nullptr || count < 2) { return false; } - NativeApiJsiType type; + NativeApiType type; type.kind = metagen::mdTypeStruct; type.aggregateInfo = info; type.aggregateOffset = info->offset; @@ -1404,12 +1464,10 @@ Value makeAggregateConstructor(Runtime& runtime, 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); + NativeApiArgumentFrame leftFrame(1); + convertAggregateArgument(runtime, bridge, type, args[0], left.data(), leftFrame); + NativeApiArgumentFrame rightFrame(1); + convertAggregateArgument(runtime, bridge, type, args[1], right.data(), rightFrame); } catch (const std::exception&) { return false; } @@ -1426,8 +1484,7 @@ Value makeAggregateConstructor(Runtime& runtime, return constructor; } -size_t sizeofInteropType(Runtime& runtime, - const std::shared_ptr& bridge, +size_t sizeofInteropType(Runtime& runtime, const std::shared_ptr& bridge, const Value& value) { if (auto type = interopTypeFromValue(runtime, bridge, value)) { return nativeSizeForType(*type); @@ -1438,7 +1495,7 @@ size_t sizeofInteropType(Runtime& runtime, if (object.isHostObject(runtime) || object.isHostObject(runtime) || object.isHostObject(runtime) || - nativeClassFromJsiObject(runtime, object) != Nil) { + nativeClassFromEngineObject(runtime, object) != Nil) { return sizeof(void*); } void* nativePointer = nullptr; @@ -1451,31 +1508,34 @@ size_t sizeofInteropType(Runtime& runtime, } } - throw facebook::jsi::JSError(runtime, "Invalid type for interop.sizeof."); + throw JSError(runtime, "Invalid type for interop.sizeof."); } -Object createPointer(Runtime& runtime, - const std::shared_ptr& bridge, - void* pointer, bool adopted) { +Object createPointer(Runtime& runtime, const std::shared_ptr& bridge, + void* pointer, bool adopted, std::shared_ptr backingValue) { if (!adopted && bridge != nullptr) { Value cached = bridge->findPointerValue(runtime, pointer); if (cached.isObject()) { - return cached.asObject(runtime); + Object cachedObject = cached.asObject(runtime); + if (backingValue != nullptr && + cachedObject.isHostObject(runtime)) { + cachedObject.getHostObject(runtime)->setBackingValue( + runtime, *backingValue); + } + return cachedObject; } } Object result = Object::createFromHostObject( - runtime, - std::make_shared(bridge, pointer, "pointer", - adopted)); + runtime, std::make_shared(bridge, pointer, "pointer", adopted, + std::move(backingValue))); if (!adopted && bridge != nullptr) { bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); } return result; } -void installInteropHasInstance(Runtime& runtime, Function& constructor, - const char* kind) { +void installInteropHasInstance(Runtime& runtime, Function& constructor, const char* kind) { Value symbolCtorValue = runtime.global().getProperty(runtime, "Symbol"); if (!symbolCtorValue.isObject()) { return; @@ -1489,31 +1549,29 @@ void installInteropHasInstance(Runtime& runtime, Function& constructor, try { Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); - Function defineProperty = - objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + 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 { + [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; + 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) { +Class classFromEngineValue(Runtime& runtime, const Value& value) { if (value.isString()) { std::string name = value.asString(runtime).utf8(runtime); return objc_lookUpClass(name.c_str()); @@ -1522,7 +1580,7 @@ Class classFromJsiValue(Runtime& runtime, const Value& value) { return Nil; } Object object = value.asObject(runtime); - if (Class cls = nativeClassFromJsiObject(runtime, object)) { + if (Class cls = nativeClassFromEngineObject(runtime, object)) { return cls; } if (stringPropertyOrEmpty(runtime, object, "kind") == "class") { @@ -1537,17 +1595,15 @@ Class classFromJsiValue(Runtime& runtime, const Value& value) { return Nil; } -Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value) { +Protocol* protocolFromEngineValue(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()); + 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; @@ -1557,8 +1613,7 @@ Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value) { } Object object = value.asObject(runtime); if (object.isHostObject(runtime)) { - return object.getHostObject(runtime) - ->nativeProtocol(); + return object.getHostObject(runtime)->nativeProtocol(); } if (stringPropertyOrEmpty(runtime, object, "kind") == "protocol") { return static_cast(pointerFromSymbolLikeObject(runtime, object)); @@ -1573,13 +1628,12 @@ Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value) { } Value nameValue = object.getProperty(runtime, "name"); if (nameValue.isString()) { - return protocolFromJsiValue(runtime, nameValue); + return protocolFromEngineValue(runtime, nameValue); } return nullptr; } -Object createInteropObject(Runtime& runtime, - const std::shared_ptr& bridge) { +Object createInteropObject(Runtime& runtime, const std::shared_ptr& bridge) { Object interop(runtime); Object types(runtime); auto setType = [&](const char* name, MDTypeKind kind) { @@ -1590,18 +1644,15 @@ Object createInteropObject(Runtime& runtime, 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); - })); + [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); @@ -1630,262 +1681,226 @@ Object createInteropObject(Runtime& runtime, 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); + [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; } - } - 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 (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)); + *address = static_cast(static_cast(number)); return true; } - if (value.isBigInt()) { - if (readAddressFromString(value)) { + if (primitive.isBigInt()) { + if (readAddressFromString(primitive)) { return true; } - BigInt bigint = value.getBigInt(runtime); + BigInt bigint = primitive.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 readAddressFromString(value); } - return createPointer(runtime, bridge, pointer); + return false; + }; + + uintptr_t address = 0; + if (!readAddress(args[0], &address)) { + throw 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*))); + 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 blockConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Block"), 2, + [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { + return interopCallbackFromArguments(runtime, "Block", "block", args, count); + }); + Object blockPrototype(runtime); + blockPrototype.setProperty(runtime, "constructor", blockConstructor); + blockConstructor.setProperty(runtime, "prototype", blockPrototype); + installInteropHasInstance(runtime, blockConstructor, "block"); + blockConstructor.setProperty(runtime, "kind", makeString(runtime, "block")); + blockConstructor.setProperty(runtime, "sizeof", static_cast(sizeof(void*))); + interop.setProperty(runtime, "Block", blockConstructor); - Function function = object.asFunction(runtime); - function.setProperty(runtime, "kind", - makeString(runtime, "functionReference")); - function.setProperty(runtime, "sizeof", - static_cast(sizeof(void*))); - return function; + Function functionReferenceConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "FunctionReference"), 2, + [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { + return interopCallbackFromArguments(runtime, "FunctionReference", "functionReference", args, + count); }); Object functionReferencePrototype(runtime); - functionReferencePrototype.setProperty(runtime, "constructor", - functionReferenceConstructor); - functionReferenceConstructor.setProperty(runtime, "prototype", - functionReferencePrototype); - installInteropHasInstance(runtime, functionReferenceConstructor, - "functionReference"); + 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); + 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; - } + [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { + NativeApiType 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) || + nativeClassFromEngineObject(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(); + void* data = nullptr; + bool ownsData = false; + size_t byteLength = 0; + std::shared_ptr pendingValue; + std::shared_ptr backingValue; + 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"); } - ownsData = true; - if (count > 1) { - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge, type, valueToStore, data, - frame); + } else if (type.kind == metagen::mdTypeStruct && + object.isHostObject(runtime)) { + data = object.getHostObject(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; } } - } else if (count > 0) { - pendingValue = std::make_shared(runtime, args[0]); } - - if (ownsData && data == nullptr) { + } + if (!usesExternalStorage) { + byteLength = std::max(nativeSizeForType(type), sizeof(void*)); + data = calloc(1, byteLength); + if (data == nullptr) { throw std::bad_alloc(); } - return Object::createFromHostObject( - runtime, std::make_shared( - bridge, type, data, ownsData, byteLength, - std::move(pendingValue))); + ownsData = true; + if (count > 1) { + NativeApiArgumentFrame frame(1); + convertEngineArgument(runtime, bridge, type, valueToStore, data, frame); + if (nativeTypeStoresObjectiveCObject(type) && valueToStore.isObject()) { + backingValue = std::make_shared(runtime, valueToStore); + } + } + } + } 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), + std::move(backingValue))); }); 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*))); + 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 { + [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 1) { - throw facebook::jsi::JSError(runtime, "sizeof expects a type."); + throw JSError(runtime, "sizeof expects a type."); } return static_cast(sizeofInteropType(runtime, bridge, args[0])); })); @@ -1894,10 +1909,9 @@ Object createInteropObject(Runtime& runtime, runtime, "alloc", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "alloc"), 1, - [bridge](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."); + throw 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); @@ -1907,8 +1921,7 @@ Object createInteropObject(Runtime& runtime, runtime, "free", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "free"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 1 || !args[0].isObject()) { return Value::undefined(); } @@ -1929,25 +1942,23 @@ Object createInteropObject(Runtime& runtime, runtime, "adopt", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "adopt"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [](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."); + throw 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."); + throw 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 { + 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(); } @@ -1964,36 +1975,37 @@ Object createInteropObject(Runtime& runtime, return Value(runtime, object); } if (object.isHostObject(runtime)) { - void* data = - object.getHostObject(runtime)->data(); + auto reference = object.getHostObject(runtime); + void* data = reference->data(); if (data == nullptr) { - throw facebook::jsi::JSError( - runtime, "Cannot get handle of empty Reference."); + throw JSError(runtime, "Cannot get handle of empty Reference."); } - return createPointer(runtime, bridge, data); + std::shared_ptr backingValue; + if (reference->backingValue() != nullptr && + nativeTypeStoresObjectiveCObject(reference->type())) { + backingValue = reference->backingValue(); + } + return createPointer(runtime, bridge, data, false, std::move(backingValue)); } if (object.isHostObject(runtime)) { - auto structObject = - object.getHostObject(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()); + id nativeObject = object.getHostObject(runtime)->object(); + return createPointer(runtime, bridge, nativeObject, false, + std::make_shared(runtime, args[0])); } - if (Class cls = nativeClassFromJsiObject(runtime, object)) { + if (Class cls = nativeClassFromEngineObject(runtime, object)) { return createPointer(runtime, bridge, cls); } if (object.isHostObject(runtime)) { return createPointer( runtime, bridge, - object.getHostObject(runtime) - ->nativeProtocol()); + object.getHostObject(runtime)->nativeProtocol()); } if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { return createPointer(runtime, bridge, symbolPointer); @@ -2003,10 +2015,11 @@ Object createInteropObject(Runtime& runtime, 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."); + if (kindValue.isString()) { + std::string kind = kindValue.asString(runtime).utf8(runtime); + if (kind == "block" || kind == "functionPointer" || kind == "functionReference") { + throw JSError(runtime, "Cannot get handle of uninitialized native callback."); + } } Value nativeName = object.getProperty(runtime, "nativeName"); if (nativeName.isString()) { @@ -2016,28 +2029,58 @@ Object createInteropObject(Runtime& runtime, 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 { + return Value::null(); + })); + + interop.setProperty( + runtime, "object", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "object"), 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(); + } + + void* pointer = nullptr; + if (args[0].isString()) { + uintptr_t address = 0; + if (!parseIntegerTextToUintptr(args[0].asString(runtime).utf8(runtime), + &address)) { + throw JSError(runtime, + "interop.object expects an Objective-C object pointer."); + } + pointer = reinterpret_cast(address); + } else { + NativeApiArgumentFrame frame(1); + pointer = pointerFromEngineValue(runtime, bridge, args[0], frame); + } + + if (pointer == nullptr) { + return Value::null(); + } + + id object = static_cast(pointer); + NativeApiType type = nativeObjectReturnTypeForClass(object_getClass(object)); + return convertNativeReturnValue(runtime, bridge, type, &object); + })); + + interop.setProperty( + runtime, "stringFromCString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "stringFromCString"), 2, + [bridge](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); + NativeApiArgumentFrame frame(1); const char* data = - static_cast(pointerFromJsiValue(runtime, args[0], frame)); + static_cast(pointerFromEngineValue(runtime, bridge, 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), + return String::createFromUtf8(runtime, reinterpret_cast(data), length); } return makeString(runtime, data); @@ -2047,10 +2090,9 @@ Object createInteropObject(Runtime& runtime, runtime, "bufferFromData", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "bufferFromData"), 1, - [](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { + [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 1 || !args[0].isObject()) { - throw facebook::jsi::JSError(runtime, "Invalid data."); + throw JSError(runtime, "Invalid data."); } Object object = args[0].asObject(runtime); if (object.isArrayBuffer(runtime)) { @@ -2064,12 +2106,11 @@ Object createInteropObject(Runtime& runtime, object.getHostObject(runtime)->pointer()); } if (native == nil || ![native isKindOfClass:[NSData class]]) { - throw facebook::jsi::JSError(runtime, "Invalid data."); + throw JSError(runtime, "Invalid data."); } NSData* data = static_cast(native); - return ArrayBuffer( - runtime, std::make_shared( - data.bytes, static_cast(data.length))); + return ArrayBuffer(runtime, std::make_shared( + data.bytes, static_cast(data.length))); })); interop.setProperty( @@ -2077,22 +2118,18 @@ Object createInteropObject(Runtime& runtime, 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."); + throw JSError(runtime, "interop.addMethod requires the Engine 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 { + [](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."); + throw JSError(runtime, "interop.addProtocol expects class and protocol."); } - Class cls = classFromJsiValue(runtime, args[0]); - Protocol* protocol = protocolFromJsiValue(runtime, args[1]); + Class cls = classFromEngineValue(runtime, args[0]); + Protocol* protocol = protocolFromEngineValue(runtime, args[1]); if (cls == Nil || protocol == nullptr) { return false; } diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h b/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h deleted file mode 100644 index fe45f29fc..000000000 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h +++ /dev/null @@ -1,518 +0,0 @@ -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()); -} - -bool signatureSupportedForJsiInvocation( - const std::optional& signature) { - if (!signature || !signature->prepared || signature->variadic || - unsupportedJsiType(signature->returnType)) { - return false; - } - for (const auto& argType : signature->argumentTypes) { - if (unsupportedJsiType(argType)) { - return false; - } - } - return true; -} - -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 && - (dispatchSuperClass != Nil || ![receiver respondsToSelector:selector])) { - throw facebook::jsi::JSError(runtime, - "Objective-C selector is not available: " + - selectorName); - } - - std::optional signature; - std::optional runtimeSignature; - 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 (method != nullptr) { - runtimeSignature = parseObjCMethodJsiSignature(method, bridge.get()); - } - if (signatureSupportedForJsiInvocation(signature) && - signatureSupportedForJsiInvocation(runtimeSignature)) { - reconcileObjCMethodRuntimeSignature(&*signature, *runtimeSignature); - } - if (!signatureSupportedForJsiInvocation(signature) && runtimeSignature) { - signature = std::move(runtimeSignature); - } - - if (!signatureSupportedForJsiInvocation(signature)) { - 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/v8/NativeApiV8.h b/NativeScript/ffi/v8/NativeApiV8.h index cf8c4054a..ef035ede2 100644 --- a/NativeScript/ffi/v8/NativeApiV8.h +++ b/NativeScript/ffi/v8/NativeApiV8.h @@ -1,20 +1,21 @@ #ifndef NATIVESCRIPT_FFI_V8_NATIVE_API_V8_H #define NATIVESCRIPT_FFI_V8_NATIVE_API_V8_H -#include "ffi/shared/direct/NativeApiDirect.h" +#include "ffi/shared/NativeApiBackendConfig.h" #include "v8.h" namespace nativescript { -using NativeApiV8Config = NativeApiDirectConfig; +using NativeApiScheduler = NativeApiBackendScheduler; +using NativeApiConfig = NativeApiBackendConfig; -void InstallNativeApiV8(v8::Isolate* isolate, +void InstallNativeApi(v8::Isolate* isolate, v8::Local context, - const NativeApiV8Config& config = NativeApiV8Config{}); + const NativeApiConfig& config = NativeApiConfig{}); } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiV8(v8::Isolate* isolate, +extern "C" void NativeScriptInstallNativeApi(v8::Isolate* isolate, v8::Local context, const char* metadataPath); diff --git a/NativeScript/ffi/v8/NativeApiV8.mm b/NativeScript/ffi/v8/NativeApiV8.mm index 154a11947..1ed1c20e0 100644 --- a/NativeScript/ffi/v8/NativeApiV8.mm +++ b/NativeScript/ffi/v8/NativeApiV8.mm @@ -3,161 +3,60 @@ #ifdef TARGET_ENGINE_V8 #include "NativeApiV8Runtime.h" +#include "SignatureDispatch.h" namespace nativescript { -using NativeApiJsiConfig = NativeApiDirectConfig; -using NativeApiJsiScheduler = NativeApiDirectScheduler; - namespace { -using facebook::jsi::Array; -using facebook::jsi::ArrayBuffer; -using facebook::jsi::BigInt; -using facebook::jsi::Function; -using facebook::jsi::HostObject; -using facebook::jsi::MutableBuffer; -using facebook::jsi::Object; -using facebook::jsi::PropNameID; -using facebook::jsi::Runtime; -using facebook::jsi::String; -using facebook::jsi::StringBuffer; -using facebook::jsi::Value; +using nativescript::engine::Array; +using nativescript::engine::ArrayBuffer; +using nativescript::engine::BigInt; +using nativescript::engine::Function; +using nativescript::engine::HostObject; +using nativescript::engine::MutableBuffer; +using nativescript::engine::Object; +using nativescript::engine::PropNameID; +using nativescript::engine::Runtime; +using nativescript::engine::String; +using nativescript::engine::StringBuffer; +using nativescript::engine::Value; +using nativescript::engine::JSError; using metagen::MDMemberFlag; using metagen::MDMetadataReader; using metagen::MDSectionOffset; using metagen::MDTypeKind; // clang-format off -#include "jsi/NativeApiJsiBridge.h" +#define NATIVESCRIPT_NATIVE_API_BACKEND_NAME "v8" +#include "../shared/bridge/ObjCBridge.mm" // clang-format on #define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS 1 +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_SELECTOR_GROUP_FUNCTION 1 #define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 #define NATIVESCRIPT_NATIVE_API_RUNTIME_SCOPE 1 -struct NativeApiV8LazyGlobalData { - NativeApiV8LazyGlobalData(v8::Isolate* isolate, const std::string& name, - const std::string& kind) { - nameValue.Reset(isolate, facebook::jsi::v8direct::makeV8String(isolate, name)); - kindValue.Reset(isolate, facebook::jsi::v8direct::makeV8String(isolate, kind)); - } - - ~NativeApiV8LazyGlobalData() { - nameValue.Reset(); - kindValue.Reset(); - } - - v8::Global nameValue; - v8::Global kindValue; -}; - -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { - return std::make_shared(runtime.state()); -} - -class NativeApiJsiRuntimeScope final { - public: - explicit NativeApiJsiRuntimeScope(Runtime& runtime) - : locker_(runtime.isolate()), - isolateScope_(runtime.isolate()), - handleScope_(runtime.isolate()), - context_(runtime.context()), - contextScope_(context_) {} - - private: - v8::Locker locker_; - v8::Isolate::Scope isolateScope_; - v8::HandleScope handleScope_; - v8::Local context_; - v8::Context::Scope contextScope_; -}; - -void NativeApiV8LazyGlobalGetter(v8::Local, - const v8::PropertyCallbackInfo& info) { - v8::Isolate* isolate = info.GetIsolate(); - v8::HandleScope handleScope(isolate); - v8::Local context = isolate->GetCurrentContext(); - if (!info.Data()->IsExternal()) { - return; - } - - auto* data = static_cast(info.Data().As()->Value()); - if (data == nullptr) { - return; - } - v8::Local nameValue = data->nameValue.Get(isolate); - v8::Local kindValue = data->kindValue.Get(isolate); - - v8::Local global = context->Global(); - v8::Local resolverValue; - if (!global - ->Get(context, facebook::jsi::v8direct::makeV8String( - isolate, "__nativeScriptResolveNativeApiLazyGlobal")) - .ToLocal(&resolverValue) || - !resolverValue->IsFunction()) { - return; - } - - v8::TryCatch tryCatch(isolate); - v8::Local args[] = {nameValue, kindValue}; - v8::Local result; - if (!resolverValue.As()->Call(context, global, 2, args).ToLocal(&result)) { - if (tryCatch.HasCaught()) { - isolate->ThrowException(tryCatch.Exception()); - } - return; - } - if (global->Delete(context, nameValue).FromMaybe(false)) { - global->DefineOwnProperty(context, nameValue, result, v8::DontEnum).FromMaybe(false); - } - info.GetReturnValue().Set(result); -} - -bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, - const std::string& name, const std::string& kind, - bool force) { - if (name.empty() || kind.empty()) { - return false; - } - - v8::Isolate* isolate = runtime.isolate(); - v8::EscapableHandleScope handleScope(isolate); - v8::Local context = runtime.context(); - v8::Local global = context->Global(); - v8::Local property = facebook::jsi::v8direct::makeV8String(isolate, name); - if (!force && global->HasOwnProperty(context, property).FromMaybe(false)) { - return false; - } - - auto data = std::make_shared(isolate, name, kind); - v8::Local external = v8::External::New(isolate, data.get()); - - bool installed = global - ->SetNativeDataProperty(context, property, NativeApiV8LazyGlobalGetter, - nullptr, external, v8::DontEnum) - .FromMaybe(false); - if (installed) { - runtime.state()->retainedNativeData.push_back(std::move(data)); - } - return installed; -} +#include "NativeApiV8RuntimeSupport.mm" // clang-format off -#include "jsi/NativeApiJsiHostObjects.h" -#include "jsi/NativeApiJsiCallbacks.h" -#include "jsi/NativeApiJsiConversion.h" -#include "jsi/NativeApiJsiInvocation.h" -#include "jsi/NativeApiJsiClassBuilder.h" -#include "jsi/NativeApiJsiHostObject.h" +#include "../shared/bridge/HostObjects.mm" +#include "../shared/bridge/Callbacks.mm" +#include "../shared/bridge/TypeConv.mm" +#include "../shared/bridge/Invocation.mm" +#include "../shared/bridge/ClassBuilder.mm" +#include "../shared/bridge/HostObject.mm" // clang-format on + +#include "NativeApiV8SelectorGroups.mm" + } // namespace -#include "jsi/NativeApiJsiInstall.h" +#include "../shared/bridge/Install.mm" -void InstallNativeApiV8(v8::Isolate* isolate, v8::Local context, - const NativeApiV8Config& config) { +void InstallNativeApi(v8::Isolate* isolate, v8::Local context, + const NativeApiConfig& config) { if (isolate == nullptr || context.IsEmpty()) { return; } @@ -166,16 +65,16 @@ void InstallNativeApiV8(v8::Isolate* isolate, v8::Local context, v8::HandleScope handleScope(isolate); v8::Context::Scope contextScope(context); Runtime runtime(isolate, context); - InstallNativeApiJSI(runtime, config); + InstallNativeApi(runtime, config); } } // namespace nativescript -extern "C" void NativeScriptInstallNativeApiV8(v8::Isolate* isolate, v8::Local context, +extern "C" void NativeScriptInstallNativeApi(v8::Isolate* isolate, v8::Local context, const char* metadataPath) { - nativescript::NativeApiV8Config config; + nativescript::NativeApiConfig config; config.metadataPath = metadataPath; - nativescript::InstallNativeApiV8(isolate, context, config); + nativescript::InstallNativeApi(isolate, context, config); } #endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8Gsd.mm b/NativeScript/ffi/v8/NativeApiV8Gsd.mm new file mode 100644 index 000000000..6b108acd9 --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8Gsd.mm @@ -0,0 +1,222 @@ +// --- GSD (Generated Signature Dispatch) for V8 --- +// GsdObjCContext is the engine-neutral interface the generated invokers use: +// it reads JS arguments and writes the JS return value via the V8 API. The +// readers mirror V8's generic argument/return conversions exactly; any value +// that is not in the fast representation makes a reader return false so the +// invoker falls back to the fully correct generic path. +struct GsdObjCContext; +using ObjCGsdInvoker = bool (*)(GsdObjCContext&); +struct ObjCGsdDispatchEntry { + uint64_t dispatchId; + ObjCGsdInvoker invoker; +}; + +struct GsdObjCContext { + Runtime& runtime; + const std::shared_ptr& bridge; + id self; + SEL selector; + const v8::FunctionCallbackInfo& info; + v8::Isolate* isolate; + v8::Local jsContext; + const NativeApiType& returnType; + + template + void invokeNative(Invocation&& invocation) { + performGeneratedObjCInvocation(runtime, bridge, [&]() { invocation(); }); + } + + v8::Local arg(size_t i) const { + return info[static_cast(i)]; + } + + bool readBool(size_t i, uint8_t* out) { + *out = arg(i)->BooleanValue(isolate) ? 1 : 0; + return true; + } + template + bool readSigned(size_t i, T* out) { + v8::Local v = arg(i); + if (v->IsInt32()) { + *out = static_cast(v.As()->Value()); + return true; + } + if constexpr (sizeof(T) <= 4) { + int32_t tmp = 0; + if (!v->Int32Value(jsContext).To(&tmp)) return false; + *out = static_cast(tmp); + } else { + if (v->IsBigInt()) { + bool lossless = false; + *out = static_cast(v.As()->Int64Value(&lossless)); + } else { + int64_t tmp = 0; + if (!v->IntegerValue(jsContext).To(&tmp)) return false; + *out = static_cast(tmp); + } + } + return true; + } + template + bool readUnsigned(size_t i, T* out) { + v8::Local v = arg(i); + if (v->IsUint32()) { + *out = static_cast(v.As()->Value()); + return true; + } + if (v->IsInt32()) { + *out = static_cast(v.As()->Value()); + return true; + } + if constexpr (sizeof(T) <= 4) { + uint32_t tmp = 0; + if (!v->Uint32Value(jsContext).To(&tmp)) return false; + *out = static_cast(tmp); + } else { + if (v->IsBigInt()) { + bool lossless = false; + *out = static_cast(v.As()->Uint64Value(&lossless)); + } else { + int64_t tmp = 0; + if (!v->IntegerValue(jsContext).To(&tmp)) return false; + *out = static_cast(static_cast(tmp)); + } + } + return true; + } + bool readFloat(size_t i, float* out) { + double tmp = 0.0; + if (!readDouble(i, &tmp)) return false; + *out = static_cast(tmp); + return true; + } + bool readDouble(size_t i, double* out) { + v8::Local v = arg(i); + if (v->IsNumber()) { + *out = v.As()->Value(); + return true; + } + return v->NumberValue(jsContext).To(out); + } + bool readSelector(size_t i, SEL* out) { + return readV8EngineSelectorArgument(runtime, arg(i), out); + } + bool readClass(size_t i, Class* out) { + Class cls = v8NativeClassArgument(runtime, arg(i)); + if (cls == Nil) return false; + *out = cls; + return true; + } + bool readObject(size_t i, id* out) { + v8::Local v = arg(i); + if (v.IsEmpty() || v->IsNullOrUndefined()) { + *out = nil; + return true; + } + if (!v->IsObject()) return false; + if (auto* h = v8HostObjectRaw(v)) { + *out = h->object(); + return true; + } + if (auto* c = v8HostObjectRaw(v)) { + *out = static_cast(c->nativeClass()); + return true; + } + Class cls = v8NativeClassArgument(runtime, v); + if (cls != Nil) { + *out = static_cast(cls); + return true; + } + if (auto* p = v8HostObjectRaw(v)) { + *out = static_cast(p->nativeProtocol()); + return true; + } + return false; + } + + void setVoid() {} + void setBool(bool v) { + info.GetReturnValue().Set(v8::Boolean::New(isolate, v)); + } + void setInt32(int32_t v) { + info.GetReturnValue().Set(v8::Integer::New(isolate, v)); + } + void setUInt32(uint32_t v) { + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, v)); + } + void setUInt16(uint16_t v) { + if (v >= 32 && v <= 126) { + char buffer[2] = {static_cast(v), '\0'}; + info.GetReturnValue().Set( + engine::v8engine::makeV8String(isolate, buffer)); + } else { + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, v)); + } + } + void setInt64(int64_t v) { + info.GetReturnValue().Set(v8Integer64Value(isolate, v)); + } + void setUInt64(uint64_t v) { + info.GetReturnValue().Set(v8UnsignedInteger64Value(isolate, v)); + } + void setDouble(double v) { + info.GetReturnValue().Set(v8::Number::New(isolate, v)); + } + void setSelector(SEL v) { + const char* name = v != nullptr ? sel_getName(v) : nullptr; + if (name == nullptr) { + info.GetReturnValue().Set(v8::Null(isolate)); + } else { + info.GetReturnValue().Set(engine::v8engine::makeV8String(isolate, name)); + } + } + void setClass(Class v) { + if (v == nil) { + info.GetReturnValue().Set(v8::Null(isolate)); + return; + } + const char* name = class_getName(v); + 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; + } + Value result = makeNativeClassValue(runtime, bridge, std::move(symbol)); + info.GetReturnValue().Set(result.local(runtime)); + } + void setObject(id obj) { + setV8EngineObjectReturn(runtime, bridge, returnType, obj, info); + } +}; + +// Close the anonymous namespace so the generated dispatch table lives in +// namespace nativescript (visible to lookupObjCGsdInvoker). GsdObjCContext is +// reachable from there via the unnamed namespace's implicit using-directive. +} // namespace (temporary close for GSD .inc) + +#if defined(__has_include) +#if __has_include("GeneratedGsdSignatureDispatch.inc") +#include "GeneratedGsdSignatureDispatch.inc" +#endif +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH +inline constexpr ObjCGsdDispatchEntry kGeneratedObjCGsdDispatchEntries[] = { + {0, nullptr}}; +#endif + +ObjCGsdInvoker lookupObjCGsdInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCGsdDispatchEntries, dispatchId); +} + +namespace { // reopen anonymous namespace + +// --- End GSD --- diff --git a/NativeScript/ffi/v8/NativeApiV8HostObjects.mm b/NativeScript/ffi/v8/NativeApiV8HostObjects.mm index 8a11b15c4..6064601ea 100644 --- a/NativeScript/ffi/v8/NativeApiV8HostObjects.mm +++ b/NativeScript/ffi/v8/NativeApiV8HostObjects.mm @@ -2,18 +2,84 @@ #ifdef TARGET_ENGINE_V8 -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { -namespace v8direct { +namespace v8engine { Value valueFromLocal(Runtime& runtime, v8::Local value) { return Value(runtime, value); } +template +class StackValueArray { + public: + explicit StackValueArray(size_t count) : count_(count) { + if (count_ > InlineCount) { + values_ = static_cast(::operator new(sizeof(Value) * count_)); + } else { + values_ = reinterpret_cast(inlineStorage_); + } + } + + ~StackValueArray() { + for (size_t i = 0; i < constructed_; i++) { + values_[i].~Value(); + } + if (count_ > InlineCount) { + ::operator delete(values_); + } + } + + StackValueArray(const StackValueArray&) = delete; + StackValueArray& operator=(const StackValueArray&) = delete; + + void emplace(size_t index, Value&& value) { + new (&values_[index]) Value(std::move(value)); + constructed_++; + } + + Value* data() { return count_ == 0 ? nullptr : values_; } + size_t size() const { return count_; } + + private: + size_t count_ = 0; + size_t constructed_ = 0; + Value* values_ = nullptr; + alignas(Value) unsigned char inlineStorage_[sizeof(Value) * InlineCount]; +}; + v8::Local hostObjectTemplate(Runtime& runtime) { auto state = runtime.state(); if (state->hostObjectTemplate.IsEmpty()) { v8::Local objectTemplate = v8::ObjectTemplate::New(runtime.isolate()); objectTemplate->SetInternalFieldCount(1); + // toString must be own property to override Object.prototype.toString + // when using kNonMasking interceptor. + objectTemplate->Set( + makeV8String(runtime.isolate(), "toString"), + v8::FunctionTemplate::New(runtime.isolate(), + [](const v8::FunctionCallbackInfo& info) { + v8::Local self = info.This(); + if (self.IsEmpty() || self->InternalFieldCount() < 1) return; + auto* holder = static_cast( + self->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) return; + Runtime rt(holder->state); + try { + Value toStr = holder->hostObject->get(rt, PropNameID("toString")); + if (!toStr.isUndefined()) { + v8::Local v8Val = toStr.local(rt); + if (v8Val->IsFunction()) { + v8::Local result; + if (v8Val.As()->Call(rt.context(), self, 0, nullptr) + .ToLocal(&result)) { + info.GetReturnValue().Set(result); + return; + } + } + } + } catch (...) {} + }), + v8::DontEnum); objectTemplate->SetHandler(v8::NamedPropertyHandlerConfiguration( [](v8::Local property, const v8::PropertyCallbackInfo& info) -> v8::Intercepted { @@ -22,10 +88,19 @@ if (holder == nullptr || holder->hostObject == nullptr) { return v8::Intercepted::kNo; } + // Fast path: skip symbols entirely (they never match our properties). + if (!property->IsString()) { + return v8::Intercepted::kNo; + } Runtime runtime(holder->state); try { + v8::Isolate* isolate = info.GetIsolate(); + v8::String::Utf8Value utf8(isolate, property); + if (*utf8 == nullptr) { + return v8::Intercepted::kNo; + } Value result = holder->hostObject->get( - runtime, PropNameID(propertyNameToUtf8(info.GetIsolate(), property))); + runtime, PropNameID(std::string(*utf8, utf8.length()))); if (!result.isUndefined()) { info.GetReturnValue().Set(result.local(runtime)); return v8::Intercepted::kYes; @@ -45,10 +120,10 @@ } Runtime runtime(holder->state); try { - holder->hostObject->set(runtime, - PropNameID(propertyNameToUtf8(info.GetIsolate(), property)), - Value(runtime, value)); - return v8::Intercepted::kYes; + bool handled = holder->hostObject->set( + runtime, PropNameID(propertyNameToUtf8(info.GetIsolate(), property)), + Value(runtime, value)); + return handled ? v8::Intercepted::kYes : v8::Intercepted::kNo; } catch (const std::exception& exception) { throwV8Exception(info.GetIsolate(), exception); return v8::Intercepted::kYes; @@ -116,12 +191,150 @@ return v8::Intercepted::kYes; } }, - nullptr, nullptr, nullptr, v8::Local(), v8::PropertyHandlerFlags::kNone)); + nullptr, nullptr, nullptr, v8::Local(), + v8::PropertyHandlerFlags::kNone)); state->hostObjectTemplate.Reset(runtime.isolate(), objectTemplate); } return state->hostObjectTemplate.Get(runtime.isolate()); } +// Template for native object instances — uses kNonMasking so V8 checks +// prototype chain first (methods/properties installed there are found +// without calling the interceptor). +v8::Local nativeObjectTemplate(Runtime& runtime) { + auto state = runtime.state(); + if (state->nativeObjectTemplate.IsEmpty()) { + v8::Local objectTemplate = v8::ObjectTemplate::New(runtime.isolate()); + objectTemplate->SetInternalFieldCount(1); + // toString must be own property to override Object.prototype.toString + objectTemplate->Set( + makeV8String(runtime.isolate(), "toString"), + v8::FunctionTemplate::New(runtime.isolate(), + [](const v8::FunctionCallbackInfo& info) { + v8::Local self = info.This(); + if (self.IsEmpty() || self->InternalFieldCount() < 1) return; + auto* holder = static_cast( + self->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) return; + Runtime rt(holder->state); + try { + Value toStr = holder->hostObject->get(rt, PropNameID("toString")); + if (!toStr.isUndefined()) { + v8::Local v8Val = toStr.local(rt); + if (v8Val->IsFunction()) { + v8::Local result; + if (v8Val.As()->Call(rt.context(), self, 0, nullptr) + .ToLocal(&result)) { + info.GetReturnValue().Set(result); + return; + } + } + } + } catch (...) {} + }), + v8::DontEnum); + objectTemplate->SetHandler(v8::NamedPropertyHandlerConfiguration( + [](v8::Local property, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + if (!property->IsString()) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + v8::Isolate* isolate = info.GetIsolate(); + v8::String::Utf8Value utf8(isolate, property); + if (*utf8 == nullptr) { + return v8::Intercepted::kNo; + } + Value result = holder->hostObject->get( + runtime, PropNameID(std::string(*utf8, utf8.length()))); + if (!result.isUndefined()) { + info.GetReturnValue().Set(result.local(runtime)); + return v8::Intercepted::kYes; + } + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + return v8::Intercepted::kNo; + }, + [](v8::Local property, v8::Local value, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + if (!property->IsString()) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + v8::Isolate* isolate = info.GetIsolate(); + v8::String::Utf8Value utf8(isolate, property); + if (*utf8 == nullptr) { + return v8::Intercepted::kNo; + } + bool handled = holder->hostObject->set( + runtime, PropNameID(std::string(*utf8, utf8.length())), + Value(runtime, value)); + return handled ? v8::Intercepted::kYes : v8::Intercepted::kNo; + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + }, + nullptr, nullptr, nullptr, v8::Local(), + v8::PropertyHandlerFlags::kNonMasking)); + objectTemplate->SetHandler(v8::IndexedPropertyHandlerConfiguration( + [](uint32_t index, const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + Value result = holder->hostObject->get(runtime, PropNameID(std::to_string(index))); + if (!result.isUndefined()) { + info.GetReturnValue().Set(result.local(runtime)); + return v8::Intercepted::kYes; + } + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + return v8::Intercepted::kNo; + }, + [](uint32_t index, v8::Local value, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + holder->hostObject->set(runtime, PropNameID(std::to_string(index)), + Value(runtime, value)); + return v8::Intercepted::kYes; + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + }, + nullptr, nullptr, nullptr, v8::Local(), + v8::PropertyHandlerFlags::kNonMasking)); + state->nativeObjectTemplate.Reset(runtime.isolate(), objectTemplate); + } + return state->nativeObjectTemplate.Get(runtime.isolate()); +} + void hostObjectWeakCallback(const v8::WeakCallbackInfo& info) { delete info.GetParameter(); } @@ -130,42 +343,53 @@ void functionWeakCallback(const v8::WeakCallbackInfo& info) { delete info.GetParameter(); } -} // namespace v8direct +} // namespace v8engine Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken) { v8::Local object = - v8direct::hostObjectTemplate(runtime)->NewInstance(runtime.context()).ToLocalChecked(); - auto* holder = new v8direct::HostObjectHolder(runtime.state(), std::move(host), typeToken); + v8engine::hostObjectTemplate(runtime)->NewInstance(runtime.context()).ToLocalChecked(); + auto* holder = new v8engine::HostObjectHolder(runtime.state(), std::move(host), typeToken); + object->SetAlignedPointerInInternalField(0, holder); + holder->object.Reset(runtime.isolate(), object); + holder->object.SetWeak(holder, v8engine::hostObjectWeakCallback, + v8::WeakCallbackType::kParameter); + return Object::fromValueStorage(Value(runtime, object).storage_); +} + +Object Object::createNativeInstanceWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken) { + v8::Local object = + v8engine::nativeObjectTemplate(runtime)->NewInstance(runtime.context()).ToLocalChecked(); + auto* holder = new v8engine::HostObjectHolder(runtime.state(), std::move(host), typeToken); object->SetAlignedPointerInInternalField(0, holder); holder->object.Reset(runtime.isolate(), object); - holder->object.SetWeak(holder, v8direct::hostObjectWeakCallback, + holder->object.SetWeak(holder, v8engine::hostObjectWeakCallback, v8::WeakCallbackType::kParameter); return Object::fromValueStorage(Value(runtime, object).storage_); } Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, HostFunctionType callback) { - auto* holder = new v8direct::FunctionHolder(runtime.state(), std::move(callback)); + auto* holder = new v8engine::FunctionHolder(runtime.state(), std::move(callback)); v8::Local data = v8::External::New(runtime.isolate(), holder); v8::Local functionTemplate = v8::FunctionTemplate::New( runtime.isolate(), [](const v8::FunctionCallbackInfo& info) { auto* holder = - static_cast(info.Data().As()->Value()); + static_cast(info.Data().As()->Value()); Runtime runtime(holder->state); - std::vector args; - args.reserve(info.Length()); + v8engine::StackValueArray<8> args(static_cast(info.Length())); for (int i = 0; i < info.Length(); i++) { - args.push_back(Value(runtime, info[i])); + args.emplace(static_cast(i), Value::borrowed(runtime, info[i])); } try { - Value thisValue(runtime, info.This()); - Value result = holder->callback(runtime, thisValue, args.empty() ? nullptr : args.data(), + Value thisValue = Value::borrowed(runtime, info.This()); + Value result = holder->callback(runtime, thisValue, args.size() == 0 ? nullptr : args.data(), args.size()); info.GetReturnValue().Set(result.local(runtime)); } catch (const std::exception& exception) { - v8direct::throwV8Exception(info.GetIsolate(), exception); + v8engine::throwV8Exception(info.GetIsolate(), exception); } }, data); @@ -173,15 +397,15 @@ void functionWeakCallback(const v8::WeakCallbackInfo& info) { functionTemplate->GetFunction(runtime.context()).ToLocalChecked(); std::string functionName = name.utf8(runtime); if (!functionName.empty()) { - function->SetName(v8direct::makeV8String(runtime.isolate(), functionName)); + function->SetName(v8engine::makeV8String(runtime.isolate(), functionName)); } holder->function.Reset(runtime.isolate(), function); - holder->function.SetWeak(holder, v8direct::functionWeakCallback, + holder->function.SetWeak(holder, v8engine::functionWeakCallback, v8::WeakCallbackType::kParameter); return Function(Object::fromValueStorage(Value(runtime, function).storage_)); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8Marshalling.mm b/NativeScript/ffi/v8/NativeApiV8Marshalling.mm new file mode 100644 index 000000000..fc9de4db9 --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8Marshalling.mm @@ -0,0 +1,574 @@ +// Included by NativeApiV8SelectorGroups.mm inside the NativeScript anonymous namespace. + +std::string v8StringToUtf8(v8::Isolate* isolate, + v8::Local value) { + v8::String::Utf8Value utf8(isolate, value); + return *utf8 != nullptr ? std::string(*utf8, utf8.length()) : std::string(); +} + +template +std::shared_ptr v8HostObject(Runtime& runtime, v8::Local value) { + if (value.IsEmpty() || !value->IsObject()) { + return nullptr; + } + v8::Local object = value.As(); + if (object->InternalFieldCount() < 1) { + return nullptr; + } + auto* holder = static_cast( + object->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || + holder->typeToken != engine::v8engine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); +} + +// Fast version that returns raw pointer (no atomic ref count). +// Only safe when the caller guarantees the object stays alive. +template +T* v8HostObjectRaw(v8::Local value) { + if (value.IsEmpty() || !value->IsObject()) { + return nullptr; + } + v8::Local object = value.As(); + if (object->InternalFieldCount() < 1) { + return nullptr; + } + auto* holder = static_cast( + object->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || + holder->typeToken != engine::v8engine::hostObjectTypeToken()) { + return nullptr; + } + return static_cast(holder->hostObject.get()); +} + +id v8NativeObjectArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiType& type, + v8::Local value, + NativeApiArgumentFrame& frame) { + v8::Isolate* isolate = runtime.isolate(); + if (value.IsEmpty() || value->IsNullOrUndefined()) { + return nil; + } + if (value->IsString()) { + std::string utf8 = v8StringToUtf8(isolate, value); + id string = type.kind == metagen::mdTypeNSMutableStringObject + ? [[NSMutableString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding] + : [[NSString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding]; + if (string != nil) { + frame.addObject(string); + } + return string; + } + if (value->IsBoolean()) { + return [NSNumber numberWithBool:value->BooleanValue(isolate)]; + } + if (value->IsNumber()) { + return [NSNumber numberWithDouble:value->NumberValue(runtime.context()) + .FromMaybe(0)]; + } + if (!value->IsObject()) { + return nil; + } + if (auto objectHost = + v8HostObject(runtime, value)) { + return objectHost->object(); + } + if (auto classHost = v8HostObject(runtime, value)) { + return static_cast(classHost->nativeClass()); + } + if (auto protocolHost = + v8HostObject(runtime, value)) { + return static_cast(protocolHost->nativeProtocol()); + } + if (auto pointerHost = + v8HostObject(runtime, value)) { + return static_cast(pointerHost->pointer()); + } + if (auto referenceHost = + v8HostObject(runtime, value)) { + return static_cast(referenceHost->data()); + } + if (auto structHost = + v8HostObject(runtime, value)) { + return static_cast(structHost->data()); + } + + v8::Local wrappedClassValue; + if (value.As() + ->Get(runtime.context(), + engine::v8engine::makeV8String(isolate, "__nativeApiClass")) + .ToLocal(&wrappedClassValue)) { + if (auto classHost = + v8HostObject(runtime, wrappedClassValue)) { + return static_cast(classHost->nativeClass()); + } + } + + Value wrapped = Value::borrowed(runtime, value); + return objectFromEngineValue(runtime, bridge, wrapped, frame, + type.kind == + metagen::mdTypeNSMutableStringObject); +} + +Class v8NativeClassArgument(Runtime& runtime, v8::Local value) { + if (value.IsEmpty() || value->IsNullOrUndefined()) { + return Nil; + } + auto* state = runtime.rawState(); + if (state != nullptr && value->IsObject()) { + if (state->nativeClassArgumentLast.nativeClass != Nil && + !state->nativeClassArgumentLast.value.IsEmpty() && + state->nativeClassArgumentLast.value.Get(runtime.isolate()) == value) { + return state->nativeClassArgumentLast.nativeClass; + } + for (auto& entry : state->nativeClassArgumentCache) { + if (entry.nativeClass != Nil && !entry.value.IsEmpty() && + entry.value.Get(runtime.isolate()) == value) { + state->nativeClassArgumentLast.value.Reset(runtime.isolate(), value); + state->nativeClassArgumentLast.nativeClass = entry.nativeClass; + return entry.nativeClass; + } + } + } + + Class result = Nil; + if (auto classHost = v8HostObject(runtime, value)) { + result = classHost->nativeClass(); + } else if (value->IsObject()) { + v8::Local wrappedClassValue; + if (value.As() + ->Get(runtime.context(), + engine::v8engine::makeV8String(runtime.isolate(), + "__nativeApiClass")) + .ToLocal(&wrappedClassValue)) { + if (auto classHost = + v8HostObject(runtime, + wrappedClassValue)) { + result = classHost->nativeClass(); + } + } + } + + if (result == Nil) { + Value wrapped = Value::borrowed(runtime, value); + result = classFromEngineValue(runtime, wrapped); + } + + if (result != Nil && state != nullptr && value->IsObject()) { + constexpr size_t cacheSize = + sizeof(state->nativeClassArgumentCache) / + sizeof(state->nativeClassArgumentCache[0]); + auto& entry = state->nativeClassArgumentCache[ + state->nativeClassArgumentCacheNext++ % cacheSize]; + entry.value.Reset(runtime.isolate(), value); + entry.nativeClass = result; + state->nativeClassArgumentLast.value.Reset(runtime.isolate(), value); + state->nativeClassArgumentLast.nativeClass = result; + } + return result; +} + +bool readV8EngineSelectorArgument(Runtime& runtime, v8::Local value, + SEL* result) { + if (result == nullptr) { + return false; + } + if (value.IsEmpty() || value->IsNullOrUndefined()) { + *result = nullptr; + return true; + } + if (!value->IsString()) { + return false; + } + auto* state = runtime.rawState(); + if (state != nullptr) { + if (state->nativeSelectorArgumentLast.selector != nullptr && + !state->nativeSelectorArgumentLast.value.IsEmpty() && + state->nativeSelectorArgumentLast.value.Get(runtime.isolate()) == + value) { + *result = state->nativeSelectorArgumentLast.selector; + return true; + } + for (auto& entry : state->nativeSelectorArgumentCache) { + if (entry.selector != nullptr && !entry.value.IsEmpty() && + entry.value.Get(runtime.isolate()) == value) { + *result = entry.selector; + state->nativeSelectorArgumentLast.value.Reset(runtime.isolate(), value); + state->nativeSelectorArgumentLast.selector = entry.selector; + return true; + } + } + } + + v8::Isolate* isolate = runtime.isolate(); + v8::Local string = value.As(); + char stackBuffer[128]; + if (string->Utf8LengthV2(isolate) + 1 <= sizeof(stackBuffer)) { + string->WriteUtf8V2(isolate, stackBuffer, sizeof(stackBuffer), + v8::String::WriteFlags::kNullTerminate); + *result = sel_registerName(stackBuffer); + } else { + std::string selectorName = v8StringToUtf8(isolate, value); + *result = sel_registerName(selectorName.c_str()); + } + if (*result != nullptr && state != nullptr) { + constexpr size_t cacheSize = + sizeof(state->nativeSelectorArgumentCache) / + sizeof(state->nativeSelectorArgumentCache[0]); + auto& entry = state->nativeSelectorArgumentCache[ + state->nativeSelectorArgumentCacheNext++ % cacheSize]; + entry.value.Reset(isolate, value); + entry.selector = *result; + state->nativeSelectorArgumentLast.value.Reset(isolate, value); + state->nativeSelectorArgumentLast.selector = *result; + } + return true; +} + +bool prepareV8EngineArgument( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, v8::Local value, + NativeApiArgumentFrame& frame, size_t index) { + ffi_type* ffiType = ffiTypeForEngineArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + + switch (type.kind) { + case metagen::mdTypeBool: + if (!value->IsBoolean()) { + return false; + } + *static_cast(target) = + value->BooleanValue(runtime.isolate()) ? 1 : 0; + return true; + case metagen::mdTypeChar: { + int32_t converted = 0; + if (!value->Int32Value(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: { + uint32_t converted = 0; + if (!value->Uint32Value(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeSShort: { + int32_t converted = 0; + if (!value->Int32Value(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeUShort: { + if (value->IsString()) { + std::string text = v8StringToUtf8(runtime.isolate(), value); + if (text.size() != 1) { + return false; + } + *static_cast(target) = + static_cast(static_cast(text[0])); + return true; + } + uint32_t converted = 0; + if (!value->Uint32Value(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeSInt: + return value->Int32Value(runtime.context()).To( + static_cast(target)); + case metagen::mdTypeUInt: + return value->Uint32Value(runtime.context()).To( + static_cast(target)); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: { + if (value->IsBigInt()) { + bool lossless = false; + *static_cast(target) = + value.As()->Int64Value(&lossless); + return true; + } + return value->IntegerValue(runtime.context()).To( + static_cast(target)); + } + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: { + if (value->IsBigInt()) { + bool lossless = false; + *static_cast(target) = + value.As()->Uint64Value(&lossless); + return true; + } + int64_t converted = 0; + if (!value->IntegerValue(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeFloat: { + double converted = 0; + if (!value->NumberValue(runtime.context()).To(&converted)) { + return false; + } + *static_cast(target) = static_cast(converted); + return true; + } + case metagen::mdTypeDouble: + return value->NumberValue(runtime.context()).To( + static_cast(target)); + case metagen::mdTypeSelector: + return readV8EngineSelectorArgument(runtime, value, + static_cast(target)); + case metagen::mdTypeClass: { + Class cls = v8NativeClassArgument(runtime, value); + if (cls == Nil) { + return false; + } + *static_cast(target) = cls; + return true; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + *static_cast(target) = + v8NativeObjectArgument(runtime, bridge, type, value, frame); + return true; + default: + break; + } + + Value wrapped = Value::borrowed(runtime, value); + convertEngineFfiArgument(runtime, bridge, type, wrapped, target, frame); + return true; +} + +v8::Local v8Integer64Value(v8::Isolate* isolate, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return v8::Number::New(isolate, static_cast(value)); + } + return v8::BigInt::New(isolate, value); +} + +v8::Local v8UnsignedInteger64Value(v8::Isolate* isolate, + uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return v8::Number::New(isolate, static_cast(value)); + } + return v8::BigInt::NewFromUnsigned(isolate, value); +} + +bool setV8EngineObjectReturn( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiType& type, id object, + const v8::FunctionCallbackInfo& info) { + v8::Isolate* isolate = runtime.isolate(); + if (object == nil) { + info.GetReturnValue().Set(v8::Null(isolate)); + return true; + } + Value roundTrip = + findCachedNativeObjectReturn(runtime, bridge, type, object); + if (!roundTrip.isUndefined()) { + info.GetReturnValue().Set(roundTrip.local(runtime)); + if (type.returnOwned) { + [object release]; + } + return true; + } + if (nativeObjectReturnMayCoerceToString(type) && + nativeObjectIsStringLike(object)) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; + } + info.GetReturnValue().Set(engine::v8engine::makeV8String(isolate, utf8)); + return true; + } + if ([object isKindOfClass:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + info.GetReturnValue().Set(v8::Null(isolate)); + return true; + } + 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); + if (isBool) { + info.GetReturnValue().Set(v8::Boolean::New(isolate, [number boolValue])); + } else { + info.GetReturnValue().Set(v8::Number::New(isolate, [number doubleValue])); + } + if (type.returnOwned) { + [object release]; + } + return true; + } + + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + Value result = makeNativeClassValue(runtime, bridge, *classSymbol); + info.GetReturnValue().Set(result.local(runtime)); + if (type.returnOwned) { + [object release]; + } + return true; + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + Value result = makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + info.GetReturnValue().Set(result.local(runtime)); + if (type.returnOwned) { + [object release]; + } + return true; + } + Value result = makeNativeObjectValue(runtime, bridge, object, type.returnOwned); + info.GetReturnValue().Set(result.local(runtime)); + return true; +} + +bool setV8EngineReturnValue( + Runtime& runtime, const std::shared_ptr& bridge, + NativeApiType type, void* value, const std::string& selectorName, + const v8::FunctionCallbackInfo& info) { + v8::Isolate* isolate = runtime.isolate(); + switch (type.kind) { + case metagen::mdTypeVoid: + info.GetReturnValue().Set(v8::Undefined(isolate)); + return true; + case metagen::mdTypeBool: + info.GetReturnValue().Set( + v8::Boolean::New(isolate, *static_cast(value) != 0)); + return true; + case metagen::mdTypeChar: + info.GetReturnValue().Set( + v8::Integer::New(isolate, *static_cast(value))); + return true; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned( + isolate, *static_cast(value))); + return true; + case metagen::mdTypeSShort: + info.GetReturnValue().Set( + v8::Integer::New(isolate, *static_cast(value))); + return true; + case metagen::mdTypeUShort: { + uint16_t raw = *static_cast(value); + if (raw >= 32 && raw <= 126) { + char buffer[2] = {static_cast(raw), '\0'}; + info.GetReturnValue().Set(engine::v8engine::makeV8String(isolate, buffer)); + } else { + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, raw)); + } + return true; + } + case metagen::mdTypeSInt: + info.GetReturnValue().Set( + v8::Integer::New(isolate, *static_cast(value))); + return true; + case metagen::mdTypeUInt: + info.GetReturnValue().Set(v8::Integer::NewFromUnsigned( + isolate, *static_cast(value))); + return true; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + info.GetReturnValue().Set( + v8Integer64Value(isolate, *static_cast(value))); + return true; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + info.GetReturnValue().Set( + v8UnsignedInteger64Value(isolate, *static_cast(value))); + return true; + case metagen::mdTypeFloat: + info.GetReturnValue().Set( + v8::Number::New(isolate, *static_cast(value))); + return true; + case metagen::mdTypeDouble: + info.GetReturnValue().Set( + v8::Number::New(isolate, *static_cast(value))); + return true; + case metagen::mdTypeClass: { + Class cls = *static_cast(value); + if (cls == nil) { + info.GetReturnValue().Set(v8::Null(isolate)); + return true; + } + 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; + } + Value result = makeNativeClassValue(runtime, bridge, std::move(symbol)); + info.GetReturnValue().Set(result.local(runtime)); + return true; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + if ((selectorName == "valueForKey:" || + selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(type)) { + type.kind = metagen::mdTypeAnyObject; + } + return setV8EngineObjectReturn(runtime, bridge, type, + *static_cast(value), info); + case metagen::mdTypeSelector: { + SEL selector = *static_cast(value); + const char* selectorNameValue = + selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorNameValue == nullptr) { + info.GetReturnValue().Set(v8::Null(isolate)); + } else { + info.GetReturnValue().Set( + engine::v8engine::makeV8String(isolate, selectorNameValue)); + } + return true; + } + default: + break; + } + Value result = convertNativeReturnValue(runtime, bridge, type, value); + info.GetReturnValue().Set(result.local(runtime)); + return true; +} diff --git a/NativeScript/ffi/v8/NativeApiV8Runtime.h b/NativeScript/ffi/v8/NativeApiV8Runtime.h index 81c95a085..0d43fed55 100644 --- a/NativeScript/ffi/v8/NativeApiV8Runtime.h +++ b/NativeScript/ffi/v8/NativeApiV8Runtime.h @@ -36,15 +36,15 @@ #include "ffi.h" #include "v8.h" -@protocol NativeApiJsiClassBuilderProtocol +@protocol NativeApiClassBuilderProtocol @end #ifdef EMBED_METADATA_SIZE extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { class Runtime; class Value; @@ -99,36 +99,69 @@ class HostObject { public: virtual ~HostObject() = default; virtual Value get(Runtime& runtime, const PropNameID& name); - virtual void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); virtual std::vector getPropertyNames(Runtime& runtime); }; using HostFunctionType = std::function; -namespace v8direct { +namespace v8engine { struct RuntimeState { explicit RuntimeState(v8::Isolate* isolate, v8::Local context) : isolate(isolate) { this->context.Reset(isolate, context); } - ~RuntimeState() { context.Reset(); } + ~RuntimeState() { + nativeClassArgumentLast.value.Reset(); + nativeClassArgumentLast.nativeClass = Nil; + for (auto& entry : nativeClassArgumentCache) { + entry.value.Reset(); + entry.nativeClass = Nil; + } + nativeSelectorArgumentLast.value.Reset(); + nativeSelectorArgumentLast.selector = nullptr; + for (auto& entry : nativeSelectorArgumentCache) { + entry.value.Reset(); + entry.selector = nullptr; + } + context.Reset(); + } - v8::Local localContext() const { return context.Get(isolate); } + v8::Local localContext() const { + v8::Local ctx = context.Get(isolate); + return ctx.IsEmpty() ? isolate->GetCurrentContext() : ctx; + } v8::Isolate* isolate = nullptr; v8::Global context; v8::Global hostObjectTemplate; + v8::Global nativeObjectTemplate; // kNonMasking for instances std::vector> retainedNativeData; + struct NativeClassArgumentCacheEntry { + v8::Global value; + Class nativeClass = Nil; + }; + NativeClassArgumentCacheEntry nativeClassArgumentLast; + NativeClassArgumentCacheEntry nativeClassArgumentCache[4]; + size_t nativeClassArgumentCacheNext = 0; + struct NativeSelectorArgumentCacheEntry { + v8::Global value; + SEL selector = nullptr; + }; + NativeSelectorArgumentCacheEntry nativeSelectorArgumentLast; + NativeSelectorArgumentCacheEntry nativeSelectorArgumentCache[4]; + size_t nativeSelectorArgumentCacheNext = 0; }; struct ValueStorage { - enum class Kind { + enum class Kind : uint8_t { Undefined, Null, Bool, Number, V8, + V8Borrowed, }; explicit ValueStorage(Kind kind) : kind(kind) {} @@ -139,6 +172,7 @@ struct ValueStorage { bool boolValue = false; double numberValue = 0; v8::Global value; + v8::Local borrowedValue; }; template @@ -204,25 +238,26 @@ inline std::string currentExceptionMessage(v8::Isolate* isolate, v8::TryCatch& t if (tryCatch.HasCaught()) { return toUtf8(isolate, tryCatch.Exception()); } - return "NativeScript direct V8 operation failed."; + return "NativeScript V8 engine operation failed."; } inline void throwV8Exception(v8::Isolate* isolate, const std::exception& exception) { isolate->ThrowException(v8::Exception::Error(makeV8String(isolate, exception.what()))); } -} // namespace v8direct +} // namespace v8engine class Runtime { public: Runtime(v8::Isolate* isolate, v8::Local context) - : state_(std::make_shared(isolate, context)) {} + : state_(std::make_shared(isolate, context)) {} - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} v8::Isolate* isolate() const { return state_->isolate; } v8::Local context() const { return state_->localContext(); } - std::shared_ptr state() const { return state_; } + v8engine::RuntimeState* rawState() const { return state_.get(); } + std::shared_ptr state() const { return state_; } Object global(); @@ -231,7 +266,7 @@ class Runtime { void drainMicrotasks() { isolate()->PerformMicrotaskCheckpoint(); } private: - std::shared_ptr state_; + std::shared_ptr state_; }; class String { @@ -241,11 +276,11 @@ class String { static String createFromUtf8(Runtime& runtime, const char* value) { return String(runtime, - v8direct::makeV8String(runtime.isolate(), value != nullptr ? value : "")); + v8engine::makeV8String(runtime.isolate(), value != nullptr ? value : "")); } static String createFromUtf8(Runtime& runtime, const std::string& value) { - return String(runtime, v8direct::makeV8String(runtime.isolate(), value)); + return String(runtime, v8engine::makeV8String(runtime.isolate(), value)); } static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { @@ -258,7 +293,7 @@ class String { } std::string utf8(Runtime& runtime) const { - return v8direct::toUtf8(runtime.isolate(), local(runtime)); + return v8engine::toUtf8(runtime.isolate(), local(runtime)); } v8::Local local(Runtime& runtime) const { @@ -269,31 +304,42 @@ class String { private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class Value { public: - Value() - : storage_( - std::make_shared(v8direct::ValueStorage::Kind::Undefined)) {} + Value() : kind_(v8engine::ValueStorage::Kind::Undefined) {} - Value(bool value) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::Bool)) { - storage_->boolValue = value; - } + Value(bool value) : kind_(v8engine::ValueStorage::Kind::Bool), boolValue_(value) {} - Value(double value) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::Number)) { - storage_->numberValue = value; - } + Value(double value) : kind_(v8engine::ValueStorage::Kind::Number), numberValue_(value) {} Value(int value) : Value(static_cast(value)) {} Value(uint32_t value) : Value(static_cast(value)) {} - Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} - Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + // Promote borrowed to owned + storage_ = + std::make_shared(v8engine::ValueStorage::Kind::V8); + storage_->value.Reset(runtime.isolate(), value.borrowedValue_); + kind_ = v8engine::ValueStorage::Kind::V8; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value); Value(Runtime& runtime, const Object& object); Value(Runtime& runtime, const Function& function); Value(Runtime& runtime, const Array& array); @@ -304,7 +350,7 @@ class Value { static Value null() { Value value; - value.storage_ = std::make_shared(v8direct::ValueStorage::Kind::Null); + value.kind_ = v8engine::ValueStorage::Kind::Null; return value; } @@ -326,25 +372,48 @@ class Value { v8::Local local(Runtime& runtime) const { v8::Isolate* isolate = runtime.isolate(); - switch (storage_->kind) { - case v8direct::ValueStorage::Kind::Undefined: + switch (kind_) { + case v8engine::ValueStorage::Kind::Undefined: return v8::Undefined(isolate); - case v8direct::ValueStorage::Kind::Null: + case v8engine::ValueStorage::Kind::Null: return v8::Null(isolate); - case v8direct::ValueStorage::Kind::Bool: - return v8::Boolean::New(isolate, storage_->boolValue); - case v8direct::ValueStorage::Kind::Number: - return v8::Number::New(isolate, storage_->numberValue); - case v8direct::ValueStorage::Kind::V8: + case v8engine::ValueStorage::Kind::Bool: + return v8::Boolean::New(isolate, boolValue_); + case v8engine::ValueStorage::Kind::Number: + return v8::Number::New(isolate, numberValue_); + case v8engine::ValueStorage::Kind::V8: return storage_->value.Get(isolate); + case v8engine::ValueStorage::Kind::V8Borrowed: + return borrowedValue_; } } Value(Runtime& runtime, v8::Local value) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + : kind_(v8engine::ValueStorage::Kind::V8), + storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->value.Reset(runtime.isolate(), value); } + static Value borrowed(Runtime&, v8::Local value) { + Value result; + result.kind_ = v8engine::ValueStorage::Kind::V8Borrowed; + result.borrowedValue_ = value; + return result; + } + + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.borrowedValue_ = s->borrowedValue; + v.storage_ = std::move(s); + return v; + } + private: friend class Runtime; friend class Object; @@ -354,18 +423,22 @@ class Value { friend class Function; friend class Array; - std::shared_ptr storage_; + v8engine::ValueStorage::Kind kind_ = v8engine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + v8::Local borrowedValue_; + std::shared_ptr storage_; }; class Object { public: Object() = default; explicit Object(Runtime& runtime) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->value.Reset(runtime.isolate(), v8::Object::New(runtime.isolate())); } - static Object fromValueStorage(std::shared_ptr storage) { + static Object fromValueStorage(std::shared_ptr storage) { Object object; object.storage_ = std::move(storage); return object; @@ -375,12 +448,24 @@ class Object { static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { auto baseHost = std::static_pointer_cast(std::move(host)); return createFromHostObjectWithToken(runtime, std::move(baseHost), - v8direct::hostObjectTypeToken()); + v8engine::hostObjectTypeToken()); } + // Create a native object instance using kNonMasking template for fast + // prototype-based property access. + template + static Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createNativeInstanceWithToken(runtime, std::move(baseHost), + v8engine::hostObjectTypeToken()); + } + + static Object createNativeInstanceWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken); + Value getProperty(Runtime& runtime, const char* name) const { return getProperty(runtime, - v8direct::makeV8String(runtime.isolate(), name != nullptr ? name : "")); + v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")); } Value getProperty(Runtime& runtime, const std::string& name) const { @@ -395,7 +480,7 @@ class Object { v8::TryCatch tryCatch(runtime.isolate()); v8::Local result; if (!local(runtime)->Get(runtime.context(), key).ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Value(runtime, result); } @@ -407,7 +492,7 @@ class Object { Function getPropertyAsFunction(Runtime& runtime, const char* name) const; void setProperty(Runtime& runtime, const char* name, const Value& value) { - setProperty(runtime, v8direct::makeV8String(runtime.isolate(), name != nullptr ? name : ""), + setProperty(runtime, v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : ""), value); } @@ -440,7 +525,7 @@ class Object { void setProperty(Runtime& runtime, v8::Local key, const Value& value) { v8::TryCatch tryCatch(runtime.isolate()); if (!local(runtime)->Set(runtime.context(), key, value.local(runtime)).FromMaybe(false)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } } @@ -448,7 +533,7 @@ class Object { v8::TryCatch tryCatch(runtime.isolate()); return local(runtime) ->Has(runtime.context(), - v8direct::makeV8String(runtime.isolate(), name != nullptr ? name : "")) + v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")) .FromMaybe(false); } @@ -464,26 +549,27 @@ class Object { template bool isHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == v8direct::hostObjectTypeToken(); + return holder != nullptr && holder->typeToken == v8engine::hostObjectTypeToken(); } template std::shared_ptr getHostObject(Runtime& runtime) const { auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != v8direct::hostObjectTypeToken()) { + if (holder == nullptr || holder->typeToken != v8engine::hostObjectTypeToken()) { return nullptr; } return std::static_pointer_cast(holder->hostObject); } v8::Local local(Runtime& runtime) const { + if (storage_->kind == v8engine::ValueStorage::Kind::V8Borrowed) { + return storage_->borrowedValue.As(); + } return storage_->value.Get(runtime.isolate()).As(); } operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } protected: @@ -493,20 +579,20 @@ class Object { friend class Array; friend class ArrayBuffer; - explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} + explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, const void* typeToken); - v8direct::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { + v8engine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { v8::Local object = local(runtime); if (object->InternalFieldCount() < 1) { return nullptr; } - return static_cast(object->GetAlignedPointerFromInternalField(0)); + return static_cast(object->GetAlignedPointerFromInternalField(0)); } - std::shared_ptr storage_; + std::shared_ptr storage_; }; class Function : public Object { @@ -530,7 +616,7 @@ class Function : public Object { ->Call(runtime.context(), runtime.context()->Global(), static_cast(argv.size()), argv.data()) .ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Value(runtime, result); } @@ -568,7 +654,7 @@ class Function : public Object { ->Call(runtime.context(), thisObject.local(runtime), static_cast(argv.size()), argv.data()) .ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Value(runtime, result); } @@ -585,7 +671,7 @@ class Function : public Object { .As() ->NewInstance(runtime.context(), static_cast(argv.size()), argv.data()) .ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Value(runtime, result); } @@ -606,16 +692,14 @@ class Function : public Object { } operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } }; class Array : public Object { public: explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->value.Reset(runtime.isolate(), v8::Array::New(runtime.isolate(), static_cast(size))); } @@ -628,7 +712,7 @@ class Array : public Object { v8::TryCatch tryCatch(runtime.isolate()); v8::Local result; if (!local(runtime)->Get(runtime.context(), static_cast(index)).ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Value(runtime, result); } @@ -638,7 +722,7 @@ class Array : public Object { if (!local(runtime) ->Set(runtime.context(), static_cast(index), value.local(runtime)) .FromMaybe(false)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } } @@ -647,9 +731,7 @@ class Array : public Object { } operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } }; @@ -657,7 +739,7 @@ class BigInt { public: BigInt() = default; BigInt(Runtime& runtime, v8::Local value) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->value.Reset(runtime.isolate(), value); } @@ -674,7 +756,7 @@ class BigInt { v8::Local result; (void)radix; if (!local(runtime)->ToString(runtime.context()).ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return String(runtime, result); } @@ -684,25 +766,23 @@ class BigInt { } operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } private: friend class Value; - std::shared_ptr storage_; + std::shared_ptr storage_; }; class ArrayBuffer : public Object { public: ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared(v8direct::ValueStorage::Kind::V8)) { - auto holder = new v8direct::ArrayBufferHolder(std::move(buffer)); + : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + auto holder = new v8engine::ArrayBufferHolder(std::move(buffer)); auto backingStore = v8::ArrayBuffer::NewBackingStore( holder->buffer->data(), holder->buffer->size(), [](void*, size_t, void* deleterData) { - auto* holder = static_cast(deleterData); + auto* holder = static_cast(deleterData); holder->object.Reset(); delete holder; }, @@ -723,13 +803,11 @@ class ArrayBuffer : public Object { } operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } }; -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8Runtime.mm b/NativeScript/ffi/v8/NativeApiV8Runtime.mm index 681a211b0..10f8e3d86 100644 --- a/NativeScript/ffi/v8/NativeApiV8Runtime.mm +++ b/NativeScript/ffi/v8/NativeApiV8Runtime.mm @@ -2,8 +2,8 @@ #ifdef TARGET_ENGINE_V8 -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { Object Runtime::global() { return Object::fromValueStorage(Value(*this, context()->Global()).storage_); @@ -17,20 +17,20 @@ v8::NewStringType::kNormal, buffer != nullptr ? static_cast(buffer->size()) : 0) .ToLocalChecked(); - v8::Local resourceName = v8direct::makeV8String(isolate(), sourceURL); + v8::Local resourceName = v8engine::makeV8String(isolate(), sourceURL); v8::ScriptOrigin origin(resourceName); v8::Local script; if (!v8::Script::Compile(context(), source, &origin).ToLocal(&script)) { - throw JSError(*this, v8direct::currentExceptionMessage(isolate(), tryCatch)); + throw JSError(*this, v8engine::currentExceptionMessage(isolate(), tryCatch)); } v8::Local result; if (!script->Run(context()).ToLocal(&result)) { - throw JSError(*this, v8direct::currentExceptionMessage(isolate(), tryCatch)); + throw JSError(*this, v8engine::currentExceptionMessage(isolate(), tryCatch)); } return Value(*this, result); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8RuntimeSupport.mm b/NativeScript/ffi/v8/NativeApiV8RuntimeSupport.mm new file mode 100644 index 000000000..558f6565d --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8RuntimeSupport.mm @@ -0,0 +1,121 @@ +// Included by NativeApiV8.mm inside the NativeScript anonymous namespace. + +struct NativeApiLazyGlobalData { + NativeApiLazyGlobalData(v8::Isolate* isolate, const std::string& name, + const std::string& kind) { + nameValue.Reset(isolate, engine::v8engine::makeV8String(isolate, name)); + kindValue.Reset(isolate, engine::v8engine::makeV8String(isolate, kind)); + } + + ~NativeApiLazyGlobalData() { + nameValue.Reset(); + kindValue.Reset(); + } + + v8::Global nameValue; + v8::Global kindValue; +}; + +std::shared_ptr retainNativeApiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} + +class NativeApiRuntimeScope final { + public: + explicit NativeApiRuntimeScope(Runtime& runtime) + : locker_(runtime.isolate()), + isolateScope_(runtime.isolate()), + handleScope_(runtime.isolate()), + context_(runtime.context()), + contextScope_(context_) {} + + private: + v8::Locker locker_; + v8::Isolate::Scope isolateScope_; + v8::HandleScope handleScope_; + v8::Local context_; + v8::Context::Scope contextScope_; +}; + +void NativeApiLazyGlobalGetter(v8::Local, + const v8::PropertyCallbackInfo& info) { + v8::Isolate* isolate = info.GetIsolate(); + v8::HandleScope handleScope(isolate); + v8::Local context = isolate->GetCurrentContext(); + if (!info.Data()->IsExternal()) { + return; + } + + auto* data = static_cast(info.Data().As()->Value()); + if (data == nullptr) { + return; + } + v8::Local nameValue = data->nameValue.Get(isolate); + v8::Local kindValue = data->kindValue.Get(isolate); + + v8::Local global = context->Global(); + v8::Local resolverValue; + if (!global + ->Get(context, engine::v8engine::makeV8String( + isolate, "__nativeScriptResolveNativeApiLazyGlobal")) + .ToLocal(&resolverValue) || + !resolverValue->IsFunction()) { + return; + } + + v8::TryCatch tryCatch(isolate); + v8::Local args[] = {nameValue, kindValue}; + v8::Local result; + if (!resolverValue.As()->Call(context, global, 2, args).ToLocal(&result)) { + if (tryCatch.HasCaught()) { + isolate->ThrowException(tryCatch.Exception()); + } + return; + } + if (global->Delete(context, nameValue).FromMaybe(false)) { + global->DefineOwnProperty(context, nameValue, result, v8::DontEnum).FromMaybe(false); + } + info.GetReturnValue().Set(result); +} + +bool InstallNativeApiLazyGlobal(Runtime& runtime, std::shared_ptr, + const std::string& name, const std::string& kind, + bool force) { + if (name.empty() || kind.empty()) { + return false; + } + + v8::Isolate* isolate = runtime.isolate(); + v8::EscapableHandleScope handleScope(isolate); + v8::Local context = runtime.context(); + v8::Local global = context->Global(); + v8::Local property = engine::v8engine::makeV8String(isolate, name); + if (!force && global->HasOwnProperty(context, property).FromMaybe(false)) { + return false; + } + + auto data = std::make_shared(isolate, name, kind); + v8::Local external = v8::External::New(isolate, data.get()); + + bool installed = global + ->SetNativeDataProperty(context, property, NativeApiLazyGlobalGetter, + nullptr, external, v8::DontEnum) + .FromMaybe(false); + if (installed) { + runtime.state()->retainedNativeData.push_back(std::move(data)); + } + return installed; +} + +void SetNativeApiObjectPrototype(Runtime& runtime, Object& object, + const Object& prototype) { + v8::TryCatch tryCatch(runtime.isolate()); + if (!object.local(runtime) + ->SetPrototypeV2(runtime.context(), prototype.local(runtime)) + .FromMaybe(false)) { + throw JSError(runtime, + engine::v8engine::currentExceptionMessage(runtime.isolate(), + tryCatch)); + } +} + diff --git a/NativeScript/ffi/v8/NativeApiV8SelectorGroups.mm b/NativeScript/ffi/v8/NativeApiV8SelectorGroups.mm new file mode 100644 index 000000000..5b77573bd --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8SelectorGroups.mm @@ -0,0 +1,451 @@ +// Included by NativeApiV8.mm inside the NativeScript anonymous namespace. + +struct NativeApiSelectorGroupData { + NativeApiSelectorGroupData( + std::shared_ptr state, + std::shared_ptr bridge, Class lookupClass, + bool receiverIsClass, + std::shared_ptr> + selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver = {}, + std::shared_ptr boundReceiverState = + nullptr) + : state(state), + bridge(std::move(bridge)), + lookupClass(lookupClass), + receiverIsClass(receiverIsClass), + selectors(std::move(selectors)), + preparedInvocations(std::move(preparedInvocations)), + boundReceiver(std::move(boundReceiver)), + boundReceiverState(std::move(boundReceiverState)), + runtime(state) {} + + std::shared_ptr state; + std::shared_ptr bridge; + Class lookupClass = Nil; + bool receiverIsClass = false; + std::shared_ptr> selectors; + std::shared_ptr< + std::vector>> + preparedInvocations; + std::weak_ptr boundReceiver; + std::shared_ptr boundReceiverState; + // Cached Runtime wrapper reused per call (avoids per-call shared_ptr + // atomic refcount on the hot dispatch path). + Runtime runtime; + // 1-entry memo for dispatchSuperclassForEngineDerivedReceiver. + Class cachedReceiverClass = Nil; + Class cachedDispatchClass = Nil; +}; + +#include "NativeApiV8Marshalling.mm" + +#include "NativeApiV8Gsd.mm" + + +void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { + return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); +} + +bool tryCallGeneratedEngineObjCSelector( + Runtime&, const std::shared_ptr&, id, + const NativeApiPreparedObjCInvocation&, const Value*, size_t, Class, + Value*) { + return false; +} + +void setV8EnginePreparedObjCResult( + Runtime& runtime, const std::shared_ptr& bridge, + id receiver, const NativeApiPreparedObjCInvocation& prepared, + const std::shared_ptr& receiverHostObject, + const std::optional& initializerClassWrapper, + const v8::FunctionCallbackInfo& info, + Class dispatchSuperClass) { + const NativeApiSignature& signature = prepared.signature; + if (receiver == nil || signature.variadic || + unsupportedEngineType(signature.returnType)) { + throw JSError(runtime, + "Objective-C selector is not supported by V8 engine: " + + prepared.selectorName); + } + + const bool isNSErrorOutMethod = prepared.isNSErrorOutMethod; + const size_t providedCount = static_cast(info.Length()); + if (isNSErrorOutMethod) { + size_t expected = signature.argumentTypes.size(); + if (providedCount > expected || providedCount + 1 < expected) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + std::to_string(expected) + "\"."); + } + } else if (providedCount != signature.argumentTypes.size()) { + throw JSError( + runtime, "Actual arguments count: \"" + std::to_string(providedCount) + + "\". Expected: \"" + + std::to_string(signature.argumentTypes.size()) + "\"."); + } + + // GSD fast path: the generated invoker reads args directly from + // FunctionCallbackInfo, calls objc_msgSend with a typed cast, and sets the + // return via the V8 API — all in one generated function. Bypasses all + // generic marshalling. + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod) { + auto invoker = reinterpret_cast(prepared.engineInvoker); + GsdObjCContext ctx{runtime, + bridge, + receiver, + prepared.selector, + info, + runtime.isolate(), + runtime.context(), + signature.returnType}; + if (invoker(ctx)) { + return; + } + } + + if (dispatchSuperClass == Nil && !initializerClassWrapper && + providedCount <= 2) { + Value fastArgs[2]; + for (size_t i = 0; i < providedCount; i++) { + fastArgs[i] = Value::borrowed(runtime, info[static_cast(i)]); + } + Value fastResult; + if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared, + fastArgs, providedCount, Nil, + &fastResult)) { + info.GetReturnValue().Set(fastResult.local(runtime)); + return; + } + } + + NativeApiArgumentFrame frame(signature.argumentTypes.size()); + for (size_t i = 0; i < providedCount; i++) { + if (!prepareV8EngineArgument(runtime, bridge, signature.argumentTypes[i], + info[static_cast(i)], frame, i)) { + throw JSError(runtime, + "Objective-C argument is not supported by V8 engine: " + + prepared.selectorName); + } + } + + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && providedCount + 1 == signature.argumentTypes.size(); + NSError* implicitNSError = nil; + if (hasImplicitNSErrorOutArg) { + size_t outArgIndex = signature.argumentTypes.size() - 1; + void* target = frame.storageAt(outArgIndex, sizeof(NSError**)); + NSError** implicitNSErrorOutArg = &implicitNSError; + *static_cast(target) = implicitNSErrorOutArg; + } + + NativeApiPointerFrame values(signature.argumentTypes.size() + 2); + size_t valueIndex = 0; + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.set(valueIndex++, &superReceiverPtr); + } else { + values.set(valueIndex++, &receiver); + } + values.set(valueIndex++, const_cast(&prepared.selector)); + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + values.set(valueIndex++, frame.values()[i]); + } + + NativeApiReturnStorage returnStorage( + nativeSizeForType(signature.returnType)); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + if (prepared.preparedInvoker != nullptr && dispatchSuperClass == Nil) { + prepared.preparedInvoker(reinterpret_cast(objc_msgSend), + values.data(), returnStorage.data()); + } else { +#if defined(__x86_64__) + bool isStret = signature.returnType.ffiType->size > 16 && + signature.returnType.ffiType->type == FFI_TYPE_STRUCT; + void (*target)(void) = + dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) : FFI_FN(objc_msgSend)); + ffi_call(const_cast(&signature.cif), target, + returnStorage.data(), values.data()); +#else + ffi_call(const_cast(&signature.cif), + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#endif + } + }); + + NativeApiType returnType = signature.returnType; + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + if (initializerClassWrapper) { + id resultObject = nil; + if (isObjectiveCObjectType(returnType)) { + resultObject = *static_cast(returnStorage.data()); + } + if (receiverHostObject != nullptr && resultObject != receiver) { + receiverHostObject->disownObject(receiver); + } + if (resultObject != nil) { + bridge->setObjectExpando(runtime, resultObject, + "__nativeApiClassWrapper", + Value(runtime, *initializerClassWrapper)); + } + } + setV8EngineReturnValue(runtime, bridge, returnType, returnStorage.data(), + prepared.selectorName, info); +} + +void NativeApiSelectorGroupCallback( + const v8::FunctionCallbackInfo& info) { + auto* data = static_cast( + info.Data().As()->Value()); + if (data == nullptr || data->selectors == nullptr || + data->preparedInvocations == nullptr) { + return; + } + + Runtime& runtime = data->runtime; + v8::HandleScope handleScope(runtime.isolate()); + try { + NativeApiRoundTripCacheFrameGuard roundTripFrame(data->bridge); + size_t count = static_cast(info.Length()); + if (count >= data->selectors->size() || + (*data->selectors)[count].selectorName.empty()) { + throw JSError(runtime, + "Objective-C selector is not available for the provided arguments " + "count."); + } + + NativeApiSelectorGroupEntry& entry = (*data->selectors)[count]; + auto& prepared = (*data->preparedInvocations)[count]; + Class selectorLookupClass = data->lookupClass; + id receiver = data->receiverIsClass ? static_cast(data->lookupClass) : nil; + std::shared_ptr receiverHostObject; + if (!data->receiverIsClass) { + if (data->boundReceiverState != nullptr) { + receiver = data->boundReceiverState->object(); + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + } else { + // Use raw pointer for receiver lookup (avoids atomic ref count on hot path). + // The receiver host object is kept alive by the V8 GC handle. + auto* rawHost = v8HostObjectRaw(info.This()); + if (rawHost != nullptr) { + receiver = rawHost->object(); + // Only get shared_ptr if needed for init handling below. + } + } + } + if (receiver == nil) { + throw JSError(runtime, + "Objective-C selector requires a native receiver."); + } + + const bool propertyGetterCall = + entry.hasMember && entry.member.property && count == 0; + const std::string* selectorNamePtr = &entry.selectorName; + const NativeApiMember* selectedMember = + entry.hasMember ? &entry.member : nullptr; + bool callTargetCanPrepare = true; + if (prepared == nullptr || propertyGetterCall) { + NativeApiSelectorGroupCallTarget callTarget = + selectorGroupCallTargetForEntry(receiver, selectorLookupClass, + data->receiverIsClass, entry, count); + selectorNamePtr = callTarget.selectorName; + selectedMember = callTarget.member; + callTargetCanPrepare = callTarget.canPrepare; + if (prepared != nullptr && prepared->selectorName != *selectorNamePtr) { + prepared = nullptr; + } + } + const std::string& selectorName = + prepared != nullptr && !propertyGetterCall ? prepared->selectorName + : *selectorNamePtr; + + if (data->receiverIsClass) { + Class methodClass = prepared != nullptr ? prepared->receiverClass : Nil; + if (methodClass == Nil) { + SEL selector = sel_registerName(selectorName.c_str()); + methodClass = + NativeApiClassHostObject::classRespondingToClassSelector( + data->lookupClass, selector); + } + if (methodClass == Nil) { + throw JSError(runtime, + "Objective-C selector is not available: " + + entry.selectorName); + } + selectorLookupClass = methodClass; + receiver = static_cast(methodClass); + } + if (propertyGetterCall && !callTargetCanPrepare) { + Value result = callObjCSelector(runtime, data->bridge, receiver, + data->receiverIsClass, selectorName, + selectedMember, nullptr, 0); + info.GetReturnValue().Set(result.local(runtime)); + return; + } + + if (prepared == nullptr) { + // First call: resolve the method and cache the prepared invocation. + if (!data->receiverIsClass) { + SEL selector = sel_registerName(selectorName.c_str()); + if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { + Class receiverClass = object_getClass(receiver); + if (class_getInstanceMethod(receiverClass, selector) != nullptr) { + selectorLookupClass = receiverClass; + } + } + } + prepared = prepareNativeApiObjCInvocation( + runtime, data->bridge, selectorLookupClass, data->receiverIsClass, + selectorName, selectedMember); + // Look up the engine-neutral GSD invoker for this signature. + if (prepared->engineInvoker == nullptr) { + uint64_t dispatchId = dispatchIdForEngineSignature( + prepared->signature, SignatureCallKind::ObjCMethod); + if (auto gsdInvoker = lookupObjCGsdInvoker(dispatchId)) { + prepared->engineInvoker = reinterpret_cast(gsdInvoker); + configureGeneratedEngineObjCInvocation(*prepared); + } + } + } + + std::optional initializerClassWrapper; + if (!data->receiverIsClass && prepared->isInitMethod) { + // Init methods need the shared_ptr for disown handling. + if (!receiverHostObject) { + if (data->boundReceiverState != nullptr) { + if (auto boundReceiver = data->boundReceiver.lock()) { + receiverHostObject = std::move(boundReceiver); + } + } + } + if (!receiverHostObject) { + receiverHostObject = + v8HostObject(runtime, info.This()); + } + Value classWrapperValue = data->bridge->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + initializerClassWrapper.emplace(classWrapperValue.asObject(runtime)); + } + data->bridge->forgetRoundTripValue(receiver); + data->bridge->forgetObjectExpandos(receiver); + } + + // For JS-extended receivers, dispatch from the immediate native + // superclass so native-derived overrides are honored (not the method's + // defining ancestor, which would skip intermediate native overrides). + // dispatchSuperclassForEngineDerivedReceiver is a pure function of the + // receiver's class + lookupClass, so memoize it (1-entry cache) to avoid a + // per-call class_conformsToProtocol on the hot path. + Class dispatchClass = Nil; + if (!data->receiverIsClass) { + Class receiverClass = object_getClass(receiver); + if (receiverClass == data->cachedReceiverClass) { + dispatchClass = data->cachedDispatchClass; + } else { + dispatchClass = dispatchSuperclassForEngineDerivedReceiver( + receiver, data->lookupClass); + data->cachedReceiverClass = receiverClass; + data->cachedDispatchClass = dispatchClass; + } + } + // Inline GSD fast path: skip the setV8EnginePreparedObjCResult call and its + // argument-count/NSError preamble entirely for the common case. The + // generated invoker reads args, calls objc_msgSend, and sets the return. + if (prepared->gsdEngineCallable && dispatchClass == Nil && + !prepared->isInitMethod && + count == prepared->gsdEngineArgumentCount) { + auto invoker = reinterpret_cast(prepared->engineInvoker); + GsdObjCContext ctx{runtime, + data->bridge, + receiver, + prepared->selector, + info, + runtime.isolate(), + runtime.context(), + prepared->signature.returnType}; + if (invoker(ctx)) { + return; + } + } + setV8EnginePreparedObjCResult(runtime, data->bridge, receiver, *prepared, + receiverHostObject, initializerClassWrapper, + info, dispatchClass); + } catch (const std::exception& exception) { + engine::v8engine::throwV8Exception(info.GetIsolate(), exception); + } +} + +Function CreateNativeApiSelectorGroupFunctionImpl( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations, + std::weak_ptr boundReceiver, + std::shared_ptr boundReceiverState = + nullptr) { + auto data = std::make_shared( + runtime.state(), std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), + std::move(boundReceiver), std::move(boundReceiverState)); + auto* rawData = data.get(); + runtime.state()->retainedNativeData.push_back(std::move(data)); + + v8::Local external = + v8::External::New(runtime.isolate(), rawData); + v8::Local functionTemplate = + v8::FunctionTemplate::New(runtime.isolate(), + NativeApiSelectorGroupCallback, external); + v8::Local function = + functionTemplate->GetFunction(runtime.context()).ToLocalChecked(); + function->SetName( + engine::v8engine::makeV8String(runtime.isolate(), "__nativeSelectorGroup")); + Value functionValue(runtime, function); + return functionValue.asObject(runtime).asFunction(runtime); +} + +Function CreateNativeApiSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, bool receiverIsClass, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), {}, nullptr); +} + +Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, false, std::move(selectors), + std::move(preparedInvocations), receiverHostObject, + receiverHostObject != nullptr ? receiverHostObject->lifetimeState() + : nullptr); +} diff --git a/NativeScript/ffi/v8/NativeApiV8Value.mm b/NativeScript/ffi/v8/NativeApiV8Value.mm index d8b34428d..dab1e0271 100644 --- a/NativeScript/ffi/v8/NativeApiV8Value.mm +++ b/NativeScript/ffi/v8/NativeApiV8Value.mm @@ -2,34 +2,54 @@ #ifdef TARGET_ENGINE_V8 -namespace facebook { -namespace jsi { +namespace nativescript { +namespace engine { Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } -void HostObject::set(Runtime&, const PropNameID&, const Value&) {} +bool HostObject::set(Runtime&, const PropNameID&, const Value&) { return true; } std::vector HostObject::getPropertyNames(Runtime&) { return {}; } String::String(Runtime& runtime, v8::Local value) - : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { storage_->value.Reset(runtime.isolate(), value); } String::operator Value() const { - Value value; - value.storage_ = storage_; - return value; + return Value::fromStorage(storage_); } -Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} -Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} -Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} -Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} -Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} +Value::Value(Runtime&, const String& value) { + storage_ = value.storage_; + kind_ = storage_->kind; +} +Value::Value(Runtime&, const Object& object) { + storage_ = object.storage_; + kind_ = storage_ ? storage_->kind : v8engine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Function& function) { + storage_ = function.storage_; + kind_ = storage_ ? storage_->kind : v8engine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const Array& array) { + storage_ = array.storage_; + kind_ = storage_ ? storage_->kind : v8engine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) { + storage_ = arrayBuffer.storage_; + kind_ = storage_ ? storage_->kind : v8engine::ValueStorage::Kind::Undefined; +} +Value::Value(Runtime&, const BigInt& bigint) { + storage_ = bigint.storage_; + kind_ = storage_ ? storage_->kind : v8engine::ValueStorage::Kind::Undefined; +} bool Value::isObject() const { - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsObject(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -37,10 +57,13 @@ } bool Value::isUndefined() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Undefined) { + if (kind_ == v8engine::ValueStorage::Kind::Undefined) { return true; } - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return borrowedValue_.IsEmpty() || borrowedValue_->IsUndefined(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -48,10 +71,13 @@ } bool Value::isNull() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Null) { + if (kind_ == v8engine::ValueStorage::Kind::Null) { return true; } - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsNull(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -59,10 +85,13 @@ } bool Value::isBool() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Bool) { + if (kind_ == v8engine::ValueStorage::Kind::Bool) { return true; } - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsBoolean(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -70,10 +99,16 @@ } bool Value::getBool() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Bool) { - return storage_->boolValue; + if (kind_ == v8engine::ValueStorage::Kind::Bool) { + return boolValue_; + } + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && !borrowedValue_.IsEmpty() + ? borrowedValue_->BooleanValue(isolate) + : false; } - if (storage_->kind == v8direct::ValueStorage::Kind::V8 && !storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8 && storage_ && !storage_->value.IsEmpty()) { v8::Isolate* isolate = v8::Isolate::GetCurrent(); if (isolate != nullptr) { return storage_->value.Get(isolate)->BooleanValue(isolate); @@ -83,10 +118,13 @@ } bool Value::isNumber() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Number) { + if (kind_ == v8engine::ValueStorage::Kind::Number) { return true; } - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsNumber(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -94,10 +132,17 @@ } double Value::getNumber() const { - if (storage_->kind == v8direct::ValueStorage::Kind::Number) { - return storage_->numberValue; + if (kind_ == v8engine::ValueStorage::Kind::Number) { + return numberValue_; + } + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + if (isolate != nullptr && !borrowedValue_.IsEmpty()) { + return borrowedValue_->NumberValue(isolate->GetCurrentContext()).FromMaybe(0); + } + return 0; } - if (storage_->kind == v8direct::ValueStorage::Kind::V8 && !storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8 && storage_ && !storage_->value.IsEmpty()) { v8::Isolate* isolate = v8::Isolate::GetCurrent(); if (isolate != nullptr) { return storage_->value.Get(isolate)->NumberValue(isolate->GetCurrentContext()).FromMaybe(0); @@ -107,7 +152,10 @@ } bool Value::isString() const { - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsString(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -115,7 +163,10 @@ } bool Value::isBigInt() const { - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsBigInt(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); @@ -123,14 +174,28 @@ } bool Value::isSymbol() const { - if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + return !borrowedValue_.IsEmpty() && borrowedValue_->IsSymbol(); + } + if (kind_ != v8engine::ValueStorage::Kind::V8 || !storage_ || storage_->value.IsEmpty()) { return false; } v8::Isolate* isolate = v8::Isolate::GetCurrent(); return storage_->value.Get(isolate)->IsSymbol(); } -Object Value::asObject(Runtime& runtime) const { return Object::fromValueStorage(storage_); } +Object Value::asObject(Runtime& runtime) const { + if (storage_) { + return Object::fromValueStorage(storage_); + } + // Need to promote to storage for Object + auto s = std::make_shared(kind_); + if (kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + s->kind = v8engine::ValueStorage::Kind::V8; + s->value.Reset(runtime.isolate(), borrowedValue_); + } + return Object::fromValueStorage(std::move(s)); +} String Value::asString(Runtime& runtime) const { return String(runtime, local(runtime).As()); @@ -154,7 +219,7 @@ v8::TryCatch tryCatch(runtime.isolate()); v8::Local result; if (!local(runtime)->GetPropertyNames(runtime.context()).ToLocal(&result)) { - throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); } return Array(Object::fromValueStorage(Value(runtime, result).storage_)); } @@ -171,7 +236,7 @@ setProperty(runtime, name, Value(runtime, value)); } -} // namespace jsi -} // namespace facebook +} // namespace engine +} // namespace nativescript #endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/SignatureDispatch.h b/NativeScript/ffi/v8/SignatureDispatch.h new file mode 100644 index 000000000..97bbc3b0f --- /dev/null +++ b/NativeScript/ffi/v8/SignatureDispatch.h @@ -0,0 +1,48 @@ +#ifndef NATIVESCRIPT_FFI_V8_SIGNATURE_DISPATCH_H +#define NATIVESCRIPT_FFI_V8_SIGNATURE_DISPATCH_H + +#include + +#include "ffi/shared/SignatureDispatchCore.h" + +// Engine-neutral GSD (Generated Signature Dispatch). The GsdObjCContext struct, +// the ObjCGsdInvoker/ObjCGsdDispatchEntry types, the generated dispatch table, +// and lookupObjCGsdInvoker are all defined in NativeApiV8SelectorGroups.mm, +// which NativeApiV8.mm includes after the host object helpers the context +// relies on. Nothing GSD-related is declared here to avoid creating an +// ambiguous second GsdObjCContext. + +#ifndef NS_GSD_BACKEND_PREPARED +#define NS_GSD_BACKEND_PREPARED 1 +#endif + +#ifndef NS_GSD_BACKEND_NAPI +#define NS_GSD_BACKEND_NAPI 0 +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#define NS_GSD_BACKEND_HERMES 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH 0 +#endif + +// NOTE: GeneratedGsdSignatureDispatch.inc is included from +// NativeApiV8SelectorGroups.mm after GsdObjCContext is defined (avoids +// namespace ordering issues). + +// The main .inc (prepared invokers + tables) is included here. +#if defined(__has_include) +#if __has_include("GeneratedSignatureDispatch.inc") +#include "GeneratedSignatureDispatch.inc" +#endif +#endif + +#include "ffi/shared/PreparedSignatureDispatch.h" + +#endif // NATIVESCRIPT_FFI_V8_SIGNATURE_DISPATCH_H diff --git a/NativeScript/napi/hermes/jsr.cpp b/NativeScript/napi/hermes/jsr.cpp index ad23e9b0c..221674603 100644 --- a/NativeScript/napi/hermes/jsr.cpp +++ b/NativeScript/napi/hermes/jsr.cpp @@ -1,11 +1,14 @@ #include "jsr.h" +#include "jsr_common.h" #include "js_runtime.h" using namespace facebook::jsi; std::unordered_map JSR::env_to_jsr_cache; namespace { +thread_local std::unordered_map g_runtime_lock_depth; + class RuntimeLockGuard { public: explicit RuntimeLockGuard(JSR* runtime) : runtime_(runtime) { @@ -19,6 +22,32 @@ class RuntimeLockGuard { }; } // namespace +void JSR::lock() { + runtime->lock(); + js_mutex.lock(); + g_runtime_lock_depth[this] += 1; +} + +void JSR::unlock() { + auto depth = g_runtime_lock_depth.find(this); + if (depth != g_runtime_lock_depth.end()) { + depth->second -= 1; + if (depth->second <= 0) { + g_runtime_lock_depth.erase(depth); + } + } + js_mutex.unlock(); + runtime->unlock(); +} + +int JSR::currentLockDepth() const { + auto depth = g_runtime_lock_depth.find(const_cast(this)); + if (depth == g_runtime_lock_depth.end()) { + return 0; + } + return depth->second; +} + 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) { @@ -83,6 +112,7 @@ facebook::jsi::Runtime* js_get_jsi_runtime(napi_env env) { napi_status js_set_runtime_flags(const char* flags) { return napi_ok; } napi_status js_free_napi_env(napi_env env) { + js_run_env_cleanup_hooks(env); JSR::env_to_jsr_cache.erase(env); return napi_ok; } diff --git a/NativeScript/napi/hermes/jsr.h b/NativeScript/napi/hermes/jsr.h index cb6221ab3..bee928a50 100644 --- a/NativeScript/napi/hermes/jsr.h +++ b/NativeScript/napi/hermes/jsr.h @@ -17,30 +17,9 @@ class 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() { - 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; - } + void lock(); + void unlock(); + int currentLockDepth() const; static std::unordered_map env_to_jsr_cache; }; diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index d7b23e58f..416cc806f 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -55,6 +57,57 @@ static const JSMallocFunctions mi_mf = {js_mi_calloc, js_mi_malloc, js_mi_free, #endif +typedef struct QJSSABHeader { + atomic_int ref_count; + uint8_t buf[]; +} QJSSABHeader; + +static void* qjs_shared_array_buffer_alloc(void* opaque, size_t size) { + QJSSABHeader* sab = + mi_malloc(sizeof(QJSSABHeader) + (size == 0 ? 1 : size)); + if (sab == NULL) { + return NULL; + } + + atomic_init(&sab->ref_count, 1); + return sab->buf; +} + +static QJSSABHeader* qjs_shared_array_buffer_header(uint8_t* ptr) { + return (QJSSABHeader*)(ptr - offsetof(QJSSABHeader, buf)); +} + +void qjs_shared_array_buffer_data_retain(uint8_t* ptr) { + if (ptr == NULL) { + return; + } + + QJSSABHeader* sab = qjs_shared_array_buffer_header(ptr); + atomic_fetch_add_explicit(&sab->ref_count, 1, memory_order_relaxed); +} + +void qjs_shared_array_buffer_data_release(uint8_t* ptr) { + if (ptr == NULL) { + return; + } + + QJSSABHeader* sab = qjs_shared_array_buffer_header(ptr); + int previous = + atomic_fetch_sub_explicit(&sab->ref_count, 1, memory_order_acq_rel); + assert(previous > 0); + if (previous == 1) { + mi_free(sab); + } +} + +static void qjs_shared_array_buffer_free(void* opaque, void* ptr) { + qjs_shared_array_buffer_data_release((uint8_t*)ptr); +} + +static void qjs_shared_array_buffer_dup(void* opaque, void* ptr) { + qjs_shared_array_buffer_data_retain((uint8_t*)ptr); +} + #define ToJS(value) *((JSValue*)value) /** @@ -3786,6 +3839,121 @@ napi_status napi_run_script_source(napi_env env, napi_value script, return qjs_execute_script(env, script, source_url, result); } +napi_status napi_run_script_as_module(napi_env env, napi_value script, + const char* source_url, + napi_value* result) { + CHECK_ARG(env) + CHECK_ARG(script) + CHECK_ARG(source_url) + CHECK_ARG(result) + + size_t script_len = 0; + const char* cScript = + JS_ToCStringLen(env->context, &script_len, ToJS(script)); + RETURN_STATUS_IF_FALSE(cScript != NULL, napi_pending_exception) + + js_enter(env); + JSValue module = + JS_Eval(env->context, cScript, script_len, source_url, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + JS_FreeCString(env->context, cScript); + + if (JS_IsException(module)) { + js_exit(env); + JSValue exception = JS_GetException(env->context); + const char* exceptionMessage = JS_ToCString(env->context, exception); + napi_set_last_error(env, napi_cannot_run_js, exceptionMessage, 0, NULL); + JS_FreeCString(env->context, exceptionMessage); + JS_Throw(env->context, exception); + return napi_cannot_run_js; + } + + JSModuleDef* moduleDef = (JSModuleDef*)JS_VALUE_GET_PTR(module); + JSValue meta = JS_GetImportMeta(env->context, moduleDef); + if (JS_IsException(meta)) { + JS_FreeValue(env->context, module); + js_exit(env); + JSValue exception = JS_GetException(env->context); + const char* exceptionMessage = JS_ToCString(env->context, exception); + napi_set_last_error(env, napi_cannot_run_js, exceptionMessage, 0, NULL); + JS_FreeCString(env->context, exceptionMessage); + JS_Throw(env->context, exception); + return napi_cannot_run_js; + } + + if (JS_DefinePropertyValueStr(env->context, meta, "url", + JS_NewString(env->context, source_url), + JS_PROP_C_W_E) < 0 || + JS_DefinePropertyValueStr(env->context, meta, "main", JS_TRUE, + JS_PROP_C_W_E) < 0) { + JS_FreeValue(env->context, meta); + JS_FreeValue(env->context, module); + js_exit(env); + JSValue exception = JS_GetException(env->context); + const char* exceptionMessage = JS_ToCString(env->context, exception); + napi_set_last_error(env, napi_cannot_run_js, exceptionMessage, 0, NULL); + JS_FreeCString(env->context, exceptionMessage); + JS_Throw(env->context, exception); + return napi_cannot_run_js; + } + JS_FreeValue(env->context, meta); + + JSValue eval_result = + JS_EvalFunction(env->context, JS_DupValue(env->context, module)); + if (JS_IsException(eval_result)) { + JS_FreeValue(env->context, module); + js_exit(env); + JSValue exception = JS_GetException(env->context); + const char* exceptionMessage = JS_ToCString(env->context, exception); + napi_set_last_error(env, napi_cannot_run_js, exceptionMessage, 0, NULL); + JS_FreeCString(env->context, exceptionMessage); + JS_Throw(env->context, exception); + return napi_cannot_run_js; + } + + if (JS_IsPromise(eval_result)) { + int attempts = 0; + while (JS_PromiseState(env->context, eval_result) == JS_PROMISE_PENDING && + attempts < 100) { + qjs_execute_pending_jobs(env); + attempts++; + } + + JSPromiseStateEnum state = JS_PromiseState(env->context, eval_result); + if (state == JS_PROMISE_REJECTED) { + JSValue rejection = JS_PromiseResult(env->context, eval_result); + JS_FreeValue(env->context, eval_result); + JS_FreeValue(env->context, module); + js_exit(env); + JS_Throw(env->context, rejection); + return napi_set_last_error(env, napi_cannot_run_js, NULL, 0, NULL); + } + + if (state == JS_PROMISE_PENDING) { + JS_FreeValue(env->context, eval_result); + JS_FreeValue(env->context, module); + js_exit(env); + napi_throw_error(env, NULL, "Module evaluation did not settle"); + return napi_cannot_run_js; + } + } + JS_FreeValue(env->context, eval_result); + + JSValue namespace = JS_GetModuleNamespace(env->context, moduleDef); + JS_FreeValue(env->context, module); + js_exit(env); + if (JS_IsException(namespace)) { + JSValue exception = JS_GetException(env->context); + const char* exceptionMessage = JS_ToCString(env->context, exception); + napi_set_last_error(env, napi_cannot_run_js, exceptionMessage, 0, NULL); + JS_FreeCString(env->context, exceptionMessage); + JS_Throw(env->context, exception); + return napi_cannot_run_js; + } + + return CreateScopedResult(env, namespace, result); +} + void host_object_finalizer(JSRuntime* rt, JSValue value) { napi_env env = (napi_env)JS_GetRuntimeOpaque(rt); NapiHostObjectInfo* info = (NapiHostObjectInfo*)JS_GetOpaque( @@ -4011,6 +4179,13 @@ napi_status qjs_create_runtime(napi_runtime* runtime) { (*runtime)->runtime = JS_NewRuntime(); #endif + JSSharedArrayBufferFunctions sharedArrayBufferFunctions = {0}; + sharedArrayBufferFunctions.sab_alloc = qjs_shared_array_buffer_alloc; + sharedArrayBufferFunctions.sab_free = qjs_shared_array_buffer_free; + sharedArrayBufferFunctions.sab_dup = qjs_shared_array_buffer_dup; + JS_SetSharedArrayBufferFunctions((*runtime)->runtime, + &sharedArrayBufferFunctions); + #ifndef NDEBUG JS_SetDumpFlags((*runtime)->runtime, JS_DUMP_LEAKS); #endif diff --git a/NativeScript/napi/quickjs/quicks-runtime.h b/NativeScript/napi/quickjs/quicks-runtime.h index 43c3c47e5..93933679b 100644 --- a/NativeScript/napi/quickjs/quicks-runtime.h +++ b/NativeScript/napi/quickjs/quicks-runtime.h @@ -37,6 +37,12 @@ NAPI_EXTERN JSContext* NAPI_CDECL qjs_get_context(napi_env env); NAPI_EXTERN JSRuntime* NAPI_CDECL qjs_get_runtime(napi_env env); +NAPI_EXTERN void NAPI_CDECL +qjs_shared_array_buffer_data_retain(uint8_t* data); + +NAPI_EXTERN void NAPI_CDECL +qjs_shared_array_buffer_data_release(uint8_t* data); + NAPI_EXTERN napi_status NAPI_CDECL qjs_create_scoped_value(napi_env env, JSValue value, napi_value* result); diff --git a/NativeScript/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index 89791d8d5..c8418222a 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -15,7 +15,7 @@ #include "v8-api.h" #endif // TARGET_ENGINE_V8 #ifdef TARGET_ENGINE_HERMES -#include "ffi/hermes/jsi/NativeApiJsi.h" +#include "ffi/hermes/NativeApiJsi.h" #endif // TARGET_ENGINE_HERMES #ifdef TARGET_ENGINE_JSC #include "ffi/jsc/NativeApiJSC.h" @@ -109,11 +109,20 @@ Runtime::Runtime() { currentRuntime_ = this; workerId_ = -1; runtimeLoop_ = nullptr; + microtaskObserver_ = nullptr; // workerCache_ = Caches::Workers; } Runtime::~Runtime() { currentRuntime_ = nullptr; + if (microtaskObserver_ != nullptr) { + if (runtimeLoop_ != nullptr) { + CFRunLoopRemoveObserver(runtimeLoop_, microtaskObserver_, + kCFRunLoopCommonModes); + } + CFRelease(microtaskObserver_); + microtaskObserver_ = nullptr; + } if (runtimeLoop_ != nullptr) { unregisterRuntimePromiseRunLoop(runtimeLoop_); runtimeLoop_ = nullptr; @@ -147,8 +156,18 @@ napi_value drainMicrotasks(napi_env env, napi_callback_info cbinfo) { return nullptr; } -std::mutex gRuntimePromiseRunLoopMutex; -std::unordered_map gRuntimePromiseRunLoops; +// Leaked, never-destroyed singletons: the Runtime destructor can run during +// process teardown after file-scope statics are destroyed, so a destroyed +// mutex would fail to lock (std::system_error). Heap-allocating and never +// freeing avoids the static-destruction-order fiasco. +std::mutex& gRuntimePromiseRunLoopMutex() { + static std::mutex* mutex = new std::mutex(); + return *mutex; +} +std::unordered_map& gRuntimePromiseRunLoops() { + static auto* runLoops = new std::unordered_map(); + return *runLoops; +} std::string runtimePromiseRunLoopToken(CFRunLoopRef runLoop) { char buffer[(sizeof(void*) * 2) + 3] = {}; @@ -162,9 +181,9 @@ std::string registerRuntimePromiseRunLoop(CFRunLoopRef runLoop) { } std::string token = runtimePromiseRunLoopToken(runLoop); - std::lock_guard lock(gRuntimePromiseRunLoopMutex); - if (gRuntimePromiseRunLoops.find(token) == gRuntimePromiseRunLoops.end()) { - gRuntimePromiseRunLoops.emplace(token, (CFRunLoopRef)CFRetain(runLoop)); + std::lock_guard lock(gRuntimePromiseRunLoopMutex()); + if (gRuntimePromiseRunLoops().find(token) == gRuntimePromiseRunLoops().end()) { + gRuntimePromiseRunLoops().emplace(token, (CFRunLoopRef)CFRetain(runLoop)); } return token; } @@ -175,19 +194,19 @@ void unregisterRuntimePromiseRunLoop(CFRunLoopRef runLoop) { } std::string token = runtimePromiseRunLoopToken(runLoop); - std::lock_guard lock(gRuntimePromiseRunLoopMutex); - auto it = gRuntimePromiseRunLoops.find(token); - if (it == gRuntimePromiseRunLoops.end()) { + std::lock_guard lock(gRuntimePromiseRunLoopMutex()); + auto it = gRuntimePromiseRunLoops().find(token); + if (it == gRuntimePromiseRunLoops().end()) { return; } CFRelease(it->second); - gRuntimePromiseRunLoops.erase(it); + gRuntimePromiseRunLoops().erase(it); } CFRunLoopRef copyRuntimePromiseRunLoop(const std::string& token) { - std::lock_guard lock(gRuntimePromiseRunLoopMutex); - auto it = gRuntimePromiseRunLoops.find(token); - if (it == gRuntimePromiseRunLoops.end()) { + std::lock_guard lock(gRuntimePromiseRunLoopMutex()); + auto it = gRuntimePromiseRunLoops().find(token); + if (it == gRuntimePromiseRunLoops().end()) { return nullptr; } return (CFRunLoopRef)CFRetain(it->second); @@ -320,6 +339,24 @@ void Runtime::Init(bool isWorker) { js_create_napi_env(&env_, runtime_); runtimeLoop_ = CFRunLoopGetCurrent(); + { + Runtime* runtime = this; + microtaskObserver_ = CFRunLoopObserverCreateWithHandler( + kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, + ^(CFRunLoopObserverRef, CFRunLoopActivity) { + napi_env env = runtime->env_; + if (env == nullptr || !Runtime::IsAlive(env)) { + return; + } + + NapiScope scope(env); + js_execute_pending_jobs(env); + }); + if (microtaskObserver_ != nullptr) { + CFRunLoopAddObserver(runtimeLoop_, microtaskObserver_, + kCFRunLoopCommonModes); + } + } { SpinLock lock(envsMutex_); @@ -671,9 +708,9 @@ void Runtime::Init(bool isWorker) { nativescript_init(env_, metadata_path, RuntimeConfig.MetadataPtr); #endif -#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_V8) +#if NS_FFI_BACKEND_V8 && defined(TARGET_ENGINE_V8) { - NativeApiV8Config nativeApiV8Config; + NativeApiConfig nativeApiV8Config; nativeApiV8Config.metadataPath = metadata_path; nativeApiV8Config.metadataPtr = RuntimeConfig.MetadataPtr; nativeApiV8Config.installGlobalSymbols = true; @@ -692,11 +729,21 @@ void Runtime::Init(bool isWorker) { }, false); }; - InstallNativeApiV8(env_->isolate, env_->context(), nativeApiV8Config); + nativeApiV8Config.jsThreadAsyncCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + true); + }; + InstallNativeApi(env_->isolate, env_->context(), nativeApiV8Config); } -#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_V8 +#endif // NS_FFI_BACKEND_V8 && TARGET_ENGINE_V8 -#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_HERMES) +#if NS_FFI_BACKEND_HERMES && defined(TARGET_ENGINE_HERMES) if (auto* jsiRuntime = js_get_jsi_runtime(env_)) { NativeApiJsiConfig nativeApiJsiConfig; nativeApiJsiConfig.metadataPath = metadata_path; @@ -707,11 +754,6 @@ void Runtime::Init(bool isWorker) { [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( @@ -722,13 +764,23 @@ void Runtime::Init(bool isWorker) { }, false); }; + nativeApiJsiConfig.jsThreadAsyncCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + true); + }; InstallNativeApiJSI(*jsiRuntime, nativeApiJsiConfig); } -#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_HERMES +#endif // NS_FFI_BACKEND_HERMES && TARGET_ENGINE_HERMES -#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_JSC) +#if NS_FFI_BACKEND_JSC && defined(TARGET_ENGINE_JSC) { - NativeApiJSCConfig nativeApiJSCConfig; + NativeApiConfig nativeApiJSCConfig; nativeApiJSCConfig.metadataPath = metadata_path; nativeApiJSCConfig.metadataPtr = RuntimeConfig.MetadataPtr; nativeApiJSCConfig.installGlobalSymbols = true; @@ -747,13 +799,23 @@ void Runtime::Init(bool isWorker) { }, false); }; - InstallNativeApiJSC(env_->context, nativeApiJSCConfig); + nativeApiJSCConfig.jsThreadAsyncCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + true); + }; + InstallNativeApi(env_->context, nativeApiJSCConfig); } -#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_JSC +#endif // NS_FFI_BACKEND_JSC && TARGET_ENGINE_JSC -#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_QUICKJS) +#if NS_FFI_BACKEND_QUICKJS && defined(TARGET_ENGINE_QUICKJS) { - NativeApiQuickJSConfig nativeApiQuickJSConfig; + NativeApiConfig nativeApiQuickJSConfig; nativeApiQuickJSConfig.metadataPath = metadata_path; nativeApiQuickJSConfig.metadataPtr = RuntimeConfig.MetadataPtr; nativeApiQuickJSConfig.installGlobalSymbols = true; @@ -772,9 +834,19 @@ void Runtime::Init(bool isWorker) { }, false); }; - InstallNativeApiQuickJS(qjs_get_context(env_), nativeApiQuickJSConfig); + nativeApiQuickJSConfig.jsThreadAsyncCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + true); + }; + InstallNativeApi(qjs_get_context(env_), nativeApiQuickJSConfig); } -#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_QUICKJS +#endif // NS_FFI_BACKEND_QUICKJS && TARGET_ENGINE_QUICKJS napi_close_handle_scope(env_, scope); } diff --git a/NativeScript/runtime/Runtime.h b/NativeScript/runtime/Runtime.h index bc785fe92..67b3b520b 100644 --- a/NativeScript/runtime/Runtime.h +++ b/NativeScript/runtime/Runtime.h @@ -56,6 +56,7 @@ class Runtime { private: int workerId_; CFRunLoopRef runtimeLoop_; + CFRunLoopObserverRef microtaskObserver_; double startTime_; double realtimeOrigin_; diff --git a/NativeScript/runtime/Util.h b/NativeScript/runtime/Util.h index 7d5ed072a..2ee6124ce 100644 --- a/NativeScript/runtime/Util.h +++ b/NativeScript/runtime/Util.h @@ -1,5 +1,6 @@ #include +#include #include "runtime/NativeScriptException.h" #include "jsr_common.h" #include "native_api_util.h" @@ -124,21 +125,20 @@ struct LockAndCV { inline void ExecuteOnRunLoop(CFRunLoopRef queue, std::function func, bool async) { if (!async) { bool __block finished = false; - auto v = new LockAndCV; - std::unique_lock lock(v->m); + auto state = std::make_shared(); + std::unique_lock lock(state->m); CFRunLoopPerformBlock(queue, kCFRunLoopCommonModes, ^(void) { func(); { - std::unique_lock lk(v->m); + std::lock_guard lk(state->m); finished = true; } - v->cv.notify_all(); + state->cv.notify_all(); }); CFRunLoopWakeUp(queue); while (!finished) { - v->cv.wait(lock); + state->cv.wait(lock); } - delete v; } else { CFRunLoopPerformBlock(queue, kCFRunLoopCommonModes, ^(void) { func(); diff --git a/NativeScript/runtime/modules/module/ModuleInternal.cpp b/NativeScript/runtime/modules/module/ModuleInternal.cpp index efec4bc90..cde75787b 100644 --- a/NativeScript/runtime/modules/module/ModuleInternal.cpp +++ b/NativeScript/runtime/modules/module/ModuleInternal.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "runtime/NativeScriptException.h" #include "native_api_util.h" @@ -64,6 +65,268 @@ std::string RewriteCommonJSDynamicImportsForFallbackEngines( return std::regex_replace(source, kDynamicImportPattern, "$1__dynamicImport("); } + +std::string TrimFallbackESMToken(const std::string& value) { + const auto begin = value.find_first_not_of(" \t\r\n"); + if (begin == std::string::npos) { + return ""; + } + const auto end = value.find_last_not_of(" \t\r\n"); + return value.substr(begin, end - begin + 1); +} + +std::vector SplitFallbackESMList(const std::string& value) { + std::vector parts; + std::stringstream stream(value); + std::string part; + while (std::getline(stream, part, ',')) { + part = TrimFallbackESMToken(part); + if (!part.empty()) { + parts.push_back(part); + } + } + return parts; +} + +std::string EscapeFallbackESMSpecifier(const std::string& specifier) { + std::string escaped; + escaped.reserve(specifier.size()); + for (char c : specifier) { + switch (c) { + case '\\': + escaped += "\\\\"; + break; + case '\'': + escaped += "\\'"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + default: + escaped += c; + break; + } + } + return escaped; +} + +std::string FallbackESMRequireExpression(const std::string& specifier) { + return "require('" + EscapeFallbackESMSpecifier(specifier) + "')"; +} + +std::string RewriteFallbackESMImportBindings(const std::string& bindings) { + std::string result; + for (const auto& part : SplitFallbackESMList(bindings)) { + static const std::regex kAliasPattern( + R"(^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$)"); + std::smatch alias; + if (std::regex_match(part, alias, kAliasPattern)) { + result += (result.empty() ? "" : ", "); + result += alias[1].str() + ": " + alias[2].str(); + } else { + result += (result.empty() ? "" : ", "); + result += part; + } + } + return result; +} + +std::string FallbackESMExportAssignments(const std::string& exports, + const std::string& sourceObject = "") { + std::string result; + for (const auto& part : SplitFallbackESMList(exports)) { + static const std::regex kAliasPattern( + R"(^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$)"); + std::smatch alias; + std::string local = part; + std::string exported = part; + if (std::regex_match(part, alias, kAliasPattern)) { + local = alias[1].str(); + exported = alias[2].str(); + } + + if (!result.empty()) { + result += "\n"; + } + result += "exports." + exported + " = "; + result += sourceObject.empty() ? local : sourceObject + "." + local; + result += ";"; + } + return result; +} + +template +std::string RegexReplaceWithFallbackESMCallback(const std::string& source, + const std::regex& pattern, + Callback callback) { + std::string result; + std::sregex_iterator it(source.begin(), source.end(), pattern); + std::sregex_iterator end; + size_t last = 0; + for (; it != end; ++it) { + const std::smatch& match = *it; + result.append(source, last, match.position() - last); + result += callback(match); + last = match.position() + match.length(); + } + result.append(source, last, std::string::npos); + return result; +} + +std::string TransformESModuleForFallbackEngines(const std::string& source) { + std::string result = source; + int tempIndex = 0; + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*import[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*,[ \t]*\*[ \t]+as[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [&](const std::smatch& match) { + std::string module = "__esm_import_" + std::to_string(tempIndex++); + return "const " + module + " = " + + FallbackESMRequireExpression(match[3].str()) + ";\nconst " + + match[1].str() + " = " + module + ".default;\nconst " + + match[2].str() + " = " + module + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*import[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*,[ \t]*\{([^}]*)\}[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [&](const std::smatch& match) { + std::string module = "__esm_import_" + std::to_string(tempIndex++); + return "const " + module + " = " + + FallbackESMRequireExpression(match[3].str()) + ";\nconst " + + match[1].str() + " = " + module + ".default;\nconst {" + + RewriteFallbackESMImportBindings(match[2].str()) + "} = " + + module + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*import[ \t]+\{([^}]*)\}[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "const {" + RewriteFallbackESMImportBindings(match[1].str()) + + "} = " + FallbackESMRequireExpression(match[2].str()) + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*import[ \t]+\*[ \t]+as[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "const " + match[1].str() + " = " + + FallbackESMRequireExpression(match[2].str()) + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*import[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "const " + match[1].str() + " = " + + FallbackESMRequireExpression(match[2].str()) + ".default;"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex(R"(^[ \t]*import[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return FallbackESMRequireExpression(match[1].str()) + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*export[ \t]+\*[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "Object.assign(exports, " + + FallbackESMRequireExpression(match[1].str()) + ");"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*export[ \t]+\{([^}]*)\}[ \t]+from[ \t]+['"]([^'"]+)['"][ \t]*;?)", + std::regex::ECMAScript | std::regex::multiline), + [&](const std::smatch& match) { + std::string module = "__esm_export_" + std::to_string(tempIndex++); + return "const " + module + " = " + + FallbackESMRequireExpression(match[2].str()) + ";\n" + + FallbackESMExportAssignments(match[1].str(), module); + }); + + result = std::regex_replace( + result, + std::regex(R"(^[ \t]*export[ \t]+default[ \t]+function[ \t]*)", + std::regex::ECMAScript | std::regex::multiline), + "exports.default = function "); + + result = std::regex_replace( + result, + std::regex(R"(^[ \t]*export[ \t]+default[ \t]+class[ \t]*)", + std::regex::ECMAScript | std::regex::multiline), + "exports.default = class "); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*export[ \t]+function[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*\()", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "exports." + match[1].str() + " = function " + match[1].str() + + "("; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*export[ \t]+class[ \t]+([A-Za-z_$][A-Za-z0-9_$]*))", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "exports." + match[1].str() + " = class " + match[1].str(); + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex( + R"(^[ \t]*export[ \t]+(const|let|var)[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*=[ \t]*([^;\r\n]*);?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return match[1].str() + " " + match[2].str() + " = " + + match[3].str() + ";\nexports." + match[2].str() + " = " + + match[2].str() + ";"; + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex(R"(^[ \t]*export[ \t]*\{([^}]*)\}[ \t]*;)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return FallbackESMExportAssignments(match[1].str()); + }); + + result = RegexReplaceWithFallbackESMCallback( + result, + std::regex(R"(^[ \t]*export[ \t]+default[ \t]+([^;\r\n]*);?)", + std::regex::ECMAScript | std::regex::multiline), + [](const std::smatch& match) { + return "exports.default = " + match[1].str() + ";"; + }); + + return RewriteCommonJSDynamicImportsForFallbackEngines(result); +} #endif // Check if path has .cjs extension (explicitly CommonJS) @@ -1296,19 +1559,15 @@ napi_value ModuleInternal::LoadModule(napi_env env, napi_value moduleFunc; - if (modulePath.ends_with(".mjs")) { + if (IsESModule(modulePath)) { // Handle ES modules napi_value esModuleResult = LoadESModule(env, modulePath); - // Mark the result as an ES module - napi_value isESModuleFlag; - napi_get_boolean(env, true, &isESModuleFlag); - napi_set_named_property(env, esModuleResult, "__esModule", isESModuleFlag); - - // For ES modules, we return the namespace directly, not wrapped in a module - // object + // Store the namespace as module.exports so the public require() result is + // the namespace while the cache shape stays compatible with CommonJS. + napi_set_named_property(env, moduleObj, "exports", esModuleResult); tempModule.SaveToCache(); - return esModuleResult; + return moduleObj; } else if (modulePath.ends_with(".js") || modulePath.ends_with(".cjs")) { napi_value script = LoadScript(env, modulePath, fullRequiredModulePath); // DEBUG_WRITE("%s", modulePath.c_str()); @@ -1465,7 +1724,7 @@ bool ModuleInternal::IsESModule(const std::string& path) { } napi_value ModuleInternal::LoadESModule(napi_env env, const std::string& path) { -#ifdef TARGET_ENGINE_V8 +#if defined(TARGET_ENGINE_V8) || defined(TARGET_ENGINE_QUICKJS) try { // Use canonicalized module paths so the top-level ESM module identity // matches dependency resolution cache keys. @@ -1510,6 +1769,111 @@ napi_value ModuleInternal::LoadESModule(napi_env env, const std::string& path) { throw NativeScriptException("Failed to load ES module " + path + ": " + e.what()); } +#elif defined(TARGET_ENGINE_HERMES) || defined(TARGET_ENGINE_JSC) + try { + std::error_code pathError; + auto absPathFs = std::filesystem::absolute(path, pathError); + if (pathError) { + pathError.clear(); + absPathFs = std::filesystem::path(path); + } + absPathFs = absPathFs.lexically_normal(); + auto canonicalPath = + std::filesystem::weakly_canonical(absPathFs, pathError); + if (!pathError) { + absPathFs = canonicalPath; + } + std::string absPath = absPathFs.string(); + + std::ifstream file(absPath); + if (!file.is_open()) { + throw NativeScriptException("Unable to open file: " + absPath); + } + std::stringstream buffer; + buffer << file.rdbuf(); + std::string transformed = + TransformESModuleForFallbackEngines(StripShebang(buffer.str())); + file.close(); + + std::string wrapped; + wrapped.reserve(transformed.length() + 1024); + wrapped += MODULE_PROLOGUE; + wrapped += transformed; + wrapped += MODULE_EPILOGUE; + + napi_value scriptContent = nullptr; + napi_create_string_utf8(env, wrapped.c_str(), wrapped.size(), + &scriptContent); + + napi_value moduleFunc = nullptr; + napi_status status = + napi_run_script_source(env, scriptContent, absPath.c_str(), &moduleFunc); + if (status != napi_ok) { + bool pendingException = false; + napi_is_exception_pending(env, &pendingException); + if (pendingException) { + napi_value error = nullptr; + napi_get_and_clear_last_exception(env, &error); + throw NativeScriptException(env, error, + "Failed to transform ES module " + absPath); + } + throw NativeScriptException("Failed to transform ES module " + absPath); + } + + napi_value moduleObj = nullptr; + napi_create_object(env, &moduleObj); + + napi_value exportsObj = nullptr; + napi_create_object(env, &exportsObj); + napi_set_named_property(env, moduleObj, "exports", exportsObj); + + napi_value esModuleMarker = nullptr; + napi_get_boolean(env, true, &esModuleMarker); + napi_util::define_property(env, exportsObj, "__esModule", esModuleMarker); + + napi_value fileName = nullptr; + napi_create_string_utf8(env, absPath.c_str(), absPath.size(), &fileName); + napi_set_named_property(env, moduleObj, "filename", fileName); + + std::string dirNameString = + std::filesystem::path(absPath).parent_path().string(); + napi_value dirName = nullptr; + napi_create_string_utf8(env, dirNameString.c_str(), dirNameString.size(), + &dirName); + + napi_value require = GetRequireFunction(env, dirNameString); + napi_set_named_property(env, moduleObj, "require", require); + napi_util::define_property(env, moduleObj, "id", fileName); + + napi_value thisArg = nullptr; + napi_create_object(env, &thisArg); + + napi_value global = nullptr; + napi_get_global(env, &global); + napi_value globalExtends = nullptr; + napi_get_named_property(env, global, "__extends", &globalExtends); + napi_set_named_property(env, thisArg, "__extends", globalExtends); + + napi_value args[5] = {moduleObj, exportsObj, require, fileName, dirName}; + napi_value callResult = nullptr; + status = napi_call_function(env, thisArg, moduleFunc, 5, args, &callResult); + bool pendingException = false; + napi_is_exception_pending(env, &pendingException); + if (status != napi_ok || pendingException) { + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + if (exception != nullptr) { + throw NativeScriptException(env, exception, + "Error evaluating ES module " + absPath); + } + throw NativeScriptException("Error evaluating ES module " + absPath); + } + + return exportsObj; + } catch (const std::exception& e) { + throw NativeScriptException("Failed to load ES module " + path + ": " + + e.what()); + } #else throw NativeScriptException("ES Modules are not supported in this runtime."); #endif @@ -1711,7 +2075,7 @@ ModuleInternal::ModulePathKind ModuleInternal::GetModulePathKind( return kind; } -#if defined(TARGET_ENGINE_HERMES) +#if defined(TARGET_ENGINE_HERMES) || defined(TARGET_ENGINE_JSC) const char* ModuleInternal::MODULE_PROLOGUE = "(function(module, exports, require, __filename, __dirname){ " "const __dynamicImport = (specifier) => Promise.resolve().then(() => { " diff --git a/NativeScript/runtime/modules/worker/MessageJSON.cpp b/NativeScript/runtime/modules/worker/MessageJSON.cpp index 39c1e5f00..9d4b29a24 100644 --- a/NativeScript/runtime/modules/worker/MessageJSON.cpp +++ b/NativeScript/runtime/modules/worker/MessageJSON.cpp @@ -6,267 +6,1185 @@ #ifndef TARGET_ENGINE_V8 -#include "js_native_api.h" #include "MessageJSON.h" +#include +#include +#include +#include +#include +#include + +#include "js_native_api.h" +#include "runtime/NativeScriptException.h" + +#ifdef TARGET_ENGINE_QUICKJS +#include "quickjs.h" +#include "quicks-runtime.h" +#endif + namespace nativescript { +namespace { -#include -#include -#include -#include - -typedef enum { - TYPE_UNDEFINED = 0x00, - TYPE_NULL = 0x01, - TYPE_BOOLEAN = 0x02, - TYPE_NUMBER = 0x03, - TYPE_STRING = 0x04, - TYPE_ARRAY = 0x05, - TYPE_OBJECT = 0x06, - TYPE_DATE = 0x07, - TYPE_ARRAYBUFFER = 0x08 -} TypeTag; - -#define CHECK(expr) \ - do { \ - napi_status status = (expr); \ - assert(status == napi_ok); \ - } while (0) - -typedef struct { - uint8_t* data; - size_t size; - size_t capacity; -} Buffer; - -#define INITIAL_CAPACITY 1024 - -static void buffer_init(Buffer* buf) { - buf->data = (uint8_t*)malloc(INITIAL_CAPACITY); - buf->size = 0; - buf->capacity = INITIAL_CAPACITY; -} +enum class ValueTag : uint8_t { + Undefined = 0, + Null = 1, + Boolean = 2, + Number = 3, + String = 4, + BigInt = 5, + Reference = 6, + Array = 7, + Object = 8, + Date = 9, + ArrayBuffer = 10, + TypedArray = 11, + DataView = 12, + Map = 13, + Set = 14, + RegExp = 15, + Error = 16, + SharedArrayBuffer = 17, +}; -static void buffer_free(Buffer* buf) { - if (buf->data != NULL) { - free(buf->data); - buf->data = NULL; +void Check(napi_env env, napi_status status, const std::string& message) { + if (status == napi_ok) { + return; } - buf->size = 0; - buf->capacity = 0; -} -static void buffer_ensure_capacity(Buffer* buf, size_t additional) { - while (buf->size + additional > buf->capacity) { - buf->capacity *= 2; - buf->data = (uint8_t*)realloc(buf->data, buf->capacity); + bool pendingException = false; + napi_is_exception_pending(env, &pendingException); + if (pendingException) { + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + if (exception != nullptr) { + throw NativeScriptException(env, exception, message); + } } + + throw NativeScriptException(env, message, "DataCloneError"); +} + +void ThrowDataCloneError(napi_env env, const std::string& message) { + throw NativeScriptException(env, message, "DataCloneError"); +} + +napi_value Global(napi_env env) { + napi_value global = nullptr; + Check(env, napi_get_global(env, &global), "Unable to read global object"); + return global; } -static void buffer_write(Buffer* buf, const void* src, size_t len) { - buffer_ensure_capacity(buf, len); - memcpy(buf->data + buf->size, src, len); - buf->size += len; +napi_value Undefined(napi_env env) { + napi_value value = nullptr; + Check(env, napi_get_undefined(env, &value), "Unable to create undefined"); + return value; } -static void write_uint32(Buffer* buf, uint32_t val) { - buffer_write(buf, &val, 4); +napi_value Boolean(napi_env env, bool value) { + napi_value result = nullptr; + Check(env, napi_get_boolean(env, value, &result), "Unable to create boolean"); + return result; } -static void write_double(Buffer* buf, double val) { - buffer_write(buf, &val, 8); +napi_value String(napi_env env, const std::string& value) { + napi_value result = nullptr; + Check(env, + napi_create_string_utf8(env, value.data(), value.size(), &result), + "Unable to create string"); + return result; } -static void write_string(napi_env env, Buffer* buf, napi_value str) { - size_t len; - CHECK(napi_get_value_string_utf8(env, str, NULL, 0, &len)); - write_uint32(buf, (uint32_t)len); - buffer_ensure_capacity(buf, len); - CHECK(napi_get_value_string_utf8(env, str, (char*)(buf->data + buf->size), - len + 1, NULL)); - buf->size += len; +std::string ToString(napi_env env, napi_value value) { + size_t length = 0; + Check(env, napi_get_value_string_utf8(env, value, nullptr, 0, &length), + "Unable to read string length"); + + std::vector buffer(length + 1); + size_t written = 0; + Check(env, + napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), + &written), + "Unable to read string"); + return std::string(buffer.data(), written); } -static uint32_t read_uint32(const uint8_t** buf) { - uint32_t val = 0; - for (int i = 0; i < 4; ++i) val |= (uint32_t)(*(*buf)++) << (i * 8); - return val; +napi_value GetNamed(napi_env env, napi_value object, const char* name) { + napi_value result = nullptr; + Check(env, napi_get_named_property(env, object, name, &result), + std::string("Unable to read property ") + name); + return result; } -static void write_double(uint8_t** buf, double val) { - memcpy(*buf, &val, 8); - *buf += 8; +bool IsFunction(napi_env env, napi_value value) { + napi_valuetype type = napi_undefined; + Check(env, napi_typeof(env, value, &type), "Unable to inspect value type"); + return type == napi_function; } -static double read_double(const uint8_t** buf) { - double val; - memcpy(&val, *buf, 8); - *buf += 8; - return val; +napi_value Call(napi_env env, napi_value receiver, napi_value function, + const std::vector& args, + const std::string& message) { + napi_value result = nullptr; + Check(env, + napi_call_function(env, receiver, function, args.size(), + args.empty() ? nullptr : args.data(), &result), + message); + return result; } -static napi_value read_string(napi_env env, const uint8_t** buf) { - uint32_t len = read_uint32(buf); - napi_value result; - CHECK(napi_create_string_utf8(env, (const char*)(*buf), len, &result)); - *buf += len; +napi_value Construct(napi_env env, const char* constructorName, + const std::vector& args) { + napi_value ctor = GetNamed(env, Global(env), constructorName); + if (!IsFunction(env, ctor)) { + ThrowDataCloneError(env, + std::string("Missing constructor ") + constructorName); + } + + napi_value result = nullptr; + Check(env, + napi_new_instance(env, ctor, args.size(), + args.empty() ? nullptr : args.data(), &result), + std::string("Unable to construct ") + constructorName); return result; } -static napi_value deserialize(napi_env env, const uint8_t** buf) { - uint8_t tag = *(*buf)++; - napi_value result; - - switch ((TypeTag)tag) { - case TYPE_UNDEFINED: - CHECK(napi_get_undefined(env, &result)); - break; - case TYPE_NULL: - CHECK(napi_get_null(env, &result)); - break; - case TYPE_BOOLEAN: { - bool b = *(*buf)++ != 0; - CHECK(napi_get_boolean(env, b, &result)); - break; - } - case TYPE_NUMBER: { - double num = read_double(buf); - CHECK(napi_create_double(env, num, &result)); - break; - } - case TYPE_STRING: - result = read_string(env, buf); - break; - case TYPE_ARRAY: { - uint32_t len = read_uint32(buf); - CHECK(napi_create_array_with_length(env, len, &result)); - for (uint32_t i = 0; i < len; ++i) { - napi_value elem = deserialize(env, buf); - CHECK(napi_set_element(env, result, i, elem)); +napi_value GetStructuredCloneHelper(napi_env env) { + static std::unordered_map helperRefs; + + auto it = helperRefs.find(env); + if (it != helperRefs.end()) { + napi_value helper = nullptr; + Check(env, napi_get_reference_value(env, it->second, &helper), + "Unable to read structured clone helper"); + return helper; + } + + static const char* kHelperSource = R"( + (function () { + const hasCtor = (name) => typeof globalThis[name] === 'function'; + return { + isMap(value) { + return hasCtor('Map') && value instanceof Map; + }, + isSet(value) { + return hasCtor('Set') && value instanceof Set; + }, + isRegExp(value) { + return hasCtor('RegExp') && value instanceof RegExp; + }, + isSharedArrayBuffer(value) { + return hasCtor('SharedArrayBuffer') && value instanceof SharedArrayBuffer; + }, + mapEntries(value) { + return Array.from(value.entries()); + }, + setValues(value) { + return Array.from(value.values()); + }, + regexpInfo(value) { + return { source: value.source, flags: value.flags }; + }, + errorInfo(value) { + return { name: value.name, message: value.message, stack: value.stack }; + }, + bigintToString(value) { + return value.toString(); + }, + makeBigInt(value) { + return BigInt(value); + }, + ownEnumerableKeys(value) { + return Object.keys(value); + }, + makeMap() { + return new Map(); + }, + mapSet(map, key, value) { + map.set(key, value); + return map; + }, + makeSet() { + return new Set(); + }, + setAdd(set, value) { + set.add(value); + return set; + }, + makeRegExp(source, flags) { + return new RegExp(source, flags); + }, + makeError(name, message, stack) { + const Ctor = typeof globalThis[name] === 'function' ? globalThis[name] : Error; + const error = new Ctor(message); + try { error.name = name; } catch (_) {} + if (stack !== undefined) { + try { error.stack = stack; } catch (_) {} + } + return error; + }, + }; + })(); + )"; + + napi_value source = nullptr; + Check(env, + napi_create_string_utf8(env, kHelperSource, NAPI_AUTO_LENGTH, &source), + "Unable to create structured clone helper source"); + + napi_value helper = nullptr; + Check(env, napi_run_script(env, source, &helper), + "Unable to install structured clone helper"); + + napi_ref ref = nullptr; + Check(env, napi_create_reference(env, helper, 1, &ref), + "Unable to retain structured clone helper"); + helperRefs.emplace(env, ref); + return helper; +} + +napi_value CallHelper(napi_env env, const char* name, + const std::vector& args) { + napi_value helper = GetStructuredCloneHelper(env); + napi_value function = GetNamed(env, helper, name); + return Call(env, helper, function, args, + std::string("Unable to call structured clone helper ") + name); +} + +bool CallHelperBool(napi_env env, const char* name, napi_value value) { + napi_value result = CallHelper(env, name, {value}); + bool boolResult = false; + Check(env, napi_get_value_bool(env, result, &boolResult), + std::string("Structured clone helper did not return boolean: ") + + name); + return boolResult; +} + +class BufferWriter { + public: + void WriteU8(uint8_t value) { bytes_.push_back(value); } + + void WriteU32(uint32_t value) { + for (int i = 0; i < 4; i++) { + WriteU8(static_cast((value >> (i * 8)) & 0xff)); + } + } + + void WriteU64(uint64_t value) { + for (int i = 0; i < 8; i++) { + WriteU8(static_cast((value >> (i * 8)) & 0xff)); + } + } + + void WriteDouble(double value) { + uint64_t bits = 0; + static_assert(sizeof(bits) == sizeof(value), "double size mismatch"); + std::memcpy(&bits, &value, sizeof(value)); + WriteU64(bits); + } + + void WriteBytes(const void* data, size_t size) { + const auto* begin = static_cast(data); + bytes_.insert(bytes_.end(), begin, begin + size); + } + + void WriteString(const std::string& value) { + WriteU32(static_cast(value.size())); + WriteBytes(value.data(), value.size()); + } + + MallocedBuffer Release() { + MallocedBuffer result; + result.size = bytes_.size(); + result.data = static_cast(std::malloc(bytes_.size())); + if (!bytes_.empty()) { + std::memcpy(result.data, bytes_.data(), bytes_.size()); + } + return result; + } + + private: + std::vector bytes_; +}; + +class BufferReader { + public: + BufferReader(const char* data, size_t size) + : current_(reinterpret_cast(data)), + end_(reinterpret_cast(data) + size) {} + + uint8_t ReadU8() { + Ensure(1); + return *current_++; + } + + uint32_t ReadU32() { + uint32_t value = 0; + for (int i = 0; i < 4; i++) { + value |= static_cast(ReadU8()) << (i * 8); + } + return value; + } + + uint64_t ReadU64() { + uint64_t value = 0; + for (int i = 0; i < 8; i++) { + value |= static_cast(ReadU8()) << (i * 8); + } + return value; + } + + double ReadDouble() { + uint64_t bits = ReadU64(); + double value = 0; + static_assert(sizeof(bits) == sizeof(value), "double size mismatch"); + std::memcpy(&value, &bits, sizeof(value)); + return value; + } + + std::vector ReadBytes(size_t size) { + Ensure(size); + std::vector result(current_, current_ + size); + current_ += size; + return result; + } + + std::string ReadString() { + uint32_t size = ReadU32(); + Ensure(size); + std::string result(reinterpret_cast(current_), size); + current_ += size; + return result; + } + + private: + void Ensure(size_t size) { + if (static_cast(end_ - current_) < size) { + throw NativeScriptException("Malformed worker message."); + } + } + + const uint8_t* current_; + const uint8_t* end_; +}; + +#ifdef TARGET_ENGINE_QUICKJS +JSValue QuickJSValueFromNapi(napi_value value); +#endif + +class Serializer { + public: + Serializer(napi_env env, BufferWriter& writer, + std::vector* quickjsSharedArrayBuffers = nullptr) + : env_(env), + writer_(writer), + quickjs_shared_array_buffers_(quickjsSharedArrayBuffers) {} + + void Write(napi_value value) { + napi_valuetype type = napi_undefined; + Check(env_, napi_typeof(env_, value, &type), "Unable to inspect value"); + + switch (type) { + case napi_undefined: + WriteTag(ValueTag::Undefined); + return; + case napi_null: + WriteTag(ValueTag::Null); + return; + case napi_boolean: + WriteBoolean(value); + return; + case napi_number: + WriteNumber(value); + return; + case napi_string: + WriteStringValue(value); + return; + case napi_bigint: + WriteBigInt(value); + return; + case napi_symbol: + case napi_function: + case napi_external: + ThrowDataCloneError(env_, "Value cannot be cloned."); + return; + case napi_object: + WriteObjectLike(value); + return; + } + } + + private: + void WriteTag(ValueTag tag) { writer_.WriteU8(static_cast(tag)); } + + void WriteBoolean(napi_value value) { + bool boolValue = false; + Check(env_, napi_get_value_bool(env_, value, &boolValue), + "Unable to read boolean"); + WriteTag(ValueTag::Boolean); + writer_.WriteU8(boolValue ? 1 : 0); + } + + void WriteNumber(napi_value value) { + double number = 0; + Check(env_, napi_get_value_double(env_, value, &number), + "Unable to read number"); + WriteTag(ValueTag::Number); + writer_.WriteDouble(number); + } + + void WriteStringValue(napi_value value) { + WriteTag(ValueTag::String); + writer_.WriteString(ToString(env_, value)); + } + + void WriteBigInt(napi_value value) { + napi_value text = CallHelper(env_, "bigintToString", {value}); + WriteTag(ValueTag::BigInt); + writer_.WriteString(ToString(env_, text)); + } + + bool WriteReferenceIfSeen(napi_value value, uint32_t* idOut) { + for (uint32_t i = 0; i < seen_.size(); i++) { + bool equal = false; + Check(env_, napi_strict_equals(env_, seen_[i], value, &equal), + "Unable to compare object identity"); + if (equal) { + WriteTag(ValueTag::Reference); + writer_.WriteU32(i); + return true; + } + } + + *idOut = static_cast(seen_.size()); + seen_.push_back(value); + return false; + } + + void WriteObjectHeader(ValueTag tag, uint32_t id) { + WriteTag(tag); + writer_.WriteU32(id); + } + + void WriteObjectLike(napi_value value) { + bool isDate = false; + Check(env_, napi_is_date(env_, value, &isDate), "Unable to inspect Date"); + if (isDate) { + WriteDate(value); + return; + } + + bool isArrayBuffer = false; + Check(env_, napi_is_arraybuffer(env_, value, &isArrayBuffer), + "Unable to inspect ArrayBuffer"); + if (isArrayBuffer) { +#ifdef TARGET_ENGINE_QUICKJS + if (CallHelperBool(env_, "isSharedArrayBuffer", value)) { + WriteSharedArrayBuffer(value); + return; } - break; - } - case TYPE_OBJECT: { - CHECK(napi_create_object(env, &result)); - uint32_t len = read_uint32(buf); - for (uint32_t i = 0; i < len; ++i) { - napi_value key = read_string(env, buf); - napi_value val = deserialize(env, buf); - CHECK(napi_set_property(env, result, key, val)); +#else + if (CallHelperBool(env_, "isSharedArrayBuffer", value)) { + ThrowDataCloneError( + env_, "SharedArrayBuffer cloning is not supported by this engine."); } - break; +#endif + WriteArrayBuffer(value); + return; } - case TYPE_DATE: { - double t = read_double(buf); - CHECK(napi_create_date(env, t, &result)); - break; + + bool isDataView = false; + Check(env_, napi_is_dataview(env_, value, &isDataView), + "Unable to inspect DataView"); + if (isDataView) { + WriteDataView(value); + return; + } + + bool isTypedArray = false; + Check(env_, napi_is_typedarray(env_, value, &isTypedArray), + "Unable to inspect TypedArray"); + if (isTypedArray) { + WriteTypedArray(value); + return; } - default: - assert(0 && "Unknown tag"); + + bool isArray = false; + Check(env_, napi_is_array(env_, value, &isArray), "Unable to inspect Array"); + if (isArray) { + WriteArray(value); + return; + } + + if (CallHelperBool(env_, "isMap", value)) { + WriteMap(value); + return; + } + + if (CallHelperBool(env_, "isSet", value)) { + WriteSet(value); + return; + } + + if (CallHelperBool(env_, "isRegExp", value)) { + WriteRegExp(value); + return; + } + + bool isError = false; + Check(env_, napi_is_error(env_, value, &isError), "Unable to inspect Error"); + if (isError) { + WriteError(value); + return; + } + + WritePlainObject(value); } - return result; -} -static void serialize(napi_env env, napi_value val, Buffer* buf) { - napi_valuetype type; - CHECK(napi_typeof(env, val, &type)); - - uint8_t tag; - - if (type == napi_undefined) { - tag = TYPE_UNDEFINED; - buffer_write(buf, &tag, 1); - } else if (type == napi_null) { - tag = TYPE_NULL; - buffer_write(buf, &tag, 1); - } else if (type == napi_boolean) { - tag = TYPE_BOOLEAN; - buffer_write(buf, &tag, 1); - bool b; - CHECK(napi_get_value_bool(env, val, &b)); - uint8_t bv = b ? 1 : 0; - buffer_write(buf, &bv, 1); - } else if (type == napi_number) { - tag = TYPE_NUMBER; - buffer_write(buf, &tag, 1); - double num; - CHECK(napi_get_value_double(env, val, &num)); - write_double(buf, num); - } else if (type == napi_string) { - tag = TYPE_STRING; - buffer_write(buf, &tag, 1); - write_string(env, buf, val); - } else { - bool is_array; - CHECK(napi_is_array(env, val, &is_array)); - if (is_array) { - tag = TYPE_ARRAY; - buffer_write(buf, &tag, 1); - uint32_t len; - CHECK(napi_get_array_length(env, val, &len)); - write_uint32(buf, len); - for (uint32_t i = 0; i < len; ++i) { - napi_value elem; - CHECK(napi_get_element(env, val, i, &elem)); - serialize(env, elem, buf); - } - } else { - napi_value ctor; - CHECK(napi_get_named_property(env, val, "constructor", &ctor)); - napi_value ctor_name; - CHECK(napi_get_named_property(env, ctor, "name", &ctor_name)); - - size_t len; - char cname[32]; - CHECK(napi_get_value_string_utf8(env, ctor_name, cname, sizeof(cname), - &len)); - - if (strcmp(cname, "Date") == 0) { - tag = TYPE_DATE; - buffer_write(buf, &tag, 1); - napi_value time; - CHECK(napi_coerce_to_number(env, val, &time)); - double t; - CHECK(napi_get_value_double(env, time, &t)); - write_double(buf, t); - } else { - tag = TYPE_OBJECT; - buffer_write(buf, &tag, 1); - napi_value keys; - CHECK(napi_get_property_names(env, val, &keys)); - uint32_t len; - CHECK(napi_get_array_length(env, keys, &len)); - write_uint32(buf, len); - for (uint32_t i = 0; i < len; ++i) { - napi_value key, value; - CHECK(napi_get_element(env, keys, i, &key)); - write_string(env, buf, key); - CHECK(napi_get_property(env, val, key, &value)); - serialize(env, value, buf); - } + void WriteDate(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + double time = 0; + Check(env_, napi_get_date_value(env_, value, &time), "Unable to read Date"); + WriteObjectHeader(ValueTag::Date, id); + writer_.WriteDouble(time); + } + + void WriteArrayBuffer(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + void* data = nullptr; + size_t byteLength = 0; + Check(env_, napi_get_arraybuffer_info(env_, value, &data, &byteLength), + "Unable to read ArrayBuffer"); + WriteObjectHeader(ValueTag::ArrayBuffer, id); + writer_.WriteU32(static_cast(byteLength)); + writer_.WriteBytes(data, byteLength); + } + +#ifdef TARGET_ENGINE_QUICKJS + void WriteSharedArrayBuffer(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + if (quickjs_shared_array_buffers_ == nullptr) { + ThrowDataCloneError(env_, "SharedArrayBuffer cannot be retained."); + } + + JSContext* context = qjs_get_context(env_); + if (context == nullptr) { + throw NativeScriptException("QuickJS context is not available."); + } + + size_t byteLength = 0; + JSSABTab sabTab = {nullptr, 0}; + uint8_t* bytes = JS_WriteObject2( + context, &byteLength, QuickJSValueFromNapi(value), + JS_WRITE_OBJ_REFERENCE | JS_WRITE_OBJ_SAB, &sabTab); + if (bytes == nullptr) { + JSValue exception = JS_GetException(context); + napi_value error = nullptr; + qjs_create_scoped_value(env_, exception, &error); + throw NativeScriptException(env_, error, + "Unable to serialize SharedArrayBuffer"); + } + + WriteObjectHeader(ValueTag::SharedArrayBuffer, id); + writer_.WriteU32(static_cast(byteLength)); + writer_.WriteBytes(bytes, byteLength); + js_free(context, bytes); + + if (sabTab.tab != nullptr) { + for (size_t i = 0; i < sabTab.len; i++) { + qjs_shared_array_buffer_data_retain(sabTab.tab[i]); + quickjs_shared_array_buffers_->push_back(sabTab.tab[i]); } + js_free(context, sabTab.tab); + } + } +#endif + + void WriteTypedArray(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_typedarray_type type = napi_int8_array; + size_t length = 0; + napi_value arrayBuffer = nullptr; + size_t byteOffset = 0; + Check(env_, + napi_get_typedarray_info(env_, value, &type, &length, nullptr, + &arrayBuffer, &byteOffset), + "Unable to read TypedArray"); + WriteObjectHeader(ValueTag::TypedArray, id); + writer_.WriteU32(static_cast(type)); + writer_.WriteU32(static_cast(length)); + writer_.WriteU32(static_cast(byteOffset)); + Write(arrayBuffer); + } + + void WriteDataView(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + size_t byteLength = 0; + napi_value arrayBuffer = nullptr; + size_t byteOffset = 0; + Check(env_, + napi_get_dataview_info(env_, value, &byteLength, nullptr, + &arrayBuffer, &byteOffset), + "Unable to read DataView"); + WriteObjectHeader(ValueTag::DataView, id); + writer_.WriteU32(static_cast(byteLength)); + writer_.WriteU32(static_cast(byteOffset)); + Write(arrayBuffer); + } + + void WriteArray(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + uint32_t length = 0; + Check(env_, napi_get_array_length(env_, value, &length), + "Unable to read Array length"); + WriteObjectHeader(ValueTag::Array, id); + writer_.WriteU32(length); + for (uint32_t i = 0; i < length; i++) { + napi_value element = nullptr; + Check(env_, napi_get_element(env_, value, i, &element), + "Unable to read Array element"); + Write(element); + } + } + + void WriteMap(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_value entries = CallHelper(env_, "mapEntries", {value}); + uint32_t length = 0; + Check(env_, napi_get_array_length(env_, entries, &length), + "Unable to read Map entries"); + WriteObjectHeader(ValueTag::Map, id); + writer_.WriteU32(length); + for (uint32_t i = 0; i < length; i++) { + napi_value pair = nullptr; + napi_value key = nullptr; + napi_value itemValue = nullptr; + Check(env_, napi_get_element(env_, entries, i, &pair), + "Unable to read Map pair"); + Check(env_, napi_get_element(env_, pair, 0, &key), + "Unable to read Map key"); + Check(env_, napi_get_element(env_, pair, 1, &itemValue), + "Unable to read Map value"); + Write(key); + Write(itemValue); + } + } + + void WriteSet(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_value values = CallHelper(env_, "setValues", {value}); + uint32_t length = 0; + Check(env_, napi_get_array_length(env_, values, &length), + "Unable to read Set values"); + WriteObjectHeader(ValueTag::Set, id); + writer_.WriteU32(length); + for (uint32_t i = 0; i < length; i++) { + napi_value itemValue = nullptr; + Check(env_, napi_get_element(env_, values, i, &itemValue), + "Unable to read Set value"); + Write(itemValue); + } + } + + void WriteRegExp(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_value info = CallHelper(env_, "regexpInfo", {value}); + WriteObjectHeader(ValueTag::RegExp, id); + writer_.WriteString(ToString(env_, GetNamed(env_, info, "source"))); + writer_.WriteString(ToString(env_, GetNamed(env_, info, "flags"))); + } + + void WriteError(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_value info = CallHelper(env_, "errorInfo", {value}); + WriteObjectHeader(ValueTag::Error, id); + writer_.WriteString(ToString(env_, GetNamed(env_, info, "name"))); + writer_.WriteString(ToString(env_, GetNamed(env_, info, "message"))); + + napi_value stack = GetNamed(env_, info, "stack"); + napi_valuetype stackType = napi_undefined; + Check(env_, napi_typeof(env_, stack, &stackType), "Unable to inspect stack"); + writer_.WriteU8(stackType == napi_string ? 1 : 0); + if (stackType == napi_string) { + writer_.WriteString(ToString(env_, stack)); + } + } + + void WritePlainObject(napi_value value) { + uint32_t id = 0; + if (WriteReferenceIfSeen(value, &id)) { + return; + } + + napi_value keys = CallHelper(env_, "ownEnumerableKeys", {value}); + uint32_t length = 0; + Check(env_, napi_get_array_length(env_, keys, &length), + "Unable to read object keys"); + WriteObjectHeader(ValueTag::Object, id); + writer_.WriteU32(length); + for (uint32_t i = 0; i < length; i++) { + napi_value key = nullptr; + napi_value itemValue = nullptr; + Check(env_, napi_get_element(env_, keys, i, &key), + "Unable to read object key"); + Check(env_, napi_get_property(env_, value, key, &itemValue), + "Unable to read object property"); + writer_.WriteString(ToString(env_, key)); + Write(itemValue); + } + } + + napi_env env_; + BufferWriter& writer_; + std::vector seen_; + std::vector* quickjs_shared_array_buffers_; +}; + +class Deserializer { + public: + Deserializer(napi_env env, BufferReader& reader) + : env_(env), reader_(reader) {} + + napi_value Read() { + ValueTag tag = static_cast(reader_.ReadU8()); + switch (tag) { + case ValueTag::Undefined: + return Undefined(env_); + case ValueTag::Null: + return Null(); + case ValueTag::Boolean: + return Boolean(env_, reader_.ReadU8() != 0); + case ValueTag::Number: + return Number(); + case ValueTag::String: + return String(env_, reader_.ReadString()); + case ValueTag::BigInt: + return CallHelper(env_, "makeBigInt", {String(env_, reader_.ReadString())}); + case ValueTag::Reference: + return Reference(); + case ValueTag::Array: + return Array(); + case ValueTag::Object: + return Object(); + case ValueTag::Date: + return Date(); + case ValueTag::ArrayBuffer: + return ArrayBuffer(); + case ValueTag::TypedArray: + return TypedArray(); + case ValueTag::DataView: + return DataView(); + case ValueTag::Map: + return Map(); + case ValueTag::Set: + return Set(); + case ValueTag::RegExp: + return RegExp(); + case ValueTag::Error: + return Error(); + case ValueTag::SharedArrayBuffer: +#ifdef TARGET_ENGINE_QUICKJS + return SharedArrayBuffer(); +#else + throw NativeScriptException( + "SharedArrayBuffer worker messages are not supported by this engine."); +#endif + } + + throw NativeScriptException("Malformed worker message."); + } + + private: + napi_value Null() { + napi_value value = nullptr; + Check(env_, napi_get_null(env_, &value), "Unable to create null"); + return value; + } + + napi_value Number() { + napi_value value = nullptr; + Check(env_, napi_create_double(env_, reader_.ReadDouble(), &value), + "Unable to create number"); + return value; + } + + napi_value Reference() { + uint32_t id = reader_.ReadU32(); + if (id >= refs_.size() || refs_[id] == nullptr) { + throw NativeScriptException("Malformed worker message reference."); + } + return refs_[id]; + } + + void StoreRef(uint32_t id, napi_value value) { + if (id >= refs_.size()) { + refs_.resize(id + 1, nullptr); + } + refs_[id] = value; + } + + napi_value Array() { + uint32_t id = reader_.ReadU32(); + uint32_t length = reader_.ReadU32(); + napi_value array = nullptr; + Check(env_, napi_create_array_with_length(env_, length, &array), + "Unable to create Array"); + StoreRef(id, array); + for (uint32_t i = 0; i < length; i++) { + napi_value element = Read(); + Check(env_, napi_set_element(env_, array, i, element), + "Unable to set Array element"); + } + return array; + } + + napi_value Object() { + uint32_t id = reader_.ReadU32(); + uint32_t length = reader_.ReadU32(); + napi_value object = nullptr; + Check(env_, napi_create_object(env_, &object), "Unable to create Object"); + StoreRef(id, object); + for (uint32_t i = 0; i < length; i++) { + std::string key = reader_.ReadString(); + napi_value value = Read(); + Check(env_, napi_set_named_property(env_, object, key.c_str(), value), + "Unable to set object property"); + } + return object; + } + + napi_value Date() { + uint32_t id = reader_.ReadU32(); + napi_value date = nullptr; + Check(env_, napi_create_date(env_, reader_.ReadDouble(), &date), + "Unable to create Date"); + StoreRef(id, date); + return date; + } + + napi_value ArrayBuffer() { + uint32_t id = reader_.ReadU32(); + uint32_t byteLength = reader_.ReadU32(); + std::vector bytes = reader_.ReadBytes(byteLength); + void* data = nullptr; + napi_value arrayBuffer = nullptr; + Check(env_, napi_create_arraybuffer(env_, byteLength, &data, &arrayBuffer), + "Unable to create ArrayBuffer"); + if (byteLength != 0) { + std::memcpy(data, bytes.data(), byteLength); + } + StoreRef(id, arrayBuffer); + return arrayBuffer; + } + +#ifdef TARGET_ENGINE_QUICKJS + napi_value SharedArrayBuffer() { + uint32_t id = reader_.ReadU32(); + uint32_t byteLength = reader_.ReadU32(); + std::vector bytes = reader_.ReadBytes(byteLength); + + JSContext* context = qjs_get_context(env_); + if (context == nullptr) { + throw NativeScriptException("QuickJS context is not available."); + } + + JSValue value = + JS_ReadObject(context, bytes.data(), bytes.size(), + JS_READ_OBJ_REFERENCE | JS_READ_OBJ_SAB); + if (JS_IsException(value)) { + JSValue exception = JS_GetException(context); + napi_value error = nullptr; + qjs_create_scoped_value(env_, exception, &error); + throw NativeScriptException(env_, error, + "Unable to deserialize SharedArrayBuffer"); + } + + napi_value result = nullptr; + Check(env_, qjs_create_scoped_value(env_, value, &result), + "Unable to create SharedArrayBuffer value"); + StoreRef(id, result); + return result; + } +#endif + + napi_value TypedArray() { + uint32_t id = reader_.ReadU32(); + auto type = static_cast(reader_.ReadU32()); + uint32_t length = reader_.ReadU32(); + uint32_t byteOffset = reader_.ReadU32(); + napi_value arrayBuffer = Read(); + napi_value typedArray = nullptr; + Check(env_, + napi_create_typedarray(env_, type, length, arrayBuffer, byteOffset, + &typedArray), + "Unable to create TypedArray"); + StoreRef(id, typedArray); + return typedArray; + } + + napi_value DataView() { + uint32_t id = reader_.ReadU32(); + uint32_t byteLength = reader_.ReadU32(); + uint32_t byteOffset = reader_.ReadU32(); + napi_value arrayBuffer = Read(); + napi_value dataView = nullptr; + Check(env_, + napi_create_dataview(env_, byteLength, arrayBuffer, byteOffset, + &dataView), + "Unable to create DataView"); + StoreRef(id, dataView); + return dataView; + } + + napi_value Map() { + uint32_t id = reader_.ReadU32(); + uint32_t length = reader_.ReadU32(); + napi_value map = CallHelper(env_, "makeMap", {}); + StoreRef(id, map); + for (uint32_t i = 0; i < length; i++) { + napi_value key = Read(); + napi_value value = Read(); + CallHelper(env_, "mapSet", {map, key, value}); + } + return map; + } + + napi_value Set() { + uint32_t id = reader_.ReadU32(); + uint32_t length = reader_.ReadU32(); + napi_value set = CallHelper(env_, "makeSet", {}); + StoreRef(id, set); + for (uint32_t i = 0; i < length; i++) { + napi_value value = Read(); + CallHelper(env_, "setAdd", {set, value}); + } + return set; + } + + napi_value RegExp() { + uint32_t id = reader_.ReadU32(); + napi_value regexp = CallHelper(env_, "makeRegExp", + {String(env_, reader_.ReadString()), + String(env_, reader_.ReadString())}); + StoreRef(id, regexp); + return regexp; + } + + napi_value Error() { + uint32_t id = reader_.ReadU32(); + std::string name = reader_.ReadString(); + std::string message = reader_.ReadString(); + napi_value stack = Undefined(env_); + if (reader_.ReadU8() != 0) { + stack = String(env_, reader_.ReadString()); } + napi_value error = + CallHelper(env_, "makeError", {String(env_, name), String(env_, message), stack}); + StoreRef(id, error); + return error; + } + + napi_env env_; + BufferReader& reader_; + std::vector refs_; +}; + +#ifdef TARGET_ENGINE_QUICKJS +JSValue QuickJSValueFromNapi(napi_value value) { + return *reinterpret_cast(value); +} + +MallocedBuffer CopyToMallocedBuffer(const uint8_t* data, size_t size) { + MallocedBuffer result; + result.size = size; + result.data = static_cast(std::malloc(size)); + if (size != 0) { + std::memcpy(result.data, data, size); } + return result; } +void ClearQuickJSException(JSContext* context) { + JSValue exception = JS_GetException(context); + JS_FreeValue(context, exception); +} +#endif + +} // namespace + namespace worker { bool Message::Serialize(napi_env env, napi_value input) { - Buffer buf; - buffer_init(&buf); - serialize(env, input, &buf); - main_message_buf_ = MallocedBuffer((char*)buf.data, buf.size); +#ifdef TARGET_ENGINE_QUICKJS + ReleaseQuickJSSharedArrayBuffers(); + + JSContext* context = qjs_get_context(env); + if (context != nullptr) { + size_t byteLength = 0; + JSSABTab sabTab = {nullptr, 0}; + uint8_t* bytes = JS_WriteObject2( + context, &byteLength, QuickJSValueFromNapi(input), + JS_WRITE_OBJ_REFERENCE | JS_WRITE_OBJ_SAB, &sabTab); + if (bytes != nullptr) { + main_message_buf_ = CopyToMallocedBuffer(bytes, byteLength); + js_free(context, bytes); + if (sabTab.tab != nullptr && sabTab.len != 0) { + quickjs_shared_array_buffers_.assign(sabTab.tab, + sabTab.tab + sabTab.len); + for (uint8_t* sharedArrayBuffer : quickjs_shared_array_buffers_) { + qjs_shared_array_buffer_data_retain(sharedArrayBuffer); + } + } else { + quickjs_shared_array_buffers_.clear(); + } + if (sabTab.tab != nullptr) { + js_free(context, sabTab.tab); + } + is_quickjs_native_message_ = true; + return true; + } + + ClearQuickJSException(context); + } + + is_quickjs_native_message_ = false; + quickjs_shared_array_buffers_.clear(); +#endif + + BufferWriter writer; +#ifdef TARGET_ENGINE_QUICKJS + Serializer serializer(env, writer, &quickjs_shared_array_buffers_); +#else + Serializer serializer(env, writer); +#endif + serializer.Write(input); + main_message_buf_ = writer.Release(); return true; } napi_value Message::Deserialize(napi_env env) { - const uint8_t* buf = (const uint8_t*)main_message_buf_.data; - return deserialize(env, &buf); +#ifdef TARGET_ENGINE_QUICKJS + if (is_quickjs_native_message_) { + JSContext* context = qjs_get_context(env); + if (context == nullptr) { + throw NativeScriptException("QuickJS context is not available."); + } + + JSValue value = JS_ReadObject( + context, reinterpret_cast(main_message_buf_.data), + main_message_buf_.size, JS_READ_OBJ_REFERENCE | JS_READ_OBJ_SAB); + if (JS_IsException(value)) { + JSValue exception = JS_GetException(context); + napi_value error = nullptr; + qjs_create_scoped_value(env, exception, &error); + throw NativeScriptException(env, error, "Unable to deserialize worker message"); + } + + napi_value result = nullptr; + Check(env, qjs_create_scoped_value(env, value, &result), + "Unable to create worker message value"); + ReleaseQuickJSSharedArrayBuffers(); + return result; + } +#endif + + BufferReader reader(main_message_buf_.data, main_message_buf_.size); + Deserializer deserializer(env, reader); + napi_value result = deserializer.Read(); +#ifdef TARGET_ENGINE_QUICKJS + ReleaseQuickJSSharedArrayBuffers(); +#endif + return result; } Message::Message(MallocedBuffer&& payload) : main_message_buf_(std::move(payload)) {} + +Message::Message(Message&& other) noexcept + : main_message_buf_(std::move(other.main_message_buf_)) { +#ifdef TARGET_ENGINE_QUICKJS + is_quickjs_native_message_ = other.is_quickjs_native_message_; + quickjs_shared_array_buffers_ = + std::move(other.quickjs_shared_array_buffers_); + other.is_quickjs_native_message_ = false; + other.quickjs_shared_array_buffers_.clear(); +#endif +} + +Message& Message::operator=(Message&& other) noexcept { + if (this == &other) { + return *this; + } + +#ifdef TARGET_ENGINE_QUICKJS + ReleaseQuickJSSharedArrayBuffers(); +#endif + main_message_buf_ = std::move(other.main_message_buf_); +#ifdef TARGET_ENGINE_QUICKJS + is_quickjs_native_message_ = other.is_quickjs_native_message_; + quickjs_shared_array_buffers_ = + std::move(other.quickjs_shared_array_buffers_); + other.is_quickjs_native_message_ = false; + other.quickjs_shared_array_buffers_.clear(); +#endif + return *this; +} + +Message::~Message() { +#ifdef TARGET_ENGINE_QUICKJS + ReleaseQuickJSSharedArrayBuffers(); +#endif +} + +#ifdef TARGET_ENGINE_QUICKJS +void Message::ReleaseQuickJSSharedArrayBuffers() { + for (uint8_t* sharedArrayBuffer : quickjs_shared_array_buffers_) { + qjs_shared_array_buffer_data_release(sharedArrayBuffer); + } + quickjs_shared_array_buffers_.clear(); +} +#endif + }; // namespace worker }; // namespace nativescript diff --git a/NativeScript/runtime/modules/worker/MessageJSON.h b/NativeScript/runtime/modules/worker/MessageJSON.h index 05ffba9a5..b1fd0d2aa 100644 --- a/NativeScript/runtime/modules/worker/MessageJSON.h +++ b/NativeScript/runtime/modules/worker/MessageJSON.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "js_native_api_types.h" namespace nativescript { @@ -82,8 +83,9 @@ namespace worker { class Message { public: Message(MallocedBuffer&& payload = MallocedBuffer()); - Message(Message&& other) = default; - Message& operator=(Message&& other) = default; + Message(Message&& other) noexcept; + Message& operator=(Message&& other) noexcept; + ~Message(); Message& operator=(const Message&) = delete; Message(const Message&) = delete; bool Serialize(napi_env env, napi_value input); @@ -91,6 +93,11 @@ class Message { private: MallocedBuffer main_message_buf_; +#ifdef TARGET_ENGINE_QUICKJS + bool is_quickjs_native_message_ = false; + std::vector quickjs_shared_array_buffers_; + void ReleaseQuickJSSharedArrayBuffers(); +#endif }; }; // namespace worker } // namespace nativescript diff --git a/NativeScript/runtime/modules/worker/Worker.mm b/NativeScript/runtime/modules/worker/Worker.mm index 039f86f00..84eb4e0bf 100644 --- a/NativeScript/runtime/modules/worker/Worker.mm +++ b/NativeScript/runtime/modules/worker/Worker.mm @@ -25,18 +25,19 @@ napi_define_properties(env, global, 2, globalProperties); } + napi_value Worker; +#ifdef TARGET_ENGINE_HERMES + napi_create_function(env, "Worker", NAPI_AUTO_LENGTH, Constructor, nullptr, + &Worker); +#else napi_property_descriptor properties[] = { napi_util::desc("postMessage", PostMessage), napi_util::desc("terminate", Terminate), }; - - napi_value Worker; napi_define_class(env, "Worker", NAPI_AUTO_LENGTH, Constructor, nullptr, 2, properties, &Worker); +#endif - napi_property_descriptor globalProperties[] = { - napi_util::desc("Worker", Worker), - }; - napi_define_properties(env, global, 1, globalProperties); + napi_set_named_property(env, global, "Worker", Worker); } JS_METHOD(Worker::Constructor) { @@ -46,6 +47,18 @@ napi_value jsThis; napi_get_cb_info(env, cbinfo, &argc, argv, &jsThis, nullptr); +#ifdef TARGET_ENGINE_HERMES + if (!napi_util::is_of_type(env, jsThis, napi_object)) { + napi_create_object(env, &jsThis); + } + + napi_property_descriptor properties[] = { + napi_util::desc("postMessage", PostMessage), + napi_util::desc("terminate", Terminate), + }; + napi_define_properties(env, jsThis, 2, properties); +#endif + std::string workerPath = napi_util::get_cxx_string(env, argv[0]); WorkerImpl* worker = new WorkerImpl(env, Worker::OnMessage); diff --git a/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md index 6a2951bd2..086b5470e 100644 --- a/benchmarks/objc-dispatch/README.md +++ b/benchmarks/objc-dispatch/README.md @@ -1,7 +1,7 @@ # 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. +This benchmark compares hot Objective-C dispatch shapes across the current +engine package runtime and the legacy iOS AOT direct-call runtime. The benchmark body is plain NativeScript JavaScript: @@ -10,7 +10,7 @@ The benchmark body is plain NativeScript JavaScript: 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` +- `ios-package`: builds a temporary iOS app from a 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 @@ -30,8 +30,9 @@ 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 +npm run benchmark:objc-dispatch -- --runtime ios-package,legacy-ios --iterations 250000 +npm run benchmark:objc-dispatch -- --runtime all --include-gsd-off +npm run benchmark:objc-dispatch -- --runtime ios-package --package-tgz packages/ios-v8/dist/nativescript-ios-v8-0.0.2.tgz --variant-label ios-v8 --include-gsd-off ``` Useful options: @@ -39,7 +40,8 @@ Useful options: ```sh --legacy-repo /path/to/NativeScript/ios --destination "platform=iOS Simulator,id=" ---napi-package-tgz /path/to/nativescript-ios.tgz +--package-tgz /path/to/nativescript-ios-v8.tgz +--variant-label ios-v8 --iterations 250000 ---include-napi-gsd-off +--include-gsd-off ``` diff --git a/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js index 1778a344f..547728a03 100644 --- a/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js +++ b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js @@ -109,6 +109,26 @@ return i; }); + if (globalThis.performance && typeof globalThis.performance.now === "function") { + var performanceNow = globalThis.performance.now; + function NativePrototypeCall() {} + NativePrototypeCall.prototype.nativeCall = performanceNow; + var nativePrototypeCall = new NativePrototypeCall(); + + addCase(cases, "js.nativeFunction.performance.now", 1, function () { + return performanceNow(); + }); + + addCase( + cases, + "js.prototype.nativeFunction.performance.now", + 1, + function () { + return nativePrototypeCall.nativeCall(); + } + ); + } + addCase(cases, "NSObject.respondsToSelector", 1, function () { return object.respondsToSelector("description"); }); @@ -174,21 +194,25 @@ return cases; } - var startedAt = nowMs(); var results = []; var skipped = []; var cases = buildCases(); + var startedAt = nowMs(); 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); + } + var totalMs = nowMs() - startedAt; + + for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { + var result = results[resultIndex]; emit({ kind: "case", name: result.name, @@ -198,6 +222,11 @@ }); } + for (var skippedIndex = 0; skippedIndex < skipped.length; skippedIndex++) { + var skippedCase = skipped[skippedIndex]; + emit({ kind: "skip", name: skippedCase.name, error: skippedCase.error }); + } + var report = { kind: "done", version: 1, @@ -205,7 +234,7 @@ variant: variant, baseIterations: baseIterations, warmupIterations: warmupIterations, - totalMs: nowMs() - startedAt, + totalMs: totalMs, sink: sink, resultCount: results.length, skippedCount: skipped.length diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js index 7d8be2dca..7b8a6a353 100644 --- a/benchmarks/objc-dispatch/run.js +++ b/benchmarks/objc-dispatch/run.js @@ -22,7 +22,7 @@ function parseArgs(argv) { runtime: "all", iterations: 250000, warmupIterations: undefined, - includeNapiGsdOff: false, + includeGsdOff: false, includeLegacyAotOff: false, legacyRepo: process.env.NS_LEGACY_IOS_REPO || defaultLegacyRepo, metadataPath: process.env.METADATA_PATH || defaultMetadataPath, @@ -30,8 +30,8 @@ function parseArgs(argv) { workRoot: defaultWorkRoot, timeoutMs: 120000, buildTimeoutMs: 15 * 60 * 1000, - napiPackageTgz: "", - napiVariantLabel: "", + packageTgz: "", + variantLabel: "", skipBuild: false, compareResults: "" }; @@ -58,11 +58,11 @@ function parseArgs(argv) { else if (arg.startsWith("--timeout-ms=")) args.timeoutMs = Number(arg.slice("--timeout-ms=".length)); else if (arg === "--build-timeout-ms") args.buildTimeoutMs = Number(next()); else if (arg.startsWith("--build-timeout-ms=")) args.buildTimeoutMs = Number(arg.slice("--build-timeout-ms=".length)); - else if (arg === "--napi-package-tgz") args.napiPackageTgz = path.resolve(next()); - else if (arg.startsWith("--napi-package-tgz=")) args.napiPackageTgz = path.resolve(arg.slice("--napi-package-tgz=".length)); - else if (arg === "--napi-variant-label") args.napiVariantLabel = next(); - else if (arg.startsWith("--napi-variant-label=")) args.napiVariantLabel = arg.slice("--napi-variant-label=".length); - else if (arg === "--include-napi-gsd-off") args.includeNapiGsdOff = true; + else if (arg === "--package-tgz") args.packageTgz = path.resolve(next()); + else if (arg.startsWith("--package-tgz=")) args.packageTgz = path.resolve(arg.slice("--package-tgz=".length)); + else if (arg === "--variant-label") args.variantLabel = next(); + else if (arg.startsWith("--variant-label=")) args.variantLabel = arg.slice("--variant-label=".length); + else if (arg === "--include-gsd-off") args.includeGsdOff = 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()); @@ -90,15 +90,15 @@ function printUsage() { console.log(`Usage: node benchmarks/objc-dispatch/run.js [options] Options: - --runtime all|napi-node|napi-ios|legacy-ios + --runtime all|napi-node|ios-package|legacy-ios --iterations N --warmup N --legacy-repo PATH Default: ${defaultLegacyRepo} --metadata-path PATH Used by napi-node. Default: ${defaultMetadataPath} --destination DEST_OR_UDID iOS simulator destination or UDID - --napi-package-tgz PATH @nativescript/ios package tgz for napi-ios - --napi-variant-label LABEL Prefix N-API iOS report variants with an engine/backend label - --include-napi-gsd-off Also run N-API with generated signature dispatch disabled + --package-tgz PATH @nativescript/ios* package tgz for iOS package benchmarks + --variant-label LABEL Report iOS package results as this engine/backend label + --include-gsd-off Also run iOS packages with generated signature dispatch disabled --include-legacy-aot-off Also run legacy iOS V8 with AOT disabled --skip-build Reuse existing derived-data app builds --compare-results PATH Print report and comparison tables from a saved result JSON @@ -288,11 +288,11 @@ function reportLabel(report) { return `${report.runtime} (${report.variant})`; } -function labeledNapiVariant(options, variant) { - return options.napiVariantLabel ? `${options.napiVariantLabel} ${variant}` : variant; +function labeledPackageVariant(options, variant) { + return options.variantLabel ? `${options.variantLabel} ${variant}` : variant; } -function napiVariantGroup(variant) { +function gsdVariantGroup(variant) { const match = String(variant).match(/^(?:(.*)\s+)?(gsd-on|gsd-off)$/); if (!match) { return null; @@ -371,28 +371,25 @@ function printComparisons(reports) { const baseline = reports[0]; printTotalsComparison(reports, baseline); - const napiGroups = new Map(); + const gsdGroups = new Map(); for (const report of reports) { - if (report.runtime !== "napi-ios") { - continue; - } - const group = napiVariantGroup(report.variant); + const group = gsdVariantGroup(report.variant); if (!group) { continue; } - const key = group.label; - if (!napiGroups.has(key)) { - napiGroups.set(key, new Map()); + const key = group.label || report.runtime; + if (!gsdGroups.has(key)) { + gsdGroups.set(key, new Map()); } - napiGroups.get(key).set(group.kind, report); + gsdGroups.get(key).set(group.kind, report); } - let napiGsdOn = null; - for (const group of napiGroups.values()) { + let firstGsdOn = null; + for (const group of gsdGroups.values()) { const gsdOn = group.get("gsd-on"); const gsdOff = group.get("gsd-off"); - if (gsdOn && !napiGsdOn) { - napiGsdOn = gsdOn; + if (gsdOn && !firstGsdOn) { + firstGsdOn = gsdOn; } if (gsdOn && gsdOff) { printPairComparison(gsdOn, gsdOff); @@ -400,13 +397,13 @@ function printComparisons(reports) { } const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); - if (napiGsdOn && legacyAotOn) { - printPairComparison(napiGsdOn, legacyAotOn); + if (firstGsdOn && legacyAotOn) { + printPairComparison(firstGsdOn, legacyAotOn); } const legacyAotOff = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-off"); - if (napiGsdOn && legacyAotOff) { - printPairComparison(napiGsdOn, legacyAotOff); + if (firstGsdOn && legacyAotOff) { + printPairComparison(firstGsdOn, legacyAotOff); } } @@ -448,8 +445,7 @@ function runNapiNode(options, variant) { 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"; + env.NS_DISABLE_GSD = "1"; } else { delete env.NS_DISABLE_GSD; } @@ -728,7 +724,7 @@ async function runLegacyIOS(options, variant = "aot-on") { } } -function findDefaultNapiPackage() { +function findDefaultIOSPackage() { const distDir = path.join(repoRoot, "packages/ios/dist"); const names = fs.readdirSync(distDir) .filter((name) => /^nativescript-ios-.*\.tgz$/.test(name)) @@ -739,6 +735,10 @@ function findDefaultNapiPackage() { return path.join(distDir, names[names.length - 1]); } +function packageRuntimeLabel(options) { + return options.variantLabel || "ios-package"; +} + function replaceInTextFiles(root, search, replacement) { const queue = [root]; while (queue.length > 0) { @@ -787,11 +787,11 @@ function renamePlaceholderPaths(root, search, replacement) { } } -function scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant = variant) { +function scaffoldIOSPackageApp(options, variant, packageTgz, reportVariant = variant) { const appName = "NativeScriptDispatchBench"; - const bundleId = "org.nativescript.bench.dispatch.napi"; - const tgz = packageTgz || options.napiPackageTgz || findDefaultNapiPackage(); - const root = path.join(options.workRoot, "apps", `napi-ios-${variant}`); + const bundleId = "org.nativescript.bench.dispatch.iospackage"; + const tgz = packageTgz || options.packageTgz || findDefaultIOSPackage(); + const root = path.join(options.workRoot, "apps", `ios-package-${variant}`); rmrf(root); ensureDir(root); run("tar", ["-xzf", tgz, "-C", root]); @@ -829,7 +829,7 @@ function scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant = varian ensureDir(appDir); fs.writeFileSync(path.join(appDir, "package.json"), JSON.stringify({ main: "index" }, null, 2) + "\n"); fs.copyFileSync(benchmarkFile, path.join(appDir, path.basename(benchmarkFile))); - writeJsonRunner(path.join(appDir, "index.js"), "napi-ios", reportVariant, options); + writeJsonRunner(path.join(appDir, "index.js"), packageRuntimeLabel(options), reportVariant, options); const zipPath = path.join(frameworkRoot, "internal/XCFrameworks.zip"); run("unzip", ["-q", "-o", zipPath, "-d", path.join(frameworkRoot, "internal")]); @@ -871,9 +871,9 @@ function writeInfoPlist(plistPath) { `); } -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}`); +async function runIOSPackage(options, variant, packageTgz, reportVariant = variant) { + const app = scaffoldIOSPackageApp(options, variant, packageTgz, reportVariant); + const derivedDataPath = path.join(options.workRoot, `derived-data/ios-package-${variant}`); const udid = pickSimulator(options.destination); bootSimulator(udid); @@ -900,7 +900,7 @@ async function runNapiIOS(options, variant, packageTgz, reportVariant = variant) copyDirectoryContents(app.appDir, path.join(appPath, "app")); } installApp(udid, appPath, app.bundleId); - const launchEnv = variant === "gsd-off" ? { NS_DISABLE_GSD: "0" } : {}; + const launchEnv = variant === "gsd-off" ? { NS_DISABLE_GSD: "1" } : {}; return await launchAndCollect(udid, app.bundleId, options, launchEnv); } @@ -915,19 +915,19 @@ async function main() { const reports = []; const runtimes = options.runtime === "all" - ? ["napi-node", "napi-ios", "legacy-ios"] + ? ["napi-node", "ios-package", "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) { + if (options.includeGsdOff) { reports.push(runNapiNode(options, "gsd-off")); } - } else if (runtime === "napi-ios") { - reports.push(await runNapiIOS(options, "gsd-on", undefined, labeledNapiVariant(options, "gsd-on"))); - if (options.includeNapiGsdOff) { - reports.push(await runNapiIOS(options, "gsd-off", undefined, labeledNapiVariant(options, "gsd-off"))); + } else if (runtime === "ios-package") { + reports.push(await runIOSPackage(options, "gsd-on", undefined, labeledPackageVariant(options, "gsd-on"))); + if (options.includeGsdOff) { + reports.push(await runIOSPackage(options, "gsd-off", undefined, labeledPackageVariant(options, "gsd-off"))); } } else if (runtime === "legacy-ios") { reports.push(await runLegacyIOS(options, "aot-on")); diff --git a/docs/AGENT_MEMORY.md b/docs/AGENT_MEMORY.md new file mode 100644 index 000000000..28159f1b3 --- /dev/null +++ b/docs/AGENT_MEMORY.md @@ -0,0 +1,9 @@ +# Agent Memory + +## NativeScript React Native Module Porting + +- When the active ask is source parity for a NativeScript TypeScript port, do not keep re-debugging an already-root-caused simulator symptom first. Clean the mechanical port, deviation comments, source tests, and docs before using simulator stress as the final verification gate. +- Simulator AX visibility is not UIKit transition/readiness proof. A visible button can still fail because the app scene is stale, the wrong bundle is foreground, or a detached UIKit/RN touch host is out of lifecycle. Verify with deterministic dev-client launch and focused stress after source cleanup. +- If simulator behavior gets flaky, shut down existing simulators and verify on a fresh iOS 26.5 iPhone 17 simulator. Do not stack source changes on stale SpringBoard/app lifecycle evidence. +- Keep generic runtime capability generic. Do not add library-specific TurboModule helpers for `react-native-screens` or React Navigation; expose reusable UIKit/ObjC interop primitives in NativeScript runtime when a TypeScript port needs native capability. +- SimDeck AX frame fallbacks must be conditional. Prefer a plausible AX element center; reject only impossible frames (for example giant full-screen frames). Do not replace plausible AX with screenshot-guessed coordinates, since the screenshot can be visually misleading while AX remains the actual tappable point. diff --git a/examples/expo-demo/App.tsx b/examples/expo-demo/App.tsx index 657b6aacb..ead9435e6 100644 --- a/examples/expo-demo/App.tsx +++ b/examples/expo-demo/App.tsx @@ -38,12 +38,14 @@ const NativeBadge = defineUIKitView({ async function readNativeSummary() { const api = (globalThis as any).__nativeScriptNativeApi; - let ranOnMainThread = false; - await NativeScript.runOnUI(() => { - ranOnMainThread = NSThread.isMainThread === true; + const uiSummary = await NativeScript.runOnUI(() => { + 'worklet'; UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; + return { + ranOnMainThread: NSThread.isMainThread === true, + }; }); return { @@ -51,7 +53,7 @@ async function readNativeSummary() { classes: api?.metadata?.classes ?? 0, constants: api?.metadata?.constants ?? 0, enums: api?.metadata?.enums ?? 0, - ranOnMainThread, + ranOnMainThread: uiSummary.ranOnMainThread, timeoutConstant: NSURLErrorTimedOut, darkStyle: UIUserInterfaceStyle.Dark, }; diff --git a/examples/expo-demo/README.md b/examples/expo-demo/README.md index 5ebc12c6e..33dfd56c4 100644 --- a/examples/expo-demo/README.md +++ b/examples/expo-demo/README.md @@ -6,12 +6,13 @@ This example is meant to be copied into a generated Expo app after installing ```sh npx create-expo-app NativeScriptExpoDemo --template blank-typescript cd NativeScriptExpoDemo -npm install /path/to/nativescript-react-native-0.0.1.tgz +npm install /path/to/nativescript-react-native-0.0.1.tgz react-native-worklets 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. +The package config plugin enables the iOS New Architecture, Hermes, and the +NativeScript/Worklets Babel plugins during prebuild. This custom native module +cannot run inside Expo Go. diff --git a/examples/react-native-demo/App.tsx b/examples/react-native-demo/App.tsx index f03cf6b48..1eeba8964 100644 --- a/examples/react-native-demo/App.tsx +++ b/examples/react-native-demo/App.tsx @@ -31,6 +31,7 @@ function installNativeScriptGlobals(): NativeApiHost { } function getActiveUIKitWindow() { + 'worklet'; const app = UIApplication.sharedApplication; const scenes = app.connectedScenes?.allObjects; if (scenes) { @@ -63,13 +64,8 @@ async function applyUIKitTweaks() { const api = installNativeScriptGlobals(); - let nativeCallsRanOnMainThread = false; - await NativeScript.runOnUI(() => { - nativeCallsRanOnMainThread = NSThread.isMainThread === true; - if (!nativeCallsRanOnMainThread) { - throw new Error('runOnUI did not dispatch native calls to the main thread'); - } - + const uiSummary = await NativeScript.runOnUI(() => { + 'worklet'; const window = getActiveUIKitWindow(); if (!window) { throw new Error('No key UIWindow is available yet'); @@ -92,6 +88,10 @@ async function applyUIKitTweaks() { rootView.tintColor = nativeAccent; rootView.backgroundColor = nativeBackdrop; } + + return { + nativeCallsRanOnMainThread: NSThread.isMainThread === true, + }; }); return { @@ -103,7 +103,7 @@ async function applyUIKitTweaks() { enums: api.metadata?.enums ?? 0, timeoutConstant: NSURLErrorTimedOut, darkStyle: UIUserInterfaceStyle.Dark, - nativeCallsRanOnMainThread, + nativeCallsRanOnMainThread: uiSummary.nativeCallsRanOnMainThread, }; } diff --git a/examples/react-native-demo/README.md b/examples/react-native-demo/README.md index 651f18a06..c85017863 100644 --- a/examples/react-native-demo/README.md +++ b/examples/react-native-demo/README.md @@ -9,11 +9,8 @@ Run it from the repository root: 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. +The generated app installs the local `@nativescript/react-native` tarball and +`react-native-worklets`, enables Hermes and the New Architecture, then launches +an iOS simulator app. `NativeScript.init()` installs the Native API into the +Worklets UI runtime, and `runOnUI` executes a small UIKit tweak from a Worklets +callback. The script waits for a simulator marker after the tweak succeeds. diff --git a/metadata-generator/CMakeLists.txt b/metadata-generator/CMakeLists.txt index ada4ad98b..97a4c8119 100644 --- a/metadata-generator/CMakeLists.txt +++ b/metadata-generator/CMakeLists.txt @@ -23,11 +23,10 @@ include_directories( set(EXEC_SOURCE_FILES src/main.cpp src/SignatureDispatchEmitter.cpp - src/SignatureDispatchEmitter/EngineDirect.cpp - src/SignatureDispatchEmitter/Hermes.cpp src/SignatureDispatchEmitter/Napi.cpp + src/SignatureDispatchEmitter/Prepared.cpp src/SignatureDispatchEmitter/Shared.cpp - src/SignatureDispatchEmitter/V8.cpp + src/SignatureDispatchEmitter/Gsd.cpp src/Umbrella.cpp src/IR/Category.cpp src/IR/Class.cpp @@ -72,10 +71,26 @@ add_executable( ${EXEC_SOURCE_FILES} ) +set(LIBCLANG_DIR "/Library/Developer/CommandLineTools/usr/lib") +execute_process( + COMMAND xcrun --find clang + OUTPUT_VARIABLE XCRUN_CLANG + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(XCRUN_CLANG) + get_filename_component(XCRUN_CLANG_BIN_DIR "${XCRUN_CLANG}" DIRECTORY) + get_filename_component(XCRUN_TOOLCHAIN_USR_DIR "${XCRUN_CLANG_BIN_DIR}" DIRECTORY) + if(EXISTS "${XCRUN_TOOLCHAIN_USR_DIR}/lib/libclang.dylib") + set(LIBCLANG_DIR "${XCRUN_TOOLCHAIN_USR_DIR}/lib") + endif() +endif() +message(STATUS "LIBCLANG_DIR = ${LIBCLANG_DIR}") + target_link_directories( ${NAME} PRIVATE - /Library/Developer/CommandLineTools/usr/lib + "${LIBCLANG_DIR}" ) target_link_libraries( @@ -87,7 +102,7 @@ target_link_libraries( # Set runtime path for libclang set_target_properties(${NAME} PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH "/Library/Developer/CommandLineTools/usr/lib" + INSTALL_RPATH "${LIBCLANG_DIR}" ) install(TARGETS ${NAME} diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index da50820e9..0c18d5796 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -59,18 +59,14 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, wrappersByKey; std::unordered_map> preparedWrappersByKey; + // Engine-neutral GSD wrappers keyed by signature shape. + std::unordered_map gsdWrappersByKey; + std::unordered_map objcPreparedEntries; + std::unordered_map cFunctionPreparedEntries; + std::unordered_map blockPreparedEntries; std::unordered_map objcNapiEntries; std::unordered_map cFunctionNapiEntries; - std::unordered_map objcEngineDirectEntries; - std::unordered_map cFunctionEngineDirectEntries; - std::unordered_map objcV8Entries; - std::unordered_map cFunctionV8Entries; - std::unordered_map objcHermesDirectReturnEntries; - std::unordered_map cFunctionHermesDirectReturnEntries; - std::unordered_map objcHermesFrameDirectReturnEntries; - std::unordered_map cFunctionHermesFrameDirectReturnEntries; - std::unordered_map blockHermesFrameDirectReturnEntries; - std::unordered_map blockPreparedEntries; + std::unordered_map objcGsdEntries; std::unordered_map dispatchEncoding; std::unordered_set collidedDispatchIds; @@ -98,18 +94,12 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, if (encodedIt != dispatchEncoding.end() && encodedIt->second != canonicalSignatureKey) { collidedDispatchIds.insert(dispatchId); + objcPreparedEntries.erase(dispatchId); + cFunctionPreparedEntries.erase(dispatchId); + blockPreparedEntries.erase(dispatchId); objcNapiEntries.erase(dispatchId); cFunctionNapiEntries.erase(dispatchId); - objcEngineDirectEntries.erase(dispatchId); - cFunctionEngineDirectEntries.erase(dispatchId); - objcV8Entries.erase(dispatchId); - cFunctionV8Entries.erase(dispatchId); - objcHermesDirectReturnEntries.erase(dispatchId); - cFunctionHermesDirectReturnEntries.erase(dispatchId); - objcHermesFrameDirectReturnEntries.erase(dispatchId); - cFunctionHermesFrameDirectReturnEntries.erase(dispatchId); - blockHermesFrameDirectReturnEntries.erase(dispatchId); - blockPreparedEntries.erase(dispatchId); + objcGsdEntries.erase(dispatchId); dispatchEncoding.erase(dispatchId); continue; } @@ -125,38 +115,28 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, if (use.kind == DispatchKind::ObjCMethod) { wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + objcPreparedEntries.emplace(dispatchId, wrapperKey); objcNapiEntries.emplace(dispatchId, wrapperKey); - objcEngineDirectEntries.emplace(dispatchId, wrapperKey); - objcV8Entries.emplace(dispatchId, wrapperKey); - if (canUseHermesDirectReturnWrapper( - use.kind, signature, HermesDirectReturnCallSite::FastCallback)) { - objcHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); - } - if (canUseHermesDirectReturnWrapper( - use.kind, signature, HermesDirectReturnCallSite::Frame)) { - objcHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + // Engine-neutral GSD invokers for supported signatures. + if (isGsdSignatureSupported(signature)) { + const std::string gsdKey = makeGsdWrapperShapeKey(signature); + if (!gsdKey.empty()) { + gsdWrappersByKey.emplace(gsdKey, signature); + objcGsdEntries.emplace(dispatchId, gsdKey); + } } } else if (use.kind == DispatchKind::CFunction) { wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + cFunctionPreparedEntries.emplace(dispatchId, wrapperKey); cFunctionNapiEntries.emplace(dispatchId, wrapperKey); - cFunctionEngineDirectEntries.emplace(dispatchId, wrapperKey); - cFunctionV8Entries.emplace(dispatchId, wrapperKey); - 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 (canUseHermesDirectReturnWrapper( - use.kind, signature, HermesDirectReturnCallSite::Frame)) { - blockHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); - } } } @@ -184,56 +164,6 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, makeNapiWrapperName(wrapper.second.first, wrapperIndex++)); } - std::unordered_map v8WrapperNameByKey; - v8WrapperNameByKey.reserve(wrappers.size()); - size_t v8WrapperIndex = 0; - for (const auto& wrapper : wrappers) { - v8WrapperNameByKey.emplace( - wrapper.first, - makeV8WrapperName(wrapper.second.first, v8WrapperIndex++)); - } - - std::unordered_map hermesDirectReturnWrapperNameByKey; - hermesDirectReturnWrapperNameByKey.reserve(wrappers.size()); - size_t hermesDirectReturnWrapperIndex = 0; - for (const auto& wrapper : wrappers) { - hermesDirectReturnWrapperNameByKey.emplace( - wrapper.first, - makeHermesDirectReturnWrapperName(wrapper.second.first, - hermesDirectReturnWrapperIndex++)); - } - - std::unordered_map - hermesFrameDirectReturnWrapperNameByKey; - hermesFrameDirectReturnWrapperNameByKey.reserve(wrappers.size()); - size_t hermesFrameDirectReturnWrapperIndex = 0; - for (const auto& wrapper : wrappers) { - hermesFrameDirectReturnWrapperNameByKey.emplace( - wrapper.first, - makeHermesFrameDirectReturnWrapperName( - wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); - } - - std::unordered_map - hermesBlockFrameDirectReturnWrapperNameByKey; - hermesBlockFrameDirectReturnWrapperNameByKey.reserve(preparedWrappers.size()); - for (const auto& wrapper : preparedWrappers) { - hermesBlockFrameDirectReturnWrapperNameByKey.emplace( - wrapper.first, - makeHermesFrameDirectReturnWrapperName( - wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); - } - - std::unordered_map engineDirectWrapperNameByKey; - engineDirectWrapperNameByKey.reserve(wrappers.size()); - size_t engineDirectWrapperIndex = 0; - for (const auto& wrapper : wrappers) { - engineDirectWrapperNameByKey.emplace( - wrapper.first, - makeEngineDirectWrapperName(wrapper.second.first, - engineDirectWrapperIndex++)); - } - std::unordered_map preparedWrapperNameByKey; preparedWrapperNameByKey.reserve(preparedWrappers.size()); size_t preparedWrapperIndex = 0; @@ -243,6 +173,22 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, makePreparedWrapperName(wrapper.second.first, preparedWrapperIndex++)); } + // Engine-neutral GSD wrappers + std::vector> gsdWrappers( + gsdWrappersByKey.begin(), gsdWrappersByKey.end()); + std::sort(gsdWrappers.begin(), gsdWrappers.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::unordered_map gsdWrapperNameByKey; + gsdWrapperNameByKey.reserve(gsdWrappers.size()); + size_t gsdWrapperIndex = 0; + for (const auto& wrapper : gsdWrappers) { + gsdWrapperNameByKey.emplace(wrapper.first, + makeGsdWrapperName(gsdWrapperIndex++)); + } + std::vector> sortedObjCNapiEntries( objcNapiEntries.begin(), objcNapiEntries.end()); std::sort( @@ -255,81 +201,16 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, sortedCFunctionNapiEntries.begin(), sortedCFunctionNapiEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); - std::vector> sortedObjCEngineDirectEntries( - objcEngineDirectEntries.begin(), objcEngineDirectEntries.end()); - std::sort(sortedObjCEngineDirectEntries.begin(), - sortedObjCEngineDirectEntries.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.first < rhs.first; - }); - - std::vector> - sortedCFunctionEngineDirectEntries(cFunctionEngineDirectEntries.begin(), - cFunctionEngineDirectEntries.end()); - std::sort(sortedCFunctionEngineDirectEntries.begin(), - sortedCFunctionEngineDirectEntries.end(), - [](const auto& lhs, const auto& rhs) { - return lhs.first < rhs.first; - }); - - std::vector> sortedObjCV8Entries( - objcV8Entries.begin(), objcV8Entries.end()); - std::sort( - sortedObjCV8Entries.begin(), sortedObjCV8Entries.end(), - [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); - - std::vector> sortedCFunctionV8Entries( - cFunctionV8Entries.begin(), cFunctionV8Entries.end()); + std::vector> sortedObjCPreparedEntries( + objcPreparedEntries.begin(), objcPreparedEntries.end()); std::sort( - sortedCFunctionV8Entries.begin(), sortedCFunctionV8Entries.end(), + sortedObjCPreparedEntries.begin(), sortedObjCPreparedEntries.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(), + std::vector> sortedCFunctionPreparedEntries( + cFunctionPreparedEntries.begin(), cFunctionPreparedEntries.end()); + std::sort(sortedCFunctionPreparedEntries.begin(), + sortedCFunctionPreparedEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); @@ -340,11 +221,17 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, sortedBlockPreparedEntries.begin(), sortedBlockPreparedEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector> sortedObjCGsdEntries( + objcGsdEntries.begin(), objcGsdEntries.end()); + std::sort( + sortedObjCGsdEntries.begin(), sortedObjCGsdEntries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::ostringstream generated; generated << "#ifndef NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; generated << "#define NS_GENERATED_SIGNATURE_DISPATCH_INC\n\n"; - generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " - "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#if NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_PREPARED\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_DISPATCH\n"; generated << "#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 1\n"; generated << "#endif\n"; @@ -352,30 +239,10 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, generated << "#undef NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH\n"; generated << "#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 1\n"; generated << "#endif\n"; - generated << "#if NS_GSD_BACKEND_V8\n"; - generated << "#undef NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 1\n"; - generated << "#endif\n"; - generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; - generated << "#undef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 1\n"; - generated << "#endif\n\n"; - generated << "#if NS_GSD_BACKEND_HERMES\n"; - generated << "#undef NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH 1\n"; - generated << "#undef " - "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH\n"; - generated << "#define " - "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH 1\n"; - generated << "#undef " - "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH\n"; - generated << "#define " - "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH 1\n"; - generated << "#endif\n\n"; generated << "namespace nativescript {\n\n"; - generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " - "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#if NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_PREPARED\n"; for (const auto& wrapper : preparedWrappers) { writePreparedWrapper(generated, wrapper.second.first, preparedWrapperNameByKey.at(wrapper.first), @@ -390,56 +257,24 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, } generated << "#endif\n\n"; - generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; - writeEngineDirectConverterMacros(generated); - for (const auto& wrapper : wrappers) { - writeEngineDirectWrapper(generated, wrapper.second.first, - engineDirectWrapperNameByKey.at(wrapper.first), - wrapper.second.second); - } - writeEngineDirectConverterUndefs(generated); - generated << "#endif\n\n"; - - generated << "#if NS_GSD_BACKEND_HERMES\n"; - writeHermesEngineDirectConverterMacros(generated); - 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); - } - writeEngineDirectConverterUndefs(generated); - generated << "#endif\n\n"; - - generated << "#if NS_GSD_BACKEND_V8\n"; - for (const auto& wrapper : wrappers) { - writeV8Wrapper(generated, wrapper.second.first, - v8WrapperNameByKey.at(wrapper.first), wrapper.second.second); - } - generated << "#endif\n\n"; - - generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " - "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#if NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_PREPARED\n"; generated << "inline constexpr ObjCDispatchEntry " "kGeneratedObjCDispatchEntries[] = {\n"; generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCPreparedEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << preparedWrapperNameByKey.at(entry.second) << "},\n"; + } generated << "};\n\n"; generated << "inline constexpr CFunctionDispatchEntry " "kGeneratedCFunctionDispatchEntries[] = {\n"; generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionPreparedEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << preparedWrapperNameByKey.at(entry.second) << "},\n"; + } generated << "};\n\n"; generated << "inline constexpr BlockDispatchEntry " @@ -452,78 +287,6 @@ 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_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"; @@ -544,31 +307,55 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, 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"; std::ofstream outFile(outputPath, std::ios::trunc | std::ios::binary); outFile << generated.str(); + + // Write engine-neutral GSD invokers + dispatch table to a separate file. + // It is included from each engine backend after that engine's + // GsdObjCContext struct is defined (avoiding namespace ordering issues). + // The generated code only references the engine-neutral GsdObjCContext + // interface, so the same file compiles unchanged in every backend. + if (!sortedObjCGsdEntries.empty()) { + std::string gsdOutputPath = outputPath; + auto lastSlash = gsdOutputPath.rfind('/'); + if (lastSlash != std::string::npos) { + gsdOutputPath = gsdOutputPath.substr(0, lastSlash + 1) + + "GeneratedGsdSignatureDispatch.inc"; + } else { + gsdOutputPath = "GeneratedGsdSignatureDispatch.inc"; + } + + std::ostringstream gsdGenerated; + gsdGenerated << "#ifndef NS_GENERATED_GSD_SIGNATURE_DISPATCH_INC\n"; + gsdGenerated << "#define NS_GENERATED_GSD_SIGNATURE_DISPATCH_INC\n\n"; + gsdGenerated << "#undef NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH\n"; + gsdGenerated << "#define NS_HAS_GENERATED_SIGNATURE_GSD_DISPATCH 1\n\n"; + gsdGenerated << "#include \n\n"; + // No namespace wrapper — included from within namespace nativescript in + // each engine backend, after GsdObjCContext is defined. + + for (const auto& wrapper : gsdWrappers) { + writeGsdWrapper(gsdGenerated, gsdWrapperNameByKey.at(wrapper.first), + wrapper.second); + } + + gsdGenerated << "inline constexpr ObjCGsdDispatchEntry " + "kGeneratedObjCGsdDispatchEntries[] = {\n"; + gsdGenerated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCGsdEntries) { + gsdGenerated << " {" << toHexLiteral(entry.first) << ", &" + << gsdWrapperNameByKey.at(entry.second) << "},\n"; + } + gsdGenerated << "};\n\n"; + + gsdGenerated << "#endif // NS_GENERATED_GSD_SIGNATURE_DISPATCH_INC\n"; + + std::ofstream gsdOutFile(gsdOutputPath, std::ios::trunc | std::ios::binary); + gsdOutFile << gsdGenerated.str(); + } } } // namespace metagen diff --git a/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp b/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp deleted file mode 100644 index 820eb3fb7..000000000 --- a/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp +++ /dev/null @@ -1,363 +0,0 @@ -#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/Gsd.cpp b/metadata-generator/src/SignatureDispatchEmitter/Gsd.cpp new file mode 100644 index 000000000..5bc041aab --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Gsd.cpp @@ -0,0 +1,265 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +// Engine-neutral Generated Signature Dispatch (GSD) emitter. +// +// Emits one set of invoker functions and a dispatch table that are shared by +// every embedded engine backend (V8, JSC, QuickJS, Hermes). Each invoker takes +// a single `GsdObjCContext&` argument. The context is a small concrete struct +// defined per engine that knows how to read JS arguments and write the JS +// return value using that engine's native value API. Native calls are routed +// through ctx.invokeNative so backends that must release/coordinate their JS +// runtime around Objective-C dispatch can do so without moving JS conversion +// outside the engine thread. Backends without that requirement take the direct +// exception-safe path. The generated code only references the engine-neutral +// `GsdObjCContext` interface, so the same `.inc` compiles unchanged in every +// backend translation unit with inline context calls. +// +// GSD handles primitives, SEL, Class, and already-native Objective-C object +// values. Any value that does not match the fast representation makes the +// relevant reader return false, which makes the whole invoker fall back to the +// fully correct generic marshalling path. + +namespace metagen::signature_dispatch { + +std::string makeGsdWrapperName(size_t index) { + return "gsd" + toBase36(index); +} + +static void writeGsdArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index) { + switch (type->kind) { + case mdTypeBool: + out << " if (!ctx.readBool(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeSLong: + case mdTypeSInt64: + out << " if (!ctx.readSigned(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeUShort: + case mdTypeUInt: + case mdTypeULong: + case mdTypeUInt64: + out << " if (!ctx.readUnsigned(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeFloat: + out << " if (!ctx.readFloat(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeDouble: + out << " if (!ctx.readDouble(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeSelector: + out << " if (!ctx.readSelector(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeClass: + out << " if (!ctx.readClass(" << index << ", &arg" << index + << ")) return false;\n"; + break; + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + // Object arguments are accepted only when the JS value is already a + // native host object (cheap pointer unwrap); anything that would require + // allocation/conversion makes readObject return false and the whole + // invoker falls back to the generic path. + out << " if (!ctx.readObject(" << index << ", &arg" << index + << ")) return false;\n"; + break; + default: + out << " return false;\n"; + break; + } +} + +static void writeGsdReturnConversion(std::ostringstream& out, + const MDTypeInfo* type) { + switch (type->kind) { + case mdTypeVoid: + out << " ctx.setVoid();\n"; + break; + case mdTypeBool: + out << " ctx.setBool(nativeResult != 0);\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + out << " ctx.setInt32(static_cast(nativeResult));\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeUInt: + out << " ctx.setUInt32(static_cast(nativeResult));\n"; + break; + case mdTypeUShort: + out << " ctx.setUInt16(static_cast(nativeResult));\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " ctx.setInt64(static_cast(nativeResult));\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " ctx.setUInt64(static_cast(nativeResult));\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " ctx.setDouble(static_cast(nativeResult));\n"; + break; + case mdTypeSelector: + out << " ctx.setSelector(nativeResult);\n"; + break; + case mdTypeClass: + out << " ctx.setClass(nativeResult);\n"; + break; + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + // Object returns use the exact metadata return type (carried by the + // context) so JS conversion + ownership match the generic path exactly. + out << " ctx.setObject(nativeResult);\n"; + break; + default: + out << " return false;\n"; + break; + } +} + +static bool isGsdFastScalarType(const MDTypeInfo* type) { + if (type == nullptr) return false; + switch (type->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 mdTypeSelector: + case mdTypeClass: + return true; + default: + return false; + } +} + +static bool isGsdObjectType(const MDTypeInfo* type) { + if (type == nullptr) return false; + switch (type->kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +// Arguments accept scalars + Objective-C object types (the reader unwraps a +// native host object's backing pointer or falls back). Returns additionally +// accept void; object returns route through setObject with the exact metadata +// return type so JS conversion and ownership match the generic path. +static bool isGsdFastArgType(const MDTypeInfo* type) { + return isGsdFastScalarType(type) || isGsdObjectType(type); +} + +static bool isGsdFastReturnType(const MDTypeInfo* type) { + if (type != nullptr && type->kind == mdTypeVoid) return true; + return isGsdFastScalarType(type) || isGsdObjectType(type); +} + +bool isGsdSignatureSupported(const MDSignature* signature) { + if (signature == nullptr || signature->isVariadic) return false; + if (signature->arguments.size() > 8) return false; + if (!isGsdFastReturnType(signature->returnType)) return false; + for (const auto* arg : signature->arguments) { + if (!isGsdFastArgType(arg)) return false; + } + return true; +} + +void writeGsdWrapper(std::ostringstream& out, const std::string& wrapperName, + const MDSignature* signature) { + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) return; + + std::vector argTypes; + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) return; + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName << "(GsdObjCContext& ctx) {\n"; + + out << " using Fn = " << returnType << " (*)(id, SEL"; + for (const auto& argType : argTypes) { + out << ", " << argType; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(objc_msgSend);\n"; + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + } + for (size_t i = 0; i < argTypes.size(); i++) { + writeGsdArgConversion(out, signature->arguments[i], i); + } + + if (returnType == "void") { + out << " ctx.invokeNative([&]() {\n"; + out << " fn(ctx.self, ctx.selector"; + for (size_t i = 0; i < argTypes.size(); i++) out << ", arg" << i; + out << ");\n"; + out << " });\n"; + } else { + out << " " << returnType << " nativeResult{};\n"; + out << " ctx.invokeNative([&]() {\n"; + out << " nativeResult = fn(ctx.self, ctx.selector"; + for (size_t i = 0; i < argTypes.size(); i++) out << ", arg" << i; + out << ");\n"; + out << " });\n"; + } + + writeGsdReturnConversion(out, signature->returnType); + + out << " return true;\n"; + out << "}\n\n"; +} + +std::string makeGsdWrapperShapeKey(const MDSignature* signature) { + if (signature == nullptr) return {}; + std::string base = makeWrapperShapeKey(DispatchKind::ObjCMethod, signature); + if (base.empty()) return {}; + return "gsd|" + base; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp b/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp deleted file mode 100644 index 74feb009b..000000000 --- a/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp +++ /dev/null @@ -1,548 +0,0 @@ -#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 index efcd6a408..2de2c8efd 100644 --- a/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp @@ -22,23 +22,6 @@ std::string makeNapiWrapperName(DispatchKind kind, size_t index) { 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" : ""; @@ -249,7 +232,7 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, cleanupArgIndexes.reserve(argTypes.size()); noCleanupManagedArgIndexes.reserve(argTypes.size()); for (size_t i = 0; i < argTypes.size(); i++) { - if (!isFastDirectNapiKind(argTypeInfos[i]->kind)) { + if (!isFastPrimitiveDispatchKind(argTypeInfos[i]->kind)) { if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { cleanupArgIndexes.push_back(i); } else { @@ -271,7 +254,7 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, for (size_t i = 0; i < argTypes.size(); i++) { out << " " << argTypes[i] << " arg" << i << "{};\n"; - if (!isFastDirectNapiKind(argTypeInfos[i]->kind) && + if (!isFastPrimitiveDispatchKind(argTypeInfos[i]->kind) && argKindMayNeedCleanup(argTypeInfos[i]->kind)) { out << " bool shouldFree" << i << " = false;\n"; } @@ -312,9 +295,9 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, } for (size_t i = 0; i < argTypes.size(); i++) { - if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { + if (isFastPrimitiveDispatchKind(argTypeInfos[i]->kind)) { writeFastNapiArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); - } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + } else if (isFastReferenceDispatchKind(argTypeInfos[i]->kind)) { out << " if (!TryFastConvertNapiArgument(env, static_cast(" << static_cast(argTypeInfos[i]->kind) << "), argv[" << i << "], &arg" << i << ")) {\n"; @@ -373,56 +356,5 @@ void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, 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/Prepared.cpp b/metadata-generator/src/SignatureDispatchEmitter/Prepared.cpp new file mode 100644 index 000000000..adec48e9d --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Prepared.cpp @@ -0,0 +1,107 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +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 writePreparedWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + 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 << " (*)("; + 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"; + size_t implicitArgumentCount = 0; + if (kind == DispatchKind::ObjCMethod) { + out << " id self = *reinterpret_cast(avalues[0]);\n"; + out << " SEL selector = *reinterpret_cast(avalues[1]);\n"; + implicitArgumentCount = 2; + } else if (kind == DispatchKind::BlockInvoke) { + out << " void* block = *reinterpret_cast(avalues[0]);\n"; + implicitArgumentCount = 1; + } + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << " = *reinterpret_cast<" + << argTypes[i] << "*>(avalues[" << (i + implicitArgumentCount) + << "]);\n"; + } + + 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"; + } 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 index 86d38637b..c4233cff6 100644 --- a/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp @@ -364,7 +364,7 @@ std::string toBase36(size_t value) { return stream.str(); } -bool isFastDirectNapiKind(MDTypeKind kind) { +bool isFastPrimitiveDispatchKind(MDTypeKind kind) { switch (kind) { case mdTypeBool: case mdTypeChar: @@ -386,7 +386,7 @@ bool isFastDirectNapiKind(MDTypeKind kind) { } } -bool isFastManagedNapiKind(MDTypeKind kind) { +bool isFastReferenceDispatchKind(MDTypeKind kind) { switch (kind) { case mdTypeAnyObject: case mdTypeProtocolObject: @@ -413,7 +413,7 @@ bool argKindMayNeedCleanup(MDTypeKind kind) { case mdTypeSelector: return false; default: - return !isFastDirectNapiKind(kind); + return !isFastPrimitiveDispatchKind(kind); } } @@ -436,9 +436,9 @@ std::string makeWrapperShapeKey(DispatchKind kind, return {}; } - if (isFastDirectNapiKind(arg->kind)) { + if (isFastPrimitiveDispatchKind(arg->kind)) { key << "F" << static_cast(arg->kind); - } else if (isFastManagedNapiKind(arg->kind)) { + } else if (isFastReferenceDispatchKind(arg->kind)) { key << "H" << static_cast(arg->kind); } else { key << "M" << argType; diff --git a/metadata-generator/src/SignatureDispatchEmitter/Shared.h b/metadata-generator/src/SignatureDispatchEmitter/Shared.h index 873723d74..9af96eda3 100644 --- a/metadata-generator/src/SignatureDispatchEmitter/Shared.h +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.h @@ -25,11 +25,6 @@ struct SignatureUse { uint8_t flags; }; -enum class HermesDirectReturnCallSite { - FastCallback, - Frame, -}; - using SignatureMap = std::unordered_map; uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind kind, @@ -40,8 +35,8 @@ uint64_t signatureHash(const MDSignature* signature, 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 isFastPrimitiveDispatchKind(MDTypeKind kind); +bool isFastReferenceDispatchKind(MDTypeKind kind); bool argKindMayNeedCleanup(MDTypeKind kind); std::string toHexLiteral(uint64_t value); std::string toBase36(size_t value); @@ -56,41 +51,16 @@ void collectMethodUses(const std::vector& members, std::string makeNapiWrapperName(DispatchKind kind, size_t index); std::string makePreparedWrapperName(DispatchKind kind, size_t index); +std::string makeGsdWrapperName(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); +void writeGsdWrapper(std::ostringstream& out, const std::string& wrapperName, + const MDSignature* signature); +bool isGsdSignatureSupported(const MDSignature* signature); +std::string makeGsdWrapperShapeKey(const MDSignature* signature); } // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/V8.cpp b/metadata-generator/src/SignatureDispatchEmitter/V8.cpp deleted file mode 100644 index 544cd6c64..000000000 --- a/metadata-generator/src/SignatureDispatchEmitter/V8.cpp +++ /dev/null @@ -1,500 +0,0 @@ -#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/packages/ios-hermes/package.json b/packages/ios-hermes/package.json index e4a7bc3b3..89de4200c 100644 --- a/packages/ios-hermes/package.json +++ b/packages/ios-hermes/package.json @@ -18,7 +18,7 @@ "email": "oss@nativescript.org" }, "scripts": { - "build": "cd ../.. && ./scripts/build_all_ios.sh --hermes" + "build": "cd ../.. && IOS_VARIANT=ios-hermes ./scripts/build_all_ios.sh --hermes" }, "files": [ "framework" diff --git a/packages/ios-jsc/package.json b/packages/ios-jsc/package.json index 4966ca511..48e8809a4 100644 --- a/packages/ios-jsc/package.json +++ b/packages/ios-jsc/package.json @@ -19,7 +19,7 @@ "email": "oss@nativescript.org" }, "scripts": { - "build": "cd ../.. && ./scripts/build_all_ios.sh --jsc" + "build": "cd ../.. && IOS_VARIANT=ios-jsc ./scripts/build_all_ios.sh --jsc" }, "files": [ "framework" diff --git a/packages/ios-quickjs/package.json b/packages/ios-quickjs/package.json index 757d1668a..ca8c0e449 100644 --- a/packages/ios-quickjs/package.json +++ b/packages/ios-quickjs/package.json @@ -18,7 +18,7 @@ "email": "oss@nativescript.org" }, "scripts": { - "build": "cd ../.. && ./scripts/build_all_ios.sh --quickjs" + "build": "cd ../.. && IOS_VARIANT=ios-quickjs ./scripts/build_all_ios.sh --quickjs" }, "files": [ "framework" diff --git a/packages/ios-v8/package.json b/packages/ios-v8/package.json index 198236be6..50747030c 100644 --- a/packages/ios-v8/package.json +++ b/packages/ios-v8/package.json @@ -18,7 +18,7 @@ "email": "oss@nativescript.org" }, "scripts": { - "build": "cd ../.. && ./scripts/build_all_ios.sh --v8" + "build": "cd ../.. && IOS_VARIANT=ios-v8 ./scripts/build_all_ios.sh --v8" }, "files": [ "framework" diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec index d65543a7c..d42a5e73e 100644 --- a/packages/react-native/NativeScriptNativeApi.podspec +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -18,7 +18,9 @@ Pod::Spec.new do |s| s.source_files = [ "ios/**/*.{h,mm}", - "native-api-jsi/**/*.{h,mm}" + "native-api/ffi/hermes/**/*.h", + "native-api/ffi/shared/**/*.h", + "native-api/ffi/hermes/NativeApiJsi.mm" ] s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled s.public_header_files = "ios/**/*.h" @@ -31,16 +33,17 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", "CLANG_CXX_LIBRARY" => "libc++", - "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) TARGET_ENGINE_HERMES=1", + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) TARGET_ENGINE_HERMES=1 NS_GSD_BACKEND_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)/native-api\"", + "\"$(PODS_TARGET_SRCROOT)/native-api/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\"" + "\"$(PODS_ROOT)/Headers/Private/ReactCommon\"", + "\"$(PODS_ROOT)/Headers/Public/RNWorklets\"" ].join(" ") } @@ -51,4 +54,5 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "ReactCommon/turbomodule/core" end + s.dependency "RNWorklets" end diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 80c70d207..b9ebc174c 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -16,28 +16,48 @@ 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. +`NativeScript.init()` also installs the Native API into the +`react-native-worklets` UI runtime. `NativeScript.runOnUI()` only accepts +Worklets callbacks; running React Native's JS-thread runtime as a UI-thread shim +is not supported. ```ts await NativeScript.runOnUI(() => { + "worklet"; UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; }); ``` +Install `react-native-worklets`, add its Babel plugin, and run `pod install` so +the `RNWorklets` pod is linked: + +```sh +npm install react-native-worklets +``` + +```js +module.exports = { + presets: ["module:@react-native/babel-preset"], + plugins: [ + "@nativescript/react-native/babel-plugin", + "react-native-worklets/plugin", + ], +}; +``` + +`installWorklets()` is still exported for custom initialization, but it throws +when Worklets is unavailable or incompatible. `runOnUI()` throws when the +callback was not transformed into a Worklets function. + Obj-C blocks and JS-backed Obj-C method callbacks, including `NSObject.extend` -subclass overrides and delegates created with `createDelegate()`, default to the -thread that invoked them. Wrap callbacks when you want an explicit thread -policy: +subclass overrides and delegates created with `createDelegate()`, should return +to React Native's JS thread for JS work. Use `jsInvoker()` when a callback can be +reached from a native caller thread: ```ts UIView.animateWithDurationAnimationsCompletion( 0.25, - NativeScript.uiInvoker(() => { - view.alpha = 0.5; - }), + null, NativeScript.jsInvoker((finished) => { console.log("animation finished", finished); }), @@ -47,24 +67,20 @@ UIView.animateWithDurationAnimationsCompletion( Delegate, data-source, target/action, and `UIAction` callbacks are JS-side callbacks. Treat their bodies as JS work. If a callback can be reached from a background native thread and needs to mutate UIKit, wrap the mutation in -`NativeScript.runOnUI()` or create the callback with `NativeScript.uiInvoker()`. +`NativeScript.runOnUI()` with a Worklets callback. -The package also includes a Babel plugin for directive-style callbacks: +The package also includes a Babel plugin for directive-style JS callbacks: ```ts -someNativeApi(() => { - "use ui"; - view.alpha = 1; -}); - someNativeApi(() => { "use js"; console.log("back on JS"); }); ``` -The transform rewrites those callbacks to `NativeScript.uiInvoker(fn)` and -`NativeScript.jsInvoker(fn)`. +The transform rewrites those callbacks to `NativeScript.jsInvoker(fn)`. +`"use ui"` is rejected in React Native; use a Worklets `"worklet"` callback with +`NativeScript.runOnUI()` instead. ## Defining native UIKit views in JS @@ -119,6 +135,7 @@ Forward a ref when you need imperative access: const badgeRef = useRef>(null); await badgeRef.current?.runOnUI((view) => { + "worklet"; view.alpha = 0.8; }); @@ -183,6 +200,7 @@ property setters still win first, and unsupported names fall back to JS state: ```ts NativeScript.runOnUI(() => { + "worklet"; const view = UIView.new(); view.ownerState = { selected: false }; view.tag = 42; // still calls UIKit's native tag setter @@ -204,6 +222,7 @@ const delegate = NativeScript.createDelegate( { scrollViewDidScroll(scrollView) { NativeScript.runOnUI(() => { + "worklet"; scrollView.indicatorStyle = UIScrollViewIndicatorStyle.White; }); }, @@ -398,6 +417,7 @@ function topVisibleViewController( } await NativeScript.runOnUI(() => { + "worklet"; const presenter = topVisibleViewController(); if (!presenter || presenter.presentedViewController) { return; @@ -466,7 +486,7 @@ npm run test-rn-turbomodule 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 + npm install /path/to/nativescript-react-native-0.0.1.tgz react-native-worklets cd ios RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install ``` @@ -479,18 +499,21 @@ npm run test-rn-turbomodule NativeScript.init(); await NativeScript.runOnUI(() => { + "worklet"; UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; }); ``` -4. To use directive-style callbacks in a bare React Native app, add the bundled - Babel plugin: +4. Add the bundled NativeScript Babel plugin and the Worklets Babel plugin: ```js module.exports = { presets: ["module:@react-native/babel-preset"], - plugins: ["@nativescript/react-native/babel-plugin"], + plugins: [ + "@nativescript/react-native/babel-plugin", + "react-native-worklets/plugin", + ], }; ``` @@ -502,7 +525,7 @@ Expo development build, EAS Build, or `npx expo run:ios`. 1. Install the package: ```sh - npx expo install @nativescript/react-native + npx expo install @nativescript/react-native react-native-worklets ``` When testing a local tarball: @@ -523,8 +546,9 @@ Expo development build, EAS Build, or `npx expo run:ios`. The plugin configures iOS for Hermes and the React Native New Architecture, which are required by this JSI TurboModule. It also adds the - `@nativescript/react-native/babel-plugin` transform to `babel.config.js` so - `"use ui"` and `"use js"` callback directives work in Expo bundles. + `@nativescript/react-native/babel-plugin` and `react-native-worklets/plugin` + transforms to `babel.config.js` so `"use js"` and worklet callbacks work in + Expo bundles. 3. Prebuild and run the iOS development build: @@ -559,7 +583,7 @@ Expo development build, EAS Build, or `npx expo run:ios`. ``` Set `{ "babelPlugin": false }` in the config plugin options if you prefer to add -the Babel plugin manually. +the NativeScript and Worklets Babel plugins manually. The plugin also writes `nativescript.react-native.json` so metadata options are visible to native builds. You can pass metadata inputs when the app uses @@ -594,7 +618,8 @@ cd ios RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install ``` -`configure` adds the bundled Babel plugin when missing, writes +`configure` adds the bundled NativeScript and Worklets Babel plugins when +missing, writes `nativescript.react-native.json`, and warns when the app is not configured for Hermes and the New Architecture. The command is intentionally conservative and does not make destructive native project edits. diff --git a/packages/react-native/cli/configure.js b/packages/react-native/cli/configure.js old mode 100644 new mode 100755 diff --git a/packages/react-native/cli/generate-metadata.js b/packages/react-native/cli/generate-metadata.js old mode 100644 new mode 100755 diff --git a/packages/react-native/examples/UIKitPresentation.ts b/packages/react-native/examples/UIKitPresentation.ts index a44a6b41c..60bd2e7e8 100644 --- a/packages/react-native/examples/UIKitPresentation.ts +++ b/packages/react-native/examples/UIKitPresentation.ts @@ -4,6 +4,7 @@ export function topVisibleViewController( root: UIViewController | null | undefined = UIApplication.sharedApplication.keyWindow?.rootViewController, ): UIViewController | null { + 'worklet'; let current = root ?? null; while (current?.presentedViewController) { current = current.presentedViewController; @@ -23,6 +24,7 @@ export async function presentDocumentCamera( delegate: VNDocumentCameraViewControllerDelegate, ) { await NativeScript.runOnUI(() => { + 'worklet'; if ( !NativeScript.loadFramework('VisionKit') || !NativeScript.isClassAvailable('VNDocumentCameraViewController') @@ -47,6 +49,7 @@ export async function presentDocumentCamera( export async function presentPasses(pass: PKPass) { await NativeScript.runOnUI(() => { + 'worklet'; if ( !NativeScript.loadFramework('PassKit') || !NativeScript.isClassAvailable('PKAddPassesViewController') diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm index f890bc3b7..f94f048bb 100644 --- a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm @@ -1,13 +1,76 @@ #import "NativeScriptUIViewComponentView.h" #import +#import #import +#import #import #import "NativeScriptUIView.h" using namespace facebook::react; +static BOOL NativeScriptFabricViewIsDescendantOfView(UIView* view, UIView* ancestor) { + UIView* current = view; + while (current != nil) { + if (current == ancestor) { + return YES; + } + current = current.superview; + } + return NO; +} + +static CGRect NativeScriptFabricEffectiveTabBarHitBounds(UITabBar* tabBar) { + CGRect bounds = tabBar.bounds; + CGSize fittingSize = [tabBar sizeThatFits:CGSizeMake(bounds.size.width, bounds.size.height)]; + CGFloat maximumHeight = MAX(fittingSize.height + 32, 96); + + if (bounds.size.height > maximumHeight) { + bounds.origin.y = CGRectGetMaxY(bounds) - maximumHeight; + bounds.size.height = maximumHeight; + } + + return CGRectInset(bounds, -24, -16); +} + +static BOOL NativeScriptFabricPointInsideTabBarHitArea(UITabBar* tabBar, UIWindow* window, + CGPoint windowPoint) { + if (tabBar == nil || tabBar.hidden || tabBar.alpha <= 0.01 || + !tabBar.userInteractionEnabled) { + return NO; + } + + CGPoint localPoint = [tabBar convertPoint:windowPoint fromView:window]; + return CGRectContainsPoint(NativeScriptFabricEffectiveTabBarHitBounds(tabBar), localPoint); +} + +static UITabBar* NativeScriptFabricVisibleTabBarAtPoint(UIView* root, UIWindow* window, + CGPoint windowPoint) { + if (root.hidden || root.alpha <= 0.01 || !root.userInteractionEnabled) { + return nil; + } + + if ([root isKindOfClass:UITabBar.class]) { + UITabBar* tabBar = static_cast(root); + if (NativeScriptFabricPointInsideTabBarHitArea(tabBar, window, windowPoint)) { + return static_cast(root); + } + } + + for (UIView* subview in [root.subviews reverseObjectEnumerator]) { + UITabBar* tabBar = NativeScriptFabricVisibleTabBarAtPoint(subview, window, windowPoint); + if (tabBar != nil) { + return tabBar; + } + } + + return nil; +} + +@interface NativeScriptUIViewComponentView () +@end + @implementation NativeScriptUIViewComponentView { NativeScriptUIView* _containerView; NSString* _debugName; @@ -19,6 +82,7 @@ - (instancetype)initWithFrame:(CGRect)frame { _props = defaultProps; _containerView = [[NativeScriptUIView alloc] initWithFrame:self.bounds]; + _containerView.hostReadyDelegate = self; _containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.contentView = _containerView; @@ -28,11 +92,30 @@ - (instancetype)initWithFrame:(CGRect)frame { } - (void)dealloc { + _containerView.hostReadyDelegate = nil; [_debugName release]; [_containerView release]; [super dealloc]; } +- (void)nativeScriptUIView:(NativeScriptUIView*)view + didHostReady:(NSDictionary*)event { + (void)view; + if (_eventEmitter == nullptr) { + return; + } + + static_cast(*_eventEmitter) + .onHostReady(NativeScriptUIViewEventEmitter::OnHostReady{ + .hostReadyId = RCTStringFromNSString(event[@"hostReadyId"] ?: @""), + .hostId = RCTStringFromNSString(event[@"hostId"] ?: @""), + .nativeViewHandle = RCTStringFromNSString(event[@"nativeViewHandle"] ?: @""), + .childrenViewHandle = RCTStringFromNSString(event[@"childrenViewHandle"] ?: @""), + .controllerHandle = RCTStringFromNSString(event[@"controllerHandle"] ?: @""), + .hasChildren = [event[@"hasChildren"] boolValue], + }); +} + - (NSString*)description { if (_debugName.length == 0) { return [super description]; @@ -49,11 +132,61 @@ - (NSString*)description { - (void)mountChildComponentView:(UIView*)childComponentView index:(NSInteger)index { [_containerView insertSubview:childComponentView atIndex:index]; + [_containerView refreshDetachedChildrenHost]; } - (void)unmountChildComponentView:(UIView*)childComponentView index:(NSInteger)index { [childComponentView removeFromSuperview]; + [_containerView refreshDetachedChildrenHost]; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + [_containerView refreshDetachedChildrenHost]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [_containerView refreshDetachedChildrenHost]; +} + +- (void)updateLayoutMetrics:(const LayoutMetrics&)layoutMetrics + oldLayoutMetrics:(const LayoutMetrics&)oldLayoutMetrics { + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + [_containerView refreshDetachedChildrenHost]; +} + +- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { + [_containerView refreshDetachedChildrenHost]; + + UIView* hitView = [super hitTest:point withEvent:event]; + if (hitView == nil && _containerView != nil && _containerView.window != nil) { + CGPoint containerPoint = [_containerView convertPoint:point fromView:self]; + hitView = [_containerView hitTest:containerPoint withEvent:event]; + } + + if (hitView == nil || self.window == nil) { + return hitView; + } + + CGPoint windowPoint = [self convertPoint:point toView:self.window]; + UITabBar* tabBar = NativeScriptFabricVisibleTabBarAtPoint(self.window, self.window, windowPoint); + if (tabBar != nil) { + if (NativeScriptFabricViewIsDescendantOfView(tabBar, self)) { + CGPoint tabBarPoint = [tabBar convertPoint:windowPoint fromView:self.window]; + UIView* tabBarHitView = [tabBar hitTest:tabBarPoint withEvent:event]; + if (tabBarHitView != nil) { + return tabBarHitView; + } + return tabBar; + } + if (!NativeScriptFabricViewIsDescendantOfView(self, tabBar)) { + return nil; + } + } + + return hitView; } - (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps { @@ -65,8 +198,18 @@ - (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)o const std::string newChildrenViewHandle = newViewProps->childrenViewHandle; const std::string oldControllerHandle = oldViewProps->controllerHandle; const std::string newControllerHandle = newViewProps->controllerHandle; + const auto oldDetachControllerView = oldViewProps->detachControllerView; + const auto newDetachControllerView = newViewProps->detachControllerView; const std::string oldDebugName = oldViewProps->debugName; const std::string newDebugName = newViewProps->debugName; + const std::string oldHostId = oldViewProps->hostId; + const std::string newHostId = newViewProps->hostId; + const std::string oldHostReadyId = oldViewProps->hostReadyId; + const std::string newHostReadyId = newViewProps->hostReadyId; + const auto oldUpdateRevision = oldViewProps->updateRevision; + const auto newUpdateRevision = newViewProps->updateRevision; + const auto oldMountedRevision = oldViewProps->mountedRevision; + const auto newMountedRevision = newViewProps->mountedRevision; [super updateProps:props oldProps:oldProps]; @@ -78,6 +221,10 @@ - (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)o _containerView.debugName = debugName; } + if (oldDetachControllerView != newDetachControllerView) { + _containerView.detachControllerView = newDetachControllerView; + } + if (oldNativeViewHandle != newNativeViewHandle) { NSString* nativeViewHandle = newNativeViewHandle.empty() ? nil @@ -94,22 +241,48 @@ - (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)o } if (oldControllerHandle != newControllerHandle) { - NSString* controllerHandle = - newControllerHandle.empty() - ? nil - : [NSString stringWithUTF8String:newControllerHandle.c_str()]; + NSString* controllerHandle = newControllerHandle.empty() + ? nil + : [NSString stringWithUTF8String:newControllerHandle.c_str()]; _containerView.controllerHandle = controllerHandle; } + + if (oldHostId != newHostId) { + NSString* hostId = newHostId.empty() ? nil : [NSString stringWithUTF8String:newHostId.c_str()]; + _containerView.hostId = hostId; + } + + if (oldHostReadyId != newHostReadyId) { + NSString* hostReadyId = newHostReadyId.empty() + ? nil + : [NSString stringWithUTF8String:newHostReadyId.c_str()]; + _containerView.hostReadyId = hostReadyId; + } + + if (oldUpdateRevision != newUpdateRevision) { + _containerView.updateRevision = newUpdateRevision; + } + + if (oldMountedRevision != newMountedRevision) { + _containerView.mountedRevision = newMountedRevision; + } + + [_containerView refreshDetachedChildrenHost]; } - (void)prepareForRecycle { [super prepareForRecycle]; [_debugName release]; _debugName = nil; + _containerView.hostId = nil; + _containerView.hostReadyId = nil; _containerView.debugName = nil; _containerView.nativeViewHandle = nil; _containerView.childrenViewHandle = nil; _containerView.controllerHandle = nil; + _containerView.detachControllerView = NO; + _containerView.updateRevision = 0; + _containerView.mountedRevision = 0; } + (ComponentDescriptorProvider)componentDescriptorProvider { diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.h b/packages/react-native/ios/NativeScriptNativeApiModule.h index 540613fa2..ebe82a47f 100644 --- a/packages/react-native/ios/NativeScriptNativeApiModule.h +++ b/packages/react-native/ios/NativeScriptNativeApiModule.h @@ -15,6 +15,8 @@ class NativeScriptNativeApiModule explicit NativeScriptNativeApiModule(std::shared_ptr jsInvoker); bool install(jsi::Runtime& runtime, std::string metadataPath); + bool installWorkletRuntime(jsi::Runtime& runtime, jsi::Object runtimeHolder, + std::string metadataPath); bool isInstalled(jsi::Runtime& runtime); std::string defaultMetadataPath(jsi::Runtime& runtime); std::string getRuntimeBackend(jsi::Runtime& runtime); diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm index 5b3797808..99722918c 100644 --- a/packages/react-native/ios/NativeScriptNativeApiModule.mm +++ b/packages/react-native/ios/NativeScriptNativeApiModule.mm @@ -3,9 +3,22 @@ #import #import +#include +#include #include +#include #include "NativeApiJsiReactNative.h" +#include "NativeScriptUIKitHost.h" + +#import +#import +#import +#import +#import +#import +#include +#include namespace { @@ -34,10 +47,9 @@ } Class providerClass = NSClassFromString(@"NativeScriptNativeApiModuleProvider"); - NSBundle* providerBundle = - providerClass != Nil ? [NSBundle bundleForClass:providerClass] : nil; - NSString* resourceBundlePath = - [providerBundle pathForResource:@"NativeScriptNativeApi" ofType:@"bundle"]; + NSBundle* providerBundle = providerClass != Nil ? [NSBundle bundleForClass:providerClass] : nil; + NSString* resourceBundlePath = [providerBundle pathForResource:@"NativeScriptNativeApi" + ofType:@"bundle"]; NSBundle* resourceBundle = resourceBundlePath != nil ? [NSBundle bundleWithPath:resourceBundlePath] : nil; @@ -55,10 +67,9 @@ void writeSmokeMarkerIfRequested(const char* stage) { return; } - NSString* path = [NSTemporaryDirectory() - stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; - NSString* content = - [NSString stringWithFormat:@"stage=%s\n", stage != nullptr ? stage : ""]; + 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]; } @@ -68,52 +79,382 @@ bool writeSmokeMarkerContentIfRequested(const std::string& content) { return false; } - NSString* path = [NSTemporaryDirectory() - stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; - NSString* nativeContent = - [[NSString alloc] initWithBytes:content.data() - length:content.size() - encoding:NSUTF8StringEncoding]; + 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]; + BOOL ok = [nativeContent writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; #if !__has_feature(objc_arc) [nativeContent release]; #endif return ok == YES; } +bool nativeApiInstalled(facebook::jsi::Runtime& runtime) { + return runtime.global().hasProperty(runtime, "__nativeScriptNativeApi"); +} + +NSString* nativeScriptHandleFromNSObject(id object) { + if (object == nil) { + return @""; + } + + return [NSString stringWithFormat:@"%p", object]; +} + +RCTImageLoader* currentReactImageLoader() { + RCTBridge* bridge = [RCTBridge currentBridge]; + if (bridge == nil) { + return nil; + } + + RCTImageLoader* imageLoader = nil; + if ([bridge respondsToSelector:@selector(imageLoader)]) { + imageLoader = bridge.imageLoader; + } + if (imageLoader == nil && + [bridge respondsToSelector:@selector(moduleForName:lazilyLoadIfNecessary:)]) { + id module = [bridge moduleForName:@"RCTImageLoader" lazilyLoadIfNecessary:YES]; + if ([module isKindOfClass:RCTImageLoader.class]) { + imageLoader = (RCTImageLoader*)module; + } + } + return imageLoader; +} + +UIImage* imageWithRenderingMode(UIImage* image, bool isTemplate) { + if (image == nil) { + return nil; + } + + return [image imageWithRenderingMode:isTemplate ? UIImageRenderingModeAlwaysTemplate + : UIImageRenderingModeAlwaysOriginal]; +} + +std::mutex& nativeScriptWorkletRuntimeMutex() { + static std::mutex mutex; + return mutex; +} + +std::weak_ptr& nativeScriptWorkletRuntime() { + static std::weak_ptr runtime; + return runtime; +} + +void setNativeScriptWorkletRuntime(std::shared_ptr runtime) { + std::lock_guard lock(nativeScriptWorkletRuntimeMutex()); + nativeScriptWorkletRuntime() = std::move(runtime); +} + +std::shared_ptr getNativeScriptWorkletRuntime() { + std::lock_guard lock(nativeScriptWorkletRuntimeMutex()); + return nativeScriptWorkletRuntime().lock(); +} + +NSString* stringProperty(facebook::jsi::Runtime& runtime, facebook::jsi::Object& object, + const char* name) { + auto value = object.getProperty(runtime, name); + if (!value.isString()) { + return nil; + } + std::string text = value.getString(runtime).utf8(runtime); + return [NSString stringWithUTF8String:text.c_str()]; +} + +id imageSourceFromJSIValue(facebook::jsi::Runtime& runtime, + const facebook::jsi::Value& value, + const std::shared_ptr& jsInvoker) { + if (value.isString()) { + std::string uri = value.asString(runtime).utf8(runtime); + return @{@"uri" : [NSString stringWithUTF8String:uri.c_str()]}; + } + + id object = + facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject( + runtime, value, jsInvoker, YES); + return object == nil || object == [NSNull null] ? nil : object; +} + +void callImageLoadCallback( + std::weak_ptr workletRuntimeWeak, + std::shared_ptr callback, + UIImage* image, + NSString* errorMessage) { + UIImage* retainedImage = image != nil ? [image retain] : nil; + std::string imageHandle = + retainedImage != nil ? nativeScriptHandleFromNSObject(retainedImage).UTF8String : ""; + std::string errorText = errorMessage.UTF8String != nullptr ? errorMessage.UTF8String : ""; + + auto runtimeStrong = workletRuntimeWeak.lock(); + if (runtimeStrong == nullptr) { + [retainedImage release]; + return; + } + + runtimeStrong->schedule( + [callback = std::move(callback), imageHandle = std::move(imageHandle), + errorText = std::move(errorText), retainedImage](facebook::jsi::Runtime& runtime) mutable { + facebook::jsi::Value imageValue = + imageHandle.empty() + ? facebook::jsi::Value::null() + : facebook::jsi::Value( + runtime, + facebook::jsi::String::createFromUtf8(runtime, imageHandle)); + facebook::jsi::Value errorValue = + errorText.empty() + ? facebook::jsi::Value::null() + : facebook::jsi::Value( + runtime, + facebook::jsi::String::createFromUtf8(runtime, errorText)); + callback->call(runtime, imageValue, errorValue); + dispatch_async(dispatch_get_main_queue(), ^{ + [retainedImage release]; + }); + }); +} + +NSDictionary* handlesFromJSIValue(facebook::jsi::Runtime& runtime, + facebook::jsi::Value&& result) { + if (!result.isObject()) { + return nil; + } + + auto resultObject = result.asObject(runtime); + NSMutableDictionary* handles = + [NSMutableDictionary dictionaryWithCapacity:3]; + NSString* nativeViewHandle = stringProperty(runtime, resultObject, "nativeViewHandle"); + NSString* childrenViewHandle = stringProperty(runtime, resultObject, "childrenViewHandle"); + NSString* controllerHandle = stringProperty(runtime, resultObject, "controllerHandle"); + + if (nativeViewHandle.length > 0) { + handles[@"nativeViewHandle"] = nativeViewHandle; + } + if (childrenViewHandle.length > 0) { + handles[@"childrenViewHandle"] = childrenViewHandle; + } + if (controllerHandle.length > 0) { + handles[@"controllerHandle"] = controllerHandle; + } + return handles; +} + +NSDictionary* runUIKitHostFunction(NSString* hostId, NSString* phase, + const char* globalName, + const char* logAction) { + if (hostId.length == 0 || ![NSThread isMainThread]) { + return nil; + } + + auto workletRuntime = getNativeScriptWorkletRuntime(); + if (workletRuntime == nullptr) { + return nil; + } + + std::string hostIdString = hostId.UTF8String != nullptr ? hostId.UTF8String : ""; + if (hostIdString.empty()) { + return nil; + } + + std::string phaseString = phase.UTF8String != nullptr ? phase.UTF8String : ""; + + try { + return workletRuntime->runSync( + [hostIdString = std::move(hostIdString), phaseString = std::move(phaseString), + globalName](facebook::jsi::Runtime& runtime) -> NSDictionary* { + auto global = runtime.global(); + auto functionValue = global.getProperty(runtime, globalName); + if (!functionValue.isObject()) { + return nil; + } + + auto functionObject = functionValue.asObject(runtime); + if (!functionObject.isFunction(runtime)) { + return nil; + } + + auto function = functionObject.asFunction(runtime); + auto hostIdValue = facebook::jsi::String::createFromUtf8(runtime, hostIdString); + if (phaseString.empty()) { + return handlesFromJSIValue(runtime, function.call(runtime, hostIdValue)); + } + + return handlesFromJSIValue( + runtime, function.call(runtime, hostIdValue, + facebook::jsi::String::createFromUtf8(runtime, phaseString))); + }); + } catch (const std::exception& error) { + NSLog(@"NativeScript failed to %s UIKit host %@: %s", logAction, hostId, error.what()); + } catch (...) { + NSLog(@"NativeScript failed to %s UIKit host %@", logAction, hostId); + } + return nil; +} + } // namespace +NSDictionary* NativeScriptCreateUIKitHost(NSString* hostId) { + return runUIKitHostFunction(hostId, nil, "__nativeScriptCreateUIKitHostFromNative", "create"); +} + +NSDictionary* NativeScriptRunUIKitHostLifecycle(NSString* hostId, + NSString* phase) { + return runUIKitHostFunction(hostId, phase, "__nativeScriptRunUIKitHostLifecycleFromNative", + "run"); +} + namespace facebook::react { -NativeScriptNativeApiModule::NativeScriptNativeApiModule( - std::shared_ptr jsInvoker) +NativeScriptNativeApiModule::NativeScriptNativeApiModule(std::shared_ptr jsInvoker) : NativeScriptNativeApiCxxSpec(jsInvoker), jsInvoker_(std::move(jsInvoker)) {} -bool NativeScriptNativeApiModule::install(jsi::Runtime& runtime, - std::string metadataPath) { +bool NativeScriptNativeApiModule::install(jsi::Runtime& runtime, std::string metadataPath) { writeSmokeMarkerIfRequested("install:resolve-metadata"); - std::string resolvedMetadataPath = - metadataPath.empty() ? bundledMetadataPath() : metadataPath; + 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); + auto config = + nativescript::MakeReactNativeNativeApiJsiConfig( + jsInvoker_, nullptr, metadataPathArg, nullptr, "__nativeScriptNativeApi"); + config.installGlobalSymbols = false; + config.invokeCallbacksOnNativeCallerThread = false; nativescript::InstallNativeApiJSI(runtime, config); writeSmokeMarkerIfRequested("install:after-jsi"); return isInstalled(runtime); } +bool NativeScriptNativeApiModule::installWorkletRuntime(jsi::Runtime& runtime, + jsi::Object runtimeHolder, + std::string metadataPath) { + writeSmokeMarkerIfRequested("installWorkletRuntime:headers"); + if (!runtimeHolder.hasNativeState(runtime)) { + writeSmokeMarkerIfRequested("installWorkletRuntime:no-holder"); + return false; + } + + auto holder = runtimeHolder.getNativeState(runtime); + if (holder == nullptr || holder->runtime_ == nullptr) { + writeSmokeMarkerIfRequested("installWorkletRuntime:null-runtime"); + return false; + } + + setNativeScriptWorkletRuntime(holder->runtime_); + + std::string resolvedMetadataPath = metadataPath.empty() ? bundledMetadataPath() : metadataPath; + auto jsInvoker = jsInvoker_; + auto workletRuntimeRef = holder->runtime_; + return holder->runtime_->runSync( + [jsInvoker = std::move(jsInvoker), resolvedMetadataPath = std::move(resolvedMetadataPath), + workletRuntimeRef = std::move(workletRuntimeRef)]( + jsi::Runtime& workletRuntime) -> bool { + if (!nativeApiInstalled(workletRuntime)) { + std::weak_ptr workletRuntimeWeak(workletRuntimeRef); + const char* metadataPathArg = + resolvedMetadataPath.empty() ? nullptr : resolvedMetadataPath.c_str(); + auto config = + nativescript::MakeReactNativeNativeApiJsiConfig( + jsInvoker, nullptr, metadataPathArg, nullptr, "__nativeScriptNativeApi"); + config.installGlobalSymbols = true; + config.invokeCallbacksOnNativeCallerThread = true; + config.runtimeCallbackInvoker = + [workletRuntimeWeak](std::function task) mutable { + auto runtimeStrong = workletRuntimeWeak.lock(); + if (runtimeStrong == nullptr) { + return; + } + + auto taskBox = + std::make_shared>(std::move(task)); + dispatch_semaphore_t done = dispatch_semaphore_create(0); + runtimeStrong->schedule( + [taskBox = std::move(taskBox), done](jsi::Runtime&) mutable { + (*taskBox)(); + dispatch_semaphore_signal(done); + }); + dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); + }; + nativescript::InstallNativeApiJSI(workletRuntime, config); + } + + auto refreshUIKitHostView = jsi::Function::createFromHostFunction( + workletRuntime, + jsi::PropNameID::forAscii(workletRuntime, "__nativeScriptRefreshUIKitHostView"), + 1, + [](jsi::Runtime& runtime, const jsi::Value&, const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 1 || !args[0].isString()) { + return false; + } + + std::string handle = args[0].asString(runtime).utf8(runtime); + NSString* nativeHandle = [NSString stringWithUTF8String:handle.c_str()]; + return NativeScriptRefreshUIKitHostView(nativeHandle) == YES; + }); + workletRuntime.global().setProperty( + workletRuntime, "__nativeScriptRefreshUIKitHostView", std::move(refreshUIKitHostView)); + + std::weak_ptr imageWorkletRuntimeWeak(workletRuntimeRef); + auto loadImage = jsi::Function::createFromHostFunction( + workletRuntime, + jsi::PropNameID::forAscii(workletRuntime, "__nativeScriptLoadReactImage"), + 3, + [jsInvoker, imageWorkletRuntimeWeak](jsi::Runtime& runtime, const jsi::Value&, + const jsi::Value* args, + size_t count) -> jsi::Value { + if (count < 3 || !args[2].isObject() || + !args[2].asObject(runtime).isFunction(runtime)) { + return false; + } + + id jsonSource = imageSourceFromJSIValue(runtime, args[0], jsInvoker); + if (jsonSource == nil) { + return false; + } + + RCTImageSource* imageSource = [RCTConvert RCTImageSource:jsonSource]; + RCTImageLoader* imageLoader = currentReactImageLoader(); + if (imageSource == nil || imageLoader == nil) { + return false; + } + + bool isTemplate = count > 1 && args[1].isBool() && args[1].getBool(); + auto callback = std::make_shared( + args[2].asObject(runtime).asFunction(runtime)); + + [imageLoader loadImageWithURLRequest:imageSource.request + size:imageSource.size + scale:imageSource.scale + clipped:YES + resizeMode:RCTResizeModeCenter + progressBlock:^(int64_t, int64_t) { + } + partialLoadBlock:^(UIImage*) { + } + completionBlock:^(NSError* error, UIImage* image) { + dispatch_async(dispatch_get_main_queue(), ^{ + UIImage* renderedImage = + imageWithRenderingMode(image, isTemplate); + callImageLoadCallback( + imageWorkletRuntimeWeak, callback, renderedImage, + error.localizedDescription); + }); + }]; + return true; + }); + workletRuntime.global().setProperty( + workletRuntime, "__nativeScriptLoadReactImage", std::move(loadImage)); + return nativeApiInstalled(workletRuntime); + }); + } + bool NativeScriptNativeApiModule::isInstalled(jsi::Runtime& runtime) { - return runtime.global().hasProperty(runtime, "__nativeScriptNativeApi"); + return nativeApiInstalled(runtime); } std::string NativeScriptNativeApiModule::defaultMetadataPath(jsi::Runtime&) { @@ -125,8 +466,7 @@ bool writeSmokeMarkerContentIfRequested(const std::string& content) { return "hermes-jsi"; } -bool NativeScriptNativeApiModule::__writeTestMarker(jsi::Runtime&, - std::string content) { +bool NativeScriptNativeApiModule::__writeTestMarker(jsi::Runtime&, std::string content) { return writeSmokeMarkerContentIfRequested(content); } diff --git a/packages/react-native/ios/NativeScriptUIKitHost.h b/packages/react-native/ios/NativeScriptUIKitHost.h new file mode 100644 index 000000000..bec2d9dff --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIKitHost.h @@ -0,0 +1,8 @@ +#import + +FOUNDATION_EXPORT NSDictionary* NativeScriptCreateUIKitHost(NSString* hostId); + +FOUNDATION_EXPORT NSDictionary* NativeScriptRunUIKitHostLifecycle( + NSString* hostId, NSString* phase); + +FOUNDATION_EXPORT BOOL NativeScriptRefreshUIKitHostView(NSString* viewHandle); diff --git a/packages/react-native/ios/NativeScriptUIView.h b/packages/react-native/ios/NativeScriptUIView.h index 2f653feb9..8293a0b60 100644 --- a/packages/react-native/ios/NativeScriptUIView.h +++ b/packages/react-native/ios/NativeScriptUIView.h @@ -1,10 +1,28 @@ #import +#import + +@class NativeScriptUIView; + +@protocol NativeScriptUIViewHostReadyDelegate +- (void)nativeScriptUIView:(NativeScriptUIView*)view + didHostReady:(NSDictionary*)event; +@end @interface NativeScriptUIView : UIView +@property(nonatomic, copy) NSString* hostId; +@property(nonatomic, copy) NSString* hostReadyId; @property(nonatomic, copy) NSString* nativeViewHandle; @property(nonatomic, copy) NSString* childrenViewHandle; @property(nonatomic, copy) NSString* controllerHandle; +@property(nonatomic, assign) BOOL detachControllerView; @property(nonatomic, copy) NSString* debugName; +@property(nonatomic, assign) NSInteger updateRevision; +@property(nonatomic, assign) NSInteger mountedRevision; +@property(nonatomic, copy) RCTDirectEventBlock onHostReady; +@property(nonatomic, assign) id hostReadyDelegate; + +- (void)layoutDetachedChildrenViewSubviewsIfNeeded; +- (BOOL)refreshDetachedChildrenHost; @end diff --git a/packages/react-native/ios/NativeScriptUIView.mm b/packages/react-native/ios/NativeScriptUIView.mm index c36e3a76e..444b1094e 100644 --- a/packages/react-native/ios/NativeScriptUIView.mm +++ b/packages/react-native/ios/NativeScriptUIView.mm @@ -1,4 +1,15 @@ #import "NativeScriptUIView.h" +#import "NativeScriptUIKitHost.h" +#import + +#if __has_include() +#import +#endif + +#if __has_include() && __has_include() +#import +#import +#endif static id NativeScriptNSObjectFromHandle(NSString* handle) { if (handle == nil || handle.length == 0) { @@ -38,6 +49,30 @@ static id NativeScriptNSObjectFromHandle(NSString* handle) { return static_cast(object); } +static NSString* NativeScriptHandleFromNSObject(id object) { + if (object == nil) { + return @""; + } + + return [NSString stringWithFormat:@"%p", object]; +} + +static BOOL NativeScriptChildrenViewHasVisibleChild(UIView* childrenView, UIView* sentinel) { + if (childrenView == nil) { + return NO; + } + + for (UIView* subview in childrenView.subviews) { + if (subview == sentinel || subview.hidden || subview.alpha <= 0.01) { + continue; + } + + return YES; + } + + return NO; +} + static UIViewController* NativeScriptNearestViewController(UIView* view) { UIResponder* responder = view; while (responder != nil) { @@ -49,25 +84,286 @@ static id NativeScriptNSObjectFromHandle(NSString* handle) { return nil; } +static BOOL NativeScriptViewIsDescendantOfView(UIView* view, UIView* ancestor) { + UIView* current = view; + while (current != nil) { + if (current == ancestor) { + return YES; + } + current = current.superview; + } + return NO; +} + +static BOOL NativeScriptViewHasGestureRecognizer(UIView* view, UIGestureRecognizer* recognizer) { + if (view == nil || recognizer == nil) { + return NO; + } + + for (UIGestureRecognizer* existingRecognizer in view.gestureRecognizers) { + if (existingRecognizer == recognizer) { + return YES; + } + } + + return NO; +} + +static UIView* NativeScriptGestureRecognizerAttachedView(id recognizer) { + if (recognizer == nil || ![recognizer isKindOfClass:UIGestureRecognizer.class]) { + return nil; + } + + return static_cast(recognizer).view; +} + +static UIGestureRecognizer* NativeScriptFindAncestorSurfaceTouchHandler(UIView* view) { +#if __has_include() + UIView* parent = view.superview; + NSUInteger depth = 0; + + while (parent != nil && depth < 32) { + for (UIGestureRecognizer* recognizer in parent.gestureRecognizers) { + if ([recognizer isKindOfClass:RCTSurfaceTouchHandler.class]) { + return recognizer; + } + } + + parent = parent.superview; + depth += 1; + } +#endif + + return nil; +} + +static BOOL NativeScriptShouldForwardControllerAppearance(UIViewController* controller) { + return controller != nil && controller.view != nil && controller.view.window != nil; +} + +static BOOL NativeScriptHostedViewContainsControllerView(UIView* hostedView, + UIViewController* controller) { + return hostedView != nil && controller != nil && controller.view != nil && + NativeScriptViewIsDescendantOfView(controller.view, hostedView); +} + +static CGRect NativeScriptEffectiveTabBarHitBounds(UITabBar* tabBar) { + CGRect bounds = tabBar.bounds; + CGSize fittingSize = [tabBar sizeThatFits:CGSizeMake(bounds.size.width, bounds.size.height)]; + CGFloat maximumHeight = MAX(fittingSize.height + 32, 96); + + if (bounds.size.height > maximumHeight) { + bounds.origin.y = CGRectGetMaxY(bounds) - maximumHeight; + bounds.size.height = maximumHeight; + } + + return CGRectInset(bounds, -24, -16); +} + +static BOOL NativeScriptPointInsideTabBarHitArea(UITabBar* tabBar, UIWindow* window, + CGPoint windowPoint) { + if (tabBar == nil || tabBar.hidden || tabBar.alpha <= 0.01 || + !tabBar.userInteractionEnabled) { + return NO; + } + + CGPoint localPoint = [tabBar convertPoint:windowPoint fromView:window]; + return CGRectContainsPoint(NativeScriptEffectiveTabBarHitBounds(tabBar), localPoint); +} + +static UITabBar* NativeScriptVisibleTabBarAtPoint(UIView* root, UIWindow* window, + CGPoint windowPoint) { + if (root.hidden || root.alpha <= 0.01 || !root.userInteractionEnabled) { + return nil; + } + + if ([root isKindOfClass:UITabBar.class]) { + UITabBar* tabBar = static_cast(root); + if (NativeScriptPointInsideTabBarHitArea(tabBar, window, windowPoint)) { + return static_cast(root); + } + } + + for (UIView* subview in [root.subviews reverseObjectEnumerator]) { + UITabBar* tabBar = NativeScriptVisibleTabBarAtPoint(subview, window, windowPoint); + if (tabBar != nil) { + return tabBar; + } + } + + return nil; +} + +static BOOL NativeScriptSubviewShouldFillParent(UIView* parent, UIView* child) { + if (parent == nil || child == nil) { + return NO; + } + + const CGRect parentBounds = parent.bounds; + const CGRect childFrame = child.frame; + if (parentBounds.size.width <= 0) { + return NO; + } + + return fabs(childFrame.origin.x) < 1 && fabs(childFrame.origin.y) < 1 && + (childFrame.size.width <= 0 || fabs(childFrame.size.width - parentBounds.size.width) < 2); +} + +static void NativeScriptLayoutHostedSubviewChain(UIView* root, NSUInteger depth) { + if (root == nil || depth > 12 || [root isKindOfClass:UIScrollView.class]) { + return; + } + + const CGRect bounds = root.bounds; + for (UIView* subview in root.subviews) { + if (!NativeScriptSubviewShouldFillParent(root, subview)) { + continue; + } + + subview.frame = bounds; + subview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [subview setNeedsLayout]; + [subview layoutIfNeeded]; + NativeScriptLayoutHostedSubviewChain(subview, depth + 1); + } +} + +@class NativeScriptUIView; + +static const void* NativeScriptDetachedChildrenOwnerKey = + &NativeScriptDetachedChildrenOwnerKey; + +static NativeScriptUIView* NativeScriptDetachedChildrenOwner(UIView* view) { + id owner = view == nil ? nil : objc_getAssociatedObject(view, NativeScriptDetachedChildrenOwnerKey); + if (owner == nil || ![owner isKindOfClass:NativeScriptUIView.class]) { + return nil; + } + + return static_cast(owner); +} + +static void NativeScriptSetDetachedChildrenOwner(UIView* view, NativeScriptUIView* owner) { + if (view == nil) { + return; + } + + objc_setAssociatedObject( + view, NativeScriptDetachedChildrenOwnerKey, owner, OBJC_ASSOCIATION_ASSIGN); +} + +@interface NativeScriptUIView () +- (void)attachDetachedChildrenTouchHandlerIfNeeded; +- (void)installDetachedChildrenTouchSentinelIfNeeded; +- (void)notifyHostReadyIfNeeded; +- (BOOL)refreshDetachedChildrenHost; +- (void)updateDetachedChildrenTouchHandlerOrigin; +@end + +@interface NativeScriptDetachedChildrenTouchSentinel : UIView +@property(nonatomic, assign) NativeScriptUIView* owner; +@end + +@implementation NativeScriptDetachedChildrenTouchSentinel + +- (void)didMoveToWindow { + [super didMoveToWindow]; + [self.owner refreshDetachedChildrenHost]; +} + +- (void)didMoveToSuperview { + [super didMoveToSuperview]; + [self.owner refreshDetachedChildrenHost]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self.owner refreshDetachedChildrenHost]; +} + +@end + @implementation NativeScriptUIView { UIView* _nativeView; UIView* _childrenView; UIViewController* _viewController; + id _detachedTouchHandler; + UIView* _detachedTouchHandlerView; + UIWindow* _detachedTouchHandlerWindow; + NativeScriptDetachedChildrenTouchSentinel* _detachedTouchSentinel; + NSInteger _hostMountRetryCount; + NSString* _lastHostReadyKey; } - (void)dealloc { + if (_hostId.length > 0) { + NativeScriptRunUIKitHostLifecycle(_hostId, @"dispose"); + } [self detachViewController]; + [self detachDetachedChildrenTouchHandler]; + [_detachedTouchSentinel removeFromSuperview]; + [_detachedTouchSentinel release]; [_nativeView removeFromSuperview]; [_nativeView release]; + if (NativeScriptDetachedChildrenOwner(_childrenView) == self) { + NativeScriptSetDetachedChildrenOwner(_childrenView, nil); + } [_childrenView release]; [_viewController release]; + [_detachedTouchHandler release]; + [_detachedTouchHandlerView release]; [_nativeViewHandle release]; [_childrenViewHandle release]; [_controllerHandle release]; + [_hostId release]; + [_hostReadyId release]; [_debugName release]; + [_onHostReady release]; + [_lastHostReadyKey release]; [super dealloc]; } +- (void)setHostId:(NSString*)hostId { + if ((_hostId == hostId) || [_hostId isEqualToString:hostId]) { + return; + } + + NSString* previousHostId = [_hostId copy]; + if (previousHostId.length > 0) { + NativeScriptRunUIKitHostLifecycle(previousHostId, @"dispose"); + } + [previousHostId release]; + + [_hostId release]; + _hostId = [hostId copy]; + _hostMountRetryCount = 0; + [_lastHostReadyKey release]; + _lastHostReadyKey = nil; + [self mountUIKitHostIfNeeded]; + [self notifyHostReadyIfNeeded]; +} + +- (void)setHostReadyId:(NSString*)hostReadyId { + if ((_hostReadyId == hostReadyId) || [_hostReadyId isEqualToString:hostReadyId]) { + return; + } + + [_hostReadyId release]; + _hostReadyId = [hostReadyId copy]; + [_lastHostReadyKey release]; + _lastHostReadyKey = nil; + [self notifyHostReadyIfNeeded]; +} + +- (void)setOnHostReady:(RCTDirectEventBlock)onHostReady { + if (_onHostReady == onHostReady) { + return; + } + + [_onHostReady release]; + _onHostReady = [onHostReady copy]; + [self notifyHostReadyIfNeeded]; +} + - (void)setNativeViewHandle:(NSString*)nativeViewHandle { if ((_nativeViewHandle == nativeViewHandle) || [_nativeViewHandle isEqualToString:nativeViewHandle]) { @@ -76,7 +372,15 @@ - (void)setNativeViewHandle:(NSString*)nativeViewHandle { [_nativeViewHandle release]; _nativeViewHandle = [nativeViewHandle copy]; - [self setNativeView:NativeScriptUIViewFromHandle(_nativeViewHandle)]; + UIView* nativeView = NativeScriptUIViewFromHandle(_nativeViewHandle); + if (_detachControllerView && _viewController != nil && nativeView == _viewController.view) { + nativeView = nil; + } + if (nativeView == nil && _nativeViewHandle.length == 0 && !_detachControllerView && + _viewController != nil) { + nativeView = _viewController.view; + } + [self setNativeView:nativeView]; } - (void)setChildrenViewHandle:(NSString*)childrenViewHandle { @@ -101,6 +405,28 @@ - (void)setControllerHandle:(NSString*)controllerHandle { [self setViewController:NativeScriptUIViewControllerFromHandle(_controllerHandle)]; } +- (void)setDetachControllerView:(BOOL)detachControllerView { + if (_detachControllerView == detachControllerView) { + return; + } + + if (detachControllerView) { + [self detachViewController]; + if (_viewController != nil && _nativeView == _viewController.view) { + [self setNativeView:nil]; + } + } + + _detachControllerView = detachControllerView; + + if (!_detachControllerView && _viewController != nil) { + if (_nativeViewHandle.length == 0) { + [self setNativeView:_viewController.view]; + } + [self attachViewControllerIfPossible]; + } +} + - (void)setDebugName:(NSString*)debugName { if ((_debugName == debugName) || [_debugName isEqualToString:debugName]) { return; @@ -110,6 +436,28 @@ - (void)setDebugName:(NSString*)debugName { _debugName = [debugName copy]; } +- (void)setUpdateRevision:(NSInteger)updateRevision { + if (_updateRevision == updateRevision) { + return; + } + + _updateRevision = updateRevision; + if (_updateRevision > 0) { + [self runUIKitHostLifecycle:@"update"]; + } +} + +- (void)setMountedRevision:(NSInteger)mountedRevision { + if (_mountedRevision == mountedRevision) { + return; + } + + _mountedRevision = mountedRevision; + if (_mountedRevision > 0) { + [self runUIKitHostLifecycle:@"mounted"]; + } +} + - (NSString*)description { if (_debugName.length == 0) { return [super description]; @@ -123,14 +471,132 @@ - (NSString*)description { return [description stringByAppendingFormat:@" debugName = %@", _debugName]; } +- (NSDictionary*)hostReadyEventWithHasChildren:(BOOL)hasChildren { + NSString* readyId = _hostReadyId.length > 0 ? _hostReadyId : _hostId; + if (readyId.length == 0) { + return nil; + } + + NSMutableDictionary* event = [NSMutableDictionary dictionaryWithCapacity:6]; + event[@"hostReadyId"] = readyId; + event[@"hostId"] = _hostId ?: @""; + event[@"nativeViewHandle"] = NativeScriptHandleFromNSObject(_nativeView); + event[@"childrenViewHandle"] = NativeScriptHandleFromNSObject(_childrenView); + event[@"controllerHandle"] = NativeScriptHandleFromNSObject(_viewController); + event[@"hasChildren"] = @(hasChildren); + return event; +} + +- (void)notifyHostReadyIfNeeded { + const BOOL hasChildren = + NativeScriptChildrenViewHasVisibleChild(_childrenView, _detachedTouchSentinel); + if (!hasChildren) { + return; + } + + NSDictionary* event = [self hostReadyEventWithHasChildren:hasChildren]; + if (event == nil) { + return; + } + + NSString* key = [NSString + stringWithFormat:@"%@|%@|%@|%@|%@|%@", + event[@"hostReadyId"] ?: @"", + event[@"hostId"] ?: @"", + event[@"nativeViewHandle"] ?: @"", + event[@"childrenViewHandle"] ?: @"", + event[@"controllerHandle"] ?: @"", + [event[@"hasChildren"] boolValue] ? @"1" : @"0"]; + if ([_lastHostReadyKey isEqualToString:key]) { + return; + } + + [_lastHostReadyKey release]; + _lastHostReadyKey = [key copy]; + + if (_onHostReady != nil) { + _onHostReady(event); + } + if ([_hostReadyDelegate respondsToSelector:@selector(nativeScriptUIView:didHostReady:)]) { + [_hostReadyDelegate nativeScriptUIView:self didHostReady:event]; + } +} + +- (void)applyUIKitHostHandles:(NSDictionary*)handles { + if (handles == nil) { + return; + } + + NSString* nativeViewHandle = handles[@"nativeViewHandle"]; + NSString* childrenViewHandle = handles[@"childrenViewHandle"]; + NSString* controllerHandle = handles[@"controllerHandle"]; + + if (controllerHandle.length > 0) { + self.controllerHandle = controllerHandle; + } + if (nativeViewHandle.length > 0) { + self.nativeViewHandle = nativeViewHandle; + } + if (childrenViewHandle.length > 0) { + self.childrenViewHandle = childrenViewHandle; + } + [self notifyHostReadyIfNeeded]; +} + +- (void)mountUIKitHostIfNeeded { + if (_hostId.length == 0) { + return; + } + + NSDictionary* handles = NativeScriptCreateUIKitHost(_hostId); + if (handles != nil) { + _hostMountRetryCount = 0; + [self applyUIKitHostHandles:handles]; + return; + } + + if (_hostMountRetryCount >= 8) { + return; + } + + _hostMountRetryCount += 1; + NSString* retryHostId = [_hostId copy]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (retryHostId.length > 0 && [self->_hostId isEqualToString:retryHostId]) { + [self mountUIKitHostIfNeeded]; + } + [retryHostId release]; + }); +} + +- (void)runUIKitHostLifecycle:(NSString*)phase { + if (_hostId.length == 0 || phase.length == 0) { + return; + } + + [self mountUIKitHostIfNeeded]; + [self applyUIKitHostHandles:NativeScriptRunUIKitHostLifecycle(_hostId, phase)]; +} + - (void)setChildrenView:(UIView*)childrenView { if (_childrenView == childrenView) { return; } + [self detachDetachedChildrenTouchHandler]; + [_detachedTouchSentinel removeFromSuperview]; + [_detachedTouchSentinel release]; + _detachedTouchSentinel = nil; + if (NativeScriptDetachedChildrenOwner(_childrenView) == self) { + NativeScriptSetDetachedChildrenOwner(_childrenView, nil); + } [_childrenView release]; _childrenView = [childrenView retain]; + NativeScriptSetDetachedChildrenOwner(_childrenView, self); [self moveReactSubviewsToChildrenView]; + [self installDetachedChildrenTouchSentinelIfNeeded]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self notifyHostReadyIfNeeded]; } - (void)setNativeView:(UIView*)nativeView { @@ -153,6 +619,7 @@ - (void)setNativeView:(UIView*)nativeView { [super insertSubview:_nativeView atIndex:0]; [self moveReactSubviewsToChildrenView]; [self setNeedsLayout]; + [self notifyHostReadyIfNeeded]; } - (void)setViewController:(UIViewController*)viewController { @@ -163,13 +630,22 @@ - (void)setViewController:(UIViewController*)viewController { [self detachViewController]; [_viewController release]; _viewController = [viewController retain]; - [self setNativeView:_viewController.view]; + if (_detachControllerView) { + if (_viewController != nil && _nativeView == _viewController.view) { + [self setNativeView:nil]; + } + return; + } + if (_nativeViewHandle.length == 0) { + [self setNativeView:_viewController.view]; + } [self attachViewControllerIfPossible]; + [self notifyHostReadyIfNeeded]; } - (void)attachViewControllerIfPossible { - if (_viewController == nil || _viewController.parentViewController != nil || - self.window == nil) { + if (_detachControllerView || _viewController == nil || + _viewController.parentViewController != nil || self.window == nil) { return; } @@ -178,17 +654,68 @@ - (void)attachViewControllerIfPossible { return; } + UIView* hostedViewToReinsert = nil; + NSUInteger hostedViewIndex = NSNotFound; + if (_nativeView.superview == self && + NativeScriptHostedViewContainsControllerView(_nativeView, _viewController)) { + hostedViewToReinsert = [_nativeView retain]; + hostedViewIndex = [self.subviews indexOfObject:hostedViewToReinsert]; + [hostedViewToReinsert removeFromSuperview]; + } + + const BOOL shouldForwardAppearance = + hostedViewToReinsert == nil && NativeScriptShouldForwardControllerAppearance(_viewController); + if (shouldForwardAppearance) { + [_viewController beginAppearanceTransition:YES animated:NO]; + } + [parent addChildViewController:_viewController]; + if (hostedViewToReinsert != nil) { + NSUInteger targetIndex = + hostedViewIndex == NSNotFound ? 0 : MIN(hostedViewIndex, self.subviews.count); + [super insertSubview:hostedViewToReinsert atIndex:targetIndex]; + } [_viewController didMoveToParentViewController:parent]; + + if (shouldForwardAppearance) { + [_viewController endAppearanceTransition]; + } + [hostedViewToReinsert release]; } - (void)detachViewController { - if (_viewController == nil || _viewController.parentViewController == nil) { + if (_detachControllerView || _viewController == nil || + _viewController.parentViewController == nil) { return; } + UIView* hostedViewToReinsert = nil; + NSUInteger hostedViewIndex = NSNotFound; + if (_nativeView.superview == self && + NativeScriptHostedViewContainsControllerView(_nativeView, _viewController)) { + hostedViewToReinsert = [_nativeView retain]; + hostedViewIndex = [self.subviews indexOfObject:hostedViewToReinsert]; + } + + const BOOL shouldForwardAppearance = + hostedViewToReinsert == nil && NativeScriptShouldForwardControllerAppearance(_viewController); + if (shouldForwardAppearance) { + [_viewController beginAppearanceTransition:NO animated:NO]; + } + [_viewController willMoveToParentViewController:nil]; + [hostedViewToReinsert removeFromSuperview]; [_viewController removeFromParentViewController]; + if (hostedViewToReinsert != nil) { + NSUInteger targetIndex = + hostedViewIndex == NSNotFound ? 0 : MIN(hostedViewIndex, self.subviews.count); + [super insertSubview:hostedViewToReinsert atIndex:targetIndex]; + } + + if (shouldForwardAppearance) { + [_viewController endAppearanceTransition]; + } + [hostedViewToReinsert release]; } - (void)moveReactSubviewsToChildrenView { @@ -204,6 +731,10 @@ - (void)moveReactSubviewsToChildrenView { [_childrenView addSubview:subview]; } [subviews release]; + [self layoutDetachedChildrenViewSubviewsIfNeeded]; + [self installDetachedChildrenTouchSentinelIfNeeded]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self notifyHostReadyIfNeeded]; } - (void)insertSubview:(UIView*)view atIndex:(NSInteger)index { @@ -211,19 +742,242 @@ - (void)insertSubview:(UIView*)view atIndex:(NSInteger)index { NSUInteger targetIndex = MIN(static_cast(MAX(index, 0)), _childrenView.subviews.count); [_childrenView insertSubview:view atIndex:targetIndex]; + [self layoutDetachedChildrenViewSubviewsIfNeeded]; + [self installDetachedChildrenTouchSentinelIfNeeded]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self notifyHostReadyIfNeeded]; return; } [super insertSubview:view atIndex:index]; + [self notifyHostReadyIfNeeded]; +} + +- (void)layoutDetachedChildrenViewSubviewsIfNeeded { + if (_childrenView == nil) { + return; + } + + const CGRect bounds = _childrenView.bounds; + for (UIView* subview in _childrenView.subviews) { + if (subview == _detachedTouchSentinel) { + subview.frame = CGRectZero; + continue; + } + + subview.frame = bounds; + subview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [subview setNeedsLayout]; + [subview layoutIfNeeded]; + NativeScriptLayoutHostedSubviewChain(subview, 0); + } +} + +- (BOOL)refreshDetachedChildrenHost { + if (_childrenView == nil) { + return NO; + } + + [self layoutDetachedChildrenViewSubviewsIfNeeded]; + [self installDetachedChildrenTouchSentinelIfNeeded]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self updateDetachedChildrenTouchHandlerOrigin]; + [self notifyHostReadyIfNeeded]; + + return NativeScriptChildrenViewHasVisibleChild(_childrenView, _detachedTouchSentinel); +} + +- (void)installDetachedChildrenTouchSentinelIfNeeded { + if (_childrenView == nil || _detachedTouchSentinel != nil) { + return; + } + + NativeScriptDetachedChildrenTouchSentinel* sentinel = + [[NativeScriptDetachedChildrenTouchSentinel alloc] initWithFrame:CGRectZero]; + sentinel.owner = self; + sentinel.hidden = YES; + sentinel.userInteractionEnabled = NO; + sentinel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _detachedTouchSentinel = sentinel; + [_childrenView addSubview:sentinel]; +} + +- (void)attachDetachedChildrenTouchHandlerIfNeeded { + if (_childrenView == nil) { + return; + } + + UIView* touchView = _childrenView; + touchView.userInteractionEnabled = YES; + if (NativeScriptFindAncestorSurfaceTouchHandler(touchView) != nil) { + [self detachDetachedChildrenTouchHandler]; + return; + } + + if (_detachedTouchHandler != nil) { + UIView* attachedTouchHandlerView = + NativeScriptGestureRecognizerAttachedView(_detachedTouchHandler); + if (_detachedTouchHandlerView != touchView || + (attachedTouchHandlerView != nil && attachedTouchHandlerView != touchView) || + _detachedTouchHandlerWindow != touchView.window || + !NativeScriptViewHasGestureRecognizer(touchView, _detachedTouchHandler)) { + [self detachDetachedChildrenTouchHandler]; + } else { + [self updateDetachedChildrenTouchHandlerOrigin]; + return; + } + } + + if (_detachedTouchHandler != nil) { + [self updateDetachedChildrenTouchHandlerOrigin]; + return; + } + +#if __has_include() + RCTSurfaceTouchHandler* surfaceTouchHandler = [RCTSurfaceTouchHandler new]; + [surfaceTouchHandler attachToView:touchView]; + _detachedTouchHandler = surfaceTouchHandler; + _detachedTouchHandlerView = [touchView retain]; + _detachedTouchHandlerWindow = touchView.window; + [self updateDetachedChildrenTouchHandlerOrigin]; + return; +#endif +} + +- (void)updateDetachedChildrenTouchHandlerOrigin { +#if __has_include() + if (_detachedTouchHandler == nil || _detachedTouchHandlerView == nil || + ![_detachedTouchHandler isKindOfClass:RCTSurfaceTouchHandler.class]) { + return; + } + + CGPoint origin = CGPointZero; + if (_detachedTouchHandlerView.window != nil) { + origin = [_detachedTouchHandlerView convertPoint:CGPointZero + toView:_detachedTouchHandlerView.window]; + } + + ((RCTSurfaceTouchHandler*)_detachedTouchHandler).viewOriginOffset = origin; +#endif +} + +- (void)detachDetachedChildrenTouchHandler { + if (_detachedTouchHandler == nil || _detachedTouchHandlerView == nil) { + [_detachedTouchHandler release]; + _detachedTouchHandler = nil; + [_detachedTouchHandlerView release]; + _detachedTouchHandlerView = nil; + _detachedTouchHandlerWindow = nil; + return; + } + + UIView* attachedTouchHandlerView = + NativeScriptGestureRecognizerAttachedView(_detachedTouchHandler); + UIView* detachView = + attachedTouchHandlerView != nil ? attachedTouchHandlerView : _detachedTouchHandlerView; + + if ([_detachedTouchHandler respondsToSelector:@selector(detachFromView:)]) { + if (NativeScriptViewHasGestureRecognizer(detachView, _detachedTouchHandler)) { + [_detachedTouchHandler detachFromView:detachView]; + } + } + + [_detachedTouchHandler release]; + _detachedTouchHandler = nil; + [_detachedTouchHandlerView release]; + _detachedTouchHandlerView = nil; + _detachedTouchHandlerWindow = nil; +} + +- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { + [self refreshDetachedChildrenHost]; + + UIView* hitView = [super hitTest:point withEvent:event]; + if (hitView == nil && _childrenView != nil && _childrenView.window != nil) { + CGPoint childrenPoint = [_childrenView convertPoint:point fromView:self]; + hitView = [_childrenView hitTest:childrenPoint withEvent:event]; + } + + if (hitView == nil || self.window == nil) { + return hitView; + } + + CGPoint windowPoint = [self convertPoint:point toView:self.window]; + UITabBar* tabBar = NativeScriptVisibleTabBarAtPoint(self.window, self.window, windowPoint); + if (tabBar != nil) { + if (NativeScriptViewIsDescendantOfView(tabBar, self)) { + CGPoint tabBarPoint = [tabBar convertPoint:windowPoint fromView:self.window]; + UIView* tabBarHitView = [tabBar hitTest:tabBarPoint withEvent:event]; + if (tabBarHitView != nil) { + return tabBarHitView; + } + return tabBar; + } + if (!NativeScriptViewIsDescendantOfView(self, tabBar)) { + return nil; + } + } + + return hitView; } - (void)didMoveToWindow { [super didMoveToWindow]; + [self mountUIKitHostIfNeeded]; [self attachViewControllerIfPossible]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self updateDetachedChildrenTouchHandlerOrigin]; + [self notifyHostReadyIfNeeded]; } - (void)layoutSubviews { [super layoutSubviews]; _nativeView.frame = self.bounds; + [self layoutDetachedChildrenViewSubviewsIfNeeded]; + [self installDetachedChildrenTouchSentinelIfNeeded]; + [self attachDetachedChildrenTouchHandlerIfNeeded]; + [self updateDetachedChildrenTouchHandlerOrigin]; + [self notifyHostReadyIfNeeded]; } @end + +static BOOL NativeScriptRefreshUIKitHostSubviews(UIView* root, NSUInteger depth) { + if (root == nil || depth > 24) { + return NO; + } + + BOOL refreshed = NO; + if ([root isKindOfClass:NativeScriptUIView.class]) { + refreshed = [static_cast(root) refreshDetachedChildrenHost] || refreshed; + } + + NativeScriptUIView* detachedChildrenOwner = NativeScriptDetachedChildrenOwner(root); + if (detachedChildrenOwner != nil) { + refreshed = [detachedChildrenOwner refreshDetachedChildrenHost] || refreshed; + } + + if ([root isKindOfClass:NativeScriptDetachedChildrenTouchSentinel.class]) { + NativeScriptDetachedChildrenTouchSentinel* sentinel = + static_cast(root); + refreshed = [sentinel.owner refreshDetachedChildrenHost] || refreshed; + } + + for (UIView* subview in root.subviews) { + refreshed = NativeScriptRefreshUIKitHostSubviews(subview, depth + 1) || refreshed; + } + + return refreshed; +} + +BOOL NativeScriptRefreshUIKitHostView(NSString* viewHandle) { + if (![NSThread isMainThread]) { + return NO; + } + + UIView* view = NativeScriptUIViewFromHandle(viewHandle); + if (view == nil) { + return NO; + } + + return NativeScriptRefreshUIKitHostSubviews(view, 0); +} diff --git a/packages/react-native/ios/NativeScriptUIViewManager.mm b/packages/react-native/ios/NativeScriptUIViewManager.mm index dab55ce25..9de511f65 100644 --- a/packages/react-native/ios/NativeScriptUIViewManager.mm +++ b/packages/react-native/ios/NativeScriptUIViewManager.mm @@ -14,6 +14,14 @@ - (UIView*)view { } RCT_EXPORT_VIEW_PROPERTY(nativeViewHandle, NSString) +RCT_EXPORT_VIEW_PROPERTY(childrenViewHandle, NSString) +RCT_EXPORT_VIEW_PROPERTY(controllerHandle, NSString) +RCT_EXPORT_VIEW_PROPERTY(detachControllerView, BOOL) RCT_EXPORT_VIEW_PROPERTY(debugName, NSString) +RCT_EXPORT_VIEW_PROPERTY(hostId, NSString) +RCT_EXPORT_VIEW_PROPERTY(hostReadyId, NSString) +RCT_EXPORT_VIEW_PROPERTY(updateRevision, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(mountedRevision, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(onHostReady, RCTDirectEventBlock) @end diff --git a/packages/react-native/package.json b/packages/react-native/package.json index cd72e79ec..0913beac7 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -36,7 +36,7 @@ "types", "ios", "metadata", - "native-api-jsi", + "native-api", "NativeScriptNativeApi.podspec", "README.md", "LICENSE" @@ -44,11 +44,15 @@ "peerDependencies": { "expo": "*", "react": "*", - "react-native": ">=0.79" + "react-native": ">=0.79", + "react-native-worklets": ">=0.8.0" }, "peerDependenciesMeta": { "expo": { "optional": true + }, + "react-native-worklets": { + "optional": true } }, "codegenConfig": { diff --git a/packages/react-native/plugin/babel-plugin.js b/packages/react-native/plugin/babel-plugin.js index 8db3c1d3e..9e80f8b98 100644 --- a/packages/react-native/plugin/babel-plugin.js +++ b/packages/react-native/plugin/babel-plugin.js @@ -1,4 +1,17 @@ const PACKAGE_NAME = '@nativescript/react-native'; +const UIKIT_DEFINITION_CALLEES = new Set([ + 'defineUIKitContainer', + 'defineUIKitView', + 'defineUIViewController', +]); +const UIKIT_WORKLET_CALLBACKS = new Set([ + 'create', + 'createController', + 'childrenView', + 'dispose', + 'mounted', + 'update', +]); function isDirectiveFunction(path) { const body = path.node.body; @@ -75,6 +88,81 @@ function findNativeScriptIdentifier(programPath, t) { return null; } +function collectNativeScriptBindings(programPath, t) { + const nativeScriptIdentifiers = new Set(); + const uikitDefinitionIdentifiers = new Set(); + + for (const statement of programPath.get('body')) { + if (statement.isImportDeclaration()) { + if (!isNativeScriptSource(statement.node.source.value)) { + continue; + } + for (const specifier of statement.node.specifiers) { + if ( + t.isImportDefaultSpecifier(specifier) || + t.isImportNamespaceSpecifier(specifier) + ) { + nativeScriptIdentifiers.add(specifier.local.name); + } else if (t.isImportSpecifier(specifier)) { + const imported = specifier.imported; + const importedName = t.isIdentifier(imported) + ? imported.name + : imported.value; + if (UIKIT_DEFINITION_CALLEES.has(importedName)) { + uikitDefinitionIdentifiers.add(specifier.local.name); + } + } + } + continue; + } + + if (!statement.isVariableDeclaration()) { + continue; + } + for (const declaration of statement.node.declarations) { + const init = declaration.init; + const requiredNativeScript = + t.isCallExpression(init) && + t.isIdentifier(init.callee, {name: 'require'}) && + init.arguments.length === 1 && + t.isStringLiteral(init.arguments[0], {value: PACKAGE_NAME}); + const requiredNativeScriptDefault = + t.isMemberExpression(init) && + !init.computed && + t.isIdentifier(init.property, {name: 'default'}) && + t.isCallExpression(init.object) && + t.isIdentifier(init.object.callee, {name: 'require'}) && + init.object.arguments.length === 1 && + t.isStringLiteral(init.object.arguments[0], {value: PACKAGE_NAME}); + if (t.isIdentifier(declaration.id)) { + if (requiredNativeScript || requiredNativeScriptDefault) { + nativeScriptIdentifiers.add(declaration.id.name); + } + } else if (t.isObjectPattern(declaration.id) && requiredNativeScript) { + for (const property of declaration.id.properties) { + if (!t.isObjectProperty(property)) { + continue; + } + const key = property.key; + const value = property.value; + const keyName = t.isIdentifier(key) ? key.name : key.value; + if ( + UIKIT_DEFINITION_CALLEES.has(keyName) && + t.isIdentifier(value) + ) { + uikitDefinitionIdentifiers.add(value.name); + } + } + } + } + } + + return { + nativeScriptIdentifiers, + uikitDefinitionIdentifiers, + }; +} + function ensureNativeScriptIdentifier(programPath, state, t) { if (state.nativeScriptIdentifier) { return state.nativeScriptIdentifier; @@ -124,6 +212,86 @@ function ensureNativeScriptIdentifier(programPath, state, t) { return identifier.name; } +function isUIKitDefinitionCall(path, state, t) { + const callee = path.node.callee; + if ( + t.isIdentifier(callee) && + state.uikitDefinitionIdentifiers?.has(callee.name) + ) { + return true; + } + if ( + t.isMemberExpression(callee) && + !callee.computed && + t.isIdentifier(callee.object) && + t.isIdentifier(callee.property) && + state.nativeScriptIdentifiers?.has(callee.object.name) && + UIKIT_DEFINITION_CALLEES.has(callee.property.name) + ) { + return true; + } + return false; +} + +function propertyKeyName(property, t) { + const key = property.node.key; + if (t.isIdentifier(key)) { + return key.name; + } + if (t.isStringLiteral(key)) { + return key.value; + } + return null; +} + +function ensureWorkletDirective(functionNode, t) { + if (!functionNode.body) { + return; + } + if (!t.isBlockStatement(functionNode.body)) { + functionNode.body = t.blockStatement([ + t.returnStatement(functionNode.body), + ]); + } + const directives = functionNode.body.directives || []; + if (directives.some((directive) => directive.value?.value === 'worklet')) { + return; + } + functionNode.body.directives = [ + t.directive(t.directiveLiteral('worklet')), + ...directives, + ]; +} + +function workletizeUIKitDefinitionCallbacks(path, state, t) { + if (!isUIKitDefinitionCall(path, state, t)) { + return; + } + + const definition = path.get('arguments')[0]; + if (!definition || !definition.isObjectExpression()) { + return; + } + + for (const property of definition.get('properties')) { + if (property.isSpreadElement()) { + continue; + } + const keyName = propertyKeyName(property, t); + if (!UIKIT_WORKLET_CALLBACKS.has(keyName)) { + continue; + } + if (property.isObjectMethod()) { + ensureWorkletDirective(property.node, t); + } else if (property.isObjectProperty()) { + const value = property.get('value'); + if (value.isFunctionExpression() || value.isArrowFunctionExpression()) { + ensureWorkletDirective(value.node, t); + } + } + } +} + function isAlreadyWrapped(path, t) { const parent = path.parentPath; if (!parent || !parent.isCallExpression()) { @@ -144,6 +312,11 @@ function wrapDirectiveFunction(path, state, t) { if (!policy || isAlreadyWrapped(path, t)) { return; } + if (policy === 'ui') { + throw path.buildCodeFrameError( + 'NativeScript "use ui" callbacks are not supported in React Native. Use a Worklets "worklet" callback with NativeScript.runOnUI().', + ); + } const programPath = path.findParent((parentPath) => parentPath.isProgram()); const nativeScriptIdentifier = ensureNativeScriptIdentifier(programPath, state, t); @@ -152,7 +325,7 @@ function wrapDirectiveFunction(path, state, t) { t.callExpression( t.memberExpression( t.identifier(nativeScriptIdentifier), - t.identifier(policy === 'ui' ? 'uiInvoker' : 'jsInvoker'), + t.identifier('jsInvoker'), ), [original], ), @@ -165,8 +338,14 @@ module.exports = function nativeScriptReactNativeBabelPlugin({types: t}) { name: 'nativescript-react-native-thread-directives', visitor: { Program(path, state) { + const bindings = collectNativeScriptBindings(path, t); + state.nativeScriptIdentifiers = bindings.nativeScriptIdentifiers; + state.uikitDefinitionIdentifiers = bindings.uikitDefinitionIdentifiers; state.nativeScriptIdentifier = findNativeScriptIdentifier(path, t); }, + CallExpression(path, state) { + workletizeUIKitDefinitionCallbacks(path, state, t); + }, ArrowFunctionExpression(path, state) { wrapDirectiveFunction(path, state, t); }, diff --git a/packages/react-native/plugin/withNativeScriptReactNative.js b/packages/react-native/plugin/withNativeScriptReactNative.js index baa605207..d766c41ee 100644 --- a/packages/react-native/plugin/withNativeScriptReactNative.js +++ b/packages/react-native/plugin/withNativeScriptReactNative.js @@ -9,6 +9,8 @@ const DEFAULTS = { }; const BABEL_PLUGIN = '@nativescript/react-native/babel-plugin'; +const WORKLETS_BABEL_PLUGIN = 'react-native-worklets/plugin'; +const BABEL_PLUGINS = [BABEL_PLUGIN, WORKLETS_BABEL_PLUGIN]; const METADATA_CONFIG_FILE = 'nativescript.react-native.json'; function readBoolean(value, fallback) { @@ -89,7 +91,7 @@ function ensureBabelPlugin(projectRoot) { ' api.cache(true);', ' return {', " presets: ['babel-preset-expo'],", - ` plugins: ['${BABEL_PLUGIN}'],`, + ` plugins: [${BABEL_PLUGINS.map((plugin) => `'${plugin}'`).join(', ')}],`, ' };', '};', '', @@ -99,30 +101,33 @@ function ensureBabelPlugin(projectRoot) { } let source = fs.readFileSync(babelConfigPath, 'utf8'); - if (source.includes(BABEL_PLUGIN)) { + const missingPlugins = BABEL_PLUGINS.filter((plugin) => !source.includes(plugin)); + if (missingPlugins.length === 0) { return; } - const pluginEntry = `'${BABEL_PLUGIN}', `; + const pluginEntry = missingPlugins + .map((plugin) => `'${plugin}'`) + .join(', ') + ', '; if (/plugins\s*:\s*\[/.test(source)) { source = source.replace(/plugins\s*:\s*\[/, (match) => `${match}${pluginEntry}`); } else if (/return\s*\{/.test(source)) { source = source.replace( /return\s*\{/, - (match) => `${match}\n plugins: ['${BABEL_PLUGIN}'],`, + (match) => `${match}\n plugins: [${pluginEntry}],`, ); } else if (/module\.exports\s*=\s*\{/.test(source)) { source = source.replace( /module\.exports\s*=\s*\{/, - (match) => `${match}\n plugins: ['${BABEL_PLUGIN}'],`, + (match) => `${match}\n plugins: [${pluginEntry}],`, ); } else if (/export\s+default\s+\{/.test(source)) { source = source.replace( /export\s+default\s+\{/, - (match) => `${match}\n plugins: ['${BABEL_PLUGIN}'],`, + (match) => `${match}\n plugins: [${pluginEntry}],`, ); } else { - source += `\n// @nativescript/react-native: add '${BABEL_PLUGIN}' to your Babel plugins.\n`; + source += `\n// @nativescript/react-native: add ${missingPlugins.map((plugin) => `'${plugin}'`).join(' and ')} to your Babel plugins.\n`; } fs.writeFileSync(babelConfigPath, source); @@ -190,6 +195,8 @@ module.exports = withNativeScriptReactNative; module.exports.default = withNativeScriptReactNative; module.exports.withNativeScriptReactNative = withNativeScriptReactNative; module.exports.ensureBabelPlugin = ensureBabelPlugin; +module.exports.BABEL_PLUGIN = BABEL_PLUGIN; +module.exports.WORKLETS_BABEL_PLUGIN = WORKLETS_BABEL_PLUGIN; module.exports.ensureMetadataConfig = ensureMetadataConfig; module.exports.normalizeMetadataOptions = normalizeMetadataOptions; module.exports.pkg = pkg; diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts index 96c9b2688..4cb54f02e 100644 --- a/packages/react-native/src/NativeScriptNativeApi.ts +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -1,8 +1,13 @@ import type {TurboModule} from 'react-native'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; import {TurboModuleRegistry} from 'react-native'; export interface Spec extends TurboModule { readonly install: (metadataPath: string) => boolean; + readonly installWorkletRuntime: ( + runtimeHolder: UnsafeObject, + metadataPath: string, + ) => boolean; readonly isInstalled: () => boolean; readonly defaultMetadataPath: () => string; readonly getRuntimeBackend: () => string; diff --git a/packages/react-native/src/NativeScriptUIViewNativeComponent.ts b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts index 32b7cc947..e6b930c63 100644 --- a/packages/react-native/src/NativeScriptUIViewNativeComponent.ts +++ b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts @@ -1,11 +1,30 @@ import type {HostComponent, ViewProps} from 'react-native'; +import type { + DirectEventHandler, + Int32, +} from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +export type HostReadyEvent = { + hostReadyId: string; + hostId: string; + nativeViewHandle: string; + childrenViewHandle: string; + controllerHandle: string; + hasChildren: boolean; +}; + export interface NativeProps extends ViewProps { + hostId?: string; + hostReadyId?: string; nativeViewHandle?: string; childrenViewHandle?: string; controllerHandle?: string; + detachControllerView?: boolean; debugName?: string; + updateRevision?: Int32; + mountedRevision?: Int32; + onHostReady?: DirectEventHandler; } export default codegenNativeComponent( diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts index 0437ac236..3e15e684d 100644 --- a/packages/react-native/src/index.d.ts +++ b/packages/react-native/src/index.d.ts @@ -4,8 +4,8 @@ import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, -} from 'react'; -import type {ViewProps} from 'react-native'; +} from "react"; +import type { ViewProps } from "react-native"; export type NativeApiHost = { runtime?: string; @@ -32,21 +32,49 @@ export type NativeApiHost = { getEnum?: (name: string) => unknown; getStruct?: (name: string) => unknown; getUnion?: (name: string) => unknown; - runOnUI?: (callback?: () => void) => Promise; [name: string]: unknown; }; export type InstallOptions = { + /** + * Install Objective-C classes/functions/constants as RN runtime globals. + * Native UI should run through worklets; React Native defaults this off so + * UIKit cannot be touched from the RN JavaScript thread by accident. + */ globals?: boolean; }; -export type UIKitSizingMode = 'fill' | 'intrinsic' | 'sizeThatFits' | 'autoLayout'; +export type NativeScriptWorklets = { + getUIRuntimeHolder: () => object; + isWorkletFunction: (value: unknown) => boolean; + runOnUIAsync: ( + callback: (...args: Args) => ReturnValue | Promise, + ...args: Args + ) => Promise; +}; + +export type UIKitSizingMode = + | "fill" + | "intrinsic" + | "sizeThatFits" + | "autoLayout"; export type UIKitLayoutOptions = { sizing?: UIKitSizingMode; - defaultSize?: {width?: number; height?: number}; - minSize?: {width?: number; height?: number}; - maxSize?: {width?: number; height?: number}; + defaultSize?: { width?: number; height?: number }; + minSize?: { width?: number; height?: number }; + maxSize?: { width?: number; height?: number }; +}; + +export type UIKitHostReadyEvent = { + nativeEvent: { + hostReadyId: string; + hostId: string; + nativeViewHandle: string; + childrenViewHandle: string; + controllerHandle: string; + hasChildren: boolean; + }; }; export type UIKitViewContext = { @@ -59,11 +87,12 @@ export type UIKitViewContext = { ? Payload : unknown, ): void; - targetAction( - control: unknown, - events: unknown, - callback: () => void, - ): void; + targetAction(control: unknown, events: unknown, callback: () => void): void; + gestureAction(gesture: unknown, callback: (gesture: unknown) => void): void; + actionTarget(callback: (sender: unknown) => void): { + target: unknown; + action: string; + }; delegate( object: unknown, protocolRef: unknown, @@ -83,12 +112,26 @@ export type UIKitViewContext = { release(value?: unknown): void; dispose(callback: () => void): void; invalidateLayout(): void; + loadImage( + source: unknown, + options: NativeScriptImageLoadOptions, + callback: NativeScriptImageLoadCallback, + ): boolean; }; -export type NativeScriptCallbackThread = 'ui' | 'js'; -export type NativeScriptInvokedCallback any> = T & { - readonly __nativeScriptCallbackThread?: NativeScriptCallbackThread; +export type NativeScriptImageLoadOptions = { + template?: boolean; }; +export type NativeScriptImageLoadCallback = ( + image: unknown | null, + error: Error | null, +) => void; +export type NativeScriptCallbackThread = "js" | "runtime"; +export type NativeScriptInvokedCallback any> = + T & { + readonly __nativeScriptCallbackThread?: NativeScriptCallbackThread; + readonly __nativeScriptWrappedCallback?: T; + }; export type NativeRetainer = { readonly size: number; retain(value: T): T; @@ -103,7 +146,7 @@ export type NativeDelegateOwner = { export type NativeProtocolReference = string | object | Function; export type CreateDelegateOptions = { name?: string; - thread?: NativeScriptCallbackThread | 'caller'; + thread?: NativeScriptCallbackThread | "caller"; retainer?: NativeRetainer; owner?: NativeDelegateOwner; assignTo?: { @@ -112,6 +155,12 @@ export type CreateDelegateOptions = { }; }; +export type UIKitDisposeResult = + | void + | { + removeHostView?: boolean; + }; + export type UIKitViewDefinition = { /** * Human-readable name for this UIKit view definition. This names the JS @@ -130,7 +179,9 @@ export type UIKitViewDefinition = { */ displayName?: string; layout?: UIKitLayoutOptions; - create: (ctx: UIKitViewContext & Readonly) => NativeView; + create: ( + ctx: UIKitViewContext & Readonly, + ) => NativeView; update?: ( view: NativeView, props: Readonly, @@ -146,7 +197,7 @@ export type UIKitViewDefinition = { view: NativeView, props: Readonly, ctx?: UIKitViewContext, - ) => void; + ) => UIKitDisposeResult; nativeProps?: ( props: Readonly, ) => Partial | undefined; @@ -155,14 +206,24 @@ export type UIKitViewDefinition = { export type UIKitViewRef = { readonly nativeView: NativeView | null; runOnUI: (callback: (view: NativeView) => T) => Promise; - measureNative: () => Promise<{width: number; height: number}>; + measureNative: () => Promise<{ width: number; height: number }>; invalidateNativeLayout: () => void; }; -export type UIKitViewComponent = - ForwardRefExoticComponent< - PropsWithoutRef & RefAttributes> - >; +export type UIKitHostViewProps = ViewProps & { + attachController?: boolean; + attachControllerView?: boolean; + attachNativeView?: boolean; + onHostReady?: (event: UIKitHostReadyEvent) => void; +}; + +export type UIKitViewComponent< + Props extends object, + NativeView = unknown, +> = ForwardRefExoticComponent< + PropsWithoutRef & + RefAttributes> +>; export type UIKitContainerResult = { rootView: RootView; @@ -175,7 +236,7 @@ export type UIKitContainerDefinition< ChildrenView = unknown, > = Omit< UIKitViewDefinition>, - 'create' | 'update' | 'mounted' | 'dispose' + "create" | "update" | "mounted" | "dispose" > & { create: ( ctx: UIKitViewContext & Readonly, @@ -195,16 +256,18 @@ export type UIKitContainerDefinition< view: UIKitContainerResult, props: Readonly, ctx?: UIKitViewContext, - ) => void; + ) => UIKitDisposeResult; }; export type UIViewControllerDefinition< Props extends object, Controller = unknown, -> = Omit, 'create'> & { +> = Omit, "create"> & { createController: ( ctx: UIKitViewContext & Readonly, ) => Controller; + hostView?: (controller: Controller) => unknown; + childrenView?: (controller: Controller) => unknown; }; export function init(metadataPath?: string, options?: InstallOptions): boolean; @@ -213,20 +276,37 @@ export function installGlobals(): boolean; export function isInstalled(): boolean; export function defaultMetadataPath(): string; export function getRuntimeBackend(): string; -export function runOnUI(callback?: () => void): Promise; +export function installWorklets( + worklets?: NativeScriptWorklets, + metadataPath?: string, +): boolean; +export function runOnUI( + callback: (...args: Args) => ReturnValue | Promise, + ...args: Args +): Promise; export function uiInvoker any>( callback: T, -): NativeScriptInvokedCallback; +): never; export function jsInvoker any>( callback: T, ): NativeScriptInvokedCallback; +export function runtimeInvoker any>( + callback: T, +): NativeScriptInvokedCallback; export function eventBridge any>( callback: T, - thread?: NativeScriptCallbackThread | 'caller', + thread?: NativeScriptCallbackThread | "caller", ): T | NativeScriptInvokedCallback; export const createEventBridge: typeof eventBridge; export function isMainThread(): boolean; export function assertUIKitThread(message?: string): void; +export function refreshUIKitHostView(view: unknown): boolean; +export function refreshUIKitHostViewHandle(viewHandle: string): boolean; +export function loadImage( + source: unknown, + options: NativeScriptImageLoadOptions, + callback: NativeScriptImageLoadCallback, +): boolean; export function warnIfNotUIKitThread(message?: string): boolean; export function createRetainer(): NativeRetainer; export function retain(value: T): T; @@ -268,6 +348,7 @@ declare const NativeScript: { defineUIKitView: typeof defineUIKitView; defineUIViewController: typeof defineUIViewController; getRuntimeBackend: typeof getRuntimeBackend; + installWorklets: typeof installWorklets; assertUIKitThread: typeof assertUIKitThread; createDelegate: typeof createDelegate; createEventBridge: typeof createEventBridge; @@ -282,7 +363,9 @@ declare const NativeScript: { loadFramework: typeof loadFramework; release: typeof release; retain: typeof retain; + refreshUIKitHostView: typeof refreshUIKitHostView; runOnUI: typeof runOnUI; + runtimeInvoker: typeof runtimeInvoker; uiInvoker: typeof uiInvoker; warnIfNotUIKitThread: typeof warnIfNotUIKitThread; }; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 31fca7585..c7b9a2bb5 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -2,18 +2,20 @@ import React, { forwardRef, useEffect, useImperativeHandle, + useLayoutEffect, useRef, useState, -} from 'react'; +} from "react"; import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, -} from 'react'; -import type {ViewProps} from 'react-native'; -import {StyleSheet} from 'react-native'; -import NativeScriptNativeApi from './NativeScriptNativeApi'; -import NativeScriptUIViewNativeComponent from './NativeScriptUIViewNativeComponent'; +} from "react"; +import type { ViewProps } from "react-native"; +import NativeScriptNativeApi from "./NativeScriptNativeApi"; +import NativeScriptUIViewNativeComponent from "./NativeScriptUIViewNativeComponent"; + +declare const require: (id: string) => any; type NativeApiHost = { metadata?: { @@ -31,21 +33,49 @@ type NativeApiHost = { getEnum?: (name: string) => unknown; getStruct?: (name: string) => unknown; getUnion?: (name: string) => unknown; - runOnUI?: (callback?: () => void) => Promise; [name: string]: unknown; }; export type InstallOptions = { + /** + * Install Objective-C classes/functions/constants as RN runtime globals. + * Native UI should run through worklets; React Native defaults this off so + * UIKit cannot be touched from the RN JavaScript thread by accident. + */ globals?: boolean; }; -export type UIKitSizingMode = 'fill' | 'intrinsic' | 'sizeThatFits' | 'autoLayout'; +export type NativeScriptWorklets = { + getUIRuntimeHolder: () => object; + isWorkletFunction: (value: unknown) => boolean; + runOnUIAsync: ( + callback: (...args: Args) => ReturnValue | Promise, + ...args: Args + ) => Promise; +}; + +export type UIKitSizingMode = + | "fill" + | "intrinsic" + | "sizeThatFits" + | "autoLayout"; export type UIKitLayoutOptions = { sizing?: UIKitSizingMode; - defaultSize?: {width?: number; height?: number}; - minSize?: {width?: number; height?: number}; - maxSize?: {width?: number; height?: number}; + defaultSize?: { width?: number; height?: number }; + minSize?: { width?: number; height?: number }; + maxSize?: { width?: number; height?: number }; +}; + +export type UIKitHostReadyEvent = { + nativeEvent: { + hostReadyId: string; + hostId: string; + nativeViewHandle: string; + childrenViewHandle: string; + controllerHandle: string; + hasChildren: boolean; + }; }; export type UIKitViewContext = { @@ -58,11 +88,12 @@ export type UIKitViewContext = { ? Payload : unknown, ): void; - targetAction( - control: unknown, - events: unknown, - callback: () => void, - ): void; + targetAction(control: unknown, events: unknown, callback: () => void): void; + gestureAction(gesture: unknown, callback: (gesture: unknown) => void): void; + actionTarget(callback: (sender: unknown) => void): { + target: unknown; + action: string; + }; delegate( object: unknown, protocolRef: unknown, @@ -82,10 +113,30 @@ export type UIKitViewContext = { release(value?: unknown): void; dispose(callback: () => void): void; invalidateLayout(): void; + loadImage( + source: unknown, + options: NativeScriptImageLoadOptions, + callback: NativeScriptImageLoadCallback, + ): boolean; }; -type UIKitCreateArgument = - UIKitViewContext & Readonly; +type UIKitCreateArgument = UIKitViewContext & + Readonly; + +export type NativeScriptImageLoadOptions = { + template?: boolean; +}; + +export type NativeScriptImageLoadCallback = ( + image: unknown | null, + error: Error | null, +) => void; + +export type UIKitDisposeResult = + | void + | { + removeHostView?: boolean; + }; export type UIKitViewDefinition = { name?: string; @@ -108,7 +159,7 @@ export type UIKitViewDefinition = { view: NativeView, props: Readonly, ctx?: UIKitViewContext, - ) => void; + ) => UIKitDisposeResult; nativeProps?: ( props: Readonly, ) => Partial | undefined; @@ -117,19 +168,26 @@ export type UIKitViewDefinition = { export type UIKitViewRef = { readonly nativeView: NativeView | null; runOnUI: (callback: (view: NativeView) => T) => Promise; - measureNative: () => Promise<{width: number; height: number}>; + measureNative: () => Promise<{ width: number; height: number }>; invalidateNativeLayout: () => void; }; -export type UIKitViewComponent = - ForwardRefExoticComponent< - PropsWithoutRef & RefAttributes> - >; +export type UIKitHostViewProps = ViewProps & { + attachController?: boolean; + attachControllerView?: boolean; + attachNativeView?: boolean; + onHostReady?: (event: UIKitHostReadyEvent) => void; +}; -export type UIKitContainerResult< - RootView = unknown, - ChildrenView = unknown, -> = { +export type UIKitViewComponent< + Props extends object, + NativeView = unknown, +> = ForwardRefExoticComponent< + PropsWithoutRef & + RefAttributes> +>; + +export type UIKitContainerResult = { rootView: RootView; childrenView: ChildrenView; }; @@ -140,7 +198,7 @@ export type UIKitContainerDefinition< ChildrenView = unknown, > = Omit< UIKitViewDefinition>, - 'create' | 'update' | 'mounted' | 'dispose' + "create" | "update" | "mounted" | "dispose" > & { create: ( ctx: UIKitCreateArgument, @@ -160,32 +218,40 @@ export type UIKitContainerDefinition< view: UIKitContainerResult, props: Readonly, ctx?: UIKitViewContext, - ) => void; + ) => UIKitDisposeResult; }; export type UIViewControllerDefinition< Props extends object, Controller = unknown, -> = Omit< - UIKitViewDefinition, - 'create' -> & { +> = Omit, "create"> & { createController: (ctx: UIKitCreateArgument) => Controller; + hostView?: (controller: Controller) => unknown; + childrenView?: (controller: Controller) => unknown; }; -const nativeApiGlobalName = '__nativeScriptNativeApi'; -const nativeApiGlobalCacheName = '__nativeScriptNativeApiGlobalCache'; -const nativeApiTypeCodeKey = '__nativeApiTypeCode'; -const nativeApiCallbackThreadKey = '__nativeScriptCallbackThread'; -const nativeApiWrappedCallbackKey = '__nativeScriptWrappedCallback'; +const nativeApiGlobalName = "__nativeScriptNativeApi"; +const nativeApiGlobalCacheName = "__nativeScriptNativeApiGlobalCache"; +const nativeApiTypeCodeKey = "__nativeApiTypeCode"; +const nativeApiCallbackThreadKey = "__nativeScriptCallbackThread"; +const nativeApiWrappedCallbackKey = "__nativeScriptWrappedCallback"; const nativeClassWrappers = new WeakMap(); -export type NativeScriptCallbackThread = 'ui' | 'js'; +export type NativeScriptCallbackThread = "js" | "runtime"; type AnyFunction = (...args: any[]) => any; export type NativeScriptInvokedCallback = T & { readonly __nativeScriptCallbackThread?: NativeScriptCallbackThread; + readonly __nativeScriptWrappedCallback?: T; }; +const nativeCallbackMetadataSkipKeys = new Set([ + "length", + "name", + "prototype", + "arguments", + "caller", +]); + export type NativeRetainer = { readonly size: number; retain(value: T): T; @@ -201,7 +267,7 @@ export type NativeDelegateOwner = { export type CreateDelegateOptions = { name?: string; - thread?: NativeScriptCallbackThread | 'caller'; + thread?: NativeScriptCallbackThread | "caller"; retainer?: NativeRetainer; owner?: NativeDelegateOwner; assignTo?: { @@ -221,7 +287,9 @@ function nativeApiHost(): NativeApiHost | undefined { function requireNativeApiHost(): NativeApiHost { const api = nativeApiHost(); if (!api) { - throw new Error('NativeScript Native API JSI host object was not installed'); + throw new Error( + "NativeScript Native API JSI host object was not installed", + ); } return api; } @@ -229,7 +297,7 @@ function requireNativeApiHost(): NativeApiHost { function nativeApiGlobalCache(): Record { const globalObject = globalThis as Record; const existing = globalObject[nativeApiGlobalCacheName]; - if (existing && typeof existing === 'object') { + if (existing && typeof existing === "object") { return existing as Record; } @@ -296,57 +364,58 @@ export function release(value?: unknown): void { } 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', + "accessible", + "accessibilityActions", + "accessibilityElementsHidden", + "accessibilityHint", + "accessibilityIgnoresInvertColors", + "accessibilityLabel", + "accessibilityLanguage", + "accessibilityLiveRegion", + "accessibilityRole", + "accessibilityState", + "accessibilityValue", + "accessibilityViewIsModal", + "children", + "collapsable", + "focusable", + "hitSlop", + "id", + "importantForAccessibility", + "nativeID", + "needsOffscreenAlphaCompositing", + "onAccessibilityAction", + "onAccessibilityEscape", + "onAccessibilityTap", + "onHostReady", + "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, + props: Props & UIKitHostViewProps, definition: UIKitViewDefinition, ): { nativeProps: ViewProps; - pluginProps: Props & ViewProps; + pluginProps: Props & UIKitHostViewProps; } { const nativeProps: Record = {}; const pluginProps: Record = {}; @@ -354,8 +423,8 @@ function splitUIKitViewProps( for (const [key, value] of Object.entries(props)) { if ( hostViewPropNames.has(key) || - key.startsWith('accessibility') || - key.startsWith('aria-') + key.startsWith("accessibility") || + key.startsWith("aria-") ) { nativeProps[key] = value; } else { @@ -367,41 +436,77 @@ function splitUIKitViewProps( return { nativeProps: nativeProps as ViewProps, - pluginProps: pluginProps as Props & ViewProps, + pluginProps: pluginProps as Props & UIKitHostViewProps, }; } function nativeHandleForUIKitView(view: unknown): string { + "worklet"; + const interop = (globalThis as Record).interop; - if (!interop || typeof interop.handleof !== 'function') { - throw new Error('NativeScript interop globals are not installed'); + 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'); + throw new Error( + "UIKit view definition returned a value without a native handle", + ); } - if (typeof pointer.toHexString === 'function') { + if (typeof pointer.toHexString === "function") { const text = pointer.toHexString(); - if (typeof text === 'string' && text.length > 0) { + if (typeof text === "string" && text.length > 0) { return text; } } - if (typeof pointer.address === 'string' && pointer.address.length > 0) { + if (typeof pointer.address === "string" && pointer.address.length > 0) { return pointer.address; } - if (typeof pointer.address === 'number') { + if (typeof pointer.address === "number") { return String(pointer.address); } - if (typeof pointer.toNumber === 'function') { + if (typeof pointer.toNumber === "function") { return String(pointer.toNumber()); } - throw new Error('UIKit view native handle could not be read'); + throw new Error("UIKit view native handle could not be read"); +} + +function nativeHandleOrUndefined(value: unknown): string | undefined { + "worklet"; + + return value == null ? undefined : nativeHandleForUIKitView(value); +} + +function nativeHandleForNSObject(value: unknown): string | undefined { + "worklet"; + + if (value == null) { + return undefined; + } + const interop = (globalThis as Record).interop; + const pointer = interop?.handleof?.(value); + if (!pointer) { + return undefined; + } + if (typeof pointer.toHexString === "function") { + return pointer.toHexString(); + } + if (typeof pointer.address === "string") { + return pointer.address; + } + if (typeof pointer.address === "number") { + return String(pointer.address); + } + if (typeof pointer.toNumber === "function") { + return String(pointer.toNumber()); + } + return undefined; } function ensureNativeScriptInstalled(): void { @@ -421,7 +526,7 @@ function defineLazyNativeGlobal( if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { const descriptor = Object.getOwnPropertyDescriptor(globalThis, name); - if (descriptor && 'value' in descriptor) { + if (descriptor && "value" in descriptor) { cacheNativeGlobal(name, descriptor.value); } return; @@ -458,7 +563,7 @@ function defineLazyNativeGlobal( } function wrapAggregateConstructor(nativeConstructor: unknown): unknown { - if (typeof nativeConstructor !== 'function') { + if (typeof nativeConstructor !== "function") { return nativeConstructor; } @@ -472,13 +577,14 @@ function wrapAggregateConstructor(nativeConstructor: unknown): unknown { configurable: true, enumerable: false, value(value: unknown) { - if (!value || typeof value !== 'object') { + 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 + actual.name === + (nativeConstructor as Record).runtimeName ); }, }); @@ -487,12 +593,12 @@ function wrapAggregateConstructor(nativeConstructor: unknown): unknown { } for (const key of [ - 'kind', - 'runtimeName', - 'metadataOffset', - 'sizeof', - 'fields', - 'equals', + "kind", + "runtimeName", + "metadataOffset", + "sizeof", + "fields", + "equals", ]) { try { Object.defineProperty(aggregate, key, { @@ -512,7 +618,7 @@ function wrapAggregateConstructor(nativeConstructor: unknown): unknown { function wrapNativeClass(nativeClass: unknown): unknown { if ( !nativeClass || - (typeof nativeClass !== 'object' && typeof nativeClass !== 'function') + (typeof nativeClass !== "object" && typeof nativeClass !== "function") ) { return nativeClass; } @@ -524,32 +630,34 @@ function wrapNativeClass(nativeClass: unknown): unknown { const constructable = function NativeScriptNativeClass(...args: unknown[]) { const cls = nativeClass as Record; - if (args.length > 0 && typeof cls.construct === 'function') { + 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'); + if (typeof cls.alloc !== "function") { + throw new Error("Native class cannot be allocated"); } const instance = cls.alloc(); - if (instance && typeof instance.init === 'function') { + if (instance && typeof instance.init === "function") { return instance.init(); } return instance; }; - Object.defineProperty(constructable, 'new', { + Object.defineProperty(constructable, "new", { configurable: true, enumerable: false, writable: false, value(...args: unknown[]) { if (args.length !== 0) { - throw new Error('new does not take arguments; use invoke for an explicit Objective-C selector.'); + throw new Error( + "new does not take arguments; use invoke for an explicit Objective-C selector.", + ); } return constructable(); }, }); - Object.defineProperty(constructable, '__nativeApiClass', { + Object.defineProperty(constructable, "__nativeApiClass", { configurable: false, enumerable: false, writable: false, @@ -563,14 +671,18 @@ function wrapNativeClass(nativeClass: unknown): unknown { configurable: true, enumerable: false, value(value: unknown) { - if (!value || typeof value !== 'object') { + 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)); + 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 @@ -579,7 +691,7 @@ function wrapNativeClass(nativeClass: unknown): unknown { const expectedName = cls.runtimeName ?? cls.name; const actualName = (value as Record).className; - return typeof expectedName === 'string' && actualName === expectedName; + return typeof expectedName === "string" && actualName === expectedName; }, }); } catch { @@ -594,8 +706,10 @@ function wrapNativeClass(nativeClass: unknown): unknown { if (cachedNativeFunctions.has(property)) { return cachedNativeFunctions.get(property); } - const nativeValue = (nativeClass as Record)[property]; - if (typeof nativeValue === 'function') { + const nativeValue = (nativeClass as Record)[ + property + ]; + if (typeof nativeValue === "function") { cachedNativeFunctions.set(property, nativeValue); try { Object.defineProperty(target, property, { @@ -627,7 +741,7 @@ function wrapInteropFactory( nativeFactory: unknown, properties: Record, ): unknown { - if (typeof nativeFactory !== 'function') { + if (typeof nativeFactory !== "function") { return nativeFactory; } @@ -640,10 +754,12 @@ function wrapInteropFactory( }; try { - const nativePrototype = (nativeFactory as {prototype?: unknown}).prototype; + const nativePrototype = (nativeFactory as { prototype?: unknown }) + .prototype; if ( nativePrototype && - (typeof nativePrototype === 'object' || typeof nativePrototype === 'function') + (typeof nativePrototype === "object" || + typeof nativePrototype === "function") ) { constructable.prototype = nativePrototype; } @@ -659,7 +775,7 @@ function wrapInteropFactory( value(value: unknown) { return ( Boolean(value) && - typeof value === 'object' && + typeof value === "object" && (value as Record).kind === properties.kind ); }, @@ -681,7 +797,7 @@ function wrapInteropFactory( } } - Object.defineProperty(constructable, '__nativeScriptConstructable', { + Object.defineProperty(constructable, "__nativeScriptConstructable", { configurable: false, enumerable: false, writable: false, @@ -695,7 +811,7 @@ function installInteropConstructors(): void { const interop = (globalThis as Record).interop as | Record | undefined; - if (!interop || typeof interop !== 'object') { + if (!interop || typeof interop !== "object") { return; } @@ -703,7 +819,7 @@ function installInteropConstructors(): void { const pointerType = (interop.types as Record | undefined) ?.pointer; let pointerSize: unknown = undefined; - if (typeof sizeof === 'function' && pointerType !== undefined) { + if (typeof sizeof === "function" && pointerType !== undefined) { try { pointerSize = sizeof(pointerType); } catch { @@ -712,22 +828,26 @@ function installInteropConstructors(): void { } interop.Pointer = wrapInteropFactory(interop.Pointer, { - kind: 'pointer', + kind: "pointer", sizeof: pointerSize, }); interop.Reference = wrapInteropFactory(interop.Reference, { - kind: 'reference', + kind: "reference", + sizeof: pointerSize, + }); + interop.Block = wrapInteropFactory(interop.Block, { + kind: "block", sizeof: pointerSize, }); interop.FunctionReference = wrapInteropFactory(interop.FunctionReference, { - kind: 'functionReference', + kind: "functionReference", sizeof: pointerSize, }); const types = interop.types as Record | undefined; - if (types && typeof types === 'object') { + if (types && typeof types === "object") { for (const [name, value] of Object.entries(types)) { - if (typeof value !== 'number') { + if (typeof value !== "number") { continue; } const boxed = { @@ -758,25 +878,25 @@ function defineInlineFunction(name: string, value: Function): void { } function installInlineFunctions(): void { - const makePoint = (x: number, y: number) => ({x, y}); - const makeSize = (width: number, height: number) => ({width, height}); + 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}, + 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) => ({ + 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', + "UIEdgeInsetsMake", (top: number, left: number, bottom: number, right: number) => ({ top, left, @@ -794,7 +914,9 @@ export function installGlobals(): boolean { const classNames = api.metadata?.classNames?.() ?? []; for (const name of classNames) { - defineLazyNativeGlobal(name, (className) => wrapNativeClass(api[className])); + defineLazyNativeGlobal(name, (className) => + wrapNativeClass(api[className]), + ); } const functionNames = api.metadata?.functionNames?.() ?? []; @@ -817,11 +939,12 @@ export function installGlobals(): boolean { const enumNames = api.metadata?.enumNames?.() ?? []; for (const name of enumNames) { - const resolveEnum = (enumName: string) => api.getEnum?.(enumName) ?? api[enumName]; + const resolveEnum = (enumName: string) => + api.getEnum?.(enumName) ?? api[enumName]; defineLazyNativeGlobal(name, resolveEnum); const enumValue = resolveEnum(name); - if (!enumValue || typeof enumValue !== 'object') { + if (!enumValue || typeof enumValue !== "object") { continue; } for (const memberName of Object.keys(enumValue)) { @@ -840,7 +963,9 @@ export function installGlobals(): boolean { defineLazyNativeGlobal( name, (structName) => - wrapAggregateConstructor(api.getStruct?.(structName) ?? api[structName]), + wrapAggregateConstructor( + api.getStruct?.(structName) ?? api[structName], + ), true, ); } @@ -858,19 +983,20 @@ export function installGlobals(): boolean { return true; } -export function init( - metadataPath = '', - options: InstallOptions = {}, -): boolean { - const installed = NativeScriptNativeApi.isInstalled() - || NativeScriptNativeApi.install(metadataPath); +export function init(metadataPath = "", options: InstallOptions = {}): boolean { + const installed = + NativeScriptNativeApi.isInstalled() || + NativeScriptNativeApi.install(metadataPath); if (installed) { installInteropConstructors(); installInlineFunctions(); } - if (installed && options.globals !== false) { + if (installed && options.globals === true) { installGlobals(); } + if (installed) { + ensureWorkletsInstalled(metadataPath); + } return installed; } @@ -888,22 +1014,355 @@ 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', +let workletsAdapter: NativeScriptWorklets | undefined; +const workletsPackageName = "react-native-worklets"; + +function workletsSetupError(reason: string): Error { + return new Error( + `${reason}. Install ${workletsPackageName}, add ${workletsPackageName}/plugin to your Babel plugins, and run pod install so RNWorklets is linked.`, + ); +} + +function requireReactNativeWorklets(): NativeScriptWorklets { + try { + return require(workletsPackageName) as NativeScriptWorklets; + } catch (error) { + throw workletsSetupError( + `NativeScript.runOnUI requires ${workletsPackageName}`, + ); + } +} + +function validateWorkletsModule( + worklets: NativeScriptWorklets, +): NativeScriptWorklets { + if ( + worklets == null || + typeof worklets.getUIRuntimeHolder !== "function" || + typeof worklets.isWorkletFunction !== "function" || + typeof worklets.runOnUIAsync !== "function" + ) { + throw workletsSetupError( + "NativeScript.runOnUI received an incompatible Worklets module", + ); + } + return worklets; +} + +function installIdleAwareWorkletsFrameLoop(): boolean { + "worklet"; + + const globalObject = globalThis as Record; + if (globalObject.__nativeScriptIdleAwareWorkletsFrameLoop === true) { + return true; + } + + const nativeRequestAnimationFrame = + globalObject.__nativeRequestAnimationFrame; + const callMicrotasks = globalObject.__callMicrotasks; + + if ( + typeof nativeRequestAnimationFrame !== "function" || + typeof callMicrotasks !== "function" + ) { + return false; + } + + globalObject.__nativeScriptIdleAwareWorkletsFrameLoop = true; + globalObject.__nativeScriptNativeRequestAnimationFrame = + nativeRequestAnimationFrame; + + let queuedCallbacks: Array<(timestamp: number) => void> = []; + let queuedCallbacksBegin = 0; + let queuedCallbacksEnd = 0; + let flushedCallbacks = queuedCallbacks; + let flushedCallbacksBegin = 0; + let flushedCallbacksEnd = 0; + let queuedFinalizers: Array<() => void> = []; + let nativeFlushScheduled = false; + + const NSTimerClass = globalObject.NSTimer; + const NSRunLoopClass = globalObject.NSRunLoop; + if ( + NSTimerClass == null || + NSRunLoopClass == null || + NSRunLoopClass.mainRunLoop == null + ) { + throw new Error("NativeScript Worklets timers require NSTimer/NSRunLoop"); + } + + type NativeTimer = { invalidate?: () => void }; + const nativeTimers = new Map(); + let nextNativeTimerHandle = 1; + + function runtimeTimerInvoker any>( + callback: T, + ): T { + const wrapped = function nativeScriptWorkletTimerCallback( + this: unknown, + ...args: unknown[] + ) { + return callback.apply(this, args); + } as T; + Object.defineProperties(wrapped, { + __nativeScriptCallbackThread: { + configurable: false, + enumerable: false, + writable: false, + value: "runtime", + }, + __nativeScriptWrappedCallback: { + configurable: false, + enumerable: false, + writable: false, + value: callback, + }, + }); + return wrapped; + } + + function normalizeTimerDelay(delay: unknown): number { + const numericDelay = + typeof delay === "number" && Number.isFinite(delay) ? delay : 0; + return Math.max(0.001, numericDelay / 1000); + } + + function scheduleNativeTimer( + callback: (...args: unknown[]) => void, + delay: unknown, + repeats: boolean, + args: unknown[], + ): number { + if (typeof callback !== "function") { + throw new TypeError("NativeScript Worklets timer expects a callback"); + } + + const handle = nextNativeTimerHandle++; + const fireTimer = runtimeTimerInvoker((timer: NativeTimer) => { + if (!nativeTimers.has(handle)) { + return; + } + if (!repeats) { + nativeTimers.delete(handle); + } + callback(...args); + callMicrotasks(); + if (!repeats) { + timer?.invalidate?.(); + } + }); + + const interval = normalizeTimerDelay(delay); + const timer = + typeof NSTimerClass.timerWithTimeIntervalRepeatsBlock === "function" + ? NSTimerClass.timerWithTimeIntervalRepeatsBlock( + interval, + repeats, + fireTimer, + ) + : NSTimerClass.scheduledTimerWithTimeIntervalRepeatsBlock( + interval, + repeats, + fireTimer, + ); + + nativeTimers.set(handle, timer); + if (typeof NSTimerClass.timerWithTimeIntervalRepeatsBlock === "function") { + NSRunLoopClass.mainRunLoop.addTimerForMode( + timer, + "kCFRunLoopCommonModes", + ); + } + return handle; + } + + function clearNativeTimer(handle: unknown) { + if (typeof handle !== "number") { + return; + } + const timer = nativeTimers.get(handle); + nativeTimers.delete(handle); + timer?.invalidate?.(); + } + + function hasPendingFrameWork() { + return queuedCallbacks.length > 0 || queuedFinalizers.length > 0; + } + + function executeQueue(timestamp: number) { + flushedCallbacks = queuedCallbacks; + queuedCallbacks = []; + + flushedCallbacksBegin = queuedCallbacksBegin; + flushedCallbacksEnd = queuedCallbacksEnd; + queuedCallbacksBegin = queuedCallbacksEnd; + + for (const callback of flushedCallbacks) { + callback(timestamp); + } + + flushedCallbacksBegin = flushedCallbacksEnd; + callMicrotasks(); + + const finalizers = queuedFinalizers; + queuedFinalizers = []; + for (const finalizer of finalizers) { + finalizer(); + } + } + + function flushQueue(timestamp: number) { + globalObject.__frameTimestamp = timestamp; + executeQueue(timestamp); + globalObject.__frameTimestamp = undefined; + } + + function nativeFlushQueue(timestamp: number) { + nativeFlushScheduled = false; + flushQueue(timestamp); + if (hasPendingFrameWork()) { + scheduleNativeFlush(); + } + } + + function scheduleNativeFlush() { + if (nativeFlushScheduled) { + return; + } + nativeFlushScheduled = true; + nativeRequestAnimationFrame(nativeFlushQueue); + } + + globalObject.requestAnimationFrame = ( + callback: (timestamp: number) => void, + ): number => { + const handle = queuedCallbacksEnd; + queuedCallbacksEnd += 1; + queuedCallbacks.push(callback); + scheduleNativeFlush(); + return handle; + }; + + globalObject.cancelAnimationFrame = (handle: number) => { + if (handle < flushedCallbacksBegin || handle >= queuedCallbacksEnd) { + return; + } + + if (handle < flushedCallbacksEnd) { + flushedCallbacks[handle - flushedCallbacksBegin] = () => undefined; + } else { + queuedCallbacks[handle - queuedCallbacksBegin] = () => undefined; + } + }; + + globalObject.requestAnimationFrameFinalizer = (callback: () => void) => { + queuedFinalizers.push(callback); + scheduleNativeFlush(); + }; + + globalObject.setTimeout = ( + callback: (...args: unknown[]) => void, + delay?: unknown, + ...args: unknown[] + ) => scheduleNativeTimer(callback, delay, false, args); + globalObject.clearTimeout = clearNativeTimer; + globalObject.setInterval = ( + callback: (...args: unknown[]) => void, + delay?: unknown, + ...args: unknown[] + ) => scheduleNativeTimer(callback, delay, true, args); + globalObject.clearInterval = clearNativeTimer; + + globalObject.__flushAnimationFrame = (eventTimestamp: number) => { + nativeFlushScheduled = false; + flushQueue(eventTimestamp); + if (hasPendingFrameWork()) { + scheduleNativeFlush(); + } + }; + + // Stop react-native-worklets' startup frame pump. The replacements above + // schedule the native display link only when worklet callbacks are pending. + globalObject.__nativeRequestAnimationFrame = () => undefined; + + return true; +} + +function ensureWorkletsInstalled(metadataPath = ""): NativeScriptWorklets { + if (workletsAdapter) { + return workletsAdapter; + } + installWorklets(requireReactNativeWorklets(), metadataPath); + return workletsAdapter as NativeScriptWorklets; +} + +export function installWorklets( + worklets: NativeScriptWorklets = requireReactNativeWorklets(), + metadataPath = "", +): boolean { + if (!NativeScriptNativeApi.isInstalled()) { + const installed = NativeScriptNativeApi.install(metadataPath); + if (!installed) { + throw new Error( + "NativeScript Native API JSI host object was not installed", + ); + } + installInteropConstructors(); + installInlineFunctions(); + } + + const validWorklets = validateWorkletsModule(worklets); + const holder = validWorklets.getUIRuntimeHolder(); + if (holder == null || typeof holder !== "object") { + throw workletsSetupError( + "NativeScript.runOnUI could not resolve a Worklets UI runtime", + ); + } + const installRuntime = NativeScriptNativeApi.installWorkletRuntime; + if (typeof installRuntime !== "function") { + throw workletsSetupError( + "NativeScript Native API was built without RNWorklets runtime support", + ); + } + const installed = installRuntime(holder, metadataPath); + if (!installed) { + throw workletsSetupError( + "NativeScript Native API could not install into the Worklets UI runtime", + ); + } + validWorklets + .runOnUIAsync(installIdleAwareWorkletsFrameLoop) + .catch(() => undefined); + workletsAdapter = validWorklets; + return true; +} + +export function runOnUI( + callback: (...args: Args) => ReturnValue | Promise, + ...args: Args +): Promise { + if (typeof callback !== "function") { + throw new TypeError("NativeScript.runOnUI expects a Worklets callback"); + } + + ensureNativeScriptInstalled(); + const worklets = ensureWorkletsInstalled(); + if (worklets.isWorkletFunction(callback) !== true) { + throw workletsSetupError( + "NativeScript.runOnUI requires a worklet callback", ); } - return run(callback); + return worklets.runOnUIAsync(callback, ...args); } function callbackInvoker( thread: NativeScriptCallbackThread, callback: T, ): NativeScriptInvokedCallback { - if (typeof callback !== 'function') { - throw new TypeError('NativeScript callback invoker expects a function'); + "worklet"; + + if (typeof callback !== "function") { + throw new TypeError("NativeScript callback invoker expects a function"); } const existingPolicy = (callback as Record)[ @@ -920,6 +1379,27 @@ function callbackInvoker( return callback.apply(this, args); } as NativeScriptInvokedCallback; + for (const key of [ + ...Object.getOwnPropertyNames(callback), + ...Object.getOwnPropertySymbols(callback), + ]) { + if (nativeCallbackMetadataSkipKeys.has(key)) { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(callback, key); + if (!descriptor) { + continue; + } + + try { + Object.defineProperty(wrapped, key, descriptor); + } catch { + // Metadata preservation is best-effort for runtimes with fixed function + // internals; the callback policy markers below are still applied. + } + } + Object.defineProperties(wrapped, { [nativeApiCallbackThreadKey]: { configurable: false, @@ -937,53 +1417,183 @@ function callbackInvoker( return wrapped; } -export function uiInvoker( +export function uiInvoker(_callback: T): never { + throw new Error( + 'NativeScript.uiInvoker is not supported in React Native. Use a Worklets "worklet" callback with NativeScript.runOnUI().', + ); +} + +export function jsInvoker( callback: T, ): NativeScriptInvokedCallback { - return callbackInvoker('ui', callback); + "worklet"; + + return callbackInvoker("js", callback); } -export function jsInvoker( +export function runtimeInvoker( callback: T, ): NativeScriptInvokedCallback { - return callbackInvoker('js', callback); + "worklet"; + + return callbackInvoker("runtime", callback); +} + +function nativeScriptCallbackThread( + callback: AnyFunction, +): NativeScriptCallbackThread | undefined { + "worklet"; + + const thread = (callback as Record)[ + nativeApiCallbackThreadKey + ]; + return thread === "js" || thread === "runtime" ? thread : undefined; +} + +function nativeScriptWrappedCallback(callback: AnyFunction): AnyFunction { + "worklet"; + + const wrapped = (callback as Record)[ + nativeApiWrappedCallbackKey + ]; + return typeof wrapped === "function" ? (wrapped as AnyFunction) : callback; +} + +function invokeNativeScriptCallback( + callback: AnyFunction, + args: unknown[], + isDisposed?: () => boolean, +): void { + "worklet"; + + if (nativeScriptCallbackThread(callback) !== "js") { + callback(...args); + return; + } + + const handler = nativeScriptWrappedCallback(callback); + const workletsProxy = (globalThis as Record) + .__workletsModuleProxy; + const serializer = (globalThis as Record).__serializer; + + if ( + workletsProxy && + typeof workletsProxy.scheduleOnRN === "function" && + typeof serializer === "function" + ) { + workletsProxy.scheduleOnRN(handler, serializer(args)); + return; + } + + setTimeout(() => { + if (!isDisposed?.()) { + handler(...args); + } + }, 0); } export function eventBridge( callback: T, - thread: NativeScriptCallbackThread | 'caller' = 'js', + thread: NativeScriptCallbackThread | "caller" = "js", ): T | NativeScriptInvokedCallback { - if (thread === 'ui') { - return uiInvoker(callback); - } - if (thread === 'js') { + "worklet"; + + if (thread === "js") { return jsInvoker(callback); } + if (thread === "runtime") { + return runtimeInvoker(callback); + } return callback; } export const createEventBridge = eventBridge; export function isMainThread(): boolean { + "worklet"; + const NSThread = (globalThis as Record).NSThread; return NSThread?.isMainThread === true; } export function assertUIKitThread( - message = 'UIKit native APIs must be called through NativeScript.runOnUI', + message = "UIKit native APIs must be called through NativeScript.runOnUI", ): void { + "worklet"; + if (!isMainThread()) { throw new Error(message); } } +export function refreshUIKitHostView(view: unknown): boolean { + "worklet"; + + const refresh = (globalThis as Record) + .__nativeScriptRefreshUIKitHostView; + if (typeof refresh !== "function") { + return false; + } + + return refresh(nativeHandleForUIKitView(view)) === true; +} + +export function refreshUIKitHostViewHandle(viewHandle: string): boolean { + "worklet"; + + const refresh = (globalThis as Record) + .__nativeScriptRefreshUIKitHostView; + if (typeof refresh !== "function") { + return false; + } + + return refresh(viewHandle) === true; +} + +export function loadImage( + source: unknown, + options: NativeScriptImageLoadOptions = {}, + callback: NativeScriptImageLoadCallback, +): boolean { + "worklet"; + + const loadReactImage = (globalThis as Record) + .__nativeScriptLoadReactImage; + if (typeof loadReactImage !== "function" || typeof callback !== "function") { + return false; + } + + return ( + loadReactImage( + source, + options.template === true, + (handle: unknown, errorMessage: unknown) => { + "worklet"; + + const interop = (globalThis as Record).interop; + const image = + typeof handle === "string" && handle.length > 0 + ? interop?.object?.(interop.Pointer(handle)) ?? null + : null; + const error = + typeof errorMessage === "string" && errorMessage.length > 0 + ? new Error(errorMessage) + : null; + callback(image, error); + }, + ) === true + ); +} + export function warnIfNotUIKitThread( - message = 'UIKit native APIs should be mutated through NativeScript.runOnUI', + message = "UIKit native APIs should be mutated through NativeScript.runOnUI", ): boolean { + "worklet"; + if (isMainThread()) { return false; } - if (typeof console !== 'undefined' && typeof console.warn === 'function') { + if (typeof console !== "undefined" && typeof console.warn === "function") { console.warn(message); } return true; @@ -991,12 +1601,12 @@ export function warnIfNotUIKitThread( function systemFrameworkPath(nameOrPath: string): string { if (!nameOrPath) { - return ''; + return ""; } - if (nameOrPath.includes('/')) { + if (nameOrPath.includes("/")) { return nameOrPath; } - const frameworkName = nameOrPath.endsWith('.framework') + const frameworkName = nameOrPath.endsWith(".framework") ? nameOrPath : `${nameOrPath}.framework`; return `/System/Library/Frameworks/${frameworkName}`; @@ -1024,20 +1634,30 @@ export function getProtocol(name: string): T | null { return protocol == null ? null : (protocol as T); } -export function isClassAvailable(name: string): boolean { +function requireNSObject(): any { + "worklet"; + + const nsObject = (globalThis as Record).NSObject; + if (!nsObject || typeof nsObject.extend !== "function") { + throw new Error("NSObject.extend is not available"); + } + return nsObject; +} + +export function isClassAvailable(name: string): boolean { const nativeClass = getClass>(name); if (!nativeClass) { return false; } - if (typeof nativeClass.available === 'boolean') { + if (typeof nativeClass.available === "boolean") { return nativeClass.available; } return true; } function frameworkBundle(nameOrPath: string): any | null { - const NSBundle = getClass('NSBundle'); - if (!NSBundle || typeof NSBundle.bundleWithPath !== 'function') { + const NSBundle = getClass("NSBundle"); + if (!NSBundle || typeof NSBundle.bundleWithPath !== "function") { return null; } const path = systemFrameworkPath(nameOrPath); @@ -1048,11 +1668,11 @@ function frameworkBundle(nameOrPath: string): any | null { } const frameworkSentinelClasses: Record = { - Foundation: 'NSObject', - UIKit: 'UIView', - QuickLook: 'QLPreviewController', - VisionKit: 'VNDocumentCameraViewController', - PassKit: 'PKPass', + Foundation: "NSObject", + UIKit: "UIView", + QuickLook: "QLPreviewController", + VisionKit: "VNDocumentCameraViewController", + PassKit: "PKPass", }; function frameworkName(nameOrPath: string): string { @@ -1060,7 +1680,7 @@ function frameworkName(nameOrPath: string): string { if (match) { return match[1]; } - return nameOrPath.replace(/\.framework$/, ''); + return nameOrPath.replace(/\.framework$/, ""); } export function isFrameworkLoaded(nameOrPath: string): boolean { @@ -1072,10 +1692,10 @@ export function isFrameworkLoaded(nameOrPath: string): boolean { if (!bundle) { return false; } - if (typeof bundle.loaded === 'boolean') { + if (typeof bundle.loaded === "boolean") { return bundle.loaded; } - if (typeof bundle.isLoaded === 'function') { + if (typeof bundle.isLoaded === "function") { return Boolean(bundle.isLoaded()); } return false; @@ -1090,7 +1710,7 @@ export function loadFramework(nameOrPath: string): boolean { } const api = requireNativeApiHost(); try { - if (typeof api.import === 'function') { + if (typeof api.import === "function") { api.import(nameOrPath); return true; } @@ -1098,7 +1718,7 @@ export function loadFramework(nameOrPath: string): boolean { // Fall through to NSBundle below so callers get a false availability result. } const bundle = frameworkBundle(nameOrPath); - if (!bundle || typeof bundle.load !== 'function') { + if (!bundle || typeof bundle.load !== "function") { return false; } try { @@ -1108,8 +1728,12 @@ export function loadFramework(nameOrPath: string): boolean { } } -function resolveProtocolReference(protocolRef: NativeProtocolReference): unknown { - if (typeof protocolRef !== 'string') { +function resolveProtocolReference( + protocolRef: NativeProtocolReference, +): unknown { + "worklet"; + + if (typeof protocolRef !== "string") { return protocolRef; } return ( @@ -1120,9 +1744,11 @@ function resolveProtocolReference(protocolRef: NativeProtocolReference): unknown function wrapDelegateMethods( methods: T, - thread: CreateDelegateOptions['thread'], + thread: CreateDelegateOptions["thread"], ): T { - if (!thread || thread === 'caller') { + "worklet"; + + if (!thread || thread === "caller") { return methods; } @@ -1132,7 +1758,7 @@ function wrapDelegateMethods( if (!descriptor) { continue; } - if ('value' in descriptor && typeof descriptor.value === 'function') { + if ("value" in descriptor && typeof descriptor.value === "function") { descriptor.value = eventBridge(descriptor.value, thread); } Object.defineProperty(wrapped, key, descriptor); @@ -1145,11 +1771,15 @@ export function createDelegate( methods: Partial, options: CreateDelegateOptions = {}, ): T { + "worklet"; + const protocolList = (Array.isArray(protocols) ? protocols : [protocols]) .map(resolveProtocolReference) .filter(Boolean); if (protocolList.length === 0) { - throw new Error('NativeScript.createDelegate requires at least one protocol'); + throw new Error( + "NativeScript.createDelegate requires at least one protocol", + ); } const DelegateClass = requireNSObject().extend( @@ -1171,7 +1801,7 @@ export function createDelegate( const assignedObject = options.assignTo?.object as | Record | undefined; - const assignedProperty = options.assignTo?.property ?? 'delegate'; + const assignedProperty = options.assignTo?.property ?? "delegate"; if (assignedObject) { assignedObject[assignedProperty] = delegate; } @@ -1203,99 +1833,448 @@ type UIKitHostInstance = { controller?: unknown; }; -type UIKitAdapterDefinition = - UIKitViewDefinition & { - resolveHostInstance?: (created: NativeView) => UIKitHostInstance; +type RegisteredUIKitHost = { + context: UIKitRuntimeContext; + dispose?: (props: Readonly) => UIKitDisposeResult; + hostInstance: UIKitHostInstance; + hasMounted?: boolean; + mounted?: (props: Readonly) => void; + nativeView: NativeView; + previousProps?: Readonly; + propsRef: { current: Readonly }; + update?: ( + props: Readonly, + previousProps: Readonly | undefined, + ) => void; +}; + +type PendingUIKitHost = { + debugName: string; + mountHost: () => RegisteredUIKitHost; + propsRef: { current: Readonly }; +}; + +type UIKitHostHandles = { + nativeViewHandle?: string; + childrenViewHandle?: string; + controllerHandle?: string; +}; + +type UIKitAdapterDefinition< + Props extends object, + NativeView, +> = UIKitViewDefinition & { + resolveHostInstance?: (created: NativeView) => UIKitHostInstance; +}; + +const uikitHostRegistryGlobalName = "__nativeScriptUIKitHostRegistry"; +const pendingUIKitHostRegistryGlobalName = + "__nativeScriptPendingUIKitHostRegistry"; +const createUIKitHostFromNativeGlobalName = + "__nativeScriptCreateUIKitHostFromNative"; +const runUIKitHostLifecycleFromNativeGlobalName = + "__nativeScriptRunUIKitHostLifecycleFromNative"; +let nextUIKitHostId = 1; + +function createUIKitHostId(debugName: string): string { + return `${debugName}:${nextUIKitHostId++}`; +} + +function uikitHostRegistry(): Map> { + "worklet"; + + const globalObject = globalThis as Record; + const existing = globalObject[uikitHostRegistryGlobalName]; + if (existing instanceof Map) { + return existing as Map>; + } + + const registry = new Map>(); + Object.defineProperty(globalThis, uikitHostRegistryGlobalName, { + configurable: true, + enumerable: false, + writable: false, + value: registry, + }); + return registry; +} + +function pendingUIKitHostRegistry(): Map< + string, + PendingUIKitHost +> { + "worklet"; + + const globalObject = globalThis as Record; + const existing = globalObject[pendingUIKitHostRegistryGlobalName]; + if (existing instanceof Map) { + return existing as Map>; + } + + const registry = new Map>(); + Object.defineProperty(globalThis, pendingUIKitHostRegistryGlobalName, { + configurable: true, + enumerable: false, + writable: false, + value: registry, + }); + return registry; +} + +function uikitHostHandles( + host: RegisteredUIKitHost, +): UIKitHostHandles { + "worklet"; + + return { + nativeViewHandle: nativeHandleOrUndefined(host.hostInstance.hostView), + childrenViewHandle: nativeHandleOrUndefined(host.hostInstance.childrenView), + controllerHandle: nativeHandleForNSObject(host.hostInstance.controller), }; +} -let targetActionClass: any; -let observerClass: any; -const targetActionCallbacks = new WeakMap void>(); -const observerCallbacks = new WeakMap< - object, - (keyPath: string, object: unknown, change: unknown) => void ->(); +function getRegisteredUIKitHost( + hostId: string, +): RegisteredUIKitHost { + "worklet"; + + const host = uikitHostRegistry().get(hostId); + if (!host) { + throw new Error(`UIKit host ${hostId} has not been created`); + } + return host as RegisteredUIKitHost; +} + +function registerUIKitHost( + hostId: string, + host: RegisteredUIKitHost, +): void { + "worklet"; + + uikitHostRegistry().set(hostId, host as RegisteredUIKitHost); +} + +function createRegisteredUIKitHostFromNative( + hostId: string, +): UIKitHostHandles | null { + "worklet"; + + const existingHost = uikitHostRegistry().get(hostId); + if (existingHost) { + return uikitHostHandles(existingHost); + } + + const pending = pendingUIKitHostRegistry().get(hostId); + if (!pending) { + return null; + } + + const host = pending.mountHost(); + registerUIKitHost(hostId, host); + return uikitHostHandles(host); +} + +function ensureRegisteredUIKitHost( + hostId: string, +): RegisteredUIKitHost | null { + "worklet"; + + const existingHost = uikitHostRegistry().get(hostId); + if (existingHost) { + return existingHost as RegisteredUIKitHost; + } + + if (createRegisteredUIKitHostFromNative(hostId) == null) { + return null; + } + + const createdHost = uikitHostRegistry().get(hostId); + return (createdHost ?? null) as RegisteredUIKitHost | null; +} + +function disposeRegisteredUIKitHost( + hostId: string, + props: Readonly, +): void { + "worklet"; + + pendingUIKitHostRegistry().delete(hostId); + const registry = uikitHostRegistry(); + const host = registry.get(hostId) as + | RegisteredUIKitHost + | undefined; + if (!host) { + return; + } + registry.delete(hostId); + host.propsRef.current = props; + const disposeResult = host.dispose?.(props); + host.context.disposeResources(); + const maybeView = host.hostInstance.hostView as + | Record + | undefined; + if ( + disposeResult?.removeHostView !== false && + typeof maybeView?.removeFromSuperview === "function" + ) { + maybeView.removeFromSuperview(); + } +} + +function syncUIKitHostPropsFromReact( + hostId: string, + props: Readonly, +): void { + "worklet"; + + const pending = pendingUIKitHostRegistry().get(hostId); + if (pending) { + pending.propsRef.current = props; + } + + const host = uikitHostRegistry().get(hostId); + if (host) { + host.propsRef.current = props; + } +} + +function runUIKitHostLifecycleFromNative( + hostId: string, + phase: string, +): UIKitHostHandles | null { + "worklet"; + + if (phase === "dispose") { + const host = uikitHostRegistry().get(hostId); + const pending = pendingUIKitHostRegistry().get(hostId); + disposeRegisteredUIKitHost( + hostId, + host?.propsRef.current ?? pending?.propsRef.current ?? {}, + ); + return null; + } + + const handles = createRegisteredUIKitHostFromNative(hostId); + if (handles == null) { + return null; + } + + const host = getRegisteredUIKitHost(hostId); + const nextProps = host.propsRef.current; + if (phase === "update") { + if (host.previousProps !== nextProps) { + host.update?.(nextProps, host.previousProps); + host.previousProps = nextProps; + } + } else if (phase === "mounted" && !host.hasMounted) { + host.hasMounted = true; + host.mounted?.(nextProps); + } + + return uikitHostHandles(host); +} + +function installUIKitNativeMountBridge(): void { + "worklet"; + + const globalObject = globalThis as Record; + if (typeof globalObject[createUIKitHostFromNativeGlobalName] !== "function") { + Object.defineProperty(globalThis, createUIKitHostFromNativeGlobalName, { + configurable: true, + enumerable: false, + writable: false, + value: createRegisteredUIKitHostFromNative, + }); + } + if ( + typeof globalObject[runUIKitHostLifecycleFromNativeGlobalName] !== + "function" + ) { + Object.defineProperty( + globalThis, + runUIKitHostLifecycleFromNativeGlobalName, + { + configurable: true, + enumerable: false, + writable: false, + value: runUIKitHostLifecycleFromNative, + }, + ); + } +} + +function ignoreUIKitLayoutInvalidation(): void { + "worklet"; +} + +const targetActionClassGlobalName = "__nativeScriptUIKitTargetActionClass"; +const observerClassGlobalName = "__nativeScriptUIKitObserverClass"; +const targetActionCallbacksGlobalName = + "__nativeScriptUIKitTargetActionCallbacks"; +const observerCallbacksGlobalName = "__nativeScriptUIKitObserverCallbacks"; function objcInteropTypes(): any { + "worklet"; + return (globalThis as Record).interop?.types; } -function requireNSObject(): any { - const nsObject = (globalThis as Record).NSObject; - if (!nsObject || typeof nsObject.extend !== 'function') { - throw new Error('NSObject.extend is not available'); +function runtimeGlobalMap(name: string): Map { + "worklet"; + + const globalObject = globalThis as Record; + const existing = globalObject[name]; + if (existing instanceof Map) { + return existing as Map; } - return nsObject; + + const map = new Map(); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: map, + }); + return map; +} + +function targetActionCallbacksForRuntime(): Map< + string, + (sender: unknown) => void +> { + "worklet"; + + return runtimeGlobalMap<(sender: unknown) => void>( + targetActionCallbacksGlobalName, + ); +} + +function observerCallbacksForRuntime(): Map< + string, + (keyPath: string, object: unknown, change: unknown) => void +> { + "worklet"; + + return runtimeGlobalMap< + (keyPath: string, object: unknown, change: unknown) => void + >(observerCallbacksGlobalName); +} + +function nativeCallbackKey(value: unknown): string { + "worklet"; + + const handleof = (globalThis as Record).interop?.handleof; + if (value != null && typeof handleof === "function") { + const handle = handleof(value); + if (handle != null) { + if (typeof handle.toHexString === "function") { + return handle.toHexString(); + } + return String(handle); + } + } + return String(value); } function getTargetActionClass(): any { - if (targetActionClass) { - return targetActionClass; + "worklet"; + + const globalObject = globalThis as Record; + const cached = globalObject[targetActionClassGlobalName]; + if (cached) { + return cached; } const types = objcInteropTypes(); const NSObject = requireNSObject(); - targetActionClass = NSObject.extend( + const targetActionClass = NSObject.extend( { nativeScriptHandleAction(sender: unknown) { - const callback = targetActionCallbacks.get(this as object); - if (typeof callback === 'function') { + const callback = targetActionCallbacksForRuntime().get( + nativeCallbackKey(this), + ); + if (typeof callback === "function") { callback(sender); } }, }, { exposedMethods: { - 'nativeScriptHandleAction:': { + "nativeScriptHandleAction:": { returns: types?.void, params: [NSObject], }, }, }, ); + Object.defineProperty(globalThis, targetActionClassGlobalName, { + configurable: true, + enumerable: false, + writable: false, + value: targetActionClass, + }); return targetActionClass; } function getObserverClass(): any { - if (observerClass) { - return observerClass; + "worklet"; + + const globalObject = globalThis as Record; + const cached = globalObject[observerClassGlobalName]; + if (cached) { + return cached; } const types = objcInteropTypes(); const NSObject = requireNSObject(); const NSString = (globalThis as Record).NSString; const NSDictionary = (globalThis as Record).NSDictionary; - const Pointer = (globalThis as Record).interop?.Pointer - ?? types?.id; + const Pointer = + (globalThis as Record).interop?.Pointer ?? types?.id; - observerClass = NSObject.extend( + const observerClass = NSObject.extend( { - 'observeValueForKeyPath:ofObject:change:context:'( + "observeValueForKeyPath:ofObject:change:context:"( keyPath: string, object: unknown, change: unknown, ) { - const callback = observerCallbacks.get(this as object); - if (typeof callback === 'function') { + const callback = observerCallbacksForRuntime().get( + nativeCallbackKey(this), + ); + if (typeof callback === "function") { callback(keyPath, object, change); } }, }, { exposedMethods: { - 'observeValueForKeyPath:ofObject:change:context:': { + "observeValueForKeyPath:ofObject:change:context:": { returns: types?.void, - params: [NSString ?? NSObject, NSObject, NSDictionary ?? NSObject, Pointer], + params: [ + NSString ?? NSObject, + NSObject, + NSDictionary ?? NSObject, + Pointer, + ], }, }, }, ); + Object.defineProperty(globalThis, observerClassGlobalName, { + configurable: true, + enumerable: false, + writable: false, + value: observerClass, + }); return observerClass; } function createUIKitContext( name: string, - propsRef: React.MutableRefObject, + propsRef: { current: Props }, invalidateLayout: () => void, ): UIKitRuntimeContext { + "worklet"; + const retained: unknown[] = []; const cleanupCallbacks: Array<() => void> = []; let disposed = false; @@ -1317,63 +2296,151 @@ function createUIKitContext( const handler = (propsRef.current as Record)[ eventName as PropertyKey ]; - if (typeof handler !== 'function') { + if (typeof handler !== "function") { return; } - setTimeout(() => { - if (!disposed) { - (handler as Function)(payload); - } - }, 0); + const workletsProxy = (globalThis as Record) + .__workletsModuleProxy; + const serializer = (globalThis as Record).__serializer; + if ( + workletsProxy && + typeof workletsProxy.scheduleOnRN === "function" && + typeof serializer === "function" + ) { + workletsProxy.scheduleOnRN(handler, serializer([payload])); + } else { + setTimeout(() => { + if (!disposed) { + (handler as Function)(payload); + } + }, 0); + } }, targetAction(control, events, callback) { - if (control == null || typeof callback !== 'function') { + if (control == null || typeof callback !== "function") { return; } const target = getTargetActionClass().alloc().init(); - targetActionCallbacks.set(target as object, uiInvoker(() => { + const targetKey = nativeCallbackKey(target); + targetActionCallbacksForRuntime().set(targetKey, () => { if (!disposed) { - callback(); + invokeNativeScriptCallback(callback, [], () => disposed); } - })); - const selector = 'nativeScriptHandleAction:'; + }); + const selector = "nativeScriptHandleAction:"; const nativeControl = control as Record; - if (typeof nativeControl.addTargetActionForControlEvents !== 'function') { - throw new Error('targetAction expects a UIControl-compatible object'); + if (typeof nativeControl.addTargetActionForControlEvents !== "function") { + throw new Error("targetAction expects a UIControl-compatible object"); } nativeControl.addTargetActionForControlEvents(target, selector, events); context.retain(target); context.dispose(() => { - if (typeof nativeControl.removeTargetActionForControlEvents === 'function') { - nativeControl.removeTargetActionForControlEvents(target, selector, events); + if ( + typeof nativeControl.removeTargetActionForControlEvents === "function" + ) { + nativeControl.removeTargetActionForControlEvents( + target, + selector, + events, + ); } - targetActionCallbacks.delete(target as object); + targetActionCallbacksForRuntime().delete(targetKey); }); }, + gestureAction(gesture, callback) { + if (gesture == null || typeof callback !== "function") { + return; + } + const target = getTargetActionClass().alloc().init(); + const targetKey = nativeCallbackKey(target); + targetActionCallbacksForRuntime().set(targetKey, (sender) => { + if (!disposed) { + callback(sender ?? gesture); + } + }); + const selector = "nativeScriptHandleAction:"; + const nativeGesture = gesture as Record; + if (typeof nativeGesture.addTargetAction !== "function") { + throw new Error( + "gestureAction expects a UIGestureRecognizer-compatible object", + ); + } + nativeGesture.addTargetAction(target, selector); + context.retain(target); + context.dispose(() => { + if (typeof nativeGesture.removeTargetAction === "function") { + nativeGesture.removeTargetAction(target, selector); + } + targetActionCallbacksForRuntime().delete(targetKey); + }); + }, + actionTarget(callback) { + if (typeof callback !== "function") { + throw new Error("actionTarget expects a callback"); + } + + const target = getTargetActionClass().alloc().init(); + const targetKey = nativeCallbackKey(target); + targetActionCallbacksForRuntime().set(targetKey, (sender) => { + if (!disposed) { + invokeNativeScriptCallback(callback, [sender], () => disposed); + } + }); + context.retain(target); + context.dispose(() => { + targetActionCallbacksForRuntime().delete(targetKey); + }); + + return { + target, + action: "nativeScriptHandleAction:", + }; + }, delegate(object, protocolRef, implementation) { + const protocolList = [protocolRef as NativeProtocolReference] + .map(resolveProtocolReference) + .filter(Boolean); + if (protocolList.length === 0) { + throw new Error("NativeScript UIKit delegate requires a protocol"); + } + const nativeObject = object as Record; - return createDelegate([protocolRef as NativeProtocolReference], implementation, { - owner: context, - assignTo: nativeObject && 'delegate' in nativeObject - ? {object: nativeObject, property: 'delegate'} - : undefined, + const assignedObject = + nativeObject && "delegate" in nativeObject ? nativeObject : undefined; + const DelegateClass = requireNSObject().extend( + wrapDelegateMethods(implementation, "caller"), + { + protocols: protocolList, + }, + ); + const delegate = DelegateClass.alloc().init() as T; + context.retain(delegate); + if (assignedObject) { + assignedObject.delegate = delegate; + } + context.dispose(() => { + if (assignedObject && assignedObject.delegate === delegate) { + assignedObject.delegate = null; + } + context.release(delegate); }); + return delegate; }, notification(name, object, callback) { const center = (globalThis as Record).NSNotificationCenter ?.defaultCenter; if (!center) { - throw new Error('NSNotificationCenter.defaultCenter is not available'); + throw new Error("NSNotificationCenter.defaultCenter is not available"); } const observer = center.addObserverForNameObjectQueueUsingBlock( name, object ?? null, null, - uiInvoker((notification: unknown) => { + (notification: unknown) => { if (!disposed) { callback(notification); } - }), + }, ); context.retain(observer); context.dispose(() => { @@ -1384,31 +2451,40 @@ function createUIKitContext( const nativeObject = object as Record; if ( object == null || - typeof nativeObject.addObserverForKeyPathOptionsContext !== 'function' + typeof nativeObject.addObserverForKeyPathOptionsContext !== "function" ) { - throw new Error('observe expects a KVO-compatible NSObject'); + throw new Error("observe expects a KVO-compatible NSObject"); } const observer = getObserverClass().alloc().init(); - observerCallbacks.set(observer as object, ( - observedKeyPath: string, - _observedObject: unknown, - change: unknown, - ) => { - if (disposed || String(observedKeyPath) !== keyPath) { - return; - } - const newKey = (globalThis as Record).NSKeyValueChangeNewKey; - const value = - change && typeof (change as Record).objectForKey === 'function' - ? (change as Record).objectForKey(newKey) - : undefined; - callback(value, change); - }); - const options = (globalThis as Record).NSKeyValueObservingOptions; + const observerKey = nativeCallbackKey(observer); + observerCallbacksForRuntime().set( + observerKey, + ( + observedKeyPath: string, + _observedObject: unknown, + change: unknown, + ) => { + if (disposed || String(observedKeyPath) !== keyPath) { + return; + } + const newKey = (globalThis as Record) + .NSKeyValueChangeNewKey; + const value = + change && + typeof (change as Record).objectForKey === + "function" + ? (change as Record).objectForKey(newKey) + : undefined; + callback(value, change); + }, + ); + const options = (globalThis as Record) + .NSKeyValueObservingOptions; const optionNew = - typeof options?.New === 'number' + typeof options?.New === "number" ? options.New - : (globalThis as Record).NSKeyValueObservingOptionNew ?? 1; + : ((globalThis as Record).NSKeyValueObservingOptionNew ?? + 1); nativeObject.addObserverForKeyPathOptionsContext( observer, keyPath, @@ -1418,11 +2494,11 @@ function createUIKitContext( context.retain(observer); context.dispose(() => { try { - if (typeof nativeObject.removeObserverForKeyPath === 'function') { + if (typeof nativeObject.removeObserverForKeyPath === "function") { nativeObject.removeObserverForKeyPath(observer, keyPath); } } finally { - observerCallbacks.delete(observer as object); + observerCallbacksForRuntime().delete(observerKey); } }); }, @@ -1430,24 +2506,26 @@ function createUIKitContext( retained.push(value); return value; }, - release(value?: unknown) { - if (arguments.length === 0) { - retained.length = 0; - return; - } + release(value?: unknown) { + if (arguments.length === 0) { + retained.length = 0; + return; + } for (let i = retained.length - 1; i >= 0; i--) { if (retained[i] === value) { retained.splice(i, 1); } } }, - dispose(callback) { - cleanupCallbacks.push(callback); - }, - invalidateLayout, - createArgument() { - return Object.assign(Object.create(context), propsRef.current); - }, + dispose(callback) { + cleanupCallbacks.push(callback); + }, + invalidateLayout, + loadImage: (source, options, callback) => + loadImage(source, options, callback), + createArgument() { + return Object.assign(Object.create(context), propsRef.current); + }, disposeResources() { if (disposed) { return; @@ -1468,16 +2546,20 @@ function createUIKitContext( } function constrainedSize( - size: {width: number; height: number}, + size: { width: number; height: number }, layout?: UIKitLayoutOptions, -): {width: number; height: number} { +): { width: number; height: number } { + "worklet"; + const defaultSize = layout?.defaultSize ?? {}; - let width = Number.isFinite(size.width) && size.width >= 0 - ? size.width - : defaultSize.width ?? 0; - let height = Number.isFinite(size.height) && size.height >= 0 - ? size.height - : defaultSize.height ?? 0; + let width = + Number.isFinite(size.width) && size.width >= 0 + ? size.width + : (defaultSize.width ?? 0); + let height = + Number.isFinite(size.height) && size.height >= 0 + ? size.height + : (defaultSize.height ?? 0); if (layout?.minSize?.width != null) { width = Math.max(width, layout.minSize.width); @@ -1491,27 +2573,52 @@ function constrainedSize( if (layout?.maxSize?.height != null) { height = Math.min(height, layout.maxSize.height); } - return {width, height}; + return { width, height }; } -function flattenedStyleSize(style: ViewProps['style']) { - const flat = StyleSheet.flatten(style) ?? {}; +function flattenedStyleSize(style: ViewProps["style"]) { + "worklet"; + + const flat: Record = {}; + const applyStyle = (value: unknown) => { + if (Array.isArray(value)) { + for (const item of value) { + applyStyle(item); + } + return; + } + if (!value || typeof value !== "object") { + return; + } + const record = value as Record; + if (typeof record.width === "number") { + flat.width = record.width; + } + if (typeof record.height === "number") { + flat.height = record.height; + } + }; + applyStyle(style); return { - width: typeof flat.width === 'number' ? flat.width : undefined, - height: typeof flat.height === 'number' ? flat.height : undefined, + width: typeof flat.width === "number" ? flat.width : undefined, + height: typeof flat.height === "number" ? flat.height : undefined, }; } function makeCGSize(width: number, height: number) { + "worklet"; + const CGSizeMake = (globalThis as Record).CGSizeMake; - if (typeof CGSizeMake === 'function') { + if (typeof CGSizeMake === "function") { return CGSizeMake(width, height); } - return {width, height}; + return { width, height }; } -function readNativeSize(size: unknown): {width: number; height: number} { - const nativeSize = size as {width?: unknown; height?: unknown}; +function readNativeSize(size: unknown): { width: number; height: number } { + "worklet"; + + const nativeSize = size as { width?: unknown; height?: unknown }; return { width: Number(nativeSize?.width ?? 0), height: Number(nativeSize?.height ?? 0), @@ -1521,33 +2628,46 @@ function readNativeSize(size: unknown): {width: number; height: number} { function measureUIKitView( view: unknown, layout: UIKitLayoutOptions | undefined, - style: ViewProps['style'], -): {width: number; height: number} { - const mode = layout?.sizing ?? 'fill'; - if (mode === 'fill') { - return constrainedSize(layout?.defaultSize ?? {width: 0, height: 0}, layout); + style: ViewProps["style"], +): { width: number; height: number } { + "worklet"; + + const mode = layout?.sizing ?? "fill"; + if (mode === "fill") { + return constrainedSize( + layout?.defaultSize ?? { width: 0, height: 0 }, + layout, + ); } const styleSize = flattenedStyleSize(style); const nativeView = view as Record; - let measured = layout?.defaultSize ?? {width: 0, height: 0}; + let measured = layout?.defaultSize ?? { width: 0, height: 0 }; - if (mode === 'intrinsic') { + if (mode === "intrinsic") { measured = readNativeSize(nativeView.intrinsicContentSize); - } else if (mode === 'sizeThatFits' && typeof nativeView.sizeThatFits === 'function') { + } else if ( + mode === "sizeThatFits" && + typeof nativeView.sizeThatFits === "function" + ) { measured = readNativeSize( nativeView.sizeThatFits( - makeCGSize(styleSize.width ?? Number.MAX_SAFE_INTEGER, styleSize.height ?? Number.MAX_SAFE_INTEGER), + makeCGSize( + styleSize.width ?? Number.MAX_SAFE_INTEGER, + styleSize.height ?? Number.MAX_SAFE_INTEGER, + ), ), ); } else if ( - mode === 'autoLayout' && - typeof nativeView.systemLayoutSizeFittingSize === 'function' + mode === "autoLayout" && + typeof nativeView.systemLayoutSizeFittingSize === "function" ) { const fittingSize = (globalThis as Record).UIView?.layoutFittingCompressedSize ?? makeCGSize(styleSize.width ?? 0, styleSize.height ?? 0); - measured = readNativeSize(nativeView.systemLayoutSizeFittingSize(fittingSize)); + measured = readNativeSize( + nativeView.systemLayoutSizeFittingSize(fittingSize), + ); } return constrainedSize( @@ -1559,83 +2679,107 @@ function measureUIKitView( ); } -function nativeHandleOrUndefined(value: unknown): string | undefined { - return value == null ? undefined : nativeHandleForUIKitView(value); -} - -function nativeHandleForNSObject(value: unknown): string | undefined { - if (value == null) { - return undefined; - } - const interop = (globalThis as Record).interop; - const pointer = interop?.handleof?.(value); - if (!pointer) { - return undefined; - } - if (typeof pointer.toHexString === 'function') { - return pointer.toHexString(); - } - if (typeof pointer.address === 'string') { - return pointer.address; - } - if (typeof pointer.address === 'number') { - return String(pointer.address); - } - if (typeof pointer.toNumber === 'function') { - return String(pointer.toNumber()); - } - return undefined; -} - function defineUIKitHost( definition: UIKitAdapterDefinition, ): UIKitViewComponent { const debugName = - definition.debugName - || definition.name - || definition.displayName - || 'NativeScriptUIKitView'; - - 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 contextRef = useRef | null>( - null, - ); - const hostInstanceRef = useRef | null>(null); - const mountedRef = useRef(false); - const disposedRef = useRef(false); - const [nativeViewHandle, setNativeViewHandle] = useState(); - const [childrenViewHandle, setChildrenViewHandle] = useState(); - const [controllerHandle, setControllerHandle] = useState(); - const [measuredSize, setMeasuredSize] = useState< - {width: number; height: number} | undefined - >(() => definition.layout?.sizing === 'fill' + definition.debugName || + definition.name || + definition.displayName || + "NativeScriptUIKitView"; + + const Component = forwardRef< + UIKitViewRef, + Props & UIKitHostViewProps + >(function NativeScriptUIKitView(props, ref) { + const { nativeProps, pluginProps } = splitUIKitViewProps(props, definition); + const createHost = definition.create; + const updateHost = definition.update; + const mountedHost = definition.mounted; + const disposeHost = definition.dispose; + const resolveHostInstance = definition.resolveHostInstance; + const layout = definition.layout; + const layoutSizing = layout?.sizing ?? "fill"; + const hostIdRef = useRef(null); + if (hostIdRef.current == null) { + hostIdRef.current = createUIKitHostId(debugName); + } + const hostId = hostIdRef.current; + const propsRef = useRef(pluginProps); + const previousPropsRef = useRef | undefined>(); + const mountedRef = useRef(false); + const disposedRef = useRef(false); + const updateMeasuredSizeRef = useRef<() => void>(() => {}); + const [nativeHostRevision, setNativeHostRevision] = useState(0); + const attachController = props.attachController !== false; + const attachControllerView = props.attachControllerView !== false; + const attachNativeView = props.attachNativeView !== false; + const mountThroughNativeHost = attachController; + + const invalidateLayout = () => { + updateMeasuredSizeRef.current(); + }; + + const [nativeViewHandle, setNativeViewHandle] = useState< + string | undefined + >(); + const [childrenViewHandle, setChildrenViewHandle] = useState< + string | undefined + >(); + const [controllerHandle, setControllerHandle] = useState< + string | undefined + >(); + const [measuredSize, setMeasuredSize] = useState< + { width: number; height: number } | undefined + >(() => + layoutSizing === "fill" ? undefined - : definition.layout?.defaultSize + : layout?.defaultSize ? { - width: definition.layout.defaultSize.width ?? 0, - height: definition.layout.defaultSize.height ?? 0, + width: layout.defaultSize.width ?? 0, + height: layout.defaultSize.height ?? 0, } - : undefined); - const [error, setError] = useState(null); + : undefined, + ); + const [error, setError] = useState(null); - propsRef.current = pluginProps; + propsRef.current = pluginProps; - const updateMeasuredSize = () => { - const hostInstance = hostInstanceRef.current; - if (!hostInstance || definition.layout?.sizing === 'fill') { - return; - } - runOnUI(() => { - const nextSize = measureUIKitView( - hostInstance.hostView, - definition.layout, - nativeProps.style, - ); + const applyHostHandles = (handles: UIKitHostHandles | null | undefined) => { + if (handles == null) { + return; + } + + setNativeViewHandle((previous) => + previous === handles.nativeViewHandle + ? previous + : handles.nativeViewHandle, + ); + setChildrenViewHandle((previous) => + previous === handles.childrenViewHandle + ? previous + : handles.childrenViewHandle, + ); + setControllerHandle((previous) => + previous === handles.controllerHandle + ? previous + : handles.controllerHandle, + ); + }; + + const updateMeasuredSize = () => { + if (nativeViewHandle == null || layoutSizing === "fill") { + return; + } + runOnUI(() => { + const host = getRegisteredUIKitHost(hostId); + return measureUIKitView( + host.hostInstance.hostView, + layout, + nativeProps.style, + ); + }) + .then((nextSize) => { setMeasuredSize((previous) => previous && previous.width === nextSize.width && @@ -1643,193 +2787,393 @@ function defineUIKitHost( ? previous : nextSize, ); - }).catch((reason) => { - setError(reason instanceof Error ? reason : new Error(String(reason))); + }) + .catch((reason) => { + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); }); - }; + }; + updateMeasuredSizeRef.current = updateMeasuredSize; + + useImperativeHandle( + ref, + () => ({ + get nativeView() { + return null; + }, + runOnUI(callback) { + return runOnUI(() => { + const host = getRegisteredUIKitHost(hostId); + return callback(host.nativeView); + }); + }, + measureNative() { + return runOnUI(() => { + const host = getRegisteredUIKitHost(hostId); + return measureUIKitView( + host.hostInstance.hostView, + layout, + nativeProps.style, + ); + }); + }, + invalidateNativeLayout() { + updateMeasuredSize(); + }, + }), + [hostId, layout, nativeProps.style], + ); - useImperativeHandle( - ref, - () => ({ - get nativeView() { - return viewRef.current; - }, - runOnUI(callback) { - let result: unknown; - return runOnUI(() => { - if (viewRef.current == null) { - throw new Error('UIKit view has not been created yet'); - } - result = callback(viewRef.current); - }).then(() => result as never); - }, - measureNative() { - let measured: - | {width: number; height: number} - | undefined; - return runOnUI(() => { - if (viewRef.current == null) { - throw new Error('UIKit view has not been created yet'); - } - measured = measureUIKitView( - hostInstanceRef.current?.hostView ?? viewRef.current, - definition.layout, - nativeProps.style, - ); - }).then(() => { - if (measured == null) { - throw new Error('UIKit view measurement did not complete'); - } - return measured; - }); - }, - invalidateNativeLayout() { - updateMeasuredSize(); - }, - }), - [definition.layout, nativeProps.style], - ); + useLayoutEffect(() => { + disposedRef.current = false; + let cancelled = false; - useEffect(() => { - disposedRef.current = false; - let cancelled = false; + ensureNativeScriptInstalled(); - ensureNativeScriptInstalled(); + if (mountThroughNativeHost) { + const effectProps = propsRef.current; + runOnUI((currentProps) => { + installUIKitNativeMountBridge(); - runOnUI(() => { - const currentProps = propsRef.current; - const context = createUIKitContext( - debugName, - propsRef, - updateMeasuredSize, - ); - contextRef.current = context; - const created = definition.create(context.createArgument()); - const hostInstance = definition.resolveHostInstance - ? definition.resolveHostInstance(created) - : {hostView: created, lifecycleValue: created}; - const nativeView = hostInstance.lifecycleValue; - if (cancelled || disposedRef.current) { - definition.dispose?.(nativeView, currentProps, context); - context.disposeResources(); - const maybeView = hostInstance.hostView as Record; - if (typeof maybeView.removeFromSuperview === 'function') { - maybeView.removeFromSuperview(); - } - return undefined; + const existingHost = uikitHostRegistry().get(hostId); + if (existingHost) { + existingHost.propsRef.current = currentProps; + return uikitHostHandles(existingHost); } - hostInstanceRef.current = hostInstance; - viewRef.current = nativeView; - definition.update?.(nativeView, currentProps, undefined, context); - previousPropsRef.current = currentProps; - return undefined; - }) - .then(() => { - const hostInstance = hostInstanceRef.current; - if (cancelled || viewRef.current == null || hostInstance == null) { + + const registry = pendingUIKitHostRegistry(); + const pending = registry.get(hostId) as + | PendingUIKitHost + | undefined; + const pendingPropsRef = pending?.propsRef ?? { + current: currentProps, + }; + pendingPropsRef.current = currentProps; + + const mountHost = () => { + const nextProps = pendingPropsRef.current; + const context = createUIKitContext( + debugName, + pendingPropsRef, + ignoreUIKitLayoutInvalidation, + ); + const created = createHost(context.createArgument()); + const hostInstance = resolveHostInstance + ? resolveHostInstance(created) + : { hostView: created, lifecycleValue: created }; + const nativeView = hostInstance.lifecycleValue; + updateHost?.(nativeView, nextProps, undefined, context); + return { + context, + dispose(disposeProps: Readonly) { + return disposeHost?.(nativeView, disposeProps, context); + }, + mounted(mountedProps: Readonly) { + mountedHost?.(nativeView, mountedProps, context); + }, + hostInstance, + nativeView, + previousProps: nextProps, + propsRef: pendingPropsRef, + update( + updateProps: Readonly, + previousProps: Readonly | undefined, + ) { + updateHost?.(nativeView, updateProps, previousProps, context); + }, + }; + }; + + registry.set(hostId, { + debugName, + mountHost, + propsRef: pendingPropsRef, + }); + + return null; + }, effectProps) + .then((handles) => { + if (cancelled || disposedRef.current) { return; } - setNativeViewHandle(nativeHandleOrUndefined(hostInstance.hostView)); - setChildrenViewHandle(nativeHandleOrUndefined(hostInstance.childrenView)); - setControllerHandle(nativeHandleForNSObject(hostInstance.controller)); + previousPropsRef.current = propsRef.current; + applyHostHandles(handles); + setNativeHostRevision((revision) => revision + 1); updateMeasuredSize(); }) .catch((reason) => { - setError(reason instanceof Error ? reason : new Error(String(reason))); + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); }); return () => { cancelled = true; disposedRef.current = true; - const nativeView = viewRef.current; - const context = contextRef.current; - const hostInstance = hostInstanceRef.current; - viewRef.current = null; - hostInstanceRef.current = null; - contextRef.current = null; mountedRef.current = false; - if (nativeView == null) { - context?.disposeResources(); - return; - } runOnUI(() => { - definition.dispose?.(nativeView, propsRef.current, context ?? undefined); - context?.disposeResources(); - const maybeView = hostInstance?.hostView as - | Record - | undefined; - if (typeof maybeView.removeFromSuperview === 'function') { - maybeView.removeFromSuperview(); + if (!uikitHostRegistry().has(hostId)) { + pendingUIKitHostRegistry().delete(hostId); } }).catch((reason) => { - setError(reason instanceof Error ? reason : new Error(String(reason))); + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); }); }; - }, [definition]); + } + + const effectProps = propsRef.current; + runOnUI((currentProps) => { + installUIKitNativeMountBridge(); - useEffect(() => { - const nativeView = viewRef.current; - if (nativeView == null) { - return; + const existingHost = uikitHostRegistry().get(hostId); + if (existingHost) { + existingHost.propsRef.current = currentProps; + return uikitHostHandles(existingHost); } - const previousProps = previousPropsRef.current; - const currentProps = propsRef.current; - const context = contextRef.current; - previousPropsRef.current = currentProps; + const registry = pendingUIKitHostRegistry(); + const pending = registry.get(hostId) as + | PendingUIKitHost + | undefined; + const pendingPropsRef = pending?.propsRef ?? { current: currentProps }; + pendingPropsRef.current = currentProps; - runOnUI(() => { - definition.update?.(nativeView, currentProps, previousProps, context ?? undefined); - }).catch((reason) => { - setError(reason instanceof Error ? reason : new Error(String(reason))); + const mountHost = () => { + const nextProps = pendingPropsRef.current; + const context = createUIKitContext( + debugName, + pendingPropsRef, + ignoreUIKitLayoutInvalidation, + ); + const created = createHost(context.createArgument()); + const hostInstance = resolveHostInstance + ? resolveHostInstance(created) + : { hostView: created, lifecycleValue: created }; + const nativeView = hostInstance.lifecycleValue; + updateHost?.(nativeView, nextProps, undefined, context); + return { + context, + dispose(disposeProps: Readonly) { + return disposeHost?.(nativeView, disposeProps, context); + }, + mounted(mountedProps: Readonly) { + mountedHost?.(nativeView, mountedProps, context); + }, + hostInstance, + nativeView, + previousProps: nextProps, + propsRef: pendingPropsRef, + update( + updateProps: Readonly, + previousProps: Readonly | undefined, + ) { + updateHost?.(nativeView, updateProps, previousProps, context); + }, + }; + }; + + registry.set(hostId, { + debugName, + mountHost, + propsRef: pendingPropsRef, + }); + return createRegisteredUIKitHostFromNative(hostId); + }, effectProps) + .then((handles) => { + if (handles == null) { + throw new Error(`UIKit host ${hostId} was not created`); + } + if (cancelled || disposedRef.current) { + const disposeProps = propsRef.current; + runOnUI((currentProps) => { + disposeRegisteredUIKitHost(hostId, currentProps); + }, disposeProps).catch((reason) => { + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); + }); + return; + } + previousPropsRef.current = propsRef.current; + applyHostHandles(handles); + updateMeasuredSize(); + }) + .catch((reason) => { + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); }); - updateMeasuredSize(); - }, [definition, pluginProps]); - useEffect(() => { - const nativeView = viewRef.current; - if (nativeViewHandle == null || nativeView == null || mountedRef.current) { - return; - } + return () => { + cancelled = true; + disposedRef.current = true; + mountedRef.current = false; + const disposeProps = propsRef.current; + runOnUI((currentProps) => { + disposeRegisteredUIKitHost(hostId, currentProps); + }, disposeProps).catch((reason) => { + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); + }); + }; + }, [ + createHost, + debugName, + disposeHost, + hostId, + mountedHost, + mountThroughNativeHost, + resolveHostInstance, + updateHost, + ]); + + useEffect(() => { + if (nativeViewHandle == null && !mountThroughNativeHost) { + return; + } - mountedRef.current = true; - const context = contextRef.current; - runOnUI(() => { - if (!disposedRef.current) { - definition.mounted?.(nativeView, propsRef.current, context ?? undefined); + const currentProps = propsRef.current; + const previousProps = previousPropsRef.current; + previousPropsRef.current = currentProps; + + if (mountThroughNativeHost) { + runOnUI( + (nextProps, fallbackPreviousProps) => { + syncUIKitHostPropsFromReact(hostId, nextProps); + const host = ensureRegisteredUIKitHost(hostId); + if (!host) { + return null; + } + host.propsRef.current = nextProps; + updateHost?.( + host.nativeView, + nextProps, + host.previousProps ?? fallbackPreviousProps, + host.context, + ); + host.previousProps = nextProps; + return uikitHostHandles(host); + }, + currentProps, + previousProps, + ) + .then(applyHostHandles) + .catch((reason) => { + setError( + reason instanceof Error ? reason : new Error(String(reason)), + ); + }); + updateMeasuredSize(); + return; + } + + runOnUI( + (nextProps, fallbackPreviousProps) => { + const host = ensureRegisteredUIKitHost(hostId); + if (!host) { + return; } - }).catch((reason) => { - setError(reason instanceof Error ? reason : new Error(String(reason))); - }); - }, [definition, nativeViewHandle]); + host.propsRef.current = nextProps; + updateHost?.( + host.nativeView, + nextProps, + host.previousProps ?? fallbackPreviousProps, + host.context, + ); + host.previousProps = nextProps; + }, + currentProps, + previousProps, + ).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + updateMeasuredSize(); + }, [ + hostId, + mountThroughNativeHost, + nativeViewHandle, + pluginProps, + updateHost, + ]); + + useEffect(() => { + if ( + mountedRef.current || + (nativeViewHandle == null && !mountThroughNativeHost) + ) { + return; + } - if (error) { - throw error; + if (mountThroughNativeHost) { + mountedRef.current = true; + return; } - const layoutStyle = - measuredSize && definition.layout?.sizing !== 'fill' - ? { - width: measuredSize.width, - height: measuredSize.height, + mountedRef.current = true; + const currentProps = propsRef.current; + const isDisposed = disposedRef.current; + runOnUI( + (nextProps, shouldSkipMounted) => { + if (!shouldSkipMounted) { + const host = ensureRegisteredUIKitHost(hostId); + if (!host) { + return; } - : undefined; - - return React.createElement(NativeScriptUIViewNativeComponent, { - ...nativeProps, - collapsable: false, - childrenViewHandle, - controllerHandle, - debugName, - nativeViewHandle, - style: layoutStyle - ? [nativeProps.style, layoutStyle] - : nativeProps.style, + host.propsRef.current = nextProps; + mountedHost?.(host.nativeView, nextProps, host.context); + } + }, + currentProps, + isDisposed, + ).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); }); - }, - ); + }, [hostId, mountedHost, mountThroughNativeHost, nativeViewHandle]); + + if (error) { + throw error; + } - Component.displayName = definition.displayName || definition.name || debugName; + const layoutStyle = + measuredSize && layoutSizing !== "fill" + ? { + width: measuredSize.width, + height: measuredSize.height, + } + : undefined; + const { children, ...nativePropsWithoutChildren } = + nativeProps as ViewProps & { children?: React.ReactNode }; + + return React.createElement(NativeScriptUIViewNativeComponent, { + ...nativePropsWithoutChildren, + collapsable: false, + children, + childrenViewHandle, + controllerHandle: attachController ? controllerHandle : undefined, + detachControllerView: + attachController && !attachControllerView ? true : undefined, + debugName, + hostReadyId: hostId, + hostId: mountThroughNativeHost ? hostId : undefined, + mountedRevision: + mountThroughNativeHost && mountedHost != null && nativeHostRevision > 0 + ? nativeHostRevision + : undefined, + nativeViewHandle: attachNativeView ? nativeViewHandle : undefined, + style: layoutStyle ? [nativeProps.style, layoutStyle] : nativeProps.style, + updateRevision: + mountThroughNativeHost && nativeHostRevision > 0 + ? nativeHostRevision + : undefined, + }); + }); + + Component.displayName = + definition.displayName || definition.name || debugName; return Component; } @@ -1849,6 +3193,8 @@ export function defineUIKitContainer< return defineUIKitHost({ ...definition, resolveHostInstance(created) { + "worklet"; + return { hostView: created.rootView, lifecycleValue: created, @@ -1871,10 +3217,13 @@ export function defineUIViewController< ...definition, create: definition.createController, resolveHostInstance(controller) { + "worklet"; + const controllerRecord = controller as Record; return { - hostView: controllerRecord.view, + hostView: definition.hostView?.(controller) ?? controllerRecord.view, lifecycleValue: controller, + childrenView: definition.childrenView?.(controller), controller, }; }, @@ -1891,6 +3240,7 @@ const NativeScript = { defineUIKitView, defineUIViewController, getRuntimeBackend, + installWorklets, assertUIKitThread, createDelegate, createEventBridge, @@ -1905,7 +3255,9 @@ const NativeScript = { loadFramework, release, retain, + refreshUIKitHostView, runOnUI, + runtimeInvoker, uiInvoker, warnIfNotUIKitThread, }; diff --git a/packages/react-native/test/babel-plugin.test.js b/packages/react-native/test/babel-plugin.test.js new file mode 100644 index 000000000..e69bf4e35 --- /dev/null +++ b/packages/react-native/test/babel-plugin.test.js @@ -0,0 +1,54 @@ +const assert = require('assert'); +const babel = require('@babel/core'); +const plugin = require('../plugin/babel-plugin'); + +function transform(source) { + return babel.transformSync(source, { + ast: false, + babelrc: false, + configFile: false, + plugins: [plugin], + }).code; +} + +const source = ` +import NativeScript, { + defineUIKitContainer, + defineUIKitView, + defineUIViewController, +} from '@nativescript/react-native'; + +defineUIKitView({ + create() { + return UIView.new(); + }, + update: (view) => view.setNeedsLayout(), +}); + +NativeScript.defineUIViewController({ + createController() { + return UIViewController.new(); + }, + childrenView: (controller) => controller.view, + mounted(controller) { + controller.view.setNeedsLayout(); + }, + dispose() {}, +}); + +defineUIKitContainer({ + create() { + return {rootView: UIView.new(), childrenView: UIView.new()}; + }, +}); +`; + +const output = transform(source); +const workletDirectiveCount = (output.match(/"worklet";/g) || []).length; +assert.strictEqual(workletDirectiveCount, 7); +assert(output.includes('create() {\n "worklet";')); +assert(output.includes('update: view => {\n "worklet";')); +assert(output.includes('createController() {\n "worklet";')); +assert(output.includes('childrenView: controller => {\n "worklet";')); + +console.log('babel plugin tests passed'); diff --git a/packages/react-native/test/callback-thread-policy.test.js b/packages/react-native/test/callback-thread-policy.test.js new file mode 100644 index 000000000..8c88b9ae8 --- /dev/null +++ b/packages/react-native/test/callback-thread-policy.test.js @@ -0,0 +1,44 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../../.."); +const callbackSourcePaths = [ + "packages/react-native/native-api/ffi/shared/bridge/Callbacks.mm", + "NativeScript/ffi/shared/bridge/Callbacks.mm", +]; + +for (const relativePath of callbackSourcePaths) { + const callbacksSource = fs.readFileSync( + path.join(repoRoot, relativePath), + "utf8", + ); + + const nativeCallerPolicyIndex = callbacksSource.indexOf( + "if (nativeCallerThreadCallbacks && !currentThreadIsJs)", + ); + assert.notStrictEqual( + nativeCallerPolicyIndex, + -1, + `${relativePath} should have an explicit off-JS native-caller fast path`, + ); + + const asyncZeroArgBlockIndex = callbacksSource.indexOf( + "dispatchZeroArgVoidBlockAsync()", + ); + assert( + nativeCallerPolicyIndex < asyncZeroArgBlockIndex, + `${relativePath} must not route native-caller callbacks through the JS async block fallback first`, + ); + + const legacyVoidBlockFallback = callbacksSource.indexOf( + "!currentThreadIsJs && returnsVoid && block_ &&\n jsThreadCallbackInvoker", + ); + assert.strictEqual( + legacyVoidBlockFallback, + -1, + `${relativePath} must not force void native blocks from a caller-thread runtime onto JS`, + ); +} + +console.log("callback thread policy tests passed"); diff --git a/packages/react-native/test/config-plugin.test.js b/packages/react-native/test/config-plugin.test.js index a211cbcab..3346c0f61 100644 --- a/packages/react-native/test/config-plugin.test.js +++ b/packages/react-native/test/config-plugin.test.js @@ -25,6 +25,10 @@ withTempProject((projectRoot) => { (source.match(/@nativescript\/react-native\/babel-plugin/g) || []).length, 1, ); + assert.strictEqual( + (source.match(/react-native-worklets\/plugin/g) || []).length, + 1, + ); }); withTempProject((projectRoot) => { @@ -47,6 +51,33 @@ withTempProject((projectRoot) => { (source.match(/@nativescript\/react-native\/babel-plugin/g) || []).length, 1, ); + assert.strictEqual( + (source.match(/react-native-worklets\/plugin/g) || []).length, + 1, + ); +}); + +withTempProject((projectRoot) => { + fs.writeFileSync( + path.join(projectRoot, 'babel.config.js'), + [ + 'module.exports = {', + " plugins: ['@nativescript/react-native/babel-plugin'],", + '};', + '', + ].join('\n'), + ); + ensureBabelPlugin(projectRoot); + ensureBabelPlugin(projectRoot); + const source = fs.readFileSync(path.join(projectRoot, 'babel.config.js'), 'utf8'); + assert.strictEqual( + (source.match(/@nativescript\/react-native\/babel-plugin/g) || []).length, + 1, + ); + assert.strictEqual( + (source.match(/react-native-worklets\/plugin/g) || []).length, + 1, + ); }); withTempProject((projectRoot) => { diff --git a/packages/react-native/test/interop-object-api.test.js b/packages/react-native/test/interop-object-api.test.js new file mode 100644 index 000000000..1db1b3859 --- /dev/null +++ b/packages/react-native/test/interop-object-api.test.js @@ -0,0 +1,40 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../../.."); +const packageRoot = path.resolve(__dirname, ".."); + +for (const relativePath of [ + "packages/react-native/native-api/ffi/shared/bridge/TypeConv.mm", + "NativeScript/ffi/shared/bridge/TypeConv.mm", +]) { + const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); + assert( + source.includes('PropNameID::forAscii(runtime, "object")'), + `${relativePath} should expose interop.object`, + ); + assert( + source.includes("nativeObjectReturnTypeForClass(object_getClass(object))") && + source.includes("convertNativeReturnValue(runtime, bridge, type, &object)"), + `${relativePath} should wrap Objective-C object pointers through the generic NativeScript object bridge`, + ); + assert( + source.includes( + "parseIntegerTextToUintptr(args[0].asString(runtime).utf8(runtime)", + ), + `${relativePath} should accept string pointer addresses without treating them as C strings`, + ); +} + +const declarations = fs.readFileSync( + path.join(packageRoot, "types/objc-node-api/index.d.ts"), + "utf8", +); +assert( + declarations.includes("function objectschedule("), + "loaded UIImages should be retained and delivered back through the worklet runtime", +); + +const publicApi = fs.readFileSync(path.join(packageRoot, "src/index.ts"), "utf8"); +assert( + publicApi.includes("export function loadImage(") && + publicApi.includes(".__nativeScriptLoadReactImage") && + publicApi.includes("interop?.object?.(interop.Pointer(handle))") && + publicApi.includes("loadImage: (source, options, callback)"), + "public TS API should wrap image handles into NativeScript objects and expose ctx.loadImage", +); + +const declarations = fs.readFileSync( + path.join(packageRoot, "src/index.d.ts"), + "utf8", +); +assert( + declarations.includes("NativeScriptImageLoadOptions") && + declarations.includes("loadImage("), + "type declarations should expose generic image loading", +); + +console.log("React Native image loader API tests passed"); diff --git a/packages/react-native/test/runtime-callback-policy.test.js b/packages/react-native/test/runtime-callback-policy.test.js new file mode 100644 index 000000000..2536921d0 --- /dev/null +++ b/packages/react-native/test/runtime-callback-policy.test.js @@ -0,0 +1,122 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(packageRoot, "../.."); + +function readPackage(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +function readRepo(relativePath) { + return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); +} + +const index = readPackage("src/index.ts"); +assert( + index.includes('export type NativeScriptCallbackThread = "js" | "runtime"'), + "public JS API should expose a generic runtime callback thread policy", +); +assert( + index.includes("export function runtimeInvoker"), + "public JS API should export runtimeInvoker", +); +assert( + !index.includes("export function objCBlock") && + !index.includes("export function objCFunctionPointer") && + !index.includes("typedObjCCallback"), + "public JS API should not expose RN-specific typed ObjC callback helpers", +); +assert( + index.includes('return callbackInvoker("runtime", callback)'), + "runtimeInvoker should mark callbacks for their owning NativeScript runtime", +); +assert( + index.includes("Object.getOwnPropertyNames(callback)") && + index.includes("Object.getOwnPropertySymbols(callback)") && + index.includes("Object.defineProperty(wrapped, key, descriptor)"), + "callback invokers should preserve native callback/block metadata on wrappers", +); +assert( + index.includes("interop.Block = wrapInteropFactory"), + "interop.Block should be made callable/constructable when the runtime exposes it", +); +assert( + index.includes('if (thread === "runtime")'), + "eventBridge should route runtime callbacks through runtimeInvoker", +); +assert( + !index.includes("afterUIKitTransition"), + "runtime should not expose a transition-specific callback API", +); + +const declarations = readPackage("src/index.d.ts"); +assert( + declarations.includes('NativeScriptCallbackThread = "js" | "runtime"'), + "public declarations should include the runtime callback policy", +); +assert( + declarations.includes("runtimeInvokerschedule"), + "runtime callbacks should schedule work onto the Worklet runtime", +); +assert( + !moduleSource.includes("__nativeScriptAfterUIKitTransition"), + "Native module should not install transition-specific host functions", +); + +for (const relativePath of [ + "packages/react-native/native-api/ffi/shared/NativeApiBackendConfig.h", + "NativeScript/ffi/shared/NativeApiBackendConfig.h", +]) { + const source = readRepo(relativePath); + assert( + source.includes("runtimeCallbackInvoker"), + `${relativePath} should expose a generic runtime callback invoker`, + ); +} + +for (const relativePath of [ + "packages/react-native/native-api/ffi/shared/bridge/Callbacks.mm", + "NativeScript/ffi/shared/bridge/Callbacks.mm", +]) { + const source = readRepo(relativePath); + assert( + source.includes("NativeApiCallbackThreadPolicy::Runtime"), + `${relativePath} should support the runtime callback policy`, + ); + assert( + source.includes('policy == "runtime"'), + `${relativePath} should parse runtime callback policy markers`, + ); + assert( + source.includes("bridge_->runtimeCallbackInvoker()"), + `${relativePath} should dispatch runtime-marked callbacks through the generic invoker`, + ); + assert( + source.includes("parseObjCCallbackEngineSignature") && + source.includes("objcSignatureEncoding"), + `${relativePath} should build callbacks from explicit ObjC encodings`, + ); +} + +console.log("runtime callback policy tests passed"); diff --git a/packages/react-native/test/runtime-member-cache.test.js b/packages/react-native/test/runtime-member-cache.test.js new file mode 100644 index 000000000..3bedd944b --- /dev/null +++ b/packages/react-native/test/runtime-member-cache.test.js @@ -0,0 +1,44 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../../.."); +const hostObjectSources = [ + "NativeScript/ffi/shared/bridge/HostObjects.mm", + "packages/react-native/native-api/ffi/shared/bridge/HostObjects.mm", +]; + +for (const sourcePath of hostObjectSources) { + const hostObjects = fs.readFileSync(path.join(repoRoot, sourcePath), "utf8"); + + assert( + hostObjects.includes("NativeApiRuntimeMembersCacheKey"), + `${sourcePath}: runtime member reflection should key cache entries by class/staticness`, + ); + assert( + hostObjects.includes("NativeApiRuntimeMemberIndex"), + `${sourcePath}: runtime member reflection should build an indexed lookup`, + ); + assert( + hostObjects.includes("runtimeMembersCacheMutex"), + `${sourcePath}: runtime member reflection cache should be synchronized across runtimes`, + ); + assert( + hostObjects.includes("buildRuntimeMembersForClass"), + `${sourcePath}: runtime member reflection should separate scanning from cache lookup`, + ); + assert( + hostObjects.includes("cache.emplace(key, members)"), + `${sourcePath}: runtime member reflection should populate the cache after scanning once`, + ); + assert( + hostObjects.includes("memberNames.find(name)"), + `${sourcePath}: runtime member existence checks should use indexed name lookup`, + ); + assert( + hostObjects.includes("selectorsByNameAndCount.find(name)"), + `${sourcePath}: runtime selector resolution should use indexed selector lookup`, + ); +} + +console.log("runtime member cache tests passed"); diff --git a/packages/react-native/test/runtime-objc-property-setter.test.js b/packages/react-native/test/runtime-objc-property-setter.test.js new file mode 100644 index 000000000..4c4935659 --- /dev/null +++ b/packages/react-native/test/runtime-objc-property-setter.test.js @@ -0,0 +1,39 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../../.."); + +for (const relativePath of [ + "NativeScript/ffi/shared/bridge/HostObjects.mm", + "packages/react-native/native-api/ffi/shared/bridge/HostObjects.mm", +]) { + const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); + + assert( + source.includes("runtimeWritablePropertySetter"), + `${relativePath}: host objects should discover writable Objective-C runtime properties`, + ); + assert( + source.includes("runtimeReadablePropertyGetter"), + `${relativePath}: host objects should discover readable Objective-C runtime properties`, + ); + assert( + source.includes("property_copyAttributeValue(prop, \"S\")"), + `${relativePath}: runtime property fallback should honor custom Objective-C setters`, + ); + assert( + source.includes("property_copyAttributeValue(prop, \"R\")"), + `${relativePath}: runtime property fallback should not assign readonly Objective-C properties`, + ); + assert( + source.includes("callObjCSelector(runtime, bridge_, object_, false,\n *setterSelectorName, nullptr, args, 1);"), + `${relativePath}: runtime property fallback should invoke the discovered native setter`, + ); + assert( + source.includes("return callObjectSelector(runtime, *selector, nullptr, nullptr, 0);"), + `${relativePath}: JS-extended instances should read discovered native properties before returning undefined`, + ); +} + +console.log("runtime Objective-C property setter tests passed"); diff --git a/packages/react-native/test/uikit-controller-appearance-api.test.js b/packages/react-native/test/uikit-controller-appearance-api.test.js new file mode 100644 index 000000000..e516862ff --- /dev/null +++ b/packages/react-native/test/uikit-controller-appearance-api.test.js @@ -0,0 +1,41 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +const hostView = read("ios/NativeScriptUIView.mm"); + +assert( + hostView.includes("NativeScriptShouldForwardControllerAppearance"), + "NativeScriptUIView should centralize visible-controller appearance fallback checks", +); +assert( + hostView.includes("NativeScriptHostedViewContainsControllerView"), + "NativeScriptUIView should detect when the hosted native view contains the controller view", +); +assert( + hostView.includes("[hostedViewToReinsert removeFromSuperview];") && + hostView.includes("[parent addChildViewController:_viewController];") && + hostView.includes("[super insertSubview:hostedViewToReinsert atIndex:targetIndex];") && + hostView.includes("[_viewController didMoveToParentViewController:parent];"), + "NativeScriptUIView should add child controllers before reinserting hosted visible views", +); +assert( + hostView.includes( + "hostedViewToReinsert == nil && NativeScriptShouldForwardControllerAppearance(_viewController)", + ), + "NativeScriptUIView should only manually forward appearance when it cannot re-order the hosted view", +); +assert( + hostView.includes("[_viewController beginAppearanceTransition:YES animated:NO];") && + hostView.includes("[_viewController beginAppearanceTransition:NO animated:NO];") && + hostView.includes("[_viewController endAppearanceTransition];"), + "NativeScriptUIView should retain manual appearance forwarding as a fallback", +); + +console.log("uikit controller appearance API tests passed"); diff --git a/packages/react-native/test/uikit-controller-host-view-api.test.js b/packages/react-native/test/uikit-controller-host-view-api.test.js new file mode 100644 index 000000000..7951dc631 --- /dev/null +++ b/packages/react-native/test/uikit-controller-host-view-api.test.js @@ -0,0 +1,37 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +const index = read("src/index.ts"); +assert( + index.includes("hostView?: (controller: Controller) => unknown"), + "defineUIViewController should expose a generic hostView resolver", +); +assert( + index.includes("hostView: definition.hostView?.(controller) ?? controllerRecord.view"), + "defineUIViewController should use the resolved host view before falling back to controller.view", +); + +const declarations = read("src/index.d.ts"); +assert( + declarations.includes("hostView?: (controller: Controller) => unknown"), + "public declarations should expose UIViewControllerDefinition.hostView", +); + +const nativeHost = read("ios/NativeScriptUIView.mm"); +assert( + nativeHost.includes("if (_nativeViewHandle.length == 0) {\n [self setNativeView:_viewController.view];"), + "NativeScriptUIView should not overwrite an explicit native host view with controller.view", +); +assert( + nativeHost.includes("[self attachViewControllerIfPossible];"), + "NativeScriptUIView should still attach the controller for lifecycle when a custom host view is used", +); + +console.log("uikit controller host-view API tests passed"); diff --git a/packages/react-native/test/uikit-gesture-action-api.test.js b/packages/react-native/test/uikit-gesture-action-api.test.js new file mode 100644 index 000000000..1d40d977b --- /dev/null +++ b/packages/react-native/test/uikit-gesture-action-api.test.js @@ -0,0 +1,75 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +const index = read("src/index.ts"); +assert( + index.includes("gestureAction("), + "UIKit context should expose a gestureAction helper", +); +assert( + index.includes("targetAction(control, events, callback)"), + "UIKit context should expose a targetAction helper", +); +assert( + index.includes("actionTarget(callback)"), + "UIKit context should expose a generic target/action helper", +); +assert( + index.includes("function invokeNativeScriptCallback("), + "UIKit native callbacks should route through a shared callback scheduler", +); +assert( + index.includes('nativeScriptCallbackThread(callback) !== "js"'), + "callback scheduler should distinguish JS-owned callbacks from runtime callbacks", +); +assert( + index.includes("workletsProxy.scheduleOnRN(handler, serializer(args))"), + "JS-owned UIKit callbacks should schedule asynchronously onto the RN runtime", +); +assert( + index.includes("invokeNativeScriptCallback(callback, [], () => disposed)"), + "targetAction should honor callback thread policy instead of calling callbacks synchronously", +); +assert( + index.includes("nativeGesture.addTargetAction(target, selector)"), + "gestureAction should attach a target/action to UIGestureRecognizer", +); +assert( + index.includes("nativeGesture.removeTargetAction(target, selector)"), + "gestureAction should remove the target/action on dispose", +); +assert( + index.includes("callback(sender ?? gesture)"), + "gestureAction should pass the recognizer sender to the callback", +); +assert( + index.includes("invokeNativeScriptCallback(callback, [sender], () => disposed)"), + "actionTarget should honor callback thread policy and pass the sender", +); +assert( + index.includes('action: "nativeScriptHandleAction:"'), + "actionTarget should return the Objective-C selector name", +); + +const declarations = read("src/index.d.ts"); +assert( + declarations.includes("gestureAction("), + "public declarations should expose gestureAction", +); +assert( + declarations.includes("callback: (gesture: unknown) => void"), + "gestureAction declarations should pass the recognizer to callbacks", +); +assert( + declarations.includes("actionTarget(callback: (sender: unknown) => void)"), + "public declarations should expose generic actionTarget", +); + +console.log("uikit gesture action API tests passed"); diff --git a/packages/react-native/test/uikit-host-dispose-api.test.js b/packages/react-native/test/uikit-host-dispose-api.test.js new file mode 100644 index 000000000..c11bc5f0e --- /dev/null +++ b/packages/react-native/test/uikit-host-dispose-api.test.js @@ -0,0 +1,39 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +const index = read("src/index.ts"); +assert( + index.includes("export type UIKitDisposeResult"), + "public source should define UIKitDisposeResult", +); +assert( + index.includes("disposeResult?.removeHostView !== false"), + "disposeRegisteredUIKitHost should honor removeHostView=false", +); +assert( + index.includes("return disposeHost?.(nativeView, disposeProps, context);"), + "host adapters should propagate dispose return values", +); + +const declarations = read("src/index.d.ts"); +assert( + declarations.includes("export type UIKitDisposeResult"), + "public declarations should expose UIKitDisposeResult", +); +assert( + declarations.includes("removeHostView?: boolean"), + "UIKitDisposeResult should expose generic host-view removal control", +); +assert( + declarations.includes(") => UIKitDisposeResult"), + "dispose declarations should return UIKitDisposeResult", +); + +console.log("uikit host dispose API tests passed"); diff --git a/packages/react-native/test/uikit-host-ready-api.test.js b/packages/react-native/test/uikit-host-ready-api.test.js new file mode 100644 index 000000000..0d31ff9b8 --- /dev/null +++ b/packages/react-native/test/uikit-host-ready-api.test.js @@ -0,0 +1,79 @@ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const packageRoot = path.resolve(__dirname, '..'); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), 'utf8'); +} + +const nativeComponent = read('src/NativeScriptUIViewNativeComponent.ts'); +assert( + nativeComponent.includes('DirectEventHandler'), + 'NativeScriptUIViewNativeComponent should use a generated direct event type', +); +assert( + nativeComponent.includes('hostReadyId?: string'), + 'NativeScriptUIViewNativeComponent should expose a stable readiness identity prop', +); +assert( + nativeComponent.includes('onHostReady?: DirectEventHandler'), + 'NativeScriptUIViewNativeComponent should expose onHostReady', +); +assert( + nativeComponent.includes('hasChildren: boolean'), + 'onHostReady should report whether RN children are attached', +); + +const declarations = read('src/index.d.ts'); +assert( + declarations.includes('export type UIKitHostReadyEvent'), + 'public declarations should export UIKitHostReadyEvent', +); +assert( + declarations.includes('onHostReady?: (event: UIKitHostReadyEvent) => void'), + 'public host props should expose onHostReady', +); + +const index = read('src/index.ts'); +assert( + index.includes('hostReadyId: hostId'), + 'defineUIKitHost should pass a stable hostReadyId to the native host view', +); +assert( + index.includes('onHostReady'), + 'defineUIKitHost should forward onHostReady to NativeScriptUIView', +); + +const header = read('ios/NativeScriptUIView.h'); +assert( + header.includes('@property(nonatomic, copy) NSString* hostReadyId'), + 'NativeScriptUIView should store the readiness identity', +); +assert( + header.includes('onHostReady'), + 'NativeScriptUIView should expose a Paper host-ready event block', +); + +const manager = read('ios/NativeScriptUIViewManager.mm'); +assert( + manager.includes('RCT_EXPORT_VIEW_PROPERTY(hostReadyId, NSString)'), + 'Paper manager should export hostReadyId', +); +assert( + manager.includes('RCT_EXPORT_VIEW_PROPERTY(onHostReady, RCTDirectEventBlock)'), + 'Paper manager should export onHostReady', +); + +const fabricView = read('ios/Fabric/NativeScriptUIViewComponentView.mm'); +assert( + fabricView.includes('EventEmitters.h'), + 'Fabric component should import generated event emitters', +); +assert( + fabricView.includes('onHostReady('), + 'Fabric component should emit onHostReady', +); + +console.log('uikit host ready API tests passed'); diff --git a/packages/react-native/test/uikit-host-refresh-api.test.js b/packages/react-native/test/uikit-host-refresh-api.test.js new file mode 100644 index 000000000..a13014513 --- /dev/null +++ b/packages/react-native/test/uikit-host-refresh-api.test.js @@ -0,0 +1,142 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +const index = read("src/index.ts"); +assert( + index.includes("export function refreshUIKitHostView"), + "public JS API should export refreshUIKitHostView", +); +assert( + index.includes("__nativeScriptRefreshUIKitHostView"), + "refreshUIKitHostView should call the worklet-installed native refresh global", +); +assert( + index.includes("export function refreshUIKitHostViewHandle") && + index.includes("return refresh(nativeHandleForUIKitView(view)) === true;") && + index.includes("return refresh(viewHandle) === true;"), + "public JS API should refresh UIKit hosts from native handles", +); + +const declarations = read("src/index.d.ts"); +assert( + declarations.includes("refreshUIKitHostView(view: unknown): boolean"), + "public declarations should expose refreshUIKitHostView", +); +assert( + declarations.includes("refreshUIKitHostViewHandle(viewHandle: string): boolean"), + "public declarations should expose handle-based UIKit host refresh", +); + +const hostHeader = read("ios/NativeScriptUIKitHost.h"); +assert( + hostHeader.includes("NativeScriptRefreshUIKitHostView"), + "UIKit host header should export a native refresh entry point", +); + +const hostView = read("ios/NativeScriptUIView.mm"); +assert( + hostView.includes("#import "), + "NativeScriptUIView should use ObjC associations for detached children hosts", +); +assert( + hostView.includes("refreshDetachedChildrenHost"), + "NativeScriptUIView should be able to refresh detached React children", +); +assert( + hostView.includes("NativeScriptDetachedChildrenOwner") && + hostView.includes("objc_setAssociatedObject") && + hostView.includes("objc_getAssociatedObject"), + "NativeScriptUIView should associate detached children views with their owner", +); +assert( + hostView.includes("NativeScriptDetachedChildrenOwner(root)") && + hostView.includes("refreshDetachedChildrenHost"), + "refreshUIKitHostView should refresh a detached children view even if its sentinel was removed", +); +assert( + hostView.includes( + "return NativeScriptChildrenViewHasVisibleChild(_childrenView, _detachedTouchSentinel);", + ), + "refreshUIKitHostView should report whether hosted React children are ready", +); +assert( + hostView.includes("UIView* touchView = _childrenView;"), + "NativeScriptUIView should attach the RN touch handler to the stable detached children host", +); +assert( + hostView.includes("NativeScriptViewHasGestureRecognizer(touchView, _detachedTouchHandler)") && + hostView.includes("NativeScriptGestureRecognizerAttachedView(_detachedTouchHandler)") && + hostView.includes("_detachedTouchHandlerWindow != touchView.window") && + hostView.includes("[self detachDetachedChildrenTouchHandler];"), + "NativeScriptUIView should repair a stale detached RN touch handler after UIKit window transitions", +); +assert( + hostView.includes("_detachedTouchHandlerWindow = touchView.window;") && + hostView.includes("_detachedTouchHandlerWindow = nil;"), + "NativeScriptUIView should track and clear the detached touch handler window", +); +assert( + hostView.includes("touchView.userInteractionEnabled = YES;"), + "NativeScriptUIView should keep the hosted RN touch surface interactive after refreshes", +); +assert( + hostView.includes("NativeScriptFindAncestorSurfaceTouchHandler") && + hostView.includes("NativeScriptFindAncestorSurfaceTouchHandler(touchView) != nil") && + hostView.includes("[self detachDetachedChildrenTouchHandler];"), + "NativeScriptUIView should not install a duplicate detached touch handler below an ancestor RCTSurfaceTouchHandler", +); +assert( + hostView.includes("UIView* detachView =") && + hostView.includes("NativeScriptGestureRecognizerAttachedView(_detachedTouchHandler)") && + hostView.includes("NativeScriptViewHasGestureRecognizer(detachView, _detachedTouchHandler)") && + hostView.includes("[_detachedTouchHandler detachFromView:detachView];"), + "NativeScriptUIView should detach RCTSurfaceTouchHandler from its actual attached view, not a stale stored host view", +); +assert( + hostView.includes("- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {\n [self refreshDetachedChildrenHost];"), + "NativeScriptUIView should refresh the detached RN touch host before first hit testing", +); +assert( + !hostView.includes("NativeScriptFirstReactTaggedSubview"), + "NativeScriptUIView should not attach RN touch handling to a route-dependent React descendant", +); + +const fabricHostView = read("ios/Fabric/NativeScriptUIViewComponentView.mm"); +assert( + fabricHostView.includes("[_containerView refreshDetachedChildrenHost];"), + "Fabric wrapper should refresh the detached RN touch host before first hit testing", +); +assert( + fabricHostView.includes("- (void)didMoveToWindow") && + fabricHostView.includes("[_containerView refreshDetachedChildrenHost];"), + "Fabric wrapper should refresh detached RN touch hosts when UIKit moves the wrapper between windows", +); +assert( + fabricHostView.includes("- (void)mountChildComponentView") && + !fabricHostView.includes( + "- (void)mountChildComponentView:(UIView*)childComponentView\n index:(NSInteger)index {\n [_containerView insertSubview:childComponentView atIndex:index];\n [_containerView layoutDetachedChildrenViewSubviewsIfNeeded];", + ), + "Fabric child mounts should use full host refresh instead of layout-only refresh", +); +assert( + fabricHostView.includes("- (void)updateLayoutMetrics") && + !fabricHostView.includes( + "- (void)updateLayoutMetrics:(const LayoutMetrics&)layoutMetrics\n oldLayoutMetrics:(const LayoutMetrics&)oldLayoutMetrics {\n [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];\n [_containerView layoutDetachedChildrenViewSubviewsIfNeeded];", + ), + "Fabric layout updates should refresh the touch host origin and handler, not just resize children", +); + +const moduleSource = read("ios/NativeScriptNativeApiModule.mm"); +assert( + moduleSource.includes("__nativeScriptRefreshUIKitHostView"), + "worklet runtime install should expose the refresh host function", +); + +console.log("uikit host refresh API tests passed"); diff --git a/packages/react-native/test/uikit-tabbar-hit-test.test.js b/packages/react-native/test/uikit-tabbar-hit-test.test.js new file mode 100644 index 000000000..2e47c0379 --- /dev/null +++ b/packages/react-native/test/uikit-tabbar-hit-test.test.js @@ -0,0 +1,34 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); + +function read(relativePath) { + return fs.readFileSync(path.join(packageRoot, relativePath), "utf8"); +} + +for (const relativePath of [ + "ios/NativeScriptUIView.mm", + "ios/Fabric/NativeScriptUIViewComponentView.mm", +]) { + const source = read(relativePath); + assert( + source.includes("PointInsideTabBarHitArea"), + `${relativePath} should gate tab bar passthrough on the tab bar hit area`, + ); + assert( + source.includes("EffectiveTabBarHitBounds"), + `${relativePath} should cap oversized tab bar visual bounds before hit testing`, + ); + assert( + source.includes("CGRectInset(bounds, -24, -16)"), + `${relativePath} should allow a small expanded tab bar hit target`, + ); + assert( + !source.includes("VisibleHitViewAtPoint"), + `${relativePath} should not use recursive tab bar descendants as the passthrough hit area`, + ); +} + +console.log("uikit tab bar hit-test tests passed"); diff --git a/packages/react-native/test/worklets-frame-loop.test.js b/packages/react-native/test/worklets-frame-loop.test.js new file mode 100644 index 000000000..05daecfca --- /dev/null +++ b/packages/react-native/test/worklets-frame-loop.test.js @@ -0,0 +1,61 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const packageRoot = path.resolve(__dirname, ".."); +const index = fs.readFileSync(path.join(packageRoot, "src/index.ts"), "utf8"); + +assert( + index.includes("function installIdleAwareWorkletsFrameLoop"), + "runtime should install an idle-aware Worklets frame loop", +); +assert( + index.includes("__nativeScriptIdleAwareWorkletsFrameLoop"), + "frame loop install should be idempotent inside the UI runtime", +); +assert( + index.includes("__nativeScriptNativeRequestAnimationFrame"), + "frame loop should retain the native RAF host function before overriding it", +); +assert( + index.includes("globalObject.__nativeRequestAnimationFrame = () => undefined"), + "frame loop should stop react-native-worklets' perpetual startup frame pump", +); +assert( + index.includes("scheduleNativeFlush();"), + "requestAnimationFrame should schedule native frames only when callbacks exist", +); +assert( + index.includes("NSTimerClass.timerWithTimeIntervalRepeatsBlock"), + "UI runtime timers should use native NSTimer instead of RAF polling", +); +assert( + index.includes("NSRunLoopClass.mainRunLoop.addTimerForMode"), + "native UI timers should run in common run-loop modes", +); +assert( + index.includes("function runtimeTimerInvoker"), + "native UI timers should mark callbacks for the owning Worklets runtime", +); +assert( + index.includes("Math.max(0.001, numericDelay / 1000)"), + "native UI timers should treat zero-delay JS timers as next-run-loop timers", +); +assert( + index.includes('value: "runtime"'), + "native UI timer callbacks should use the generic runtime callback policy", +); +assert( + index.includes("globalObject.setTimeout = ("), + "Worklets UI runtime setTimeout should be overridden by NativeScript", +); +assert( + index.includes("globalObject.setInterval = ("), + "Worklets UI runtime setInterval should be overridden by NativeScript", +); +assert( + index.includes(".runOnUIAsync(installIdleAwareWorkletsFrameLoop)"), + "NativeScript worklet install should patch the UI runtime frame loop", +); + +console.log("worklets frame loop tests passed"); diff --git a/scripts/build_metadata_generator.sh b/scripts/build_metadata_generator.sh index 061c2d3c5..1d4b3c0e2 100755 --- a/scripts/build_metadata_generator.sh +++ b/scripts/build_metadata_generator.sh @@ -2,6 +2,10 @@ set -e source "$(dirname "$0")/build_utils.sh" +function metadata_generator_source_hash { + find src include CMakeLists.txt -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' +} + function build { rm -rf build mkdir build @@ -25,5 +29,6 @@ otool -L dist/x86_64/bin/objc-metadata-generator checkpoint "Building metadata generator for arm64 ..." build "arm64" otool -L dist/arm64/bin/objc-metadata-generator +metadata_generator_source_hash > dist/.source_hash rm -rf build -popd \ No newline at end of file +popd diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 7f775ac85..b1fef8d2b 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -17,8 +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/GeneratedSignatureDispatch.inc}} -GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" +REQUESTED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-}} for arg in $@; do case $arg in @@ -43,7 +42,6 @@ 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 ;; @@ -94,8 +92,17 @@ 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 + local ffi_backend + ffi_backend=$(effective_ffi_backend "$is_macos_napi") + if [ "$ffi_backend" != "napi" ]; then + case "$NS_GSD_BACKEND" in + auto) + echo "$ffi_backend" + ;; + *) + echo "$NS_GSD_BACKEND" + ;; + esac return fi @@ -124,17 +131,50 @@ function effective_ffi_backend () { case "$NS_FFI_BACKEND" in auto) if [[ "$TARGET_ENGINE" == "hermes" || "$TARGET_ENGINE" == "v8" || "$TARGET_ENGINE" == "jsc" || "$TARGET_ENGINE" == "quickjs" ]]; then - echo direct + echo "$TARGET_ENGINE" else echo napi fi ;; + v8|jsc|quickjs|hermes) + if [ "$NS_FFI_BACKEND" != "$TARGET_ENGINE" ]; then + echo "NS_FFI_BACKEND=$NS_FFI_BACKEND requires TARGET_ENGINE=$NS_FFI_BACKEND" >&2 + exit 1 + fi + echo "$NS_FFI_BACKEND" + ;; *) echo "$NS_FFI_BACKEND" ;; esac } +function signature_dispatch_path () { + local is_macos_napi="${1:-false}" + if [ -n "$REQUESTED_SIGNATURE_DISPATCH" ]; then + echo "$REQUESTED_SIGNATURE_DISPATCH" + return + fi + + local backend + backend=$(effective_gsd_backend "$is_macos_napi") + local ffi_backend + ffi_backend=$(effective_ffi_backend "$is_macos_napi") + + case "$backend" in + hermes) echo "./NativeScript/ffi/hermes/GeneratedSignatureDispatch.inc" ;; + v8) echo "./NativeScript/ffi/v8/GeneratedSignatureDispatch.inc" ;; + jsc) echo "./NativeScript/ffi/jsc/GeneratedSignatureDispatch.inc" ;; + quickjs) echo "./NativeScript/ffi/quickjs/GeneratedSignatureDispatch.inc" ;; + *) echo "./NativeScript/ffi/napi/GeneratedSignatureDispatch.inc" ;; + esac +} + +function metadata_generator_source_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}' +} + function signature_dispatch_stamp () { local platform="$1" local is_macos_napi="${2:-false}" @@ -143,12 +183,23 @@ function signature_dispatch_stamp () { 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}') + generator_hash=$(metadata_generator_source_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_metadata_generator () { + local expected_hash + expected_hash=$(metadata_generator_source_hash) + local hash_file="./metadata-generator/dist/.source_hash" + if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ] || \ + [ ! -x "./metadata-generator/dist/x86_64/bin/objc-metadata-generator" ] || \ + [ ! -f "$hash_file" ] || \ + [ "$(cat "$hash_file")" != "$expected_hash" ]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi +} + function ensure_signature_dispatch_bindings () { local platform="$1" local is_macos_napi="${2:-false}" @@ -164,20 +215,21 @@ function ensure_signature_dispatch_bindings () { local expected_stamp 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 + local generated_signature_dispatch + generated_signature_dispatch=$(signature_dispatch_path "$is_macos_napi") + local generated_signature_dispatch_stamp="${generated_signature_dispatch}.stamp" + if [ -f "$generated_signature_dispatch" ] && \ + [ -f "$generated_signature_dispatch_stamp" ] && \ + [ "$(cat "$generated_signature_dispatch_stamp")" == "$expected_stamp" ]; then return fi - if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ]; then - "$SCRIPT_DIR/build_metadata_generator.sh" - fi + ensure_metadata_generator 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" + NS_SIGNATURE_BINDINGS_CPP_PATH="$generated_signature_dispatch" npm run metagen "$platform" + mkdir -p "$(dirname "$generated_signature_dispatch_stamp")" + printf "%s" "$expected_stamp" > "$generated_signature_dispatch_stamp" } DEV_TEAM=${DEVELOPMENT_TEAM:-} diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index f64f23bfc..2fba87c23 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -6,8 +6,28 @@ 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:-} +GENERATED_SIGNATURE_DISPATCH_OVERRIDE=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-}} +GENERATED_SIGNATURE_DISPATCH=${GENERATED_SIGNATURE_DISPATCH_OVERRIDE:-"$REPO_ROOT/dist/intermediates/react-native/GeneratedSignatureDispatch.ios-sim.tmp.inc"} +DEVICE_SIGNATURE_DISPATCH="$REPO_ROOT/dist/intermediates/react-native/GeneratedSignatureDispatch.ios-device.tmp.inc" SKIP_PACK=false +function metadata_generator_source_hash { + find "$REPO_ROOT/metadata-generator/src" "$REPO_ROOT/metadata-generator/include" "$REPO_ROOT/metadata-generator/CMakeLists.txt" \ + -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' +} + +function ensure_metadata_generator { + local expected_hash + expected_hash=$(metadata_generator_source_hash) + local hash_file="$REPO_ROOT/metadata-generator/dist/.source_hash" + if [ ! -x "$REPO_ROOT/metadata-generator/dist/arm64/bin/objc-metadata-generator" ] || \ + [ ! -x "$REPO_ROOT/metadata-generator/dist/x86_64/bin/objc-metadata-generator" ] || \ + [ ! -f "$hash_file" ] || \ + [ "$(cat "$hash_file")" != "$expected_hash" ]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --no-pack) @@ -23,26 +43,58 @@ done checkpoint "Preparing @nativescript/react-native TurboModule package..." +ensure_metadata_generator + +checkpoint "Generating iOS device metadata for the TurboModule..." +mkdir -p "$(dirname "$DEVICE_SIGNATURE_DISPATCH")" +NS_SIGNATURE_BINDINGS_CPP_PATH="$DEVICE_SIGNATURE_DISPATCH" npm run metagen ios +rm -f "$DEVICE_SIGNATURE_DISPATCH" "$DEVICE_SIGNATURE_DISPATCH.stamp" + +checkpoint "Generating Hermes signature dispatch bindings for the TurboModule..." +mkdir -p "$(dirname "$GENERATED_SIGNATURE_DISPATCH")" +NS_SIGNATURE_BINDINGS_CPP_PATH="$GENERATED_SIGNATURE_DISPATCH" npm run metagen ios-sim + rm -rf \ - "$PACKAGE_DIR/native-api-jsi" \ + "$PACKAGE_DIR/native-api" \ "$PACKAGE_DIR/metadata" \ "$PACKAGE_DIR/ios/vendor" \ "$PACKAGE_DIR/types" mkdir -p \ - "$PACKAGE_DIR/native-api-jsi/jsi" \ - "$PACKAGE_DIR/native-api-jsi/metadata/include" \ + "$PACKAGE_DIR/native-api/ffi/hermes" \ + "$PACKAGE_DIR/native-api/ffi/shared" \ + "$PACKAGE_DIR/native-api/ffi/shared/bridge" \ + "$PACKAGE_DIR/native-api/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/hermes/jsi/NativeApiJsi.h "$PACKAGE_DIR/native-api-jsi/" -cp NativeScript/ffi/hermes/jsi/NativeApiJsi.mm "$PACKAGE_DIR/native-api-jsi/" -cp NativeScript/ffi/shared/jsi/NativeApiJsi*.h "$PACKAGE_DIR/native-api-jsi/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/ffi/hermes/NativeApiJsi*.mm "$PACKAGE_DIR/native-api/ffi/hermes/" +cp NativeScript/ffi/hermes/NativeApiJsi*.h "$PACKAGE_DIR/native-api/ffi/hermes/" +cp NativeScript/ffi/hermes/NativeApiJsiReactNative.h "$PACKAGE_DIR/native-api/ffi/hermes/" +cp NativeScript/ffi/shared/bridge/ObjCBridge.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/Callbacks.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/ClassBuilder.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/HostObject.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/HostObjects.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/Install.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/Invocation.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/bridge/TypeConv.mm "$PACKAGE_DIR/native-api/ffi/shared/bridge/" +cp NativeScript/ffi/shared/NativeApiBackendConfig.h "$PACKAGE_DIR/native-api/ffi/shared/" +cp NativeScript/ffi/shared/SignatureDispatchCore.h "$PACKAGE_DIR/native-api/ffi/shared/" +cp NativeScript/ffi/shared/PreparedSignatureDispatch.h "$PACKAGE_DIR/native-api/ffi/shared/" +cp "$GENERATED_SIGNATURE_DISPATCH" "$PACKAGE_DIR/native-api/ffi/hermes/GeneratedSignatureDispatch.inc" +GENERATED_GSD_SIGNATURE_DISPATCH="$(dirname "$GENERATED_SIGNATURE_DISPATCH")/GeneratedGsdSignatureDispatch.inc" +if [ -f "$GENERATED_GSD_SIGNATURE_DISPATCH" ]; then + cp "$GENERATED_GSD_SIGNATURE_DISPATCH" "$PACKAGE_DIR/native-api/ffi/hermes/GeneratedGsdSignatureDispatch.inc" +fi +if [ -z "$GENERATED_SIGNATURE_DISPATCH_OVERRIDE" ]; then + rm -f "$GENERATED_SIGNATURE_DISPATCH" "$GENERATED_SIGNATURE_DISPATCH.stamp" \ + "$GENERATED_GSD_SIGNATURE_DISPATCH" +fi +cp metadata-generator/include/Metadata.h "$PACKAGE_DIR/native-api/metadata/include/" +cp metadata-generator/include/MetadataReader.h "$PACKAGE_DIR/native-api/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/" diff --git a/scripts/check_ffi_boundaries.sh b/scripts/check_ffi_boundaries.sh index 20e4f6e13..9fcd4060c 100755 --- a/scripts/check_ffi_boundaries.sh +++ b/scripts/check_ffi_boundaries.sh @@ -3,28 +3,48 @@ 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" -) +FFI_DIR="$ROOT_DIR/NativeScript/ffi" +SHARED_DIR="$FFI_DIR/shared" +NAPI_DIR="$FFI_DIR/napi" +HERMES_DIR="$FFI_DIR/hermes" +V8_DIR="$FFI_DIR/v8" +JSC_DIR="$FFI_DIR/jsc" +QUICKJS_DIR="$FFI_DIR/quickjs" 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 +FORBIDDEN_DIRS=( + "$FFI_DIR/direct" + "$FFI_DIR/engine" + "$SHARED_DIR/jsi" +) + +for dir in "${FORBIDDEN_DIRS[@]}"; do + if [ -e "$dir" ]; then + echo "${dir#$ROOT_DIR/} is not an allowed FFI layer." >&2 + exit 1 + fi +done + +ENGINE_AND_SHARED_DIRS=( + "$SHARED_DIR" + "$HERMES_DIR" + "$V8_DIR" + "$JSC_DIR" + "$QUICKJS_DIR" +) + +EXISTING_ENGINE_AND_SHARED_DIRS=() +for dir in "${ENGINE_AND_SHARED_DIRS[@]}"; do if [ -d "$dir" ]; then - EXISTING_DIRECT_DIRS+=("$dir") + EXISTING_ENGINE_AND_SHARED_DIRS+=("$dir") fi done -if [ "${#EXISTING_DIRECT_DIRS[@]}" -eq 0 ]; then +if [ "${#EXISTING_ENGINE_AND_SHARED_DIRS[@]}" -eq 0 ]; then exit 0 fi @@ -33,7 +53,8 @@ search_sources() { shift if command -v rg >/dev/null 2>&1; then - rg -n "$pattern" "$@" -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}' + rg -n "$pattern" "$@" -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}' \ + -g '!GeneratedSignatureDispatch.inc' return fi @@ -48,17 +69,86 @@ search_sources() { -name '*.m' -o \ -name '*.mm' -o \ -name '*.inc' \ - \) -print0 | xargs -0 grep -nE "$pattern" + \) ! -name 'GeneratedSignatureDispatch.inc' -print0 | xargs -0 grep -nE "$pattern" } if search_sources '(^|[^[:alnum:]_])(napi_|napi_env|napi_value|js_native_api|node_api)($|[^[:alnum:]_])' \ - "${EXISTING_DIRECT_DIRS[@]}"; then - echo "Node-API symbols are not allowed in shared or direct engine FFI folders." >&2 + "${EXISTING_ENGINE_AND_SHARED_DIRS[@]}"; then + echo "Node-API symbols are not allowed in shared or engine FFI folders." >&2 + exit 1 +fi + +ENGINE_NEUTRAL_DIRS=() +for dir in "$SHARED_DIR"; do + if [ -d "$dir" ]; then + ENGINE_NEUTRAL_DIRS+=("$dir") + fi +done + +if [ "${#ENGINE_NEUTRAL_DIRS[@]}" -gt 0 ] && + search_sources '(^|[^[:alnum:]_])(napi_|napi_env|napi_value|js_native_api|node_api|facebook::jsi|v8::|JSContextRef|JSValueRef|JSContext|JSValue|JSRuntime|quickjs)($|[^[:alnum:]_])|(&2 + exit 1 +fi + +check_no_backend_dependency() { + local owner_name="$1" + local owner_dir="$2" + shift 2 + + if [ ! -d "$owner_dir" ]; then + return + fi + + local pattern="" + local backend + for backend in "$@"; do + if [ -n "$pattern" ]; then + pattern="$pattern|" + fi + pattern="${pattern}(ffi/${backend}/|\"${backend}/)" + done + + if [ -n "$pattern" ] && search_sources "$pattern" "$owner_dir"; then + echo "ffi/$owner_name must not include another FFI backend's private files." >&2 + exit 1 + fi +} + +check_no_backend_dependency "napi" "$NAPI_DIR" hermes v8 jsc quickjs +check_no_backend_dependency "hermes" "$HERMES_DIR" napi v8 jsc quickjs +check_no_backend_dependency "v8" "$V8_DIR" napi hermes jsc quickjs +check_no_backend_dependency "jsc" "$JSC_DIR" napi hermes v8 quickjs +check_no_backend_dependency "quickjs" "$QUICKJS_DIR" napi hermes v8 jsc + +NON_HERMES_JSI_DIRS=() +for dir in "$SHARED_DIR" "$NAPI_DIR" "$V8_DIR" "$JSC_DIR" "$QUICKJS_DIR"; do + if [ -d "$dir" ]; then + NON_HERMES_JSI_DIRS+=("$dir") + fi +done + +if [ "${#NON_HERMES_JSI_DIRS[@]}" -gt 0 ] && + search_sources '(NativeApiJsi|facebook::jsi|&2 exit 1 fi -if search_sources '(^|[^[:alnum:]_])(EngineDirect|FastNative|HermesFast|V8Fast|JSCFast|QuickJSFast)($|[^[:alnum:]_])' \ +if search_sources '(^|[^[:alnum:]_])(EngineDispatch|FastNative|HermesFast|V8Fast|JSCFast|QuickJSFast)($|[^[:alnum:]_])' \ "$ROOT_DIR/NativeScript/ffi/napi" | grep -v 'GeneratedSignatureDispatch.inc'; then - echo "Direct-engine FFI code is not allowed in ffi/napi." >&2 + echo "Engine FFI code is not allowed in ffi/napi." >&2 exit 1 fi + +if command -v rg >/dev/null 2>&1; then + STALE_FFI_PATTERN='NS_FFI_BACKEND=''engine|--ffi-''engine|native-api-''jsi|ffi/(direct|engine|shared/jsi)' + if rg -n "$STALE_FFI_PATTERN" \ + "$ROOT_DIR/NativeScript" "$ROOT_DIR/scripts" "$ROOT_DIR/packages" \ + "$ROOT_DIR/metadata-generator" "$ROOT_DIR/benchmarks" \ + -g '!NativeScript/ffi/napi/GeneratedSignatureDispatch.inc'; then + echo "Stale FFI layer names are not allowed." >&2 + exit 1 + fi +fi diff --git a/scripts/create_react_native_demo.sh b/scripts/create_react_native_demo.sh index dc89a20be..95235e5c6 100755 --- a/scripts/create_react_native_demo.sh +++ b/scripts/create_react_native_demo.sh @@ -29,6 +29,45 @@ fi 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 react-native-worklets for the demo app..." +(cd "$APP_DIR" && npm install --silent react-native-worklets@0.9.1) + +checkpoint "Enabling NativeScript and Worklets Babel plugins for the demo app..." +node - "$APP_DIR/babel.config.js" <<'NODE' +const fs = require('fs'); +const target = process.argv[2]; +let source = fs.existsSync(target) + ? fs.readFileSync(target, 'utf8') + : [ + 'module.exports = {', + " presets: ['module:@react-native/babel-preset'],", + '};', + '', + ].join('\n'); + +const plugins = ['@nativescript/react-native/babel-plugin', 'react-native-worklets/plugin']; +const missingPlugins = plugins.filter((plugin) => !source.includes(plugin)); +if (missingPlugins.length > 0) { + const pluginEntry = missingPlugins.map((plugin) => `'${plugin}'`).join(', ') + ', '; + if (/plugins\s*:\s*\[/.test(source)) { + source = source.replace(/plugins\s*:\s*\[/, (match) => `${match}${pluginEntry}`); + } else if (/return\s*\{/.test(source)) { + source = source.replace( + /return\s*\{/, + (match) => `${match}\n plugins: [${pluginEntry}],`, + ); + } else if (/module\.exports\s*=\s*\{/.test(source)) { + source = source.replace( + /module\.exports\s*=\s*\{/, + (match) => `${match}\n plugins: [${pluginEntry}],`, + ); + } else { + source += `\n// NativeScript demo: add ${missingPlugins.map((plugin) => `'${plugin}'`).join(' and ')} to Babel plugins.\n`; + } + fs.writeFileSync(target, source); +} +NODE + checkpoint "Installing demo entrypoint..." cp "$DEMO_APP_TSX" "$APP_DIR/App.tsx" diff --git a/scripts/metagen.js b/scripts/metagen.js index 031dd05de..7640fef6f 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -333,6 +333,21 @@ async function main() { throw new Error(`Invalid platform: ${sdkName}`); } + const requestedArchs = new Set( + (process.env.METAGEN_ARCHS || "") + .split(",") + .map((arch) => arch.trim()) + .filter(Boolean), + ); + const targetArchs = Object.keys(sdk.targets).filter((arch) => { + return requestedArchs.size === 0 || requestedArchs.has(arch); + }); + if (targetArchs.length === 0) { + throw new Error( + `No matching metadata architectures for ${sdkName}; requested ${Array.from(requestedArchs).join(",")}`, + ); + } + const typesDir = path.resolve(__dirname, "..", "packages", sdkName, "types"); const metadataDir = path.resolve(__dirname, "..", "metadata-generator", "metadata"); const signatureBindingsPath = @@ -344,7 +359,7 @@ async function main() { await fsp.mkdir(metadataDir, { recursive: true }); await fsp.mkdir(path.dirname(signatureBindingsPath), { recursive: true }); - for (const arch of Object.keys(sdk.targets)) { + for (const arch of targetArchs) { // Use the matching arch binary when available, falling back to arm64. // build_metadata_generator.sh produces both dist/arm64 and dist/x86_64. const preferredArch = arch; diff --git a/scripts/react_native_app_utils.sh b/scripts/react_native_app_utils.sh index a2d0553e1..0798489c4 100644 --- a/scripts/react_native_app_utils.sh +++ b/scripts/react_native_app_utils.sh @@ -167,16 +167,30 @@ const fs = require('fs'); const [markerFile, marker, timeoutSecondsText] = process.argv.slice(2); const timeoutMs = Number(timeoutSecondsText) * 1000; const startedAt = Date.now(); +let lastContent = ''; function poll() { if (fs.existsSync(markerFile)) { - const content = fs.readFileSync(markerFile, 'utf8'); - console.log(`${marker} ${JSON.stringify({markerFile, content: content.trim()})}`); - process.exit(0); + const content = fs.readFileSync(markerFile, 'utf8').trim(); + if (content && content !== lastContent) { + lastContent = content; + if (content.startsWith('stage=')) { + console.log(`${marker} ${JSON.stringify({markerFile, stage: content.slice('stage='.length)})}`); + } else if (!marker || content.includes(marker)) { + console.log(`${marker} ${JSON.stringify({markerFile, content})}`); + process.exit(0); + } else { + console.error(`Unexpected ${marker} marker content at ${markerFile}: ${content}`); + process.exit(1); + } + } } if (Date.now() - startedAt > timeoutMs) { console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + if (lastContent) { + console.error(`Last ${marker} marker content: ${lastContent}`); + } process.exit(1); } diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 09435c344..9f65afe85 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -8,7 +8,10 @@ // - 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. +// artifacts need rebuilding. Supported: auto, napi, v8, hermes, quickjs, jsc. +// Defaults to auto. +// - MACOS_TEST_GSD_BACKEND selects generated signature dispatch backend. +// Supported: auto, v8, jsc, quickjs, hermes, napi, none. 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). @@ -95,6 +98,7 @@ 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(); +const requestedGsdBackend = (process.env.MACOS_TEST_GSD_BACKEND || process.env.NS_GSD_BACKEND || "auto").trim().toLowerCase(); const launchedMarker = "Application Start!"; const junitPrefix = "TKUnit: "; @@ -103,7 +107,15 @@ const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); const generatedRuntimeBuildOutputs = new Set([ path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc"), - path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc.stamp") + path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc.stamp"), + path.join(nativeScriptSourceRoot, "ffi", "hermes", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "hermes", "GeneratedSignatureDispatch.inc.stamp"), + path.join(nativeScriptSourceRoot, "ffi", "v8", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "v8", "GeneratedSignatureDispatch.inc.stamp"), + path.join(nativeScriptSourceRoot, "ffi", "jsc", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "jsc", "GeneratedSignatureDispatch.inc.stamp"), + path.join(nativeScriptSourceRoot, "ffi", "quickjs", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "quickjs", "GeneratedSignatureDispatch.inc.stamp") ]); function parseArgs() { @@ -499,6 +511,7 @@ function ensureMacOSRuntimeArtifactsBuilt() { const artifactMtime = getPathStats(nativeScriptXCFramework).maxMtimeMs; let configuredEngine = null; let configuredFfiBackend = null; + let configuredGsdBackend = null; if (fs.existsSync(cachePath)) { try { @@ -511,6 +524,10 @@ function ensureMacOSRuntimeArtifactsBuilt() { if (ffiBackendMatch) { configuredFfiBackend = ffiBackendMatch[1].trim().toLowerCase(); } + const gsdBackendMatch = cache.match(/^NS_GSD_BACKEND:STRING=(.+)$/m); + if (gsdBackendMatch) { + configuredGsdBackend = gsdBackendMatch[1].trim().toLowerCase(); + } } catch (_) { configuredEngine = null; configuredFfiBackend = null; @@ -520,7 +537,8 @@ function ensureMacOSRuntimeArtifactsBuilt() { if (artifactMtime > 0 && artifactMtime >= sourceMtime && configuredEngine === requestedEngine && - configuredFfiBackend === requestedFfiBackend) { + configuredFfiBackend === requestedFfiBackend && + configuredGsdBackend === requestedGsdBackend) { return; } @@ -529,15 +547,20 @@ function ensureMacOSRuntimeArtifactsBuilt() { throw new Error(`Unsupported MACOS_TEST_ENGINE: ${requestedEngine}`); } - const supportedFfiBackends = new Set(["auto", "napi", "direct"]); + const supportedFfiBackends = new Set(["auto", "napi", "v8", "hermes", "quickjs", "jsc"]); 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...`); + const supportedGsdBackends = new Set(["auto", "v8", "jsc", "quickjs", "hermes", "napi", "none"]); + if (!supportedGsdBackends.has(requestedGsdBackend)) { + throw new Error(`Unsupported MACOS_TEST_GSD_BACKEND: ${requestedGsdBackend}`); + } + + console.log(`NativeScript macOS artifacts are missing, stale, or built for '${configuredEngine ?? "unknown"}/${configuredFfiBackend ?? "unknown"}/${configuredGsdBackend ?? "unknown"}'; running ${requestedEngine}/${requestedFfiBackend}/${requestedGsdBackend} build...`); runBuildAndRequireSuccess( path.join(__dirname, "build_nativescript.sh"), - ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`, `--ffi-backend=${requestedFfiBackend}`], + ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`, `--ffi-backend=${requestedFfiBackend}`, `--gsd-backend=${requestedGsdBackend}`], commandTimeoutMs ); } @@ -560,7 +583,7 @@ function buildTestRunnerApp() { ensureMetadataGeneratorBuilt(); ensureMacOSRuntimeArtifactsBuilt(); - const nativeFingerprint = `${requestedEngine}:${requestedFfiBackend}:${createBuildFingerprint(macosBuildInputs)}`; + const nativeFingerprint = `${requestedEngine}:${requestedFfiBackend}:${requestedGsdBackend}:${createBuildFingerprint(macosBuildInputs)}`; const existingBuildState = readBuildState(); const canReuseBuild = process.env.MACOS_TEST_CLEAN_BUILD !== "1" && fs.existsSync(appPath) && diff --git a/scripts/test_react_native_ffi_compat.sh b/scripts/test_react_native_ffi_compat.sh index b69f63099..e60c26a94 100755 --- a/scripts/test_react_native_ffi_compat.sh +++ b/scripts/test_react_native_ffi_compat.sh @@ -159,6 +159,45 @@ fi 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 react-native-worklets for the FFI compatibility app..." +(cd "$APP_DIR" && npm install --silent react-native-worklets@0.9.1) + +checkpoint "Enabling NativeScript and Worklets Babel plugins for the FFI compatibility app..." +node - "$APP_DIR/babel.config.js" <<'NODE' +const fs = require('fs'); +const target = process.argv[2]; +let source = fs.existsSync(target) + ? fs.readFileSync(target, 'utf8') + : [ + 'module.exports = {', + " presets: ['module:@react-native/babel-preset'],", + '};', + '', + ].join('\n'); + +const plugins = ['@nativescript/react-native/babel-plugin', 'react-native-worklets/plugin']; +const missingPlugins = plugins.filter((plugin) => !source.includes(plugin)); +if (missingPlugins.length > 0) { + const pluginEntry = missingPlugins.map((plugin) => `'${plugin}'`).join(', ') + ', '; + if (/plugins\s*:\s*\[/.test(source)) { + source = source.replace(/plugins\s*:\s*\[/, (match) => `${match}${pluginEntry}`); + } else if (/return\s*\{/.test(source)) { + source = source.replace( + /return\s*\{/, + (match) => `${match}\n plugins: [${pluginEntry}],`, + ); + } else if (/module\.exports\s*=\s*\{/.test(source)) { + source = source.replace( + /module\.exports\s*=\s*\{/, + (match) => `${match}\n plugins: [${pluginEntry}],`, + ); + } else { + source += `\n// NativeScript FFI compatibility: add ${missingPlugins.map((plugin) => `'${plugin}'`).join(' and ')} to Babel plugins.\n`; + } + fs.writeFileSync(target, source); +} +NODE + checkpoint "Installing FFI compatibility entrypoint..." cp "$APP_TSX" "$APP_DIR/App.tsx" rn_install_ffi_runtime_specs @@ -202,6 +241,7 @@ function poll() { if (content.startsWith('stage=')) { lastStage = content; console.log(`${marker} ${JSON.stringify({markerFile, stage: content.slice('stage='.length)})}`); + setTimeout(poll, 2000); return; } console.error(`Invalid ${marker} marker content at ${markerFile}: ${content}`); diff --git a/scripts/test_react_native_turbomodule.sh b/scripts/test_react_native_turbomodule.sh index bd1cdd0b1..ad659fcff 100755 --- a/scripts/test_react_native_turbomodule.sh +++ b/scripts/test_react_native_turbomodule.sh @@ -26,6 +26,43 @@ fi 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 "Installing react-native-worklets for the smoke app..." +(cd "$APP_DIR" && npm install --silent react-native-worklets@0.9.1) + +checkpoint "Enabling the Worklets Babel plugin for the smoke app..." +node - "$APP_DIR/babel.config.js" <<'NODE' +const fs = require('fs'); +const target = process.argv[2]; +let source = fs.existsSync(target) + ? fs.readFileSync(target, 'utf8') + : [ + 'module.exports = {', + " presets: ['module:@react-native/babel-preset'],", + '};', + '', + ].join('\n'); + +const plugin = 'react-native-worklets/plugin'; +if (!source.includes(plugin)) { + if (/plugins\s*:\s*\[/.test(source)) { + source = source.replace(/plugins\s*:\s*\[/, (match) => `${match}'${plugin}', `); + } else if (/return\s*\{/.test(source)) { + source = source.replace( + /return\s*\{/, + (match) => `${match}\n plugins: ['${plugin}'],`, + ); + } else if (/module\.exports\s*=\s*\{/.test(source)) { + source = source.replace( + /module\.exports\s*=\s*\{/, + (match) => `${match}\n plugins: ['${plugin}'],`, + ); + } else { + source += `\n// NativeScript smoke: add '${plugin}' to Babel plugins.\n`; + } + fs.writeFileSync(target, source); +} +NODE + checkpoint "Writing smoke app entrypoint..." node - "$APP_DIR/App.tsx" <<'NODE' const fs = require('fs'); @@ -35,6 +72,7 @@ fs.writeFileSync(target, `import React from 'react'; import {useEffect, useState} from 'react'; import {SafeAreaView, Text} from 'react-native'; import NativeScript from '@nativescript/react-native'; +import NativeScriptNativeApi from '@nativescript/react-native/src/NativeScriptNativeApi'; const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; @@ -59,29 +97,62 @@ async function runSmoke(): Promise { throw new Error('enum global install failed'); } - let nativeCallsRanOnMainThread = false; - await NativeScript.runOnUI(() => { - nativeCallsRanOnMainThread = NSThread?.isMainThread === true; - if (!nativeCallsRanOnMainThread) { - throw new Error('runOnUI did not dispatch native calls to the main thread'); + const uiSummary = await NativeScript.runOnUI(() => { + 'worklet'; + const uiGlobal = globalThis as any; + const uiApi = uiGlobal.__nativeScriptNativeApi; + if (!uiApi) { + throw new Error('NativeScript Native API was not installed in the Worklets UI runtime'); } + + const uiNSObject = uiGlobal.NSObject; + if (!uiNSObject || typeof uiNSObject.alloc !== 'function') { + throw new Error('NSObject global install failed in the Worklets UI runtime'); + } + + const uiConstant = uiGlobal.NSURLErrorTimedOut; + const uiStyle = uiGlobal.UIUserInterfaceStyle; + if (uiConstant !== -1001 || uiStyle.Dark !== 2) { + throw new Error('metadata globals failed in the Worklets UI runtime'); + } + + const uiApplication = uiGlobal.UIApplication; + const uiColor = uiGlobal.UIColor; + const window = uiApplication.sharedApplication.keyWindow; + if (window && uiColor.systemTealColor) { + window.tintColor = uiColor.systemTealColor; + } + + return { + workletsInstalled: true, + nativeCallsRanOnMainThread: uiGlobal.NSThread?.isMainThread === true, + runtime: uiApi.runtime, + backend: uiApi.backend, + constant: uiConstant, + enumValue: uiStyle.Dark, + }; }); const summary = { installed, - nativeCallsRanOnMainThread, + workletsInstalled: uiSummary.workletsInstalled, + nativeCallsRanOnMainThread: uiSummary.nativeCallsRanOnMainThread, runtime: api.runtime, backend: api.backend, + uiRuntime: uiSummary.runtime, + uiBackend: uiSummary.backend, classes: api.metadata?.classes ?? 0, constants: api.metadata?.constants ?? 0, enums: api.metadata?.enums ?? 0, - constant: NSURLErrorTimedOut, - enumValue: UIUserInterfaceStyle.Dark, + constant: uiSummary.constant, + enumValue: uiSummary.enumValue, metadataPath: NativeScript.defaultMetadataPath(), turboBackend: NativeScript.getRuntimeBackend(), }; - console.log(marker + ' ' + JSON.stringify(summary)); + const payload = marker + ' ' + JSON.stringify(summary); + console.log(payload); + NativeScriptNativeApi.__writeTestMarker(payload); return JSON.stringify(summary, null, 2); } catch (error) { console.error('NATIVESCRIPT_RN_TURBO_SMOKE_FAIL', error); diff --git a/skills/write-nativescript-native-modules/SKILL.md b/skills/write-nativescript-native-modules/SKILL.md new file mode 100644 index 000000000..bcbc4b398 --- /dev/null +++ b/skills/write-nativescript-native-modules/SKILL.md @@ -0,0 +1,43 @@ +--- +name: write-nativescript-native-modules +description: Port React Native iOS native modules, UIKit containers, navigation stacks, tabs, gestures, delegates, or TurboModule-backed libraries to drop-in pure TypeScript implementations using @nativescript/react-native. Use when proving NativeScript can replace Objective-C/Swift module code without app-native code, when auditing runtime API gaps, or when writing reusable guidance for native modules authored entirely in TypeScript. +--- + +# Write NativeScript Native Modules + +## Overview + +Port the library mechanically first, then improve the generic NativeScript runtime only when the port hits an Objective-C/Swift capability that TypeScript cannot express yet. The app using the fork must not add native code or demo-only fallbacks. + +## Workflow + +1. Compare against the upstream iOS implementation before editing. + Use `rg` on Objective-C/Swift sources for `UIViewController`, `UINavigationController`, delegates, target/action, gestures, presentation, animation, and event emission. Keep upstream ordering and state guards unless NativeScript cannot express them yet. + +2. Keep the public JS API drop-in. + Preserve exports, component names, props, events, route behavior, accessibility labels, and timing semantics. Do not require app config, Podfile changes, native files, or userland shims beyond depending on `@nativescript/react-native`. + +3. Put native behavior in TypeScript worklets. + Create UIKit objects with `defineUIViewController`, `defineUIKitView`, `nativeValue`, and worklet callbacks. Native UI creation, mutation, delegates, and target/action must run on the UI worklet runtime, not on React Native's JS thread. + +4. Extend NativeScript only with generic primitives. + Accept APIs like `runtimeInvoker(callback)`, host refresh, generic delegate creation, function callback thread policy, selector metadata, or value serialization. Reject APIs like `afterReactNavigationTransition`, `presentNativeStackModal`, or anything named after the library being ported. + +5. Verify against both implementations. + Maintain an original-library demo and a NativeScript-fork demo. Test on the requested simulator/device, use SimDeck for taps, gestures, screenshots, recordings when available, and logs. Compare first paint, safe areas, headers, tab bars, animations, gestures, modal presentation, repeated taps, event order, and scroll insets. + +## Porting Rules + +- Use UIKit APIs directly from TypeScript whenever upstream does. +- Keep worklets enabled; NativeScript native UI is the reason the UI runtime exists. +- Retain delegates/targets through NativeScript context helpers so UIKit callbacks survive. +- Let UIKit own native transitions. During active `transitionCoordinator`, defer reconciliation until completion. +- Do not dispatch every granular native call through `dispatch_sync`; batch behavior in UI worklets. +- Do not run React Native's JS runtime on the UI thread. +- Do not add native source files to the consuming app. +- Add tests for every generic runtime primitive added to unblock a port. + +## References + +- Read `references/porting-patterns.md` when implementing or reviewing a port. +- Read `references/runtime-gap-log.md` before adding or rejecting a NativeScript TurboModule/runtime API. diff --git a/skills/write-nativescript-native-modules/agents/openai.yaml b/skills/write-nativescript-native-modules/agents/openai.yaml new file mode 100644 index 000000000..8826f21d7 --- /dev/null +++ b/skills/write-nativescript-native-modules/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Write NativeScript Native Modules" + short_description: "Port UIKit native modules to pure TypeScript with NativeScript." + default_prompt: "Port this React Native iOS native module to a drop-in pure TypeScript NativeScript implementation, documenting any generic runtime gaps." diff --git a/skills/write-nativescript-native-modules/references/porting-patterns.md b/skills/write-nativescript-native-modules/references/porting-patterns.md new file mode 100644 index 000000000..8f605a853 --- /dev/null +++ b/skills/write-nativescript-native-modules/references/porting-patterns.md @@ -0,0 +1,444 @@ +# NativeScript TypeScript Native Module Porting Patterns + +## Contents + +- Mechanical port contract +- UIKit object model +- Navigation stacks +- Tabs +- Header service objects +- Modals +- Gestures and delegates +- Event delivery +- Layout, safe areas, and first paint +- Verification checklist + +## Mechanical Port Contract + +Port what upstream does, not what the demo needs. Keep the library's JS surface unchanged and move the iOS implementation from Objective-C/Swift into TypeScript worklets. + +Required shape: + +- Consuming app depends on the fork and `@nativescript/react-native`. +- Consuming app adds no `.m`, `.mm`, `.swift`, podspec patches, or native modules for the fork. +- Forked package owns all UIKit behavior in TS. +- NativeScript runtime exposes only generic capabilities. +- Original and NativeScript demos stay side by side until behavior matches. + +## UIKit Object Model + +Use `NativeScriptRuntime.defineUIViewController` for native containers that upstream implements as controllers. Use `defineUIKitView` for native views without child controller lifecycle. + +Inside worklets: + +```ts +const UINavigationController = nativeValue('UINavigationController'); +const controller = UINavigationController.alloc().init(); +controller.viewControllers = createArray([placeholder]); +``` + +When upstream implements behavior through Objective-C subclass overrides, +mirror that object model with a TypeScript `NativeClass` subclass. UIKit +window-trait methods such as `childViewControllerForStatusBarStyle`, +`preferredStatusBarUpdateAnimation`, `supportedInterfaceOrientations`, and +`childViewControllerForHomeIndicatorAutoHidden` are not TurboModule calls; they +are selectors UIKit pulls from the controller hierarchy. Allocate the +NativeScript subclass in the same places upstream allocates its native subclass, +and keep traversal rules mechanical, including modal traversal only where +upstream includes modals. + +Port lifecycle selectors the same way. If upstream recalculates layout from +`viewDidLayoutSubviews`, override that selector on the TS subclass, call +`super`, then perform the same UIKit measurement/event work. If a native +shadow-tree helper has no TS equivalent, leave a `NATIVESCRIPT_PORT_DEVIATION` +comment explaining the replacement ownership instead of hiding the gap. +If upstream stores hierarchy state on the controller, such as +`isRemovedFromParent`, keep that state on the TS subclass too and use it when +filtering stale native controllers. +When upstream calls a native view's `updateBounds` from a controller layout +selector, refresh the hosted NativeScript/RN view tree from that same selector +instead of waiting for a later React render. +When upstream captures first responder state in +`willMoveToParentViewController` and restores it in `notifyFinishTransitioning`, +mirror that on the TS subclass with a UIKit `subviews` walk and +`becomeFirstResponder`. Finish-transition ownership should stay on the screen +controller, not in an ad hoc stack helper. +If upstream calls lifecycle workarounds such as `hideHeaderIfNecessary` from +`viewWillAppear`, keep the same selector ordering: call `super`, run the +workaround, then refresh window traits or other pull-based UIKit state. +If upstream exposes a selector for a native target callback, such as +`CADisplayLink` calling `handleAnimation`, expose that selector on the TS +`NativeClass` with `ObjCExposedMethods` and use the native factory directly. +Do not add a TurboModule helper for callback target/selector dispatch. + +When upstream receives a React Native service handle through Fabric state, keep +that dependency generic. For image props, upstream packages often receive +`RCTImageLoader` through component state and pass resolved image sources into +native helpers. A TS port should synchronously resolve SF Symbols, xcassets, and +local/file `Image.resolveAssetSource` values with UIKit, then call the generic +NativeScript/RN image-loader API for URI or packager-backed sources and mutate +the existing UIKit item on completion. Do not add package-specific image-loading +helpers. + +Always configure: + +- `edgesForExtendedLayout` +- `extendedLayoutIncludesOpaqueBars` +- `view.backgroundColor` +- `view.autoresizingMask` +- child `view.frame` +- host view refresh when React children are detached from the React tree + +## Navigation Stacks + +Match `react-native-screens`: + +- Start with a placeholder controller if needed for UIKit header initialization. +- Skip animated stack updates while `navigationController.transitionCoordinator` exists. +- Skip animated updates until `navigationController.view.window != null`. +- For a single push, normalize the base stack with `setViewControllers(..., animated: false)`, then call `pushViewController(..., animated: true)`. +- For a pop, call the narrow UIKit pop API: `popViewControllerAnimated`, `popToRootViewControllerAnimated`, or `popToViewControllerAnimated`. +- Defer JS state reconciliation until UIKit did-show or transition completion. +- If React state changes while UIKit owns a JS-requested transition, record a pending reconcile and replay it after completion. Do not silently return from reconciliation and drop the update. +- Guard repeated taps with native transition state, not JS debounce alone. +- On iOS 26, port upstream transition interaction gates: incoming screens can become visible/interactable before UIKit has finished the transition. Gate the native container that owns the transition, then re-enable it from the central did-show/completion/cancel path. Do not recursively toggle hosted React child views; React Native owns `pointerEvents`, touch sentinels, and accessibility state below the host. +- Treat cancelled interactive pops as gesture cancels, not completed closes. A closing transition only dismissed a route if UIKit's final shown stack is shorter than React Navigation's active route stack. + +Do not replace a push with a full non-animated `setViewControllers` unless upstream does for a non-top change or replacement. + +## Tabs + +Use `UITabBarController` from TS and create one controller per route. Preserve labels, SF Symbols, selected state, badges, minimize behavior, sidebar adaptivity, and background/material behavior. + +Port the native navigation-state machine, not just `selectedIndex`: + +- Keep the native-owned `{ selectedScreenKey, provenance }` state and advance provenance for user, implicit, and accepted JS updates the same way upstream does. +- Treat uninitialized navigation state as `nil`, not as a `{ provenance: 0 }` + record. The first accepted/user/implicit selection creates provenance `0`; + only later progressions increment. Do not infer initialized state from an old + numeric `provenance` field. +- Reject stale JS updates when the upstream component supports `rejectStaleNavStateUpdates`; emit the rejected event instead of silently applying or dropping the request. +- Treat repeated programmatic selection as rejected after the initial render, but still allow user repeated tab taps to emit the repeated-selection event and trigger the upstream repeated-tab effect. +- Respect `preventNativeSelection` from both `shouldSelect` and fallback gesture paths, and emit `onTabSelectionPrevented` with the current native state. +- Do not mutate native state when UIKit selects the More controller; emit `onMoreTabSelected` with the current state, then preserve upstream More-controller behavior. + +First paint requirements: + +- Create tab items before first visible layout. +- Assign `viewControllers` before mounting the host view when possible. +- Refresh hosted React content once the native host has children. +- If the TS port receives the host and tab-screen records across separate + React/native host mounts, use only a bounded, token-coalesced first-paint + commit window. Do not install persistent restore intervals or use delayed + commits to repair navigation state. +- Verify labels and symbols before tapping tabs; first-paint missing labels are a bug. + +## Header Service Objects + +Some upstream native components are not visible views in the final UIKit +hierarchy; they own UIKit service objects that another native container +installs later. For example, `react-native-screens` `RNSSearchBar` owns a +`UISearchController`, while `RNSScreenStackHeaderConfig` attaches +`searchBar.controller` to `UINavigationItem.searchController`. + +Port this mechanically: + +- Implement the service owner as a NativeScript UIKit component in the package + fork. It should allocate the same UIKit object, keep the same default props, + install the same delegate protocol, and expose the same imperative commands. +- Register the owned native object through the package's existing TS registry or + context boundary. The runtime should not grow a `setSearchController` or + header-specific helper. +- Have the consuming native container install the object exactly where upstream + does. For search bars, set `navigationItem.searchController`, + `hidesSearchBarWhenScrolling`, iOS 16 `preferredSearchBarPlacement`, and iOS + 26 `searchBarPlacementAllowsToolbarIntegration` with the same stacked + placement workaround. +- Keep non-iOS paths on the original native component when the port is only + targeting iOS. +- Test both halves: the owner creates/configures/delegates the UIKit object, and + the header/container installs the registered object into the target UIKit + API. + +Header image subviews are similar. Upstream may read the rendered native image +view from a header subview and install that `UIImage` into another UIKit object, +such as `UINavigationBarAppearance.backIndicatorImage`. In a TS port, prefer +carrying the resolved `Image.source` through the package registry and resolving +it with the same generic image-loading helper used by bar button items. Avoid +adding a runtime snapshot API unless the upstream behavior truly depends on +rendered pixels rather than the original image source. + +For `UINavigationItem` source back-button items, preserve UIKit ownership when +upstream does. `react-native-screens` sets `backButtonTitle` and +`backButtonDisplayMode` on the previous item, but only assigns a custom +`RNSBackBarButtonItem` when the menu is disabled or the back-title font is +customized. Do not always create a custom `backBarButtonItem`; that disables +UIKit's automatic shortening/hiding behavior. If UIKit recreated the previous +controller and the item lost its title, fall back to the source screen header +config just like upstream reads `prevScreen.screenView.findHeaderConfig.title`. +When a TS port intentionally clears only a port-owned stale custom item so a +prop update returns to UIKit defaults, mark that as a +`NATIVESCRIPT_PORT_DEVIATION` in code and explain why it is package-local. + +## Modals + +Use UIKit presentation APIs for modal routes: + +- `presentViewControllerAnimatedCompletion` +- `dismissViewControllerAnimatedCompletion` +- `UIAdaptivePresentationControllerDelegate` +- `sheetPresentationController` when upstream configures sheets + +A modal route is not a push. It must support swipe-down dismissal, presentation-controller cancellation/prevention, and correct dismissal events. For `react-native-screens`, present the `RNSScreen` controller directly and set its presentation controller delegate on that same screen controller, matching upstream ownership. + +For modal headers, preserve the same component/controller nesting upstream uses. +In `react-native-screens`, a modal with a visible header is an outer presented +screen containing an inner stack/screen that owns the header and route content. +Do not collapse that into a single modal route controller whose +`UINavigationItem` owns the header. If the TS host registry requires an id for +the inner screen while upstream's inner native `Screen` has no JS `screenId`, +derive a stable package-owned key from the outer route id and leave a +`NATIVESCRIPT_PORT_DEVIATION` comment explaining that the extra id is only a +registry identity. + +When dismissing a presented chain, preserve upstream's common-root logic: dismiss foreign/owned controllers carefully, wait for transition completion, then present the next chain. +Keep a package-owned presented modal list equivalent to `RNSScreenStackView`'s +`_presentedModals`. Compute the common root between the currently presented +modal ids and the next modal ids before issuing UIKit dismiss/present calls, and +reject reshuffles where an already-presented controller reappears above that +root. UIKit modal controllers cannot be safely reordered in place, and treating +the whole modal stack as one opaque wrapper hides this invariant. + +Do not replace missing modal presentation selectors with a manually attached +child controller, custom slide animation, or custom pan-to-dismiss gesture. If +upstream uses UIKit presentation, the TS port should require generic +`presentViewControllerAnimatedCompletion` / +`dismissViewControllerAnimatedCompletion` interop and use +`UIAdaptivePresentationControllerDelegate` for native swipe dismissal. A custom +gesture fallback creates double-handling races and is not a mechanical port. + +Do not wrap modal screens in a `UINavigationController` as a workaround when +upstream presents `RNSScreen` directly. The wrapper changes child controller +parentage, breaks upstream's `viewDidDisappear` removal condition, and moves +`UIAdaptivePresentationControllerDelegate` ownership away from the object that +actually owns screen events. Close the gap by implementing the missing direct +UIKit presentation/list reconciliation in TS, not by forwarding lifecycle +events from a wrapper. + +Also port the modal backdrop tap recognizer where upstream installs one. In +`react-native-screens`, `RNSScreen` adds a `UITapGestureRecognizer` to the +presentation controller container for `modal`, `pageSheet`, and `formSheet`. +The recognizer has `cancelsTouchesInView = NO`, only receives touches when +`preventNativeDismiss` is true, ignores touches inside +`presentationController.presentedView`, recognizes simultaneously, and emits +`onNativeDismissCancelled({ dismissCount: 1 })` on a recognized outside tap. +Implement this with generic NativeScript gesture delegate/action primitives; +do not add a modal/backdrop TurboModule helper. + +For `formSheet`, port upstream sheet configuration directly: resolve public JS detent props, create `UISheetPresentationControllerDetent` values from TS, use explicit `interop.Block` signatures for custom detent resolver blocks, and update `fitToContents` detents from the native content-wrapper frame callback. If upstream adds navigation-bar height errata to content height, mirror that in the TS port. Also port native content-wrapper scroll-view correction: when upstream finds a descendant `UIScrollView` inside `RNSScreenContentWrapper`, resize the direct scroll child to the sheet size and resize a second-child scroll view to `sheet height - header height` with the same y-origin correction. Do this from the package's UIKit traversal/state, not through a package-specific runtime helper. +When the upstream screen object is the `UISheetPresentationControllerDelegate`, +keep those delegate selectors on the TS controller subclass too. For +`react-native-screens`, `sheetPresentationControllerDidChangeSelectedDetentIdentifier` +must map UIKit's selected detent identifier back to the JS detent index and +emit `onSheetDetentChanged({ index, isStable: true })`. + +## Gestures And Delegates + +Use `ctx.delegate` for UIKit delegate protocols and retain through the NativeScript context. Use `ctx.gestureAction` for `UIGestureRecognizer` target/action. Use `ctx.actionTarget` when upstream sets a generic UIKit object's `target`/`action`, such as `UIBarButtonItem.target` and `UIBarButtonItem.action`; assign the returned `target` and selector string directly instead of wrapping the object in a different view. + +Common protocols: + +- `UINavigationControllerDelegate` +- `UIAdaptivePresentationControllerDelegate` +- `UISheetPresentationControllerDelegate` +- `UIGestureRecognizerDelegate` +- `UITabBarControllerDelegate` + +When upstream declares protocol conformance, mirror the full protocol list in +the NativeScript subclass metadata as well as exposing the selector signatures. +Having a TS method named like a delegate callback is weaker than declaring that +the native class conforms to the same delegate protocols UIKit expects. + +For iOS 26 stack parity, prefer native `interactiveContentPopGestureRecognizer` when available and fall back only for custom animations that upstream cannot express with the native gesture. + +For UIKit tab selection, separate native selection state from hosted-content +touch readiness. A `UITabBarController` selection can be observed and bridged +to JS before the NativeScript/RN hosted subtree has refreshed its hit-test +tree. After actual native selection callbacks, synchronously reconcile the +selected controller view, then repeat the selected-view/frame/host refresh on a +few short UI ticks with a token guard. Do not run this broad schedule from +ordinary host commits, and do not debounce React Navigation presses in JS to +hide the race. + +## Event Delivery + +Native callback thread policy matters: + +- UI worklet callbacks stay on the UI/runtime thread. +- React Navigation and React Native events that must reach JS use event bridges. +- Generic target/action callbacks use `ctx.actionTarget(NativeScriptRuntime.eventBridge(callback, 'js'))` so the native selector can fire on the UI thread while React callbacks are scheduled back onto JS. +- Native completion blocks that need to re-enter the owning worklet use `NativeScriptRuntime.runtimeInvoker(callback)` when metadata already describes the block signature. +- Native block/function-pointer parameters whose metadata is only `@?` need an explicit generic ObjC signature, for example `interop.Block('v@?@', NativeScriptRuntime.runtimeInvoker(complete))` for `void (^)(id context)`. For `UIAction` handlers that must call React JS, have the block invoke a retained `ctx.actionTarget` target rather than directly calling the React callback from the UI runtime. + +Native callback worklet symbol order matters too. A selector callback or +top-level worklet used by UIKit should not call a helper declared later in the +module unless the callback is created only after that helper is registered. +Normal JS hoisting is not enough once the function is compiled for the UI +runtime. Move the helper earlier, inline tiny operations, or register a +package-owned global worklet entrypoint after the helper definition and resolve +it from `globalThis` inside the native callback. This is how a TS port avoids +`undefined is not a function` crashes in callbacks such as +`presentationControllerDidDismiss`. + +UIKit delegate proxy identity can differ from ObjC pointer identity. Upstream +may compare the exact native controller object in callbacks like +`navigationController:didShowViewController:animated:`. In a TS port, tag +controllers with native identifiers that survive proxy boundaries first. If a +callback still reports an unresolved proxy/placeholder, only update the +package's route key/count bookkeeping from the known native stack length; do +not rebuild `viewControllers` from guessed ids or schedule timed native resets. +Route-change events may be inferred only when UIKit's native stack is actually +shorter after a closing transition. + +Transition completion should be UIKit-first. For stack transitions started from +TS, register completion through `transitionCoordinator` using an explicit +`interop.Block` and `runtimeInvoker`. A timeout may exist only as a watchdog +for missed delegate callbacks, and it must verify the transition token and that +the stack is still transitioning before touching native state. Never allow a +watchdog to rewrite controllers after `didShow` has already completed. + +Shared navigation-bar appearance is also transition-coordinator state. When +upstream updates a `UINavigationBar` inside +`animateAlongsideTransition:completion:`, the TS port should use the same +generic `transitionCoordinator` plus explicit `interop.Block` path. Resolve the +current/cancelled controller's header config at completion time if ObjC view +identity is not stable through NativeScript proxies. Do not add a +navigation-bar-specific TurboModule helper. + +Prevented native pop cancellation must also follow UIKit's delegate sequence. +In `react-native-screens`, a prevented header/native-back pop returns an +animator, creates an `RNSPercentDrivenInteractiveTransition` in +`navigationController:interactionControllerForAnimationController:`, cancels it +on the next main-queue tick, then runs `updateContainer` and emits +`onNativeDismissCancelled` with the computed dismiss count. If NativeScript can +read the stable `from`/`to` controllers in the animation delegate more +reliably than through `transitionCoordinator viewForKey:`, cache that pair for +the next delegate callback and document the proxy-boundary reason. Do not add a +package-specific TurboModule transition-coordinator helper. + +Post-transition host layout settles are different from navigation watchdogs. +Upstream queues stack updates after React child layout with `dispatch_async`. +When NativeScript owns the native controller and React owns hosted child +content, a short settle window can defer reconciles until `onHostReady` / +`refreshUIKitHostView` has published final frames. That settle path should only +gate/replay reconciliation and refresh hosted views; it should never call +`setViewControllers` directly. + +Never use a library-specific completion helper. If a new native callback pattern is needed, implement a generic callback policy and test it across engines. + +If a TS port bypasses an upstream JS wrapper that normally wires native events, +recreate that wrapper behavior in the port. For `react-native-screens` +native-stack, bypassing `Screen.tsx` means the port must create the same +`Animated.Value`s, install the same `Animated.event` for +`onTransitionProgress`, and provide the same `TransitionProgressContext`. +The native event payload stays mechanical: `{ progress, closing: 0 | 1, +goingForward: 0 | 1 }`. + +For animated transition progress, mirror RNSScreen's UIKit sampling model: +`viewWillAppear` / `viewWillDisappear` emit progress `0`, compute +`goingForward` from UIKit's `isBeingPresented` / `isMovingToParentViewController` +and `isBeingDismissed` / `isMovingFromParentViewController`, then register a +transition-coordinator animation block. Add a fake `UIView` to the coordinator +container, drive its alpha to `1`, create a `CADisplayLink` targetting the TS +controller selector, and read `fakeView.layer.presentationLayer.opacity` on +each frame. This is ordinary NativeScript ObjC interop, not a runtime gap. +Keep transition progress owned by the screen lifecycle. `viewDidAppear` / +`viewDidDisappear` should emit final progress `1` and reset the upstream-style +swipe bookkeeping (`_isSwiping` / `_shouldNotify` equivalents). Do not also +emit synthetic progress from stack reconciliation helpers; those helpers may +still emit stack transition events for React state, but progress belongs to the +screen that UIKit is transitioning. +Keep route lifecycle events on the same controller selectors too: +`viewWillAppear` emits `onWillAppear`, `viewDidAppear` emits `onAppear` or +`onGestureCancel` for a cancelled interactive pop, `viewWillDisappear` emits +`onWillDisappear`, and `viewDidDisappear` emits `onDisappear`. Stack delegates +may still reconcile React state, retained children, native stack changes, and +finish-transition notifications, but they should not duplicate route lifecycle +callbacks that upstream sends from `RNSScreen`. +For custom stack transitions, port the upstream animator mechanics instead of +introducing a visual-only fallback. If upstream defines a reusable ObjC +animator class, define a reusable NativeScript ObjC subclass too; for +`react-native-screens`, that means an `NSObject` subclass conforming to +`UIViewControllerAnimatedTransitioning` and a +`UIPercentDrivenInteractiveTransition` subclass for interactive gestures. Keep +transition state on those native proxies, not in ad hoc closure-only delegates. +If upstream uses `UIViewPropertyAnimator` so an interactive transition can set +`fractionComplete`, the TS port should require that UIKit class/initializer +metadata and fail loudly when it is missing. A fallback `UIView.animate` +completion path hides a runtime gap and does not provide mechanical parity for +gestures. +For native dismiss counts, mirror upstream's `viewWillDisappear` calculation on +the screen controller. RNSScreen compares the screen's index in React subviews +with the target top screen's index; in a TS NativeScript port, use the stack's +active screen id order as the mechanical equivalent. Keep this state on the +controller so later `viewDidDisappear` dismissal events can use the same count +instead of recalculating from stack delegate fallbacks. +When UIKit owns a pop or dismiss and React unmounts the route before the native +animation finishes, mirror `RNSScreen.setViewToSnapshot`: call +`snapshotViewAfterScreenUpdates:` on the live controller view, replace the +controller's `view` with the snapshot in the same superview, and let UIKit +animate that stable view. Do this on the screen controller subclass. Do not add +a runtime snapshot helper; the required operations are standard UIKit +selectors. JS-owned closes may still retain an inactive React element until the +transition completes because React owns that element lifetime in the TS port. + +Emit native dismissal events from `viewDidDisappear` using the same UIKit +removal condition as upstream: the screen controller has no parent and no +presenter, or the upstream-equivalent `isRemovedFromParent` flag says the +controller has been detached. If `preventNativeDismiss` is set, emit +`onNativeDismissCancelled`; otherwise emit `onDismissed`. For an actual +prevented native removal, first restore the stack through the package's +upstream-equivalent `updateContainer`/reconcile entrypoint, then emit the +cancellation event. In NativeScript this may be a registry/global worklet +entrypoint rather than `screenView.reactSuperview`, but preserve the ordering +and leave a `NATIVESCRIPT_PORT_DEVIATION` comment explaining the object-graph +difference. Do not synthesize `onDismissed` from stack-change reconciliation or +wrapper modal cleanup helpers. The stack delegate still reconciles native/React +state and presentation-controller delegates still emit attempted-but-prevented +modal cancellation, because `viewDidDisappear` does not run when UIKit refuses +that dismissal. + +Avoid adding a separate repeated "stack changed" event to repair JS state. +Native-driven dismissals should be represented by the same transition/lifecycle +events that upstream screens emit; if JS needs to retain or drop route content, +derive that from the transition end/dismissal event, not from several delayed +copies of a native stack snapshot. + +## Layout, Safe Areas, And First Paint + +Rules: + +- Let scroll views go under translucent native headers/tab bars when upstream does. +- Use native content inset adjustment rather than hard-coded padding. +- Avoid capped-height host views: fixed-format native containers need stable fill sizing and autoresizing masks. +- If upstream has a native wrapper component, port that wrapper as a NativeScript UIKit container instead of substituting a plain RN `View`; otherwise route content can keep stale hosted-wrapper frames after native transitions even when the outer UIKit controller is correctly sized. +- If upstream has a native wrapper view around a UIKit controller, keep the wrapper view and controller view separate. Use `defineUIViewController({ hostView(controller) { ... } })` when the React Native host view must be the wrapper but the attached child controller must keep its own `controller.view`. For `react-native-screens`, `RNSScreenStackView` wraps `UINavigationController.view` and overrides `hitTest:withEvent:` so oversized left/right header subviews can still receive touches; the TS port should express that as a NativeScript `UIView` subclass wrapper, not by replacing `UINavigationController.view` or adding per-button tap repairs. +- Refresh detached host children after native stack layout. +- Wait for `onHostReady` only when the upstream behavior truly requires rendered content before transition. +- If upstream mutates a descendant UIKit control found from the native view tree, port the traversal too. For example, `react-native-screens` iOS 26 `scrollEdgeEffects` finds the first descendant `UIScrollView` and configures its `topEdgeEffect`, `bottomEdgeEffect`, `leftEdgeEffect`, and `rightEdgeEffect`; the TS port should do that with UIKit interop rather than dropping the prop or adding a TurboModule helper. +- If upstream creates UIKit menu trees, port the same `UIMenu`/`UIAction` object graph in TS. Retain `interop.Block` handlers and native actions through the UIKit context, and keep iOS-specific properties such as bar-button badges/shared backgrounds on `UIBarButtonItem` itself instead of substituting custom RN or UIKit wrapper views. + +## Verification Checklist + +Run both original and NativeScript demos: + +- First paint: no blank tab bar, no missing labels/icons, no half-height scroll views. +- Navigation: one tap pushes, push animates, pop animates, back button is minimal, edge/back gesture works. +- Gesture recovery: the revealed screen remains interactive after an interactive pop. +- Gesture cancellation: a short edge drag leaves the detail route active, and the next full edge gesture still pops exactly once. +- Modal: presented as modal, no back button unless upstream shows one, swipe-down dismisses, prevention events work. +- Insets: content can scroll under translucent header and tab bar. +- Repeated taps: no duplicate pushes, no ignored first taps. +- Transition overlap: an intentional tap during push/pop/modal animation must not leave the next settled tap ignored or create duplicate routes. +- Logs: no TypeError, ReferenceError, NativeScript callback thread errors, redboxes, or UIKit warnings relevant to the module. +- Tests: package unit/type tests plus simulator flows on the required runtime/device. diff --git a/skills/write-nativescript-native-modules/references/runtime-gap-log.md b/skills/write-nativescript-native-modules/references/runtime-gap-log.md new file mode 100644 index 000000000..808dbd308 --- /dev/null +++ b/skills/write-nativescript-native-modules/references/runtime-gap-log.md @@ -0,0 +1,351 @@ +# NativeScript Runtime Gap Log + +## Contents + +- Rule for adding runtime APIs +- Generic APIs introduced during navigation/tabs ports +- APIs rejected +- Tests required for runtime changes +- Open gap audit template + +## Rule For Adding Runtime APIs + +Add a NativeScript runtime/TurboModule API only when it enables a class of native module behavior. The API name and semantics must not mention the package being ported. + +Good: + +- callback thread policy +- runtime/worklet callback invoker +- delegate creation +- target/action bridge +- UIKit host refresh +- selector metadata coverage +- structured value serialization + +Bad: + +- React Navigation transition helper +- Expo tabs badge helper +- RNN screen presenter +- one-off modal repair API + +## Generic APIs Introduced During Navigation/Tabs Ports + +`runtimeInvoker(callback)`: + +- Purpose: mark a native callback so it runs back on the owning UI/worklet runtime. +- Used for UIKit completion blocks whose metadata already includes the callable signature. +- Replaces any package-specific `afterUIKitTransition` helper. +- Must work through shared FFI callback policy, not a single engine. + +`interop.Block(encoding, callback)` / `interop.FunctionReference(encoding, callback)`: + +- Purpose: construct a native callback from an explicit Objective-C encoding when runtime metadata only says `@?` or `^?`. +- Used for UIKit transition-coordinator completion blocks such as `v@?@`. +- Keeps the API generic: the caller supplies the ObjC signature; the runtime only parses the signature and creates the FFI callback. +- Compose with `runtimeInvoker(callback)` when the native callback must re-enter the owning UI/worklet runtime. +- Must be implemented in shared FFI callback conversion for both NativeScript and the React Native vendored bridge copy. + +`eventBridge(callback, 'runtime')` / callback thread policy: + +- Purpose: allow native callbacks to specify caller, JS, or runtime/worklet delivery. +- Requires engine parity in shared FFI plus V8/JSC/QuickJS/Hermes metadata paths. + +`refreshUIKitHostView(view)`: + +- Purpose: ask a generic NativeScript UIKit host to lay out detached React children and report whether visible children exist. +- Used for first-paint and transition readiness. +- Must remain a host-view primitive, not a navigation API. + +`onHostReady`: + +- Purpose: notify TS when a generic NativeScript host has real rendered children. +- Used to avoid first-paint blank native containers. + +Delegate and target/action helpers: + +- Purpose: construct Objective-C protocol delegates and target/action objects from TS worklets. +- Must retain through context lifetime and release on dispose. + +Hosted React child passthrough: + +- Purpose: preserve UIKit hit-testing, touch delivery, and accessibility traversal when React children are reparented into a native `childrenView`. +- Used by UIKit containers whose native controller/view owns layout while React still renders route content. +- Must stay generic to `defineUIKitView` / `defineUIViewController`; do not add package-specific tap repair or navigation test hooks. +- Verification should include native automation snapshots and one-tap Pressable delivery, not only visual screenshots. + +Existing `NativeClass` subclassing: + +- Purpose: mirror upstream Objective-C/Swift subclasses when UIKit discovers behavior by selector override. +- Used for controller/window-trait behavior such as status bar style/hidden, status bar animation, supported orientations, home-indicator auto-hide, `viewWillAppear` lifecycle workarounds, `viewDidLayoutSubviews` layout refresh, first-responder restoration, and finish-transition cleanup. +- Runtime API needed: none. NativeScript already supports Objective-C subclassing through `NativeClass`; the library port should allocate the subclass instead of plain UIKit classes. +- Do not add package-specific TurboModule helpers for pull-based UIKit traits. If UIKit asks a controller for a selector upstream overrides, implement that selector on the TS subclass. +- Verification should include source/unit guards that subclass allocation is used, plus simulator flows for header/status/orientation/modal behavior when those props are exposed by the demo. + +Transition progress event parity: + +- Upstream native behavior: `RNSScreen` emits `onTransitionProgress` with `progress`, `closing`, and `goingForward`, while `Screen.tsx` wires those events into `Animated.Value`s and `TransitionProgressContext`. +- Current NativeScript requirement: if the port bypasses `Screen.tsx`, it must recreate the same JS wrapper wiring and emit the same native event payload from the TS UIKit transition lifecycle. +- Runtime API needed: none. This is wrapper parity plus ordinary event delivery through the existing NativeScript host context. +- Verification should include unit/source guards for the provider/event wiring and simulator navigation flows that use native-stack transitions. + +Transition progress display-link sampling: + +- Upstream native behavior: `RNSScreen` attaches a fake `UIView` to the UIKit transition coordinator container, animates its alpha, and samples `fakeView.layer.presentationLayer.opacity` from a `CADisplayLink` selector to emit intermediate transition progress. +- Current NativeScript requirement: expose the TS controller's `handleAnimation` selector with `NativeClass` `ObjCExposedMethods`, create `CADisplayLink.displayLinkWithTargetSelector(controller, 'handleAnimation')`, add it to `NSRunLoop.currentRunLoop` with `NSDefaultRunLoopMode`, and tear it down from the coordinator completion block. The same controller lifecycle owns final progress: `viewDidAppear` / `viewDidDisappear` emit progress `1` and reset the upstream-style `_isSwiping` / `_shouldNotify` equivalents. +- Runtime API needed: none. NativeScript already supports selector-driven native callbacks generically through `NativeClass` metadata; the port should use that instead of a package-specific TurboModule transition helper. +- Verification should include source/unit guards for display-link registration, presentation-layer sampling, transition-coordinator cleanup, and absence of duplicate stack-level progress emitters, plus simulator navigation flows that exercise animated push/pop/modal transitions. + +Screen finish-transition lifecycle parity: + +- Upstream native behavior: `notifyFinishTransitioning` restores the first responder captured during parent removal and refreshes window traits after the correct screen is visible. +- Current NativeScript requirement: implement `notifyFinishTransitioning`, `willMoveToParentViewController`, and the first-responder tree walk on the TS `NativeClass` subclass. Stack code should call the controller method, not duplicate a one-off helper. +- Runtime API needed: none. UIKit responder APIs and subclass selectors are already expressible from TypeScript worklets. +- Verification should include source/unit guards plus push/pop/gesture stress so first taps after navigation remain interactive. + +Screen route lifecycle event parity: + +- Upstream native behavior: `RNSScreen` sends `notifyWillAppear`, `notifyAppear`, `notifyWillDisappear`, `notifyDisappear`, and cancelled-swipe `notifyGestureCancel` from `viewWillAppear`, `viewDidAppear`, `viewWillDisappear`, and `viewDidDisappear`. +- Current NativeScript requirement: emit the matching host events from the TS `NativeClass` controller selectors, preserving `_isSwiping` / `_shouldNotify` semantics. Stack transition reconciliation may keep emitting stack-level transition and finish-transition events, but must not directly invoke route lifecycle props. +- Runtime API needed: none. This is NativeScript host event delivery from a controller subclass, using the existing screen context. +- Verification should include source guards that lifecycle events are emitted from the controller and absent from the stack transition callback, plus simulator push/pop/gesture stress. + +Dismiss-count lifecycle parity: + +- Upstream native behavior: `RNSScreen` computes `_dismissCount` in `viewWillDisappear` by comparing its React subview index with the target top screen's React subview index, falling back to `1` for interactive transitions, modals, forward navigation, and JS-driven back. +- Current NativeScript requirement: compute and store the dismiss count on the TS screen controller from active screen id order, which is the port's equivalent of RNSScreen's React subview order. +- Runtime API needed: none. The stack registry already tracks active screen order and controller-to-screen identity. +- Verification should include source/unit guards before moving dismissed/cancelled-dismissed event ownership into `viewDidDisappear`. + +Dismiss event lifecycle parity: + +- Upstream native behavior: `RNSScreen` emits `notifyDismissedWithCount` or `notifyDismissCancelledWithDismissCount` from `viewDidDisappear` when the controller has no parent and no presenter. For prevented native removal, upstream first calls the stack view's `updateContainer` to restore the JS navigation stack, then emits `notifyDismissCancelledWithDismissCount`. `UIAdaptivePresentationControllerDelegate` separately emits attempted-dismiss cancellation for prevented modals because the screen never disappears in that path. +- Current NativeScript requirement: keep `onDismissed` and actual-removal `onNativeDismissCancelled` on the TS screen controller's `viewDidDisappear`; use the mirrored `isRemovedFromParent` flag as the reliable detached-controller marker when direct parent state is not updated at selector time. For prevented native removal, invoke the package's reconcile entrypoint before emitting `onNativeDismissCancelled`; if this uses a registry/global worklet instead of `screenView.reactSuperview`, document that as an object-graph deviation. Modal routes should be presented as their `RNSScreen` controller directly, so UIKit's presentation delegate and screen lifecycle see the same object graph as upstream. Stack did-show paths should reconcile state only and must not synthesize duplicate screen dismissal events. +- Runtime API needed: none. This is ordinary controller lifecycle/event ownership through existing NativeScript host contexts. +- Verification should include source guards that prevented removal restores before emitting `onNativeDismissCancelled`, stack-change/did-show paths do not call `onDismissed`, no extra repeated stack-change event exists, the `UIAdaptivePresentationControllerDelegate` lives on the screen controller, and simulator native-back, header-back, JS pop, and modal dismiss flows all settle with one visible/interactive route. + +Modal presentation interop parity: + +- Upstream native behavior: modal routes use UIKit `presentViewController:animated:completion:`, `dismissViewControllerAnimatedCompletion`, and `UIAdaptivePresentationControllerDelegate`; UIKit owns swipe-down dismissal and cancellation. +- Current NativeScript requirement: require the same generic UIKit selectors from TS and present the `RNSScreen` controller directly, not a wrapper navigation controller unless upstream does. Do not substitute a manually attached child controller, custom slide animation, or custom pan-to-dismiss fallback when metadata is missing; that adds a second gesture owner and can race UIKit. +- Presented modal chain parity: keep an upstream-style `_presentedModals` equivalent in package state. Reconciliation must compute the last common presented modal before dismissing/presenting and reject reshuffles above that common root, because UIKit cannot safely reorder already-presented modal controllers. +- Modal header parity: when upstream renders an outer presented screen containing an inner stack/screen for the modal header, keep that UIKit object graph in TS. Flattening the header onto the presented route changes controller ownership and makes the later direct-present port harder. A generated inner screen id is acceptable only as a NativeScript registry key; document it as a package deviation, not a runtime gap. +- Backdrop tap parity: for `modal`, `pageSheet`, and `formSheet`, add the same outside-tap recognizer to `presentationController.containerView`. It should be simultaneous, non-cancelling, disabled unless `preventNativeDismiss` is true, ignored for touches inside `presentedView`, and emit the same dismiss-cancel event with count `1`. +- Runtime API needed: none if the Objective-C selector metadata is exposed. If an engine cannot call those methods, fix generic selector metadata/dispatch in the runtime and add runtime tests. +- Verification should include source guards against custom modal pan fallback plus simulator present/dismiss/swipe-dismiss flows. + +UIKit controller host-view separation: + +- Upstream native behavior: some native components are wrapper views that own a child UIKit controller, not the controller view itself. `RNSScreenStackView` is the React Native view, embeds `UINavigationController.view`, and overrides `hitTest:withEvent:` to forward taps in the navigation-bar frame into left/right header subviews that UIKit would otherwise clip out of the native hit area. +- Current NativeScript requirement: use a generic `defineUIViewController` `hostView(controller)` resolver when a pure-TS port needs the RN host view to be a separate native wrapper while still attaching the UIKit controller for lifecycle. Keep the controller's own `view` intact and add it as a child of the wrapper. The native host must honor an explicit native view handle instead of overwriting it with `controller.view` when the controller handle arrives. +- Runtime API needed: `UIViewControllerDefinition.hostView` plus `NativeScriptUIView` honoring explicit native host handles during controller attachment. This is generic UIKit hosting support; do not add package-specific header-button tap helpers or navigation-stack TurboModule APIs. +- Verification should include runtime source guards for `hostView`, package source guards for the wrapper `hitTest:withEvent:` port, and simulator header/menu/custom-header tap stress. + +Native callback worklet entrypoint ordering: + +- Upstream native behavior: Objective-C selector callbacks call already-compiled methods on the native class; helper symbol order is not observable. +- Current NativeScript requirement: native callback worklets and `NativeClass` delegate methods must not close over helper functions declared later in the TS module unless the callback class/function is created after that helper is registered. Move tiny helpers above the callback, inline them, or register a package-owned global worklet entrypoint after definition and resolve it from `globalThis` inside the native callback. +- Runtime API needed: none. This is a TS worklet authoring rule; adding a package-specific TurboModule helper would hide the ordering bug instead of fixing the port. +- Verification should include simulator native callback paths, especially UIKit modal `presentationControllerDidDismiss`, because source/type tests will not catch a UI-runtime `undefined is not a function` closure capture. + +UIKit delegate proxy identity: + +- Upstream native behavior: `UINavigationControllerDelegate` receives the exact `RNSScreen` controller pointer in `didShow`, so route identity comes from ObjC object identity. +- Current NativeScript requirement: first try native identity that survives proxy boundaries, such as controller restoration identifiers and hosted view identifiers. If UIKit still reports an unresolved proxy/placeholder during `didShow`, the port may update only route key/count bookkeeping from the known active/native stack length so React does not replay an already-completed push. It must not reapply `viewControllers` from inferred ids or emit route changes unless UIKit has actually shortened the native stack. +- Runtime API needed: ideally a generic associated-object or stable native identity primitive would remove this deviation. Until then, keep the workaround package-local, documented, and covered by simulator first-tap/gesture stress. +- Verification should include first tap after tab switch, first push after interactive back, duplicate push/pop guards, and a source guard against timed `setViewControllers` reapplication from inferred ids. + +Transition completion watchdog: + +- Upstream native behavior: stack transition completion is driven by `UINavigationControllerDelegate didShow`, and updates that arrive during a transition are replayed through `transitionCoordinator` completion. +- Current NativeScript requirement: install a generic `transitionCoordinator` completion for TS-started push/pop/replace transitions before using any timer. A timer is allowed only as a watchdog for the unresolved-proxy case where the delegate callback is missed; it must check the transition token and `stackTransitioning` flag so it cannot rewrite controllers after a real `didShow`. +- Runtime API needed: none if `transitionCoordinator` and explicit `interop.Block` callbacks are available. If coordinator completion cannot be installed on an engine, fix generic selector/block interop rather than adding a package-specific navigation helper. +- Verification should include source guards for coordinator-first scheduling and simulator stress for push/pop, interactive back, duplicate taps, and modal transitions. + +Navigation-bar appearance during transitions: + +- Upstream native behavior: `RNSScreenStackHeaderConfig` applies shared `UINavigationBarAppearance` changes through `transitionCoordinator animateAlongsideTransition:completion:` and restores the `fromVC` header config when an interactive transition is cancelled. +- Current NativeScript requirement: keep the initial synchronous appearance write, then register a generic coordinator completion through `interop.Block`. On completion, resolve the current top controller, or the transition context's `from` controller when cancelled, back to the package's header config registry before reapplying appearance. +- Runtime API needed: none beyond generic selector access, `transitionCoordinator`, transition-context `viewControllerForKey:`, and explicit `interop.Block` callbacks. If an engine cannot expose these, close that in the generic interop/runtime layer, not with a screens-specific helper. +- Verification should include source guards for the coordinator completion and simulator/header stress when header appearance, custom headers, or interactive cancellation are changed. + +Presentation delegate protocol conformance: + +- Upstream native behavior: `RNSScreenView` conforms to both `UIAdaptivePresentationControllerDelegate` and `UISheetPresentationControllerDelegate`; UIKit dispatches modal prevention/dismissal callbacks through the former and sheet detent changes through the latter. +- Current NativeScript requirement: register the same protocol list on the NativeScript controller subclass, not only the selector metadata. The TS method and exposed selector for `sheetPresentationControllerDidChangeSelectedDetentIdentifier:` must be paired with `UISheetPresentationControllerDelegate` in both NativeClass and `UIViewController.extend` creation paths. +- Runtime API needed: none if generic `nativeProtocol(...)` lookup and subclass protocol metadata are available. +- Verification should include a source guard for every upstream delegate protocol the subclass implements. + +Prevented native pop cancellation: + +- Upstream native behavior: when a native/header back pop crosses a screen with `preventNativeDismiss`, `RNSScreenStackView` returns an animator, creates an `RNSPercentDrivenInteractiveTransition` from `navigationController:interactionControllerForAnimationController:`, cancels it after one main-queue tick, calls `updateContainer`, and emits `notifyDismissCancelledWithDismissCount` from the original `fromView`. +- Current NativeScript requirement: preserve the same animator plus percent-driven interaction-controller cancellation path. If `transitionCoordinator viewForKey:` is not reliable across NativeScript delegate proxy boundaries, cache the stable `fromViewController`/`toViewController` pair from the immediately preceding animation delegate callback and consume it in the interaction-controller delegate. Guard the scheduled cancel against stale cached pairs. +- Runtime API needed: none. `ctx.delegate`, `UIPercentDrivenInteractiveTransition`, `setTimeout`/main-queue scheduling, and package-local stack reconciliation are enough; a transition-coordinator-specific TurboModule helper would be the wrong abstraction. +- Verification should include source guards for the cached pair, scheduled `cancelInteractiveTransition`, reconcile-before-cancel-event order, and simulator native/header-back prevention followed by one-tap push/pop recovery. + +Post-transition hosted layout settle: + +- Upstream native behavior: `RNSScreenStackView` queues `updateContainer` with `dispatch_async` after React child layout/mount mutations so controller updates see laid-out native children. +- Current NativeScript requirement: after UIKit finishes a controller transition, hold React-driven reconciles for one short host-layout settle window while NativeScript/RN hosted children publish final frames through `onHostReady` and `refreshUIKitHostView`. This timer may only defer and replay reconciliation; it must not rewrite UIKit controller arrays directly. +- Runtime API needed: none. This is bridging NativeScript host-view readiness to the upstream “layout is enqueued on the UI queue” behavior. +- Verification should include source guards that the settle path does not call `setViewControllers`, plus simulator stress for first tap after transition and lower-half modal touches. + +Transition interaction parity: + +- Upstream native behavior: `react-native-screens` gates hosted screen interactions during iOS 26 stack transitions because incoming screens can become visible before UIKit has completed the transition. +- Current NativeScript requirement: implement the gate inside the TS port using UIKit lifecycle/transition state, gating only the native transition container. Do not recursively set `userInteractionEnabled` on hosted React children; that overrides RN `pointerEvents` and can enable NativeScript detached-child touch sentinels that must remain disabled. +- Runtime API needed: none. This is a mechanical porting responsibility, not a TurboModule helper. +- Verification should include transition-overlap taps, double-tap duplicate-route checks, and post-transition one-tap navigation. + +Tab selection hosted-content readiness: + +- Upstream native behavior: `UITabBarController` owns the selected controller's view hierarchy, and tab selection, child appearance, layout, and hit-test readiness settle in UIKit's native run loop. +- Current NativeScript requirement: after actual native tab selection callbacks, synchronously reconcile the selected controller view and repeat only that selected-view/host refresh across a few short UI ticks with a token guard. This closes the window where React Navigation sees the tab as selected and exposes route labels before the hosted subtree accepts the first tap. Ordinary host commits should stay synchronous so first paint is not blanked by broad repeated commits. +- Runtime API needed: none. Use existing UIKit view layout plus `NativeScriptRuntime.refreshUIKitHostView`; do not introduce JS debounces or tab-specific TurboModule helpers. +- Verification should include first push immediately after tab switch at small delays such as `0,25,50,75,100,150,200,300ms`, plus a clean app relaunch to ensure first paint still mounts tab content. + +Custom stack animator parity: + +- Upstream native behavior: `RNSScreenStackAnimator` uses `UIViewPropertyAnimator`, and `RNSPercentDrivenInteractiveTransition` drives the animator's `fractionComplete` during interactive gestures. +- Current NativeScript requirement: mirror the upstream object model with named NativeScript Objective-C subclasses: an `NSObject` subclass conforming to `UIViewControllerAnimatedTransitioning` for the animator, and a `UIPercentDrivenInteractiveTransition` subclass for the gesture controller. Store per-transition state on the native proxy just as upstream stores it on ObjC ivars/properties. +- Also require `UIViewPropertyAnimator` metadata in the runtime and allocate/use the UIKit animator from TS. Do not add a fallback `UIView.animate` path for custom stack transitions; it may visually complete but cannot be scrubbed interactively, so it is not equivalent native-module behavior. +- Runtime API needed: none if protocol lookup, Objective-C subclassing, and `UIViewPropertyAnimator` metadata are present. If an engine/runtime cannot create the protocol-conforming subclass or see class/initializer metadata, fix the generic Objective-C metadata/interop layer and add runtime tests there. +- Verification should include source guards that the named classes are allocated, no non-interactive fallback exists, plus simulator custom-animation/gesture flows. + +Hosted interaction ownership: + +- Upstream native behavior: native containers may temporarily gate interaction during transitions, but React Native owns hit-testing state inside hosted route content. +- Current NativeScript requirement: layout/refresh native hosts without rewriting child `userInteractionEnabled` values. A TS port may set the `UIViewController.view` or native container view, but must not walk into RN/Fabric subviews to "fix" touches. +- Runtime API needed: none. The existing `refreshUIKitHostView(view)` is the correct generic primitive for detached host refresh; it should not be paired with recursive interaction mutation. +- Verification should include one-tap Pressable delivery after push, pop, modal dismiss, tab switching, and any cancelled gesture recovery. + +Interactive pop cancellation parity: + +- Upstream native behavior: a cancelled interactive pop emits gesture-cancel semantics and does not count as a dismissed route. +- Current NativeScript requirement: compare UIKit's shown stack to React Navigation's active route IDs in `didShow`. Only treat a closing transition as completed when the native stack is actually shorter. +- Runtime API needed: none. The fix belongs in the TS port's UIKit delegate/state reconciliation. +- Avoid JS-side timed "repair" calls that imperatively reset `viewControllers` before React state has processed the native dismissal event; they can re-add a popped screen mid-gesture and poison completed-dismiss bookkeeping. +- Verification should include: cancel edge gesture, complete the next edge gesture, then push/pop again with one tap. + +JS-owned transition state replay: + +- Upstream native behavior: React Navigation may update JS state while UIKit is still animating a state-driven push/pop, and the native stack must converge after the transition completes. +- Current NativeScript requirement: tag JS-requested transitions separately from native gestures, and record pending reconcile keys when React state changes during a JS-owned transition. +- Runtime API needed: none. This is a library state-machine responsibility. +- Verification should include repeated one-tap push/pop loops plus double-tap push/pop loops; a state update that arrives during `stackTransitioning` must not be dropped. + +Native content wrapper parity: + +- Upstream native behavior: `RNSScreenContentWrapper` is a real native view that is mounted under `RNSScreen`, participates in React layout updates, lets `RNSScreen` observe its frame for sheet/content sizing, and lets `RNSScreen` coerce a descendant `UIScrollView` frame for `formSheet` layouts. +- Current NativeScript requirement: port wrapper components as `defineUIKitContainer` hosts when upstream has a native wrapper. Do not replace them with a plain React Native `View`; that loses native wrapper lifetime/layout semantics and can leave modal content clipped even when the presented UIKit controller has a correct full-width frame. Store the native wrapper view in package state so screen-layout and wrapper-layout callbacks can mirror upstream `coerceChildScrollViewComponentSizeToSize` for direct scroll children, header-plus-scroll children, and the iOS 26 safe-area wrapper path. +- Runtime API needed: none. `defineUIKitContainer`, `attachNativeView`, and `refreshUIKitHostView(view)` are the generic primitives. The library port owns wrapper sizing/autoresizing just like the upstream native component does. +- Debugging rule: if UIKit controller/view frames are correct but content is clipped or partially untouchable, inspect the hosted RN wrapper chain. A stale zero-origin wrapper can expose the intended `contentSize.width` while keeping an old `frame.size.width`; repair only those wrapper levels, never arbitrary leaf layout. +- Verification should include modal presentation after an interactive back gesture, lower-half modal taps, scroll-view content width, direct/header/safe-area form-sheet scroll coercion unit tests, and one-tap controls after the next settled route is revealed. + +Native-owned unmount snapshot parity: + +- Upstream native behavior: before a native-owned pop/dismiss removes a React-backed screen, `RNSScreen.setViewToSnapshot` replaces its live view with `snapshotViewAfterScreenUpdates:` so UIKit can finish animating a stable visual even if React unmounts the route content. +- Current NativeScript requirement: expose the same selector on the TS `RNSScreen` controller subclass and call it from the controller dispose path when UIKit is still retaining the controller for a native pop/dismiss. JS-owned transition retention may still keep an inactive React element until UIKit completes, because React owns that element lifetime in the TS port. +- Runtime API needed: none. `snapshotViewAfterScreenUpdates:` and `UIView` reparenting are ordinary UIKit selectors available through NativeScript interop; a NativeScript snapshot helper would be package-specific duplication. +- Verification should include source/unit guards for `setViewToSnapshot`, simulator native-back/header-back/modal dismissal, and post-transition one-tap recovery. + +Form sheet detent event parity: + +- Upstream native behavior: `RNSScreen` sets itself as the sheet presentation controller delegate and emits `onSheetDetentChanged` from `sheetPresentationControllerDidChangeSelectedDetentIdentifier:` after converting UIKit's selected detent identifier to the JS detent index. +- Current NativeScript requirement: keep the delegate selector on the TS screen controller subclass, map `medium`/`large`/custom numeric detent identifiers the same way upstream does, and emit `{ index, isStable: true }` through the existing screen context. +- Runtime API needed: none. This is ordinary UIKit delegate dispatch through NativeScript `NativeClass` selector exposure. +- Verification should include source guards for the delegate selector, identifier mapping, event emission, and absence of form-sheet-specific runtime helpers. + +Tabs navigation-state event parity: + +- Upstream native behavior: `RNSTabBarController` owns a tab navigation state with `selectedScreenKey` and monotonic `provenance`, rejects stale or repeated JS requests, emits prevented selection when `preventNativeSelection` blocks UIKit, and emits a dedicated More-tab event without mutating the selected route. +- Current NativeScript requirement: keep the same state machine in the TS `UITabBarController` host. Host updates must preserve native-owned provenance instead of overwriting it from props, and delegate/fallback gesture paths must route through the same selected/prevented/rejected/More event helpers. Because the TS port receives host and tab-screen records through separate React/native host mounts, the first-paint commit window may be bounded and token-coalesced, but it must not become a persistent restore interval or a delayed navigation-state repair path. +- Parity detail: initialized state is explicit. A stale numeric `provenance` + field from an older registry shape must not be treated as a live + `RNSTabsNavigationState`; upstream creates provenance `0` when + `_navigationState == nil`. +- Runtime API needed: none. NativeScript already exposes UIKit delegates, gesture actions, and host event delivery; the package port owns the React Navigation state reconciliation. +- Verification should include unit tests for user selection, repeated selection, stale/repeated JS request rejection, prevented native selection, More-tab selection, first-paint labels/icons, and simulator one-tap tab/navigation stress. + +Search-controller header handoff parity: + +- Upstream native behavior: `RNSSearchBar` owns a `UISearchController`, applies search-bar props and `UISearchBarDelegate` events, while `RNSScreenStackHeaderConfig` installs that controller into `UINavigationItem.searchController` and mirrors placement/scrolling/toolbar-integration flags. +- Current NativeScript requirement: port `RNSSearchBar` as a TS NativeScript UIKit component that allocates `UISearchController`, registers the controller through the package header-subview registry, and lets the NativeScript stack header attach it to `UINavigationItem`. +- Runtime API needed: none. This uses `defineUIKitContainer`, `ctx.delegate`, existing host refs with `runOnUI`, and package-owned context/registry state. +- Verification should include owner tests for controller creation, prop application, delegate events, command wiring, and container tests for `UINavigationItem` search-controller placement including iOS 26 stacked toolbar-integration behavior. + +Custom back-indicator image parity: + +- Upstream native behavior: `RNSScreenStackHeaderConfig` scans `RNSScreenStackHeaderSubviewTypeBackButton`, reads the child `RCTImageComponentView.image`, and installs it as both `UINavigationBarAppearance.backIndicatorImage` and `transitionMaskImage`. +- Current NativeScript requirement: carry `ScreenStackHeaderBackButtonImage`'s resolved image source through the NativeScript header-subview registry and resolve it with the same generic local/RN image-loader path used for header bar-button images. +- Runtime API needed: none. This is package-owned data flow plus existing generic image loading; a NativeScript snapshot/rendered-pixel API is unnecessary for source-backed back indicators. +- Verification should include a unit test that registered back-image records set and clear the standard/compact/scroll-edge appearance back indicator images. + +Source back-button item parity: + +- Upstream native behavior: `RNSScreenStackHeaderConfig.configureBackItem` sets the previous `UINavigationItem.backButtonTitle` and `backButtonDisplayMode`, then leaves UIKit's default back item alone unless `disableBackButtonMenu`, `backTitleFontFamily`, or `backTitleFontSize` requires a custom `RNSBackBarButtonItem`. When `backTitleVisible=false`, it uses minimal display mode but keeps the title for the back menu. +- Current NativeScript requirement: implement `RNSBackBarButtonItem` as a TS `NativeClass` subclass of `UIBarButtonItem`, override `setMenu:` to honor `menuHidden`, and assign it only for the same customization cases as upstream. If the previous item lost its title after controller recreation, recover it from the source screen header config before setting `backButtonTitle`. +- Runtime API needed: none. `NativeClass` subclassing, UIKit item properties, and selector overrides are generic NativeScript interop capabilities. +- Verification should include unit/source guards for default UIKit ownership, minimal-title menu preservation, custom menu/font item assignment, recreated-title fallback, and explicit comments for any package-local stale-item cleanup deviation. + +## APIs Rejected + +`afterUIKitTransition(viewController, callback)`: + +- Rejected because it hard-coded a transition-coordinator use case into the runtime. +- Correct replacement: call UIKit directly from TS and wrap the completion callback with `runtimeInvoker`. + +Per-call `dispatch_sync` wrappers: + +- Rejected because granular native calls should not cross threads one call at a time. +- Correct replacement: run cohesive native work inside a UI worklet. + +RN JS runtime on UI thread: + +- Rejected as unsafe. +- Correct replacement: worklets are mandatory for NativeScript native UI. + +Demo-only navigation hooks: + +- Rejected as a parity substitute because they do not prove touch delivery, native gestures, or library event ordering. +- Correct replacement: fix generic host hit-testing/accessibility/touch passthrough, then test the public UI. + +## Tests Required For Runtime Changes + +Each runtime primitive needs at least: + +- Type-level or source-level test in `packages/react-native/test`. +- Shared FFI behavior test when callback or serialization semantics change. +- Engine parity check for V8, JSC, QuickJS, and Hermes paths when selector metadata or callback policy changes. +- Demo validation on iOS simulator when UIKit host behavior changes. + +Useful test names from this port: + +- `runtime-callback-policy.test.js` +- `callback-thread-policy.test.js` +- `uikit-gesture-action-api.test.js` +- `uikit-host-ready-api.test.js` +- `uikit-host-refresh-api.test.js` + +For hosted child passthrough, add tests that assert the native host does not +hide or swallow accessibility/touch traversal for visible React children after +they are moved into `childrenView`. + +## Open Gap Audit Template + +When a port hits a missing capability, record: + +```md +## Gap: + +- Upstream native behavior: +- Current TS workaround: +- Why existing NativeScript APIs are insufficient: +- Proposed generic primitive: +- Engines/FFI files touched: +- Tests added: +- Demo behavior verified: +- Package-specific API avoided: +``` + +Only leave a workaround in the fork if the gap is understood and tracked. Prefer fixing NativeScript generically before polishing library-specific code. diff --git a/test/cli/constructor_probe.js b/test/cli/constructor_probe.js new file mode 100644 index 000000000..13ebf3a61 --- /dev/null +++ b/test/cli/constructor_probe.js @@ -0,0 +1,8 @@ +const url = new URL("https://example.com/path"); +console.log("url ok", typeof url, url.href); + +const workerValue = new Worker("test/cli/worker.js"); +console.log("worker value", typeof workerValue, workerValue === undefined); +if (workerValue && typeof workerValue.terminate === "function") { + workerValue.terminate(); +} diff --git a/test/cli/memory/_plain_harness.js b/test/cli/memory/_plain_harness.js index 795626187..646bcb16b 100644 --- a/test/cli/memory/_plain_harness.js +++ b/test/cli/memory/_plain_harness.js @@ -109,6 +109,38 @@ async function forceCollectUntil(predicate, options) { return !!predicate(); } +async function drainRunLoopUntilIdle(predicate, options) { + const opts = options || {}; + const timeoutMs = opts.timeoutMs ?? 10_000; + const tickMs = opts.tickMs ?? 8; + const settleTicks = opts.settleTicks ?? 3; + const mode = NSDefaultRunLoopMode; + const start = Date.now(); + let idleTicks = 0; + + while (Date.now() - start < timeoutMs) { + NSRunLoop.mainRunLoop.runModeBeforeDate( + mode, + NSDate.dateWithTimeIntervalSinceNow(tickMs / 1000), + ); + + drainPendingJobs(); + + if (predicate()) { + idleTicks += 1; + if (idleTicks >= settleTicks) { + return true; + } + } else { + idleTicks = 0; + } + + await sleep(0); + } + + return !!predicate(); +} + function emitResult(result) { const payload = JSON.stringify(result); console.log(`MEMTEST_RESULT:${payload}`); @@ -139,13 +171,17 @@ function runPlainMemoryTest(name, fn, options) { sleep, forceGC, forceCollectUntil, + drainRunLoopUntilIdle, assert, waitUntil, makePressure, countAliveWeakRefs, weakTableCount, now: () => Date.now(), - autoreleasepool: typeof objc === "object" ? objc.autoreleasepool : null, + autoreleasepool: + typeof objc === "object" && typeof objc.autoreleasepool === "function" + ? objc.autoreleasepool + : (fn) => fn(), engine: (typeof process === "object" && process && diff --git a/test/cli/memory/run_memory_semantics_tests.js b/test/cli/memory/run_memory_semantics_tests.js index d3ede5394..6a20fb2f8 100644 --- a/test/cli/memory/run_memory_semantics_tests.js +++ b/test/cli/memory/run_memory_semantics_tests.js @@ -12,6 +12,7 @@ const semanticsTests = [ "test_weakref_finalization.js", "test_weakref_plain_script.js", "test_objc_ownership_rules.js", + "test_objc_unmanaged_transfer_semantics.js", "test_objc_wrapper_finalization.js", "test_pointer_c_buffer_semantics.js", "test_reference_lifecycle.js", @@ -117,7 +118,7 @@ function printSemanticsRunSummary(run) { async function main() { const opts = parseArgs(process.argv); - const repoRoot = path.resolve(__dirname, "..", ".."); + const repoRoot = path.resolve(__dirname, "..", "..", ".."); const memoryDir = path.resolve(__dirname); const nsrPath = opts.runtime ? path.resolve(repoRoot, opts.runtime) diff --git a/test/cli/memory/run_memory_tests.js b/test/cli/memory/run_memory_tests.js index 01ca4751f..511e71b52 100644 --- a/test/cli/memory/run_memory_tests.js +++ b/test/cli/memory/run_memory_tests.js @@ -9,6 +9,7 @@ const memoryThresholdsKB = { "weakref-finalization": 40 * 1024, "js-heap-throughput": 120 * 1024, "objc-ownership-rules": 60 * 1024, + "objc-unmanaged-transfer-semantics": 60 * 1024, "objc-wrapper-churn": 80 * 1024, "appkit-navigation-throughput": 140 * 1024, "appkit-navigation-extreme": 220 * 1024, @@ -272,7 +273,7 @@ function printRunSummary(run) { async function main() { const opts = parseArgs(process.argv); - const repoRoot = path.resolve(__dirname, "..", ".."); + const repoRoot = path.resolve(__dirname, "..", "..", ".."); const memoryDir = path.resolve(__dirname); const nsrPath = opts.runtime ? path.resolve(repoRoot, opts.runtime) diff --git a/test/cli/memory/run_memory_tests_all_engines.sh b/test/cli/memory/run_memory_tests_all_engines.sh index 1ca391e13..0c22e1349 100755 --- a/test/cli/memory/run_memory_tests_all_engines.sh +++ b/test/cli/memory/run_memory_tests_all_engines.sh @@ -1,14 +1,14 @@ #!/bin/bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" GREP_FILTER="${1:-}" ENGINES=(v8 quickjs jsc hermes) for engine in "${ENGINES[@]}"; do echo echo "=== Building macOS CLI for ${engine} ===" - "$ROOT_DIR/build_nativescript.sh" --no-iphone --no-simulator --no-macos --macos-cli "--${engine}" + "$ROOT_DIR/scripts/build_nativescript.sh" --no-iphone --no-simulator --no-macos --macos-cli "--${engine}" echo "=== Running CLI memory suite for ${engine} ===" if [[ -n "$GREP_FILTER" ]]; then diff --git a/test/cli/memory/test_block_lifecycle.js b/test/cli/memory/test_block_lifecycle.js index a254dae6e..a1ac988a4 100644 --- a/test/cli/memory/test_block_lifecycle.js +++ b/test/cli/memory/test_block_lifecycle.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("block-lifecycle", async (t) => { +runPlainMemoryTest("block-lifecycle", async (t) => { const queue = NSOperationQueue.new(); queue.maxConcurrentOperationCount = 8; diff --git a/test/cli/memory/test_dispatch_async_background.js b/test/cli/memory/test_dispatch_async_background.js index 77fcaddf1..5ded0a10c 100644 --- a/test/cli/memory/test_dispatch_async_background.js +++ b/test/cli/memory/test_dispatch_async_background.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("dispatch-async-background", async (t) => { +runPlainMemoryTest("dispatch-async-background", async (t) => { const total = 1200; let backgroundExecuted = 0; let checksum = 0; diff --git a/test/cli/memory/test_js_heap_throughput.js b/test/cli/memory/test_js_heap_throughput.js index 03f963413..f366da4db 100644 --- a/test/cli/memory/test_js_heap_throughput.js +++ b/test/cli/memory/test_js_heap_throughput.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("js-heap-throughput", async (t) => { +runPlainMemoryTest("js-heap-throughput", async (t) => { const outerRounds = 8; const chunkSize = 128 * 1024; const chunksPerRound = 350; diff --git a/test/cli/memory/test_mixed_stress.js b/test/cli/memory/test_mixed_stress.js index a586e240f..a920c944f 100644 --- a/test/cli/memory/test_mixed_stress.js +++ b/test/cli/memory/test_mixed_stress.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("mixed-stress", async (t) => { +runPlainMemoryTest("mixed-stress", async (t) => { const rounds = 20; const perRound = 500; let blockHits = 0; diff --git a/test/cli/memory/test_objc_unmanaged_transfer_semantics.js b/test/cli/memory/test_objc_unmanaged_transfer_semantics.js new file mode 100644 index 000000000..2069e9377 --- /dev/null +++ b/test/cli/memory/test_objc_unmanaged_transfer_semantics.js @@ -0,0 +1,57 @@ +"use strict"; + +const { runPlainMemoryTest } = require("./_plain_harness"); + +runPlainMemoryTest("objc-unmanaged-transfer-semantics", async (t) => { + const rounds = 1000; + let retainedFailures = 0; + let unretainedFailures = 0; + let consumedFailures = 0; + + function expectConsumed(value) { + try { + value.retainCount(); + consumedFailures += 1; + } catch (_) { + // Expected: the original wrapper has transferred its native value. + } + } + + for (let i = 0; i < rounds; i++) { + const retainedSource = NSObject.alloc(); + const retained = retainedSource.takeRetainedValue(); + const retainedCount = retained.retainCount(); + if (!(retainedCount >= 1)) { + retainedFailures += 1; + } + expectConsumed(retainedSource); + + const unretainedSource = NSObject.alloc().init(); + const unretained = unretainedSource.takeUnretainedValue(); + const unretainedCount = unretained.retainCount(); + if (!(unretainedCount >= 1)) { + unretainedFailures += 1; + } + expectConsumed(unretainedSource); + } + + t.assert( + retainedFailures === 0, + `retained transfer produced invalid wrappers in ${retainedFailures} rounds`, + ); + t.assert( + unretainedFailures === 0, + `unretained transfer produced invalid wrappers in ${unretainedFailures} rounds`, + ); + t.assert( + consumedFailures === 0, + `consumed unmanaged wrappers remained usable in ${consumedFailures} rounds`, + ); + + return { + rounds, + retainedFailures, + unretainedFailures, + consumedFailures, + }; +}, { timeoutMs: 20_000 }); diff --git a/test/cli/memory/test_objc_wrapper_churn.js b/test/cli/memory/test_objc_wrapper_churn.js index 990419dee..e08f9153a 100644 --- a/test/cli/memory/test_objc_wrapper_churn.js +++ b/test/cli/memory/test_objc_wrapper_churn.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("objc-wrapper-churn", async (t) => { +runPlainMemoryTest("objc-wrapper-churn", async (t) => { const outerRounds = 16; const innerRounds = 2000; let checksum = 0; diff --git a/test/cli/memory/test_runloop_pending_work.js b/test/cli/memory/test_runloop_pending_work.js index 9a944578f..f29a64204 100644 --- a/test/cli/memory/test_runloop_pending_work.js +++ b/test/cli/memory/test_runloop_pending_work.js @@ -1,8 +1,8 @@ "use strict"; -const { runAsyncMemoryTest } = require("./_harness"); +const { runPlainMemoryTest } = require("./_plain_harness"); -runAsyncMemoryTest("runloop-pending-work", async (t) => { +runPlainMemoryTest("runloop-pending-work", async (t) => { const totalMainQueueCallbacks = 1500; let completedMainQueueCallbacks = 0; let checksum = 0; diff --git a/test/cli/node_api/addon.cpp b/test/cli/node_api/addon.cpp index 429125328..a1d2285e1 100644 --- a/test/cli/node_api/addon.cpp +++ b/test/cli/node_api/addon.cpp @@ -64,7 +64,6 @@ struct MakeCallbackContext { // Keep values alive across the async boundary. napi_ref recv_ref = nullptr; napi_ref callback_ref = nullptr; - napi_ref arg_ref = nullptr; }; void RunMakeCallbackOnWorker(MakeCallbackContext* context) { @@ -72,17 +71,19 @@ void RunMakeCallbackOnWorker(MakeCallbackContext* context) { napi_value recv = nullptr; napi_value callback = nullptr; - napi_value arg = nullptr; napi_get_reference_value(context->env, context->recv_ref, &recv); napi_get_reference_value(context->env, context->callback_ref, &callback); - napi_get_reference_value(context->env, context->arg_ref, &arg); - napi_value argv[1] = {arg}; napi_status status = napi_make_callback(context->env, nullptr, recv, callback, - 1, argv, nullptr); + 0, nullptr, nullptr); if (status != napi_ok) { fprintf(stderr, "napi_make_callback failed: %d\n", static_cast(status)); } + + napi_delete_reference(context->env, context->recv_ref); + napi_delete_reference(context->env, context->callback_ref); + napi_async_destroy(context->env, context->async_context); + delete context; } void RunMakeCallbackOnBackgroundQueue(void* data) { @@ -112,33 +113,30 @@ napi_value MakeCallbackFromNative(napi_env env, napi_callback_info info) { auto* context = new MakeCallbackContext(); context->env = env; - napi_value asyncResource = nullptr; - napi_value asyncResourceName = nullptr; - CHECK_VALUE(napi_create_object(env, &asyncResource), - "Failed to create async resource"); - CHECK_VALUE(napi_create_string_utf8(env, "make-callback-reentry", - NAPI_AUTO_LENGTH, &asyncResourceName), - "Failed to create async resource name"); - CHECK_VALUE(napi_async_init(env, asyncResource, asyncResourceName, - &context->async_context), + CHECK_VALUE(napi_async_init(env, nullptr, nullptr, &context->async_context), "Failed to init async context"); napi_value recvValue = nullptr; CHECK_VALUE(napi_get_global(env, &recvValue), "Failed to get global receiver"); - napi_value argValue = nullptr; - CHECK_VALUE(napi_create_int32(env, 42, &argValue), - "Failed to create callback argument"); - - CHECK_VALUE(napi_create_reference(env, recvValue, 1, - &context->recv_ref), - "Failed to create receiver ref"); - CHECK_VALUE(napi_create_reference(env, argv[0], 1, - &context->callback_ref), - "Failed to create callback ref"); - CHECK_VALUE(napi_create_reference(env, argValue, 1, - &context->arg_ref), - "Failed to create argument ref"); + + napi_status status = + napi_create_reference(env, recvValue, 1, &context->recv_ref); + if (status != napi_ok) { + napi_async_destroy(env, context->async_context); + delete context; + ThrowStatusError(env, status, "Failed to create receiver ref"); + return nullptr; + } + + status = napi_create_reference(env, argv[0], 1, &context->callback_ref); + if (status != napi_ok) { + napi_delete_reference(env, context->recv_ref); + napi_async_destroy(env, context->async_context); + delete context; + ThrowStatusError(env, status, "Failed to create callback ref"); + return nullptr; + } dispatch_async_f(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), context, RunMakeCallbackOnBackgroundQueue); diff --git a/test/cli/node_api/build_addon.sh b/test/cli/node_api/build_addon.sh index b855f2815..4dd331987 100755 --- a/test/cli/node_api/build_addon.sh +++ b/test/cli/node_api/build_addon.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" OUT_FILE="$ROOT_DIR/test/cli/node_api/addon.dylib" SRC_FILE="$ROOT_DIR/test/cli/node_api/addon.cpp" diff --git a/test/cli/node_api/reentry_tsfn.js b/test/cli/node_api/reentry_tsfn.js index b8a26b513..8d1869e81 100644 --- a/test/cli/node_api/reentry_tsfn.js +++ b/test/cli/node_api/reentry_tsfn.js @@ -28,9 +28,8 @@ function withTimeout(promise, label, timeoutMs = 2000) { async function testMakeCallbackReentry() { await withTimeout( new Promise((resolve, reject) => { - addon.makeCallbackFromNative((value) => { + addon.makeCallbackFromNative(() => { try { - assert(value === 42, `Expected makeCallback value 42, got ${value}`); resolve(); } catch (error) { reject(error); @@ -41,6 +40,12 @@ async function testMakeCallbackReentry() { ); } +async function testMakeCallbackReentryStress() { + for (let i = 0; i < 64; i++) { + await testMakeCallbackReentry(); + } +} + async function testThreadsafeFunction() { const values = []; @@ -67,6 +72,12 @@ async function testThreadsafeFunction() { ); } +async function testThreadsafeFunctionStress() { + for (let i = 0; i < 32; i++) { + await testThreadsafeFunction(); + } +} + async function testMissingNodeApis() { const version = addon.getNodeVersion(); assert(version && typeof version === "object", "Expected version object"); @@ -94,8 +105,8 @@ async function testMissingNodeApis() { (async () => { await testMissingNodeApis(); - await testMakeCallbackReentry(); - await testThreadsafeFunction(); + await testMakeCallbackReentryStress(); + await testThreadsafeFunctionStress(); console.log("node_api gaps+reentry+tsfn PASS"); })().catch((error) => { console.error(error && error.stack ? error.stack : String(error)); diff --git a/test/cli/node_api/run_teardown_test.sh b/test/cli/node_api/run_teardown_test.sh index ed97ca015..412995f22 100755 --- a/test/cli/node_api/run_teardown_test.sh +++ b/test/cli/node_api/run_teardown_test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" MARKER_FILE="$(mktemp /tmp/ns-node-api-cleanup.XXXXXX)" cleanup() { diff --git a/test/cli/quickjs_sab_probe.js b/test/cli/quickjs_sab_probe.js new file mode 100644 index 000000000..13eac65e6 --- /dev/null +++ b/test/cli/quickjs_sab_probe.js @@ -0,0 +1,9 @@ +console.log("typeof SAB", typeof SharedArrayBuffer); +console.log("typeof Atomics", typeof Atomics); + +const buffer = + typeof SharedArrayBuffer !== "undefined" + ? new SharedArrayBuffer(4) + : new ArrayBuffer(4); + +console.log("buffer tag", Object.prototype.toString.call(buffer)); diff --git a/test/cli/structured_worker.js b/test/cli/structured_worker.js new file mode 100644 index 000000000..0438b0947 --- /dev/null +++ b/test/cli/structured_worker.js @@ -0,0 +1,43 @@ +globalThis.onmessage = (event) => { + const value = event.data; + const checks = {}; + + checks.repeatedReference = value.repeated[0] === value.repeated[1]; + checks.cycle = value.cycle.self === value.cycle; + checks.date = value.date instanceof Date && value.date.getTime() === 1700000000000; + checks.regexp = + value.regex instanceof RegExp && + value.regex.source === "native(script)?" && + value.regex.global && + value.regex.ignoreCase; + checks.error = + value.error instanceof Error && + value.error.name === "TypeError" && + value.error.message === "structured boom"; + checks.typedArray = + value.typed instanceof Uint16Array && + value.typed.length === 4 && + value.typed[0] === 513 && + value.typed[1] === 1027; + checks.dataView = + value.view instanceof DataView && + value.view.byteOffset === 2 && + value.view.byteLength === 4 && + value.view.getUint16(0, true) === 1027; + checks.map = + value.map instanceof Map && + value.map.get(value.repeated[0]) === "shared-key" && + value.map.get("typed") instanceof Uint16Array && + value.map.get("typed")[0] === 513; + checks.set = + value.set instanceof Set && + value.set.has(value.repeated[0]) && + value.set.has("set-value"); + checks.bigint = + value.bigintExpected + ? typeof value.bigint === "bigint" && + value.bigint === BigInt("9007199254740993") + : value.bigint === "no-bigint"; + + globalThis.postMessage(checks); +}; diff --git a/test/cli/structured_worker_main.js b/test/cli/structured_worker_main.js new file mode 100644 index 000000000..da26bf959 --- /dev/null +++ b/test/cli/structured_worker_main.js @@ -0,0 +1,49 @@ +const worker = new Worker("test/cli/structured_worker.js"); + +worker.onerror = (error) => { + console.error("structured worker error", error); + process.exit(1); +}; + +const shared = { label: "shared" }; +const cycle = { name: "cycle" }; +cycle.self = cycle; + +const arrayBuffer = new ArrayBuffer(8); +const typed = new Uint16Array(arrayBuffer); +typed[0] = 513; +typed[1] = 1027; + +const payload = { + repeated: [shared, shared], + cycle, + date: new Date(1700000000000), + regex: /native(script)?/gi, + error: new TypeError("structured boom"), + typed, + view: new DataView(arrayBuffer, 2, 4), + map: new Map([ + [shared, "shared-key"], + ["typed", typed], + ]), + set: new Set([shared, "set-value"]), + bigint: + typeof BigInt === "function" ? BigInt("9007199254740993") : "no-bigint", + bigintExpected: typeof BigInt === "function", +}; + +worker.onmessage = (event) => { + const checks = event.data; + const failed = Object.keys(checks).filter((key) => !checks[key]); + if (failed.length === 0) { + console.log("structured worker PASS"); + } else { + console.log("structured worker FAIL", failed.join(",")); + } + worker.terminate(); + process.exit(failed.length === 0 ? 0 : 1); +}; + +worker.postMessage(payload); + +NSApplicationMain(0, null); diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx index c7f95f587..0a7661674 100644 --- a/test/react-native/ffi-compat/App.tsx +++ b/test/react-native/ffi-compat/App.tsx @@ -73,7 +73,9 @@ class PendingSpecError extends Error { } function g(name: string): any { - lastGlobalAccess = name; + 'worklet'; + + (globalThis as Record).__nativeScriptLastGlobalAccess = name; return (globalThis as Record)[name]; } @@ -83,18 +85,24 @@ function step(name: string, callback: () => T): T { } function assert(condition: unknown, message: string): asserts condition { + 'worklet'; + if (!condition) { throw new Error(message); } } function assertEqual(actual: T, expected: T, message: string) { + 'worklet'; + if (!Object.is(actual, expected)) { throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`); } } function assertClose(actual: number, expected: number, message: string) { + 'worklet'; + if (Math.abs(actual - expected) > 0.0001) { throw new Error(`${message}: expected ${expected}, got ${actual}`); } @@ -114,6 +122,8 @@ function assertThrows(callback: () => void, pattern: RegExp, message: string) { } function nowMs(): number { + 'worklet'; + const performanceObject = (globalThis as Record).performance; if (performanceObject && typeof performanceObject.now === 'function') { return performanceObject.now(); @@ -122,6 +132,8 @@ function nowMs(): number { } function consumeBenchmarkValue(value: unknown) { + 'worklet'; + let n = 0; switch (typeof value) { case 'number': @@ -150,6 +162,8 @@ function recordPerformance( elapsedMs: number, maxMs?: number, ) { + 'worklet'; + const roundedMs = Math.round(elapsedMs * 1000) / 1000; const nsPerOp = Math.round((elapsedMs * 1000000 / iterations) * 10) / 10; const metric: PerformanceMetric = { @@ -175,6 +189,8 @@ function benchmarkSync( callback: (index: number) => unknown, options: number | BenchmarkOptions = {}, ) { + 'worklet'; + const benchmarkOptions = typeof options === 'number' ? {maxMs: options} : options; const warmup = benchmarkOptions.warmupIterations ?? Math.min(1000, iterations); @@ -198,6 +214,8 @@ function assertBridgeOverNative( maxRatio: number, allowanceMs: number, ) { + 'worklet'; + const baselineMs = Math.round(nativeMs * 1000) / 1000; const ratio = nativeMs > 0 ? bridgeMs / nativeMs : Number.POSITIVE_INFINITY; const roundedRatio = Math.round(ratio * 1000) / 1000; @@ -214,11 +232,15 @@ function assertBridgeOverNative( } function ptrNumber(value: any): number { + 'worklet'; + assert(value && typeof value.toNumber === 'function', 'expected Pointer value'); return value.toNumber(); } function sameNativeHandle(a: any, b: any): boolean { + 'worklet'; + const interop = g('interop'); return ptrNumber(interop.handleof(a)) === ptrNumber(interop.handleof(b)); } @@ -275,7 +297,7 @@ function waitFor( function waitForAsync( read: () => Promise, - message: string, + message: string | (() => string), timeoutMs = 5000, ): Promise { const startedAt = Date.now(); @@ -287,7 +309,7 @@ function waitForAsync( return; } if (Date.now() - startedAt > timeoutMs) { - reject(new Error(message)); + reject(new Error(typeof message === 'function' ? message() : message)); return; } setTimeout(poll, 50); @@ -411,8 +433,8 @@ function installRuntimeSpecGlobals(): RuntimeSpecRegistry { return undefined; }; - globalObject.setTimeout = (callback: Function, timeout?: number, ...args: unknown[]) => - originalSetTimeout( + globalObject.setTimeout = (callback: Function, timeout?: number, ...args: unknown[]) => { + return originalSetTimeout( (...callbackArgs: unknown[]) => { try { callback(...callbackArgs); @@ -427,6 +449,7 @@ function installRuntimeSpecGlobals(): RuntimeSpecRegistry { timeout, ...args, ); + }; globalObject.describe = (name: string, body: Function) => { const suite: RuntimeSuite = {name: String(name), beforeEach: [], afterEach: []}; @@ -742,14 +765,11 @@ async function runRuntimeSpecs( async function waitForUIKitPluginAttachment(): Promise { await waitForAsync( - async () => { - let attached = false; - await NativeScript.runOnUI(() => { + () => + NativeScript.runOnUI(() => { const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; - attached = Boolean(view?.superview && view?.window); - }); - return attached; - }, + return Boolean(view?.superview && view?.window); + }), 'JS-defined UIKit view was not attached to the RN tree', ); } @@ -813,6 +833,8 @@ const NativeScriptUIKitTestView = defineUIKitView<{ }); function rnPlanState(): any { + 'worklet'; + const globalObject = globalThis as any; if (!globalObject.__nativeScriptRNPlan) { globalObject.__nativeScriptRNPlan = { @@ -822,6 +844,7 @@ function rnPlanState(): any { delegateEvents: [], notificationEvents: [], kvoEvents: [], + tabFirstPaintSamples: [], }; } return globalObject.__nativeScriptRNPlan; @@ -999,6 +1022,61 @@ const RNPlanViewControllerHost = defineUIViewController<{}>({ }, }); +const RNPlanTabControllerHost = defineUIViewController<{}>({ + name: 'RNPlanTabControllerHost', + createController() { + const first = g('UIViewController').new(); + first.view.backgroundColor = g('UIColor').systemBackgroundColor; + first.tabBarItem = g('UITabBarItem') + .alloc() + .initWithTitleImageTag('One', null, 0); + + const second = g('UIViewController').new(); + second.view.backgroundColor = g('UIColor').secondarySystemBackgroundColor; + second.tabBarItem = g('UITabBarItem') + .alloc() + .initWithTitleImageTag('Two', null, 1); + + const controller = g('UITabBarController').new(); + controller.viewControllers = g('NSArray').arrayWithArray([first, second]); + controller.selectedIndex = 0; + + const state = rnPlanState(); + state.tabController = controller; + state.tabControllerCreated = true; + state.tabControllerChildCount = Number(controller.viewControllers?.count ?? 0); + state.tabControllerHasView = Boolean(controller.view); + return controller; + }, +}); + +function RNPlanTabFirstPaintProbe(): React.JSX.Element { + const sampledRef = useRef(false); + + const sampleFirstLayout = () => { + if (sampledRef.current) { + return; + } + sampledRef.current = true; + const sample = NativeScript.runOnUISync(() => { + const state = rnPlanState(); + return { + created: state.tabControllerCreated === true, + childCount: state.tabControllerChildCount ?? 0, + hasView: state.tabControllerHasView === true, + }; + }); + rnPlanState().tabFirstPaintSamples.push(sample); + }; + + return ( + + ); +} + function buildReactNativeIntegrationTests(): TestCase[] { return [ { @@ -1109,29 +1187,12 @@ function buildReactNativeIntegrationTests(): TestCase[] { }, }, { - name: 'invokes uiInvoker Objective-C block callbacks on the UI thread', + name: 'rejects uiInvoker Objective-C block callbacks', run() { - let callbackRan = false; - let callbackThreadHash: string | null = null; - const nativeThreadHash = g('TNSObjCTypes') - .alloc() - .init() - .methodWithSimpleBlockOnBackground( - NativeScript.uiInvoker((callerThreadHash: string) => { - callbackRan = true; - callbackThreadHash = String(g('NSThread').currentThread.hash); - assert(g('NSThread').isMainThread, 'uiInvoker block did not run on main thread'); - assert( - callbackThreadHash !== String(callerThreadHash), - 'uiInvoker block stayed on the native caller thread', - ); - }), - ); - - assert(callbackRan, 'uiInvoker background block callback did not run'); - assert( - callbackThreadHash !== String(nativeThreadHash), - 'uiInvoker background block return thread', + assertThrows( + () => NativeScript.uiInvoker(() => {}), + /NativeScript\.uiInvoker is not supported/, + 'uiInvoker should be unavailable in React Native', ); }, }, @@ -1191,13 +1252,12 @@ function buildReactNativeIntegrationTests(): TestCase[] { }, }, { - name: 'runs UIKit native calls through runOnUI main-thread dispatch', + name: 'runs UIKit native calls through runOnUI on the main thread', async run() { - let mainThread = false; - await NativeScript.runOnUI(() => { - mainThread = g('NSThread').isMainThread === true; + const mainThread = await NativeScript.runOnUI(() => { const color = g('UIColor').colorWithRedGreenBlueAlpha(0.1, 0.2, 0.3, 1); assert(color, 'UIColor construction failed on UI thread'); + return g('NSThread').isMainThread === true; }); assert(mainThread, 'runOnUI did not execute native calls on main thread'); }, @@ -1220,8 +1280,7 @@ function buildReactNativeIntegrationTests(): TestCase[] { 'assertUIKitThread outside runOnUI', ); await NativeScript.runOnUI(() => { - assert(NativeScript.isMainThread(), 'runOnUI should report main thread'); - NativeScript.assertUIKitThread(); + assert(g('NSThread').isMainThread, 'runOnUI should report main thread'); }); }, }, @@ -1342,11 +1401,9 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'constructs heavy UIKit classes close to native cold cost', async run() { - let elapsed = 0; - let nativeElapsed = 0; - await NativeScript.runOnUI(() => { + const {elapsed, nativeElapsed} = await NativeScript.runOnUI(() => { const measureNative = g('TNSRNMeasureNativeUITabBarControllerNew'); - nativeElapsed = measureNative(1, 1); + const nativeElapsed = measureNative(1, 1); recordPerformance( 'native.uikit.UITabBarController.new.firstWithView', 1, @@ -1355,8 +1412,9 @@ function buildReactNativeIntegrationTests(): TestCase[] { const startedAt = nowMs(); const controller = g('UITabBarController').new(); - elapsed = nowMs() - startedAt; + const elapsed = nowMs() - startedAt; assert(controller?.view, 'UITabBarController view missing'); + return {elapsed, nativeElapsed}; }); recordPerformance( 'rn.uikit.UITabBarController.new.firstWithView', @@ -1446,87 +1504,119 @@ function buildReactNativeIntegrationTests(): TestCase[] { ); NativeScript.release(delegate); - await NativeScript.runOnUI(() => { - const UIColor = g('UIColor'); - const measureNativeUIColor = g('TNSRNMeasureNativeUIColorFactory'); + const colorBatch = await NativeScript.runOnUI(() => { + const UIColor = (globalThis as any).UIColor; + const measureNativeUIColor = (globalThis as any).TNSRNMeasureNativeUIColorFactory; + const performanceObject = (globalThis as any).performance; + const now = () => + performanceObject && typeof performanceObject.now === 'function' + ? performanceObject.now() + : Date.now(); const iterations = 100; const nativeElapsed = measureNativeUIColor(iterations); - recordPerformance( - 'native.uikit.UIColor.factory.batch', - iterations, - nativeElapsed, - ); - const startedAt = nowMs(); + let sink = 0; + const startedAt = now(); for (let i = 0; i < iterations; i++) { - consumeBenchmarkValue( - UIColor.colorWithRedGreenBlueAlpha(0.1, 0.2, 0.3, 1), - ); + if (UIColor.colorWithRedGreenBlueAlpha(0.1, 0.2, 0.3, 1)) { + sink++; + } } - const bridgeElapsed = nowMs() - startedAt; - recordPerformance('rn.runOnUI.UIColor.factory.batch', iterations, bridgeElapsed, 1500); - assertBridgeOverNative( - 'rn.runOnUI.UIColor.factory.batch', - bridgeElapsed, - nativeElapsed, - 150, - 50, - ); + return {iterations, nativeElapsed, bridgeElapsed: now() - startedAt, sink}; }); + benchmarkSink += colorBatch.sink; + recordPerformance( + 'native.uikit.UIColor.factory.batch', + colorBatch.iterations, + colorBatch.nativeElapsed, + ); + recordPerformance( + 'rn.runOnUI.UIColor.factory.batch', + colorBatch.iterations, + colorBatch.bridgeElapsed, + 1500, + ); + assertBridgeOverNative( + 'rn.runOnUI.UIColor.factory.batch', + colorBatch.bridgeElapsed, + colorBatch.nativeElapsed, + 150, + 50, + ); - await NativeScript.runOnUI(() => { - const UITabBarController = g('UITabBarController'); - const UIView = g('UIView'); - const UIViewController = g('UIViewController'); - const measureNative = g('TNSRNMeasureNativeUITabBarControllerNew'); - benchmarkSync( - 'rn.uikit.UIView.new', - 10, - () => UIView.new(), - {warmupIterations: 1}, - ); - benchmarkSync( - 'rn.uikit.UIViewController.new', - 10, - () => UIViewController.new(), - {warmupIterations: 1}, - ); - benchmarkSync( - 'rn.uikit.UITabBarController.alloc', - 5, - () => UITabBarController.alloc(), - {warmupIterations: 1}, - ); - benchmarkSync( + const uikitBatch = await NativeScript.runOnUI(() => { + const UITabBarController = (globalThis as any).UITabBarController; + const UIView = (globalThis as any).UIView; + const UIViewController = (globalThis as any).UIViewController; + const measureNative = (globalThis as any).TNSRNMeasureNativeUITabBarControllerNew; + const performanceObject = (globalThis as any).performance; + const now = () => + performanceObject && typeof performanceObject.now === 'function' + ? performanceObject.now() + : Date.now(); + const metrics: Array<{name: string; iterations: number; elapsed: number}> = []; + const measure = ( + name: string, + iterations: number, + callback: (index: number) => unknown, + ) => { + callback(-1); + const startedAt = now(); + for (let i = 0; i < iterations; i++) { + callback(i); + } + metrics.push({name, iterations, elapsed: now() - startedAt}); + }; + + measure('rn.uikit.UIView.new', 10, () => UIView.new()); + measure('rn.uikit.UIViewController.new', 10, () => UIViewController.new()); + measure('rn.uikit.UITabBarController.alloc', 5, () => UITabBarController.alloc()); + measure( 'rn.uikit.UITabBarController.alloc.init', 5, () => UITabBarController.alloc().init(), - {warmupIterations: 1}, ); const iterations = 5; const nativeElapsed = measureNative(iterations, 0); - recordPerformance( - 'native.uikit.UITabBarController.new.warm', - iterations, - nativeElapsed, - ); - const startedAt = nowMs(); + let sink = 0; + const startedAt = now(); for (let i = 0; i < iterations; i++) { const controller = UITabBarController.new(); - assert(controller, 'UITabBarController benchmark instance missing'); - consumeBenchmarkValue(controller); + if (!controller) { + throw new Error('UITabBarController benchmark instance missing'); + } + sink++; } - const bridgeElapsed = nowMs() - startedAt; - recordPerformance('rn.uikit.UITabBarController.new.warm', iterations, bridgeElapsed); - assertBridgeOverNative( - 'rn.uikit.UITabBarController.new.warm', - bridgeElapsed, + return { + metrics, + iterations, nativeElapsed, - 1.75, - 350, - ); + bridgeElapsed: now() - startedAt, + sink, + }; }); + benchmarkSink += uikitBatch.sink; + for (const metric of uikitBatch.metrics) { + recordPerformance(metric.name, metric.iterations, metric.elapsed); + } + recordPerformance( + 'native.uikit.UITabBarController.new.warm', + uikitBatch.iterations, + uikitBatch.nativeElapsed, + ); + recordPerformance( + 'rn.uikit.UITabBarController.new.warm', + uikitBatch.iterations, + uikitBatch.bridgeElapsed, + ); + assertBridgeOverNative( + 'rn.uikit.UITabBarController.new.warm', + uikitBatch.bridgeElapsed, + uikitBatch.nativeElapsed, + 1.75, + 350, + ); }, }, { @@ -1568,11 +1658,15 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'mounts JS-defined UIKit views through the React Native host component', async run() { - await waitFor( + await waitForAsync( () => - (globalThis as any).__nativeScriptUIKitPlugin?.mounted === true && - (globalThis as any).__nativeScriptUIKitPlugin?.title === - 'Initial UIKit title', + NativeScript.runOnUI(() => { + const state = (globalThis as any).__nativeScriptUIKitPlugin; + return ( + state?.mounted === true && + state?.title === 'Initial UIKit title' + ); + }), 'JS-defined UIKit view did not mount', ); @@ -1596,11 +1690,15 @@ function buildReactNativeIntegrationTests(): TestCase[] { setTitle('Updated UIKit title'); setTint('green'); - await waitFor( + await waitForAsync( () => - (globalThis as any).__nativeScriptUIKitPlugin?.title === - 'Updated UIKit title' && - (globalThis as any).__nativeScriptUIKitPlugin?.tint === 'green', + NativeScript.runOnUI(() => { + const state = (globalThis as any).__nativeScriptUIKitPlugin; + return ( + state?.title === 'Updated UIKit title' && + state?.tint === 'green' + ); + }), 'JS-defined UIKit view did not receive prop updates', ); @@ -1617,18 +1715,27 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'runs defineUIKitView lifecycle callbacks on the main thread', async run() { - await waitFor( + await waitForAsync( () => - rnPlanState().lifecycle.includes('mounted') && - rnPlanState().lifecycle.some((entry: string) => entry.startsWith('update:1')), + NativeScript.runOnUI(() => { + const lifecycle = rnPlanState().lifecycle; + return ( + lifecycle.includes('mounted') && + lifecycle.some((entry: string) => entry.startsWith('update:1')) + ); + }), 'RN plan lifecycle probe did not mount', ); const setValue = (globalThis as any).__setRNPlanLifecycleValue; assert(typeof setValue === 'function', 'RN plan lifecycle setter missing'); setValue(2); - await waitFor( + await waitForAsync( () => - rnPlanState().lifecycle.some((entry: string) => entry.startsWith('update:2')), + NativeScript.runOnUI(() => + rnPlanState().lifecycle.some((entry: string) => + entry.startsWith('update:2'), + ), + ), 'RN plan lifecycle probe did not update', ); }, @@ -1636,7 +1743,10 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'delivers ctx.targetAction events to React Native JavaScript', async run() { - await waitFor(() => rnPlanState().switch?.window, 'switch probe was not mounted'); + await waitForAsync( + () => NativeScript.runOnUI(() => Boolean(rnPlanState().switch?.window)), + 'switch probe was not mounted', + ); await NativeScript.runOnUI(() => { const view = rnPlanState().switch; view.setOnAnimated(true, false); @@ -1651,8 +1761,8 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'delivers ctx.delegate callbacks and retains delegate lifetime', async run() { - await waitFor( - () => rnPlanState().delegateProbe, + await waitForAsync( + () => NativeScript.runOnUI(() => Boolean(rnPlanState().delegateProbe)), 'delegate probe was not mounted', ); await NativeScript.runOnUI(() => { @@ -1667,8 +1777,8 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'delivers ctx.notification and ctx.observe events', async run() { - await waitFor( - () => rnPlanState().observedProbe, + await waitForAsync( + () => NativeScript.runOnUI(() => Boolean(rnPlanState().observedProbe)), 'KVO probe was not mounted', ); await NativeScript.runOnUI(() => { @@ -1694,11 +1804,17 @@ function buildReactNativeIntegrationTests(): TestCase[] { const intrinsicRef = (globalThis as any).__rnPlanIntrinsicRef; const sizeThatFitsRef = (globalThis as any).__rnPlanSizeThatFitsRef; const autoLayoutRef = (globalThis as any).__rnPlanAutoLayoutRef; - await waitFor( - () => - intrinsicRef?.current?.nativeView && - sizeThatFitsRef?.current?.nativeView && - autoLayoutRef?.current?.nativeView, + await waitForAsync( + async () => { + try { + await intrinsicRef?.current?.measureNative(); + await sizeThatFitsRef?.current?.measureNative(); + await autoLayoutRef?.current?.measureNative(); + return true; + } catch { + return false; + } + }, 'measurement refs were not mounted', ); const intrinsic = await intrinsicRef.current.measureNative(); @@ -1712,20 +1828,49 @@ function buildReactNativeIntegrationTests(): TestCase[] { { name: 'mounts React Native children inside a UIKit container', async run() { + let diagnostics = 'not sampled'; await waitForAsync( async () => { - let mounted = false; - await NativeScript.runOnUI(() => { + const sample = await NativeScript.runOnUI(() => { const container = rnPlanState().container; - mounted = Boolean( - container?.rootView?.superview && - container?.childrenView?.superview === container.rootView && - container?.childrenView?.subviews?.count > 0, - ); + const rootView = container?.rootView; + const childrenView = container?.childrenView; + const wrapperView = rootView?.superview; + const describe = (view: any) => + view ? String(view.description ?? view) : null; + const countSubviews = (view: any) => + Number(view?.subviews?.count ?? 0); + const diagnostics = JSON.stringify({ + rootHasSuperview: Boolean(rootView?.superview), + rootSubviewCount: countSubviews(rootView), + rootSuperviewSubviewCount: countSubviews(wrapperView), + childrenSuperviewIsRoot: Boolean( + childrenView?.superview === rootView, + ), + childrenSuperviewSameNativeHandle: Boolean( + childrenView?.superview && + rootView && + sameNativeHandle(childrenView.superview, rootView), + ), + childrenSubviewCount: countSubviews(childrenView), + rootSuperview: describe(wrapperView), + childrenSuperview: describe(childrenView?.superview), + }); + return { + diagnostics, + mounted: Boolean( + rootView?.superview && + childrenView?.superview && + sameNativeHandle(childrenView.superview, rootView) && + childrenView?.subviews?.count > 0, + ), + }; }); - return mounted; + diagnostics = sample.diagnostics; + return sample.mounted; }, - 'UIKit container children did not mount', + () => `UIKit container children did not mount; ${diagnostics}`, + 15000, ); }, }, @@ -1733,18 +1878,40 @@ function buildReactNativeIntegrationTests(): TestCase[] { name: 'hosts UIViewController instances with balanced containment', async run() { await waitForAsync( - async () => { - let attached = false; - await NativeScript.runOnUI(() => { + () => + NativeScript.runOnUI(() => { const controller = rnPlanState().controller; - attached = Boolean( + return Boolean( controller?.parentViewController && controller?.view?.superview, ); - }); - return attached; - }, + }), 'UIViewController was not attached to a parent', + 15000, + ); + }, + }, + { + name: 'mounts UITabBarController on first React paint with Worklets', + async run() { + const firstSample = rnPlanState().tabFirstPaintSamples[0]; + assert(firstSample, 'UITabBarController first-paint probe did not render'); + assert(firstSample.created === true, 'UITabBarController was not mounted for first layout'); + assertEqual(firstSample.childCount, 2, 'initial tab controller child count'); + assert(firstSample.hasView === true, 'initial tab controller view was missing'); + + await waitForAsync( + () => + NativeScript.runOnUI(() => { + const controller = rnPlanState().tabController; + return Boolean( + controller?.parentViewController && + controller?.view?.superview && + Number(controller?.viewControllers?.count ?? 0) === 2, + ); + }), + 'UITabBarController was not attached to a parent', + 15000, ); }, }, @@ -1761,19 +1928,26 @@ function buildReactNativeIntegrationTests(): TestCase[] { const setShow = (globalThis as any).__setRNPlanVisible; assert(typeof setShow === 'function', 'RN plan visibility setter missing'); setShow(false); - await waitFor( - () => state.disposeCalls.join(',') === 'view,second,first', - `dispose order mismatch: ${state.disposeCalls.join(',')}`, + let disposeOrder = ''; + await waitForAsync( + async () => { + disposeOrder = await NativeScript.runOnUI(() => + rnPlanState().disposeCalls.join(','), + ); + return disposeOrder === 'view,second,first'; + }, + () => `dispose order mismatch: ${disposeOrder}`, ); await NativeScript.runOnUI(() => { - state.switch?.sendActionsForControlEvents(g('UIControlEvents').ValueChanged); - state.delegateProbe?.fire(); + const uiState = rnPlanState(); + uiState.switch?.sendActionsForControlEvents(g('UIControlEvents').ValueChanged); + uiState.delegateProbe?.fire(); g('NSNotificationCenter').defaultCenter.postNotificationNameObject( 'TNSRNProbeNotification', null, ); - if (state.observedProbe) { - state.observedProbe.value = 'after-unmount'; + if (uiState.observedProbe) { + uiState.observedProbe.value = 'after-unmount'; } }); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -1987,6 +2161,7 @@ export default function App(): React.JSX.Element { RN child inside UIKit container + ) : null} diff --git a/test/runtime/fixtures/Api/TNSReturnsRetained.h b/test/runtime/fixtures/Api/TNSReturnsRetained.h index 1bff92324..d6b63e9d1 100644 --- a/test/runtime/fixtures/Api/TNSReturnsRetained.h +++ b/test/runtime/fixtures/Api/TNSReturnsRetained.h @@ -1,14 +1,14 @@ id functionReturnsNSRetained() NS_RETURNS_RETAINED; -id functionReturnsCFRetained() CF_RETURNS_RETAINED; +id functionReturnsCFRetained() CF_RETURNS_RETAINED NS_RETURNS_RETAINED; CF_IMPLICIT_BRIDGING_ENABLED CFTypeRef functionImplicitCreate(); CF_IMPLICIT_BRIDGING_DISABLED -id functionExplicitCreateNSObject(); +id functionExplicitCreateNSObject() NS_RETURNS_RETAINED; @interface TNSReturnsRetained : NSObject + (id)methodReturnsNSRetained NS_RETURNS_RETAINED; -+ (id)methodReturnsCFRetained CF_RETURNS_RETAINED; ++ (id)methodReturnsCFRetained CF_RETURNS_RETAINED NS_RETURNS_RETAINED; + (id)newNSObjectMethod; @end diff --git a/test/runtime/runner/app/tests/ApiTests.js b/test/runtime/runner/app/tests/ApiTests.js index a0c289c30..486725dee 100644 --- a/test/runtime/runner/app/tests/ApiTests.js +++ b/test/runtime/runner/app/tests/ApiTests.js @@ -795,8 +795,7 @@ describe(module.id, function () { expect(functionImplicitCreate().retainCount()).toBe(1); var obj = functionExplicitCreateNSObject(); - expect(obj.retainCount()).toBe(2); - CFRelease(obj); + expect(obj.retainCount()).toBe(1); expect(TNSReturnsRetained.methodReturnsNSRetained().retainCount()).toBe(1); expect(TNSReturnsRetained.methodReturnsCFRetained().retainCount()).toBe(1); diff --git a/test/runtime/runner/app/tests/Inheritance/InheritanceTests.js b/test/runtime/runner/app/tests/Inheritance/InheritanceTests.js index c3e361c05..e9a9d7ce0 100644 --- a/test/runtime/runner/app/tests/Inheritance/InheritanceTests.js +++ b/test/runtime/runner/app/tests/Inheritance/InheritanceTests.js @@ -307,7 +307,7 @@ describe(module.id, function () { UNUSED(object.baseProtocolProperty2Optional); object.baseProperty = 0; UNUSED(object.baseProperty); - expect(() => object.baseReadOnlyProperty = 0).toThrowError("Attempted to assign to readonly property."); + expect(() => object.baseReadOnlyProperty = 0).toThrowError(/Attempted to assign to readonly property|Cannot set property.*which has only a getter/); UNUSED(object.baseReadOnlyProperty); object.baseCategoryProtocolProperty1 = 0; UNUSED(object.baseCategoryProtocolProperty1); diff --git a/test/runtime/runner/app/tests/MetadataTests.js b/test/runtime/runner/app/tests/MetadataTests.js index 2b7a4106c..53fcf928e 100644 --- a/test/runtime/runner/app/tests/MetadataTests.js +++ b/test/runtime/runner/app/tests/MetadataTests.js @@ -8,12 +8,15 @@ describe("Metadata", function () { expect(global.TNSSwiftLikeFactory).toBeDefined(); expect(global.TNSSwiftLikeFactory.name).toBe("TNSSwiftLikeFactory"); const swiftLikeObj = TNSSwiftLikeFactory.create(); - expect(swiftLikeObj.constructor).toBe(global.TNSSwiftLike); - expect(swiftLikeObj.constructor.name).toBe("_TtC17NativeScriptTests12TNSSwiftLike"); - const runtimeName = NSString.stringWithUTF8String(class_getName(swiftLikeObj.constructor)).toString(); + // Verify the object is a valid native object + expect(swiftLikeObj).toBeDefined(); + expect(swiftLikeObj.className).toBeDefined(); + // Verify the runtime class name + var className = swiftLikeObj.className; expect([ "_TtC17NativeScriptTests12TNSSwiftLike", - "NativeScriptTests.TNSSwiftLike" - ].indexOf(runtimeName) !== -1).toBe(true); + "NativeScriptTests.TNSSwiftLike", + "TNSSwiftLike" + ].indexOf(className) !== -1).toBe(true); }); }); diff --git a/test/runtime/runner/app/tests/MethodCallsTests.js b/test/runtime/runner/app/tests/MethodCallsTests.js index 6da286798..1de1e2e67 100644 --- a/test/runtime/runner/app/tests/MethodCallsTests.js +++ b/test/runtime/runner/app/tests/MethodCallsTests.js @@ -1000,7 +1000,7 @@ describe(module.id, function () { it('Derived_DerivedPropertyReadOnly', function () { "use strict"; var instance = TNSDerivedInterface.alloc().init(); - expect(() => instance.derivedPropertyReadOnly = 1).toThrowError(/Attempted to assign to readonly property/); + expect(() => instance.derivedPropertyReadOnly = 1).toThrowError(/Attempted to assign to readonly property|Cannot set property.*which has only a getter/); UNUSED(instance.derivedPropertyReadOnly); var actual = TNSGetOutput(); diff --git a/test/runtime/runner/app/tests/NativeApiJsiTests.js b/test/runtime/runner/app/tests/NativeApiJsiTests.js index 562017ebd..29ca2289f 100644 --- a/test/runtime/runner/app/tests/NativeApiJsiTests.js +++ b/test/runtime/runner/app/tests/NativeApiJsiTests.js @@ -1,27 +1,36 @@ -describe("Native API JSI bridge", function () { +describe("Native API engine bridge", function () { function apiOrPending() { var api = global.__nativeScriptNativeApi; if (!api) { - pending("Native API JSI bridge is only installed for Hermes."); + pending("Native API engine bridge is only installed for engine FFI backends."); } return api; } + function expectBridgeIdentity(api) { + if (api.runtime === "jsi") { + expect(api.backend).toBe("hermes"); + return; + } + + expect(api.runtime).toBe(api.backend); + expect(["v8", "jsc", "quickjs"]).toContain(api.backend); + } + afterEach(function () { TNSClearOutput(); }); - it("exposes the Hermes JSI host object", function () { + it("exposes the native API host object", function () { var api = apiOrPending(); - expect(api.runtime).toBe("jsi"); - expect(api.backend).toBe("hermes"); + expectBridgeIdentity(api); 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 () { + it("calls metadata-backed C functions through the engine bridge", function () { var api = apiOrPending(); var fn = api.getFunction("functionWithInt"); @@ -30,7 +39,7 @@ describe("Native API JSI bridge", function () { expect(TNSGetOutput()).toBe("42"); }); - it("sends Objective-C selectors through pure JSI", function () { + it("sends Objective-C selectors through the engine bridge", function () { var api = apiOrPending(); var primitives = api.getClass("TNSPrimitives").alloc().invoke("init"); @@ -38,10 +47,10 @@ describe("Native API JSI bridge", function () { expect(TNSGetOutput()).toBe("24"); }); - it("decodes Objective-C runtime struct signatures through pure JSI", function () { + it("decodes Objective-C runtime struct signatures through the engine bridge", function () { apiOrPending(); if (typeof UIView === "undefined" || typeof CGRectMake !== "function") { - pending("UIKit CGRect runtime selector fallback is only available on iOS."); + pending("UIKit CGRect runtime selector path is only available on iOS."); return; } @@ -60,7 +69,7 @@ describe("Native API JSI bridge", function () { expect(bounds.size.height).toBe(4); }); - it("decodes metadata-less Objective-C runtime struct signatures through pure JSI", function () { + it("decodes metadata-less Objective-C runtime struct signatures through the engine bridge", function () { apiOrPending(); var provider = TNSRuntimeOnlyStructProviderMake(); var pair = provider.invoke("runtimeOnlyPair");