diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 627aeb28..b0a5aa54 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 edc6abe5..31b025ad 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 b9386f46..f2f77274 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 00000000..ea303961 --- /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 00000000..02b8e3eb --- /dev/null +++ b/NativeScript/ffi/hermes/NativeApiJsi.mm @@ -0,0 +1,521 @@ +#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 + +// --- 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 --- + +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) { + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount || dispatchingNativeCallToUI) { + 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). + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (prepared->gsdEngineCallable && gsdDispatchClass == Nil && + count == prepared->gsdEngineArgumentCount && + !(!receiverIsClass && prepared->isInitMethod) && + !dispatchingNativeCallToUI) { + 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/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 00000000..279dbd74 --- /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 00000000..35c04d97 --- /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 82df7614..00000000 --- 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 70a80bbf..00000000 --- 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 ca07222b..00000000 --- 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 0bf60c96..7b013f55 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 b1a6fa39..e9d92ac7 100644 --- a/NativeScript/ffi/jsc/NativeApiJSC.mm +++ b/NativeScript/ffi/jsc/NativeApiJSC.mm @@ -3,68 +3,1319 @@ #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) { +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)); +} + // 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 +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; +}; + +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); +} + +// --- 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 --- + +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) { + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount || dispatchingNativeCallToUI) { + 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. + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod && + !dispatchingNativeCallToUI) { + 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)); + bool retainedReturn = false; + 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 = 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 + } + if (dispatchingNativeCallToUI && !signature.returnType.returnOwned && + isObjectiveCObjectType(signature.returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiType returnType = signature.returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + 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); +} + } // 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/NativeApiJSCHostObjects.mm b/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm index ab2da20a..7af17b3e 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/NativeApiJSCRuntime.h b/NativeScript/ffi/jsc/NativeApiJSCRuntime.h index 305399f4..ecf8db1d 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 3396af69..da5aa2ce 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/NativeApiJSCValue.mm b/NativeScript/ffi/jsc/NativeApiJSCValue.mm index ca407156..f6691376 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 00000000..c53da941 --- /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 3f9d2671..2ee6eb74 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 63c17990..08ffcea2 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 dfe18c8c..bd1f24dd 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 e7bc296a..de88b84d 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 7ceafd49..d81a43f4 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 ab90659c..d3c2e277 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/SignatureDispatch.h b/NativeScript/ffi/napi/SignatureDispatch.h index c42a8237..7df628f8 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 73c31984..513a05c3 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 57988a26..ef3290ce 100644 --- a/NativeScript/ffi/quickjs/NativeApiQuickJS.mm +++ b/NativeScript/ffi/quickjs/NativeApiQuickJS.mm @@ -3,39 +3,41 @@ #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, +static JSValue NativeApiLazyGlobalGetter(JSContext* context, JSValueConst, int, JSValueConst*, int, JSValueConst* data) { JSValue global = JS_GetGlobalObject(context); JSValue resolver = JS_GetPropertyStr(context, global, "__nativeScriptResolveNativeApiLazyGlobal"); @@ -63,7 +65,7 @@ static JSValue NativeApiQuickJSLazyGlobalGetter(JSContext* context, JSValueConst return result; } -bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, +bool InstallNativeApiLazyGlobal(Runtime& runtime, std::shared_ptr, const std::string& name, const std::string& kind, bool force) { if (name.empty() || kind.empty()) { @@ -102,7 +104,7 @@ bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr= 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."); + } +} + // clang-format off -#include "jsi/NativeApiJsiHostObjects.h" +#include "../shared/bridge/HostObjects.mm" // clang-format on -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { +std::shared_ptr retainNativeApiRuntime(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" +#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 +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; +}; + +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); +} + +// --- 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 --- + +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) { + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (result == nullptr || receiver == nil || + !prepared.gsdEngineCallable || dispatchSuperClass != Nil || + count != prepared.gsdEngineArgumentCount || dispatchingNativeCallToUI) { + 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. + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod && + !dispatchingNativeCallToUI) { + 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)); + bool retainedReturn = false; + 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 = 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 + } + if (dispatchingNativeCallToUI && !signature.returnType.returnOwned && + isObjectiveCObjectType(signature.returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiType returnType = signature.returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + 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); +} + } // 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/NativeApiQuickJSHostObjects.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm index 1cd706a3..5917fd68 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/NativeApiQuickJSRuntime.h b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h index 438a7816..c20ad480 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 6a64b8e5..d38eb3ac 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/NativeApiQuickJSValue.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm index 94ae05e0..bb685201 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 00000000..a42fe6fa --- /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 60% rename from NativeScript/ffi/shared/direct/NativeApiDirect.h rename to NativeScript/ffi/shared/NativeApiBackendConfig.h index ef55cda4..222bcb36 100644 --- a/NativeScript/ffi/shared/direct/NativeApiDirect.h +++ b/NativeScript/ffi/shared/NativeApiBackendConfig.h @@ -1,30 +1,31 @@ -#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)> 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 00000000..c941006f --- /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 00000000..229347a7 --- /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 69% rename from NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h rename to NativeScript/ffi/shared/bridge/Callbacks.mm index 7df14ed0..f349356f 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, }; -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; + return NativeApiCallbackThreadPolicy::UI; } if (policy == "js") { - return NativeApiJsiCallbackThreadPolicy::JS; + return NativeApiCallbackThreadPolicy::JS; } } 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,34 +495,89 @@ 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; @@ -452,7 +588,7 @@ class NativeApiJsiCallback final std::this_thread::get_id() == bridge_->jsThreadId(); auto callOnNativeCallerThread = [&]() { - ScopedNativeCallerThreadJsiCallback callbackScope; + ScopedNativeCallerThreadEngineCallback callbackScope; if (nativeCallbackInvoker) { nativeCallbackInvoker(call); } else { @@ -495,20 +631,20 @@ class NativeApiJsiCallback final error = "Native callback was invoked off the JS thread without a JS scheduler."; }; - if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::UI) { + if (threadPolicy_ == NativeApiCallbackThreadPolicy::UI) { callOnUIThread(); if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } return; } - if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::JS) { + if (threadPolicy_ == NativeApiCallbackThreadPolicy::JS) { callOnJSThread(); if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } return; @@ -522,17 +658,52 @@ class NativeApiJsiCallback final bridge_->invokeCallbacksOnNativeCallerThread(); bool nativeCallerThreadCallback = nativeCallerThreadCallbacks && !currentThreadIsJs && - (block_ || bindThis_ || - (activeSynchronousNativeInvocation && !returnsVoid)); + activeSynchronousNativeInvocation && + (block_ || bindThis_ || !returnsVoid); bool direct = currentThreadIsJs || gExecutingDispatchedUINativeCall || gSynchronousNativeInvocationDepth > 0 || - nativeCallerThreadCallback || - (nativeCallerThreadCallbacks && !nativeCallbackInvoker && - activeSynchronousNativeInvocation); + nativeCallerThreadCallback; bool waitForNativeThreadCallback = currentThreadIsJs && nativeCallbackInvoker && - gActiveNativeThreadJsiCallbacks.load(std::memory_order_acquire) > 0; + 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; + } + if (auto scheduler = bridge_->scheduler()) { + scheduler->invokeOnJS(std::move(asyncCall)); + return true; + } + return false; + }; + + if (dispatchZeroArgVoidBlockAsync()) { + return; + } + if (direct && !waitForNativeThreadCallback) { if (nativeCallerThreadCallback) { callOnNativeCallerThread(); @@ -547,20 +718,20 @@ class NativeApiJsiCallback final } 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 +747,7 @@ class NativeApiJsiCallback final if (!error.empty()) { if (!recordNativeCallbackException(error)) { - throwNativeApiJsiCallbackException(error); + throwNativeApiCallbackException(error); } } } @@ -584,7 +755,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 +789,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 +804,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 +878,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 +891,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 +950,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 +992,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 +1001,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 +1047,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 +1110,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 +1157,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 +1201,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 +1218,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 +1233,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 +1255,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 +1317,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 +1353,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 +1369,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 +1402,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 +1441,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 +1450,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 +1472,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 +1488,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( @@ -1386,7 +1564,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 +1602,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 +1623,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 +1669,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 +1682,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 +1737,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 +1777,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 +1793,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 +1803,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 +1823,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 +1838,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 +1923,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 +1937,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 +1959,17 @@ NativeApiJsiType parseObjCEncodedJsiType( return finishPrimitive(encoding + 1); } -std::optional parseObjCMethodJsiSignature( - Method method, NativeApiJsiBridge* bridge = nullptr) { +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 +1977,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 +1987,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 +2000,7 @@ std::optional parseObjCMethodJsiSignature( return signature; } -bool prepareJsiMethodSignature(NativeApiJsiSignature* signature) { +bool prepareEngineMethodSignature(NativeApiSignature* signature) { if (signature == nullptr) { return false; } @@ -1832,7 +2010,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 +2027,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 +2093,97 @@ 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)); - auto callback = std::make_shared( + 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); - 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 bf4d3b2f..9c816f6e 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 06b82774..7a515216 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( @@ -76,21 +96,21 @@ class NativeApiHostObject final : public HostObject { size_t count) -> Value { auto scheduler = bridge->scheduler(); if (scheduler == nullptr) { - throw facebook::jsi::JSError( + throw JSError( runtime, - "NativeApiJsi was installed without a UI scheduler."); + "NativeApi 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( + throw JSError( runtime, "runOnUI expects a function callback."); } Object callbackObject = args[0].asObject(runtime); if (!callbackObject.isFunction(runtime)) { - throw facebook::jsi::JSError( + throw JSError( runtime, "runOnUI expects a function callback."); } callback = std::make_shared( @@ -98,6 +118,7 @@ class NativeApiHostObject final : public HostObject { } Runtime* runtimePtr = &runtime; + std::thread::id jsThreadId = bridge->jsThreadId(); auto promiseCtor = runtime.global().getPropertyAsFunction(runtime, "Promise"); return promiseCtor.callAsConstructor( @@ -105,7 +126,7 @@ class NativeApiHostObject final : public HostObject { Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "runOnUIPromise"), 2, - [scheduler, runtimePtr, callback]( + [scheduler, runtimePtr, callback, jsThreadId]( Runtime& promiseRuntime, const Value&, const Value* promiseArgs, size_t promiseArgc) -> Value { @@ -129,7 +150,7 @@ class NativeApiHostObject final : public HostObject { return Value::undefined(); } - scheduler->invokeOnJS([runtimePtr, callback, resolve, reject]() { + auto runCallback = [runtimePtr, callback, resolve, reject]() { try { { ScopedNativeApiUINativeCallDispatch uiDispatch; @@ -141,7 +162,12 @@ class NativeApiHostObject final : public HostObject { *runtimePtr, String::createFromUtf8(*runtimePtr, error.what())); } - }); + }; + if (std::this_thread::get_id() == jsThreadId) { + runCallback(); + } else { + scheduler->invokeOnJS(std::move(runCallback)); + } return Value::undefined(); })); @@ -162,7 +188,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 +242,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 +251,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 +356,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 +382,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 +416,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 +429,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 +457,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 +557,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", @@ -468,6 +616,7 @@ class NativeApiHostObject final : public HostObject { 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 +705,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 57% rename from NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h rename to NativeScript/ffi/shared/bridge/HostObjects.mm index a1dbf289..0c78d600 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)) { @@ -352,7 +407,7 @@ NativeApiSymbol nativeApiSymbolForRuntimeClass( 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 +433,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 +453,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 +466,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 +483,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 +499,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 +509,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 +519,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 +535,129 @@ 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; +}; + +std::vector runtimeMembersForClass(Class cls, + bool staticMembers) { + std::vector members; + if (cls == Nil) { + return members; + } + + 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; + } + + members.push_back(NativeApiRuntimeMember{ + .name = std::move(name), + .selectorName = std::move(selectorString), + .argumentCount = argumentCount, + }); + } + if (methods != nullptr) { + free(methods); + } + current = class_getSuperclass(current); + } + + return members; +} + +bool hasRuntimeMemberForName(Class cls, bool staticMembers, + const std::string& name) { + auto members = runtimeMembersForClass(cls, staticMembers); + for (const auto& member : members) { + if (member.name == name) { + return true; + } + } + return false; +} + +std::optional selectRuntimeSelectorForName( + Class cls, bool staticMembers, const std::string& name, size_t count) { + auto members = runtimeMembersForClass(cls, staticMembers); + for (const auto& member : members) { + if (member.name == name && member.argumentCount == count) { + return member.selectorName; + } + } + return std::nullopt; +} + +Array runtimeMembersArray(Runtime& runtime, Class cls, bool staticMembers) { + auto members = runtimeMembersForClass(cls, staticMembers); + Array result(runtime, members.size()); + for (size_t i = 0; i < members.size(); i++) { + const auto& member = 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 +665,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 +709,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 +736,7 @@ class NativeApiObjectHostObject final if (classWrapperValue.isObject()) { classWrapper.emplace(classWrapperValue.asObject(runtime)); } - bridge_->forgetRoundTripValue(receiver); + bridge_->forgetRoundTripValue(runtime, receiver); bridge_->forgetObjectExpandos(receiver); } @@ -570,18 +744,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 +1065,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 +1127,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 +1169,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 +1182,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 +1221,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 +1242,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 +1275,7 @@ class NativeApiObjectHostObject final selectorName, nullptr, args, count); } } - throw facebook::jsi::JSError( + throw JSError( runtime, "NSColor RGB initializer is not available."); }); } @@ -764,7 +1292,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 +1303,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 +1313,154 @@ 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 + 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 +1469,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 +1478,30 @@ 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]) { - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, object_, false, setterSelectorName, - nullptr, args, 1); - return; + // 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 +1522,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 +1540,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 +1578,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 +1603,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 +1626,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 +1638,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 +1651,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 +1697,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 +1727,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 +1736,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 +1751,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 +1792,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 +1833,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 +2001,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 +2034,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 +2091,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 +2103,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 +2212,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 +2241,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 +2278,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 +2319,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 +2335,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 78% rename from NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h rename to NativeScript/ffi/shared/bridge/Install.mm index 6e711d5b..622389c1 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h +++ b/NativeScript/ffi/shared/bridge/Install.mm @@ -1,10 +1,10 @@ -Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { - auto bridge = std::make_shared(config); +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; @@ -88,9 +88,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 +238,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 +298,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 +455,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 +523,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 +559,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 +570,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 +605,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 +701,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 +718,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 +746,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 +933,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 +949,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 +1027,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 +1079,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 +1136,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 +1187,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 +1223,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 +1361,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 +1487,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 +1506,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) { @@ -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 00000000..e522f3a7 --- /dev/null +++ b/NativeScript/ffi/shared/bridge/Invocation.mm @@ -0,0 +1,1832 @@ +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)); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + 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()); + } + if (dispatchingNativeCallToUI && + !signature.returnType.returnOwned && + isObjectiveCObjectType(signature.returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiType returnType = signature.returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + 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) { + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + + auto finalizeObjectReturn = [&](id object) -> Value { + NativeApiType effectiveReturnType = returnType; + if (dispatchingNativeCallToUI && !effectiveReturnType.returnOwned && + object != nil) { + [object retain]; + retainedReturn = true; + } + if (retainedReturn) { + effectiveReturnType.returnOwned = true; + } + return convertNativeReturnValue(runtime, bridge, effectiveReturnType, + &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) { + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + NativeApiScopedObjCObjectRetain receiverLifetime( + dispatchingNativeCallToUI ? receiver : nil); + + 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; + } + if (dispatchingNativeCallToUI && !effectiveReturnType.returnOwned && + object != nil) { + [object retain]; + retainedReturn = true; + } + if (retainedReturn) { + effectiveReturnType.returnOwned = true; + } + 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)); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + 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 = 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 + } + if (dispatchingNativeCallToUI && + !signature.returnType.returnOwned && + isObjectiveCObjectType(signature.returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + 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 (retainedReturn) { + returnType.returnOwned = true; + } + 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)); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + 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 = 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; + } + } + }); + + 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 (retainedReturn) { + returnType.returnOwned = true; + } + 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 59% rename from NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h rename to NativeScript/ffi/shared/bridge/ObjCBridge.mm index d543b1ec..0cc97001 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h +++ b/NativeScript/ffi/shared/bridge/ObjCBridge.mm @@ -1,23 +1,23 @@ 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); @@ -59,20 +59,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,7 +132,7 @@ void performNativeInvocation(Runtime& runtime, } }; - bool skipInvoker = gNativeCallerThreadJsiCallbackDepth > 0; + bool skipInvoker = gNativeCallerThreadEngineCallbackDepth > 0; if (shouldDispatchNativeCallToUI()) { dispatch_sync(dispatch_get_main_queue(), ^{ bool previous = gExecutingDispatchedUINativeCall; @@ -153,10 +153,39 @@ 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]; + } + }; + + if (shouldDispatchNativeCallToUI()) { + dispatch_sync(dispatch_get_main_queue(), ^{ + bool previous = gExecutingDispatchedUINativeCall; + gExecutingDispatchedUINativeCall = true; + run(); + gExecutingDispatchedUINativeCall = previous; + }); + } else { + run(); + } + + if (exceptionDescription != nil) { + std::string message = exceptionDescription.UTF8String ?: ""; + [exceptionDescription release]; + throw JSError(runtime, message); } } @@ -189,13 +218,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 NativeApiJsiFfiType { +struct NativeApiAggregateInfo; + +struct NativeApiFfiType { ffi_type type = {}; std::vector elements; - NativeApiJsiFfiType() { + NativeApiFfiType() { type.type = FFI_TYPE_STRUCT; type.size = 0; type.alignment = 0; @@ -208,7 +254,7 @@ struct NativeApiJsiFfiType { } }; -struct NativeApiJsiType { +struct NativeApiType { MDTypeKind kind = metagen::mdTypeVoid; ffi_type* ffiType = &ffi_type_void; bool supported = true; @@ -217,24 +263,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 +310,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 +350,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 +358,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 +368,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 +535,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 +546,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 +570,36 @@ 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), jsThreadCallbackInvoker_(config.jsThreadCallbackInvoker), + jsThreadAsyncCallbackInvoker_(config.jsThreadAsyncCallbackInvoker), invokeCallbacksOnNativeCallerThread_( config.invokeCallbacksOnNativeCallerThread) { selfDl_ = dlopen(nullptr, RTLD_NOW); buildSymbolIndexes(); } - ~NativeApiJsiBridge() { + ~NativeApiBridge() { if (selfDl_ != nullptr) { dlclose(selfDl_); } @@ -472,21 +614,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 +667,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 + } + + void rememberScopedRoundTripValue(Runtime& runtime, const void* native, + const Value& value, + bool stringLikeNative = false, + bool persistBeyondFrame = true) { + rememberScopedRoundTripValueWithValidationKey( + runtime, native, value, stringLikeNative, persistBeyondFrame, + nativeObjectClassKey(native)); } - Value findRoundTripValue(Runtime& runtime, const void* native) const { + 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 +996,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 +1004,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 +1056,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 +1197,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_; @@ -718,12 +1210,17 @@ class NativeApiJsiBridge { 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 +1228,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 +1373,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 +1416,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 +1585,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 +1730,7 @@ class NativeApiJsiBridge { if (!isUnion) { offset += sizeof(uint16_t); } - skipMetadataJsiType(metadata_.get(), &offset); + skipMetadataEngineType(metadata_.get(), &offset); } } @@ -1535,13 +2141,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 +2171,11 @@ 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)> jsThreadCallbackInvoker_; + std::function)> jsThreadAsyncCallbackInvoker_; bool invokeCallbacksOnNativeCallerThread_ = false; mutable std::unordered_map> membersByClassOffset_; @@ -1564,7 +2185,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 +2193,91 @@ 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 if (shouldDispatchNativeCallToUI()) { + performDirectObjCInvocation(runtime, [&]() { 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 +2285,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 +2328,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 +2416,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 +2442,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 83% rename from NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h rename to NativeScript/ffi/shared/bridge/TypeConv.mm index 9e37ec0a..74879089 100644 --- a/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h +++ b/NativeScript/ffi/shared/bridge/TypeConv.mm @@ -2,9 +2,9 @@ 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, +id objectFromEngineValue(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value, NativeApiArgumentFrame& frame, bool mutableString) { if (value.isNull() || value.isUndefined()) { return nil; @@ -32,7 +32,7 @@ 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)) { @@ -67,7 +67,7 @@ id objectFromJsiValue(Runtime& 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; } } @@ -80,16 +80,16 @@ id objectFromJsiValue(Runtime& runtime, .callWithThis(runtime, object, nullptr, 0); if (primitiveValue.isString() || primitiveValue.isBool() || primitiveValue.isNumber()) { - return objectFromJsiValue(runtime, bridge, primitiveValue, frame, + 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; } @@ -98,12 +98,12 @@ id objectFromJsiValue(Runtime& runtime, NSMutableArray* nativeArray = [NSMutableArray arrayWithCapacity:array.size(runtime)]; for (size_t i = 0; i < array.size(runtime); i++) { - id element = objectFromJsiValue(runtime, bridge, + 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,12 +114,12 @@ 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( + 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; } @@ -150,10 +150,10 @@ id objectFromJsiValue(Runtime& runtime, if (pair.size(runtime) < 2) { continue; } - id key = objectFromJsiValue(runtime, bridge, + id key = objectFromEngineValue(runtime, bridge, pair.getValueAtIndex(runtime, 0), frame, false); - id nativeValue = objectFromJsiValue(runtime, bridge, + id nativeValue = objectFromEngineValue(runtime, bridge, pair.getValueAtIndex(runtime, 1), frame, false); if (key != nil) { @@ -161,7 +161,7 @@ id objectFromJsiValue(Runtime& runtime, forKey:key]; } } - bridge->rememberRoundTripValue(runtime, nativeMap, value); + bridge->rememberScopedRoundTripValue(runtime, nativeMap, value, false, false); return nativeMap; } } @@ -179,17 +179,17 @@ id objectFromJsiValue(Runtime& runtime, continue; } id nativeValue = - objectFromJsiValue(runtime, bridge, propertyValue, frame, false); + 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]; } } - bridge->rememberRoundTripValue(runtime, dictionary, value); + bridge->rememberScopedRoundTripValue(runtime, dictionary, value, false, false); return dictionary; } - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, "Value cannot be converted to Objective-C object."); } @@ -215,6 +215,23 @@ std::string utf8StringFromNSString(NSString* string) { return result; } +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) { @@ -281,8 +298,9 @@ 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,7 +315,7 @@ 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)) { @@ -324,16 +342,24 @@ 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,7 +383,7 @@ 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; } @@ -391,7 +417,7 @@ void writeNumericArgument(Runtime& runtime, const Value& value, void* target, } if (!numericValue->isNumber() && !numericValue->isBool()) { - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, std::string("Expected numeric ") + typeName + " argument."); } @@ -400,18 +426,39 @@ void writeNumericArgument(Runtime& runtime, const Value& value, void* target, *static_cast(target) = static_cast(number); } -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); -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 +478,15 @@ 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 std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, - NativeApiJsiArgumentFrame& frame) { + NativeApiArgumentFrame& frame) { size_t size = nativeSizeForType(type); if (size == 0) { return; @@ -477,7 +524,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 +533,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 +547,16 @@ 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, + convertEngineArgument(runtime, bridge, field.type, fieldValue, fieldTarget, frame); } } void convertIndexedAggregateArgument(Runtime& runtime, - const std::shared_ptr& bridge, - const NativeApiJsiType& type, + const std::shared_ptr& bridge, + const NativeApiType& type, const Value& value, void* target, - NativeApiJsiArgumentFrame& frame) { + NativeApiArgumentFrame& frame) { size_t size = nativeSizeForType(type); std::memset(target, 0, size); if (value.isNull() || value.isUndefined()) { @@ -518,7 +565,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 +573,28 @@ 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, + 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 +605,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); } } @@ -576,21 +623,21 @@ void convertJsiFfiArgument(Runtime& runtime, *static_cast(target) = pointer; } -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) { - if (unsupportedJsiType(type)) { - throw facebook::jsi::JSError(runtime, + NativeApiArgumentFrame& frame) { + if (unsupportedEngineType(type)) { + throw JSError(runtime, "This native signature is not supported by " - "the pure JSI bridge yet."); + "the engine bridge yet."); } switch (type.kind) { case metagen::mdTypeBool: if (!value.isNumber() && !value.isBool()) { - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, "Expected boolean or numeric argument."); } *static_cast(target) = @@ -611,7 +658,7 @@ void convertJsiArgument(Runtime& runtime, if (value.isString()) { std::string text = value.asString(runtime).utf8(runtime); if (text.size() != 1) { - throw facebook::jsi::JSError( + throw JSError( runtime, "Expected a single-character string."); } *static_cast(target) = @@ -649,12 +696,20 @@ 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)) { + 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; @@ -668,16 +723,32 @@ void convertJsiArgument(Runtime& runtime, 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 +758,17 @@ void convertJsiArgument(Runtime& runtime, case metagen::mdTypeInstanceObject: case metagen::mdTypeNSStringObject: case metagen::mdTypeNSMutableStringObject: { - id object = objectFromJsiValue( + 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 +777,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()); @@ -734,17 +808,19 @@ void convertJsiArgument(Runtime& runtime, } 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: { @@ -761,16 +837,21 @@ void convertJsiArgument(Runtime& runtime, } } - auto threadPolicy = readJsiCallbackThreadPolicy(runtime, object); + auto threadPolicy = readEngineCallbackThreadPolicy(runtime, object); auto callback = - createJsiCallback(runtime, bridge, type, object.asFunction(runtime), + createEngineCallback(runtime, bridge, type, object.asFunction(runtime), type.kind == metagen::mdTypeBlock, threadPolicy); void* pointer = callback->functionPointer(); + uintptr_t roundTripValidationKey = + NativeApiBridge::callbackRoundTripValidationKey(type); 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", @@ -784,7 +865,8 @@ void convertJsiArgument(Runtime& runtime, break; } } - *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + *static_cast(target) = + pointerFromEngineValue(runtime, bridge, value, frame); break; } case metagen::mdTypeStruct: @@ -798,17 +880,17 @@ void convertJsiArgument(Runtime& runtime, 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, + 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 pure JSI bridge yet."); + "the engine bridge yet."); } switch (type.kind) { @@ -837,10 +919,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, + return unsignedInteger64ToEngineValue(runtime, *static_cast(value)); case metagen::mdTypeFloat: return static_cast(*static_cast(value)); @@ -851,11 +933,28 @@ Value convertNativeReturnValue(Runtime& runtime, if (string == nullptr) { return Value::null(); } - NativeApiJsiType cStringType = + 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,22 +983,28 @@ 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]]) { @@ -916,13 +1021,6 @@ Value convertNativeReturnValue(Runtime& runtime, } return result; } - Value roundTrip = bridge->findRoundTripValue(runtime, object); - if (!roundTrip.isUndefined()) { - if (type.returnOwned) { - [object release]; - } - return roundTrip; - } if (const NativeApiSymbol* classSymbol = bridge->findClassForRuntimePointer((void*)object)) { return makeNativeClassValue(runtime, bridge, *classSymbol); @@ -955,7 +1053,18 @@ Value convertNativeReturnValue(Runtime& runtime, } 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); } @@ -972,7 +1081,9 @@ 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; } @@ -1007,15 +1118,15 @@ 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, + 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); @@ -1037,7 +1148,7 @@ void NativeApiReferenceHostObject::ensureStorage( 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(); } } @@ -1058,6 +1169,9 @@ 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)) { @@ -1082,27 +1196,29 @@ Value NativeApiReferenceHostObject::get(Runtime& runtime, return Value::undefined(); } -void NativeApiReferenceHostObject::set(Runtime& runtime, +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(); + backingValue_.reset(); void* slot = static_cast(data_) + (slotIndex * referenceElementStride(type_)); - convertJsiArgument(runtime, bridge_, type_, value, slot, frame); + convertEngineArgument(runtime, bridge_, type_, value, slot, frame); + NATIVE_API_SET_RETURN(true); } Value NativeApiStructObjectHostObject::get(Runtime& runtime, @@ -1126,7 +1242,7 @@ Value NativeApiStructObjectHostObject::get(Runtime& runtime, runtime, PropNameID::forAscii(runtime, "toString"), 0, [info](Runtime& runtime, const Value&, const Value*, size_t) -> Value { return makeString(runtime, - std::string("[NativeApiJsi ") + + std::string("[NativeApi ") + (info != nullptr && info->isUnion ? "Union " : "Struct ") + (info != nullptr ? info->name : "") + "]"); }); @@ -1151,23 +1267,23 @@ Value NativeApiStructObjectHostObject::get(Runtime& runtime, return Value::undefined(); } -void NativeApiStructObjectHostObject::set(Runtime& runtime, +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, + NativeApiArgumentFrame frame(1); + convertEngineArgument(runtime, bridge_, field.type, value, static_cast(data_) + field.offset, frame); - return; + 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( @@ -1186,15 +1302,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,8 +1343,8 @@ std::optional primitiveInteropTypeFromCode(int32_t code) { } } -std::optional interopTypeFromValue( - Runtime& runtime, const std::shared_ptr& bridge, +std::optional interopTypeFromValue( + Runtime& runtime, const std::shared_ptr& bridge, const Value& value) { if (value.isNumber()) { return primitiveInteropTypeFromCode(static_cast(value.getNumber())); @@ -1256,7 +1372,7 @@ std::optional interopTypeFromValue( } } - Class descriptorClass = nativeClassFromJsiObject(runtime, object); + Class descriptorClass = nativeClassFromEngineObject(runtime, object); if (descriptorClass == Nil && stringPropertyOrEmpty(runtime, object, "kind") == "class") { descriptorClass = @@ -1268,7 +1384,7 @@ 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 @@ -1318,7 +1434,7 @@ std::optional interopTypeFromValue( bool isUnion = kindName == "union"; auto info = bridge->aggregateInfoFor( static_cast(offsetValue.getNumber()), isUnion); - NativeApiJsiType type; + NativeApiType type; type.kind = metagen::mdTypeStruct; type.aggregateInfo = info; type.aggregateOffset = info != nullptr ? info->offset : MD_SECTION_OFFSET_NULL; @@ -1333,7 +1449,7 @@ std::optional interopTypeFromValue( } Value makeAggregateConstructor(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, const NativeApiSymbol& symbol) { auto info = bridge->aggregateInfoFor(symbol); auto constructor = Function::createFromHostFunction( @@ -1341,12 +1457,12 @@ 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, + 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; @@ -1366,7 +1482,7 @@ Value makeAggregateConstructor(Runtime& runtime, std::vector storage(info->size, 0); if (count > 0) { - NativeApiJsiArgumentFrame frame(1); + NativeApiArgumentFrame frame(1); convertAggregateArgument(runtime, bridge, type, args[0], storage.data(), frame); } @@ -1393,7 +1509,7 @@ Value makeAggregateConstructor(Runtime& runtime, return false; } - NativeApiJsiType type; + NativeApiType type; type.kind = metagen::mdTypeStruct; type.aggregateInfo = info; type.aggregateOffset = info->offset; @@ -1404,10 +1520,10 @@ Value makeAggregateConstructor(Runtime& runtime, std::vector left(info->size, 0); std::vector right(info->size, 0); try { - NativeApiJsiArgumentFrame leftFrame(1); + NativeApiArgumentFrame leftFrame(1); convertAggregateArgument(runtime, bridge, type, args[0], left.data(), leftFrame); - NativeApiJsiArgumentFrame rightFrame(1); + NativeApiArgumentFrame rightFrame(1); convertAggregateArgument(runtime, bridge, type, args[1], right.data(), rightFrame); } catch (const std::exception&) { @@ -1427,7 +1543,7 @@ Value makeAggregateConstructor(Runtime& runtime, } size_t sizeofInteropType(Runtime& runtime, - const std::shared_ptr& bridge, + const std::shared_ptr& bridge, const Value& value) { if (auto type = interopTypeFromValue(runtime, bridge, value)) { return nativeSizeForType(*type); @@ -1438,7 +1554,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,23 +1567,32 @@ 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) { + 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)); + adopted, + std::move(backingValue))); if (!adopted && bridge != nullptr) { bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); } @@ -1513,7 +1638,7 @@ void installInteropHasInstance(Runtime& runtime, Function& constructor, } } -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 +1647,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,7 +1662,7 @@ 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()); @@ -1573,13 +1698,13 @@ 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) { + const std::shared_ptr& bridge) { Object interop(runtime); Object types(runtime); auto setType = [&](const char* name, MDTypeKind kind) { @@ -1711,7 +1836,7 @@ Object createInteropObject(Runtime& runtime, uintptr_t address = 0; if (!readAddress(args[0], &address)) { - throw facebook::jsi::JSError(runtime, + throw JSError(runtime, "Pointer expects a numeric address."); } pointer = reinterpret_cast(address); @@ -1732,13 +1857,13 @@ Object createInteropObject(Runtime& runtime, [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 1 || !args[0].isObject()) { - throw facebook::jsi::JSError( + throw JSError( runtime, "FunctionReference expects a function."); } Object object = args[0].asObject(runtime); if (!object.isFunction(runtime)) { - throw facebook::jsi::JSError( + throw JSError( runtime, "FunctionReference expects a function."); } @@ -1768,7 +1893,7 @@ Object createInteropObject(Runtime& runtime, runtime, PropNameID::forAscii(runtime, "Reference"), 2, [bridge](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { - NativeApiJsiType type = primitiveInteropType(metagen::mdTypePointer); + NativeApiType type = primitiveInteropType(metagen::mdTypePointer); bool firstArgumentIsType = false; if (count > 1) { firstArgumentIsType = true; @@ -1779,12 +1904,12 @@ Object createInteropObject(Runtime& runtime, Value kindValue = object.getProperty(runtime, "kind"); firstArgumentIsType = typeCodeValue.isNumber() || object.isFunction(runtime) || - nativeClassFromJsiObject(runtime, object) != Nil || + nativeClassFromEngineObject(runtime, object) != Nil || (kindValue.isString() && (kindValue.asString(runtime).utf8(runtime) == "class" || kindValue.asString(runtime).utf8(runtime) == "protocol")); } - std::optional requestedType = + std::optional requestedType = firstArgumentIsType ? interopTypeFromValue(runtime, bridge, args[0]) : std::nullopt; @@ -1797,6 +1922,7 @@ Object createInteropObject(Runtime& runtime, bool ownsData = false; size_t byteLength = 0; std::shared_ptr pendingValue; + std::shared_ptr backingValue; if (hasType) { bool usesExternalStorage = false; Value valueToStore = Value::undefined(); @@ -1851,9 +1977,14 @@ Object createInteropObject(Runtime& runtime, } ownsData = true; if (count > 1) { - NativeApiJsiArgumentFrame frame(1); - convertJsiArgument(runtime, bridge, type, valueToStore, data, + 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) { @@ -1866,7 +1997,8 @@ Object createInteropObject(Runtime& runtime, return Object::createFromHostObject( runtime, std::make_shared( bridge, type, data, ownsData, byteLength, - std::move(pendingValue))); + std::move(pendingValue), + std::move(backingValue))); }); Object referencePrototype(runtime); referencePrototype.setProperty(runtime, "constructor", referenceConstructor); @@ -1885,7 +2017,7 @@ Object createInteropObject(Runtime& runtime, [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])); })); @@ -1897,7 +2029,7 @@ Object createInteropObject(Runtime& runtime, [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); @@ -1932,11 +2064,11 @@ Object createInteropObject(Runtime& runtime, [](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); @@ -1964,13 +2096,20 @@ 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( + 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 = @@ -1981,12 +2120,14 @@ Object createInteropObject(Runtime& runtime, return createPointer(runtime, bridge, structObject->data()); } if (object.isHostObject(runtime)) { - return createPointer( - runtime, bridge, + id nativeObject = object.getHostObject(runtime) - ->object()); + ->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)) { @@ -2005,7 +2146,7 @@ Object createInteropObject(Runtime& runtime, Value kindValue = object.getProperty(runtime, "kind"); if (kindValue.isString() && kindValue.asString(runtime).utf8(runtime) == "functionReference") { - throw facebook::jsi::JSError( + throw JSError( runtime, "Cannot get handle of uninitialized FunctionReference."); } Value nativeName = object.getProperty(runtime, "nativeName"); @@ -2023,14 +2164,15 @@ Object createInteropObject(Runtime& runtime, runtime, "stringFromCString", Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "stringFromCString"), 2, - [](Runtime& runtime, const Value&, const Value* args, + [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(); } @@ -2050,7 +2192,7 @@ Object createInteropObject(Runtime& runtime, [](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,7 +2206,7 @@ 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( @@ -2077,9 +2219,9 @@ 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( + throw JSError( runtime, - "interop.addMethod requires the JSI class builder layer."); + "interop.addMethod requires the Engine class builder layer."); })); interop.setProperty( runtime, "addProtocol", @@ -2088,11 +2230,11 @@ Object createInteropObject(Runtime& runtime, [](Runtime& runtime, const Value&, const Value* args, size_t count) -> Value { if (count < 2) { - throw facebook::jsi::JSError( + 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 fe45f29f..00000000 --- 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 cf8c4054..ef035ede 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 154a1194..d5c597d6 100644 --- a/NativeScript/ffi/v8/NativeApiV8.mm +++ b/NativeScript/ffi/v8/NativeApiV8.mm @@ -3,47 +3,48 @@ #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, +struct NativeApiLazyGlobalData { + NativeApiLazyGlobalData(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)); + nameValue.Reset(isolate, engine::v8engine::makeV8String(isolate, name)); + kindValue.Reset(isolate, engine::v8engine::makeV8String(isolate, kind)); } - ~NativeApiV8LazyGlobalData() { + ~NativeApiLazyGlobalData() { nameValue.Reset(); kindValue.Reset(); } @@ -52,13 +53,13 @@ v8::Global kindValue; }; -std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { +std::shared_ptr retainNativeApiRuntime(Runtime& runtime) { return std::make_shared(runtime.state()); } -class NativeApiJsiRuntimeScope final { +class NativeApiRuntimeScope final { public: - explicit NativeApiJsiRuntimeScope(Runtime& runtime) + explicit NativeApiRuntimeScope(Runtime& runtime) : locker_(runtime.isolate()), isolateScope_(runtime.isolate()), handleScope_(runtime.isolate()), @@ -73,7 +74,7 @@ explicit NativeApiJsiRuntimeScope(Runtime& runtime) v8::Context::Scope contextScope_; }; -void NativeApiV8LazyGlobalGetter(v8::Local, +void NativeApiLazyGlobalGetter(v8::Local, const v8::PropertyCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); v8::HandleScope handleScope(isolate); @@ -82,7 +83,7 @@ void NativeApiV8LazyGlobalGetter(v8::Local, return; } - auto* data = static_cast(info.Data().As()->Value()); + auto* data = static_cast(info.Data().As()->Value()); if (data == nullptr) { return; } @@ -92,7 +93,7 @@ void NativeApiV8LazyGlobalGetter(v8::Local, v8::Local global = context->Global(); v8::Local resolverValue; if (!global - ->Get(context, facebook::jsi::v8direct::makeV8String( + ->Get(context, engine::v8engine::makeV8String( isolate, "__nativeScriptResolveNativeApiLazyGlobal")) .ToLocal(&resolverValue) || !resolverValue->IsFunction()) { @@ -114,7 +115,7 @@ void NativeApiV8LazyGlobalGetter(v8::Local, info.GetReturnValue().Set(result); } -bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, +bool InstallNativeApiLazyGlobal(Runtime& runtime, std::shared_ptr, const std::string& name, const std::string& kind, bool force) { if (name.empty() || kind.empty()) { @@ -125,16 +126,16 @@ bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr context = runtime.context(); v8::Local global = context->Global(); - v8::Local property = facebook::jsi::v8direct::makeV8String(isolate, name); + 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); + auto data = std::make_shared(isolate, name, kind); v8::Local external = v8::External::New(isolate, data.get()); bool installed = global - ->SetNativeDataProperty(context, property, NativeApiV8LazyGlobalGetter, + ->SetNativeDataProperty(context, property, NativeApiLazyGlobalGetter, nullptr, external, v8::DontEnum) .FromMaybe(false); if (installed) { @@ -143,21 +144,1291 @@ bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptrSetPrototypeV2(runtime.context(), prototype.local(runtime)) + .FromMaybe(false)) { + throw JSError(runtime, + engine::v8engine::currentExceptionMessage(runtime.isolate(), + tryCatch)); + } +} + // 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 + +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; +}; + +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; +} + +// --- 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 --- + +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. + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (prepared.gsdEngineCallable && dispatchSuperClass == Nil && + providedCount == prepared.gsdEngineArgumentCount && + !initializerClassWrapper && !isNSErrorOutMethod && + !dispatchingNativeCallToUI) { + 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)); + bool retainedReturn = false; + 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 = 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 + } + if (dispatchingNativeCallToUI && !signature.returnType.returnOwned && + isObjectiveCObjectType(signature.returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiType returnType = signature.returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + 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. + const bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + if (prepared->gsdEngineCallable && dispatchClass == Nil && + !prepared->isInitMethod && + count == prepared->gsdEngineArgumentCount && + !dispatchingNativeCallToUI) { + 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); +} + } // 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 +1437,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/NativeApiV8HostObjects.mm b/NativeScript/ffi/v8/NativeApiV8HostObjects.mm index 8a11b15c..6064601e 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/NativeApiV8Runtime.h b/NativeScript/ffi/v8/NativeApiV8Runtime.h index 81c95a08..0d43fed5 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 681a211b..10f8e3d8 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/NativeApiV8Value.mm b/NativeScript/ffi/v8/NativeApiV8Value.mm index d8b34428..dab1e027 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 00000000..ae2f9950 --- /dev/null +++ b/NativeScript/ffi/v8/SignatureDispatch.h @@ -0,0 +1,46 @@ +#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 NativeApiV8.mm (after the host +// object helpers the context relies on), so 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 NativeApiV8.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 ad23e9b0..22167460 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 cb6221ab..bee928a5 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/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index 89791d8d..c8418222 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 bc785fe9..67b3b520 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 7d5ed072..2ee6124c 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/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md index 6a2951bd..086b5470 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 1778a344..547728a0 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 7d8be2dc..7b8a6a35 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/metadata-generator/CMakeLists.txt b/metadata-generator/CMakeLists.txt index ada4ad98..bdb47764 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 diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index da50820e..0c18d579 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 820eb3fb..00000000 --- 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 00000000..5bc041aa --- /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 74feb009..00000000 --- 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 efcd6a40..2de2c8ef 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 00000000..adec48e9 --- /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 86d38637..c4233cff 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 873723d7..9af96eda 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 544cd6c6..00000000 --- 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 e4a7bc3b..89de4200 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 4966ca51..48e8809a 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 757d1668..ca8c0e44 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 198236be..50747030 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 d65543a7..6353d7f6 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,11 +33,11 @@ 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\"", diff --git a/packages/react-native/ios/NativeScriptUIViewManager.mm b/packages/react-native/ios/NativeScriptUIViewManager.mm index dab55ce2..7d8feac5 100644 --- a/packages/react-native/ios/NativeScriptUIViewManager.mm +++ b/packages/react-native/ios/NativeScriptUIViewManager.mm @@ -14,6 +14,8 @@ - (UIView*)view { } RCT_EXPORT_VIEW_PROPERTY(nativeViewHandle, NSString) +RCT_EXPORT_VIEW_PROPERTY(childrenViewHandle, NSString) +RCT_EXPORT_VIEW_PROPERTY(controllerHandle, NSString) RCT_EXPORT_VIEW_PROPERTY(debugName, NSString) @end diff --git a/packages/react-native/package.json b/packages/react-native/package.json index cd72e79e..013c884c 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" diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 31fca758..49a59632 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1210,9 +1210,9 @@ type UIKitAdapterDefinition = let targetActionClass: any; let observerClass: any; -const targetActionCallbacks = new WeakMap void>(); -const observerCallbacks = new WeakMap< - object, +const targetActionCallbacks = new Map void>(); +const observerCallbacks = new Map< + string, (keyPath: string, object: unknown, change: unknown) => void >(); @@ -1228,6 +1228,20 @@ function requireNSObject(): any { return nsObject; } +function nativeCallbackKey(value: unknown): string { + 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; @@ -1237,7 +1251,7 @@ function getTargetActionClass(): any { targetActionClass = NSObject.extend( { nativeScriptHandleAction(sender: unknown) { - const callback = targetActionCallbacks.get(this as object); + const callback = targetActionCallbacks.get(nativeCallbackKey(this)); if (typeof callback === 'function') { callback(sender); } @@ -1273,7 +1287,7 @@ function getObserverClass(): any { object: unknown, change: unknown, ) { - const callback = observerCallbacks.get(this as object); + const callback = observerCallbacks.get(nativeCallbackKey(this)); if (typeof callback === 'function') { callback(keyPath, object, change); } @@ -1331,7 +1345,8 @@ function createUIKitContext( return; } const target = getTargetActionClass().alloc().init(); - targetActionCallbacks.set(target as object, uiInvoker(() => { + const targetKey = nativeCallbackKey(target); + targetActionCallbacks.set(targetKey, uiInvoker(() => { if (!disposed) { callback(); } @@ -1347,7 +1362,7 @@ function createUIKitContext( if (typeof nativeControl.removeTargetActionForControlEvents === 'function') { nativeControl.removeTargetActionForControlEvents(target, selector, events); } - targetActionCallbacks.delete(target as object); + targetActionCallbacks.delete(targetKey); }); }, delegate(object, protocolRef, implementation) { @@ -1389,7 +1404,8 @@ function createUIKitContext( throw new Error('observe expects a KVO-compatible NSObject'); } const observer = getObserverClass().alloc().init(); - observerCallbacks.set(observer as object, ( + const observerKey = nativeCallbackKey(observer); + observerCallbacks.set(observerKey, ( observedKeyPath: string, _observedObject: unknown, change: unknown, @@ -1422,7 +1438,7 @@ function createUIKitContext( nativeObject.removeObserverForKeyPath(observer, keyPath); } } finally { - observerCallbacks.delete(observer as object); + observerCallbacks.delete(observerKey); } }); }, @@ -1814,10 +1830,13 @@ function defineUIKitHost( height: measuredSize.height, } : undefined; + const {children, ...nativePropsWithoutChildren} = + nativeProps as ViewProps & {children?: React.ReactNode}; return React.createElement(NativeScriptUIViewNativeComponent, { - ...nativeProps, + ...nativePropsWithoutChildren, collapsable: false, + children: childrenViewHandle ? children : undefined, childrenViewHandle, controllerHandle, debugName, diff --git a/scripts/build_metadata_generator.sh b/scripts/build_metadata_generator.sh index 061c2d3c..1d4b3c0e 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 7f775ac8..b1fef8d2 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 f64f23bf..bcf51a80 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,59 @@ 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.h "$PACKAGE_DIR/native-api/ffi/hermes/" +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 20e4f6e1..9fcd4060 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/react_native_app_utils.sh b/scripts/react_native_app_utils.sh index a2d0553e..0798489c 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 09435c34..9f65afe8 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 b69f6309..2a7141be 100755 --- a/scripts/test_react_native_ffi_compat.sh +++ b/scripts/test_react_native_ffi_compat.sh @@ -202,6 +202,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 bd1cdd0b..3da28c23 100755 --- a/scripts/test_react_native_turbomodule.sh +++ b/scripts/test_react_native_turbomodule.sh @@ -35,6 +35,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'; @@ -81,7 +82,9 @@ async function runSmoke(): Promise { 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/test/cli/memory/_plain_harness.js b/test/cli/memory/_plain_harness.js index 79562618..646bcb16 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 d3ede539..6a20fb2f 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 01ca4751..511e71b5 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 1ca391e1..0c22e134 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 a254dae6..a1ac988a 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 77fcaddf..5ded0a10 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 03f96341..f366da4d 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 a586e240..a920c944 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 00000000..2069e937 --- /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 990419de..e08f9153 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 9a944578..f29a6420 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 42912532..a1d2285e 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 b855f281..4dd33198 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 b8a26b51..8d1869e8 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 ed97ca01..412995f2 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/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx index c7f95f58..cb11a646 100644 --- a/test/react-native/ffi-compat/App.tsx +++ b/test/react-native/ffi-compat/App.tsx @@ -275,7 +275,7 @@ function waitFor( function waitForAsync( read: () => Promise, - message: string, + message: string | (() => string), timeoutMs = 5000, ): Promise { const startedAt = Date.now(); @@ -287,7 +287,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 +411,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 +427,7 @@ function installRuntimeSpecGlobals(): RuntimeSpecRegistry { timeout, ...args, ); + }; globalObject.describe = (name: string, body: Function) => { const suite: RuntimeSuite = {name: String(name), beforeEach: [], afterEach: []}; @@ -1712,20 +1713,46 @@ 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 container = rnPlanState().container; + 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); + 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), + }); mounted = Boolean( - container?.rootView?.superview && - container?.childrenView?.superview === container.rootView && - container?.childrenView?.subviews?.count > 0, + rootView?.superview && + childrenView?.superview && + sameNativeHandle(childrenView.superview, rootView) && + childrenView?.subviews?.count > 0, ); }); return mounted; }, - 'UIKit container children did not mount', + () => `UIKit container children did not mount; ${diagnostics}`, + 15000, ); }, }, @@ -1745,6 +1772,7 @@ function buildReactNativeIntegrationTests(): TestCase[] { return attached; }, 'UIViewController was not attached to a parent', + 15000, ); }, }, diff --git a/test/runtime/fixtures/Api/TNSReturnsRetained.h b/test/runtime/fixtures/Api/TNSReturnsRetained.h index 1bff9232..d6b63e9d 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 a0c289c3..486725de 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 c3e361c0..e9a9d7ce 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 2b7a4106..53fcf928 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 6da28679..1de1e2e6 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 562017eb..29ca2289 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");