From 83f6dbae44954a82c899b43f5e59b5b9c1f06112 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 2 Aug 2026 18:00:34 -0400 Subject: [PATCH 1/2] refactor(runtime): split engine backends and harden lifetimes --- .../ffi/objc/jsc/NativeApiJSCRuntime.h | 6 + NativeScript/ffi/objc/napi/ObjCBridge.h | 26 +- NativeScript/ffi/objc/napi/ObjCBridge.mm | 5 +- NativeScript/ffi/objc/napi/TypeConv.mm | 86 + .../objc/quickjs/NativeApiQuickJSRuntime.h | 9 + .../ffi/objc/shared/bridge/HostObjects.mm | 2476 +---------------- .../ffi/objc/shared/bridge/TypeConv.mm | 83 +- .../objc/shared/bridge/host_objects/Class.mm | 501 ++++ .../shared/bridge/host_objects/Interop.mm | 225 ++ .../objc/shared/bridge/host_objects/Object.mm | 1380 +++++++++ .../shared/bridge/host_objects/Protocol.mm | 318 +++ .../objc/shared/bridge/host_objects/Struct.mm | 47 + NativeScript/ffi/objc/v8/NativeApiV8Runtime.h | 5 + .../cli/benchmark/run_foundation_bench.js | 20 +- .../cli/memory/run_memory_semantics_tests.js | 4 +- .../apple/test/cli/memory/run_memory_tests.js | 12 +- .../test_circular_js_to_native_conversion.js | 103 + ...st_circular_native_wrapper_finalization.js | 115 + 18 files changed, 2899 insertions(+), 2522 deletions(-) create mode 100644 NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm create mode 100644 NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm create mode 100644 NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm create mode 100644 NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm create mode 100644 NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm create mode 100644 platforms/apple/test/cli/memory/test_circular_js_to_native_conversion.js create mode 100644 platforms/apple/test/cli/memory/test_circular_native_wrapper_finalization.js diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h b/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h index ecf8db1d3..bada6420b 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h +++ b/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h @@ -374,6 +374,12 @@ class Value { return value; } + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + return JSValueIsStrictEqual(runtime.context(), lhs.local(runtime), + rhs.local(runtime)); + } + bool isUndefined() const { return kind_ == jscengine::ValueStorage::Kind::Undefined || (isJSC() && JSValueIsUndefined(jscContext(), jscValue())); diff --git a/NativeScript/ffi/objc/napi/ObjCBridge.h b/NativeScript/ffi/objc/napi/ObjCBridge.h index 6e7eb19a0..7b6228334 100644 --- a/NativeScript/ffi/objc/napi/ObjCBridge.h +++ b/NativeScript/ffi/objc/napi/ObjCBridge.h @@ -50,7 +50,7 @@ struct HandleObjectRef { struct RecentObjectWrapperRef { uintptr_t objectKey = 0; uintptr_t objectClassKey = 0; - napi_ref ref = nullptr; + napi_ref borrowedRef = nullptr; }; void finalize_objc_object(napi_env /*env*/, void* data, void* hint); @@ -330,16 +330,9 @@ class ObjCBridgeState { handleObjectRefs.erase(it); bumpHandleObjectRefsGeneration(); } - inline void deleteRecentObjectWrapperRef(napi_env env, - RecentObjectWrapperRef& entry) { - if (env != nullptr && entry.ref != nullptr) { - napi_delete_reference(env, entry.ref); - } - entry = {}; - } inline void cacheRecentObjectWrapper(napi_env env, id object, - napi_value value) { - if (env == nullptr || object == nil || value == nullptr) { + napi_value value, napi_ref ref) { + if (env == nullptr || object == nil || value == nullptr || ref == nullptr) { return; } @@ -350,7 +343,7 @@ class ObjCBridgeState { continue; } - napi_value existing = get_ref_value(env, entry.ref); + napi_value existing = get_ref_value(env, entry.borrowedRef); if (existing != nullptr) { bool isSameValue = false; if (napi_strict_equals(env, existing, value, &isSameValue) == napi_ok && @@ -359,8 +352,7 @@ class ObjCBridgeState { } } - deleteRecentObjectWrapperRef(env, entry); - napi_create_reference(env, value, 1, &entry.ref); + entry.borrowedRef = ref; entry.objectKey = objectKey; entry.objectClassKey = objectClassKey; return; @@ -369,9 +361,8 @@ class ObjCBridgeState { RecentObjectWrapperRef entry{ .objectKey = objectKey, .objectClassKey = objectClassKey, - .ref = nullptr, + .borrowedRef = ref, }; - napi_create_reference(env, value, 1, &entry.ref); static constexpr size_t kRecentObjectWrapperLimit = 16; if (recentObjectWrappers.size() < kRecentObjectWrapperLimit) { @@ -381,7 +372,6 @@ class ObjCBridgeState { RecentObjectWrapperRef& replaced = recentObjectWrappers[nextRecentObjectWrapperSlot++ % kRecentObjectWrapperLimit]; - deleteRecentObjectWrapperRef(env, replaced); replaced = entry; } inline napi_value getRecentObjectWrapper(napi_env env, id object) { @@ -397,12 +387,11 @@ class ObjCBridgeState { continue; } - napi_value value = get_ref_value(env, it->ref); + napi_value value = get_ref_value(env, it->borrowedRef); if (value != nullptr) { return value; } - deleteRecentObjectWrapperRef(env, *it); it = recentObjectWrappers.erase(it); } @@ -417,7 +406,6 @@ class ObjCBridgeState { const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (auto it = recentObjectWrappers.begin(); it != recentObjectWrappers.end();) { if (it->objectKey == objectKey && it->objectClassKey == objectClassKey) { - deleteRecentObjectWrapperRef(env, *it); it = recentObjectWrappers.erase(it); } else { ++it; diff --git a/NativeScript/ffi/objc/napi/ObjCBridge.mm b/NativeScript/ffi/objc/napi/ObjCBridge.mm index d3b250123..9dd865fd2 100644 --- a/NativeScript/ffi/objc/napi/ObjCBridge.mm +++ b/NativeScript/ffi/objc/napi/ObjCBridge.mm @@ -877,9 +877,6 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat } handleObjectRefs.clear(); - for (auto& entry : recentObjectWrappers) { - deleteRef(entry.ref); - } recentObjectWrappers.clear(); std::unordered_set classAndProtocolConstructorRefs; @@ -1005,7 +1002,7 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat storeObjectRef(nativeObject, ref); cacheHandleObjectRef(env, nativeObject, ref); - cacheRecentObjectWrapper(env, nativeObject, result); + cacheRecentObjectWrapper(env, nativeObject, result, ref); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/objc/napi/TypeConv.mm b/NativeScript/ffi/objc/napi/TypeConv.mm index 6a94b32de..a27a38a33 100644 --- a/NativeScript/ffi/objc/napi/TypeConv.mm +++ b/NativeScript/ffi/objc/napi/TypeConv.mm @@ -131,6 +131,58 @@ static bool getJSBufferData(napi_env env, napi_value value, void** data, size_t* return false; } +struct ActiveObjectConversion { + napi_env env; + napi_value value; +}; + +thread_local std::vector activeObjectConversions; + +class ScopedObjectConversion { + public: + ScopedObjectConversion(napi_env env, napi_value value) { + for (const auto& active : activeObjectConversions) { + if (active.env != env) { + continue; + } + + bool isSameObject = false; + status_ = napi_strict_equals(env, active.value, value, &isSameObject); + if (status_ != napi_ok) { + return; + } + + if (isSameObject) { + napi_throw_error( + env, nullptr, + "Circular JavaScript object graphs cannot be converted to Objective-C collections."); + return; + } + } + + activeObjectConversions.push_back({env, value}); + entered_ = true; + } + + ~ScopedObjectConversion() { + if (entered_) { + activeObjectConversions.pop_back(); + } + } + + bool entered() const { return entered_; } + napi_status status() const { return status_; } + + private: + bool entered_ = false; + napi_status status_ = napi_ok; +}; + +static bool hasPendingException(napi_env env) { + bool pending = false; + return napi_is_exception_pending(env, &pending) == napi_ok && pending; +} + static uint16_t encodeFloat16(double value) { if (std::isnan(value)) { return 0x7e00; @@ -2438,6 +2490,16 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, } } + ScopedObjectConversion conversion(env, value); + if (!conversion.entered()) { + *res = nil; + if (conversion.status() != napi_ok) { + status = conversion.status(); + NAPI_THROW_LAST_ERROR + } + return; + } + bool isArray = false; napi_is_array(env, value, &isArray); if (isArray) { @@ -2450,6 +2512,10 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, napi_get_element(env, value, i, &elem); id obj = nil; toNative(env, elem, (void*)&obj, shouldFree, shouldFreeAny); + if (hasPendingException(env)) { + *res = nil; + return; + } [(*res) addObject:obj != nil ? obj : [NSNull null]]; } @@ -2484,6 +2550,10 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, napi_get_named_property(env, value, "valueOf", &valueOfMethod); napi_value primitiveValue; napi_call_function(env, value, valueOfMethod, 0, nullptr, &primitiveValue); + if (hasPendingException(env)) { + *res = nil; + return; + } toNative(env, primitiveValue, result, shouldFree, shouldFreeAny); return; } @@ -2538,7 +2608,15 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, id keyObject = nil; id valueObject = nil; toNative(env, keyValue, (void*)&keyObject, shouldFree, shouldFreeAny); + if (hasPendingException(env)) { + *res = nil; + return; + } toNative(env, elementValue, (void*)&valueObject, shouldFree, shouldFreeAny); + if (hasPendingException(env)) { + *res = nil; + return; + } if (keyObject != nil && valueObject != nil) { [(*res) setObject:valueObject forKey:keyObject]; @@ -2576,6 +2654,10 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, napi_get_element(env, value, i, &elem); id obj = nil; toNative(env, elem, (void*)&obj, shouldFree, shouldFreeAny); + if (hasPendingException(env)) { + *res = nil; + return; + } [(*res) addObject:obj != nil ? obj : [NSNull null]]; } cacheRoundTrip(*res); @@ -2638,6 +2720,10 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, continue; } toNative(env, elem, (void*)&obj, shouldFree, shouldFreeAny); + if (hasPendingException(env)) { + *res = nil; + return; + } if (obj != nil) { [(*res) setObject:obj forKey:nsKey]; } diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h index c20ad480a..8d1e0983d 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h +++ b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h @@ -313,6 +313,15 @@ class Value { value.kind_ = quickjsengine::ValueStorage::Kind::Null; return value; } + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + JSValue lhsValue = lhs.local(runtime); + JSValue rhsValue = rhs.local(runtime); + bool equal = JS_IsStrictEqual(runtime.context(), lhsValue, rhsValue); + JS_FreeValue(runtime.context(), lhsValue); + JS_FreeValue(runtime.context(), rhsValue); + return equal; + } bool isUndefined() const { if (kind_ == quickjsengine::ValueStorage::Kind::Undefined) { return true; diff --git a/NativeScript/ffi/objc/shared/bridge/HostObjects.mm b/NativeScript/ffi/objc/shared/bridge/HostObjects.mm index e0ea2b3c0..d3e4dbfca 100644 --- a/NativeScript/ffi/objc/shared/bridge/HostObjects.mm +++ b/NativeScript/ffi/objc/shared/bridge/HostObjects.mm @@ -43,2478 +43,12 @@ void setObject(id object) { }; -class NativeApiPointerHostObject final - : public HostObject, - public std::enable_shared_from_this { - public: - NativeApiPointerHostObject(std::shared_ptr bridge, - void* pointer, std::string kind = "pointer", - bool adopted = false, - std::shared_ptr backingValue = nullptr) - : bridge_(std::move(bridge)), - pointer_(pointer), - kind_(std::move(kind)), - adopted_(adopted), - backingValue_(std::move(backingValue)) {} - - ~NativeApiPointerHostObject() override { - if (adopted_ && pointer_ != nullptr) { - if (bridge_ != nullptr) { - bridge_->forgetPointerValue(pointer_); - } - free(pointer_); - pointer_ = nullptr; - } - } - - 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() { - if (bridge_ != nullptr) { - bridge_->forgetPointerValue(pointer_); - } - pointer_ = nullptr; - adopted_ = false; - backingValue_.reset(); - } - - Value get(Runtime& runtime, const PropNameID& name) override { - std::string property = name.utf8(runtime); - if (property == "kind") { - return makeString(runtime, kind_); - } - if (property == "address") { - return static_cast(reinterpret_cast(pointer_)); - } - if (property == "adopted") { - return adopted_; - } - if (property == "takeRetainedValue" || property == "takeUnretainedValue") { - bool retained = property == "takeRetainedValue"; - std::weak_ptr weakSelf = shared_from_this(); - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [weakSelf, retained](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { - auto self = weakSelf.lock(); - if (!self || self->pointer_ == nullptr || self->consumed_) { - throw 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); - }); - } - if (property == "add" || property == "subtract") { - void* pointer = pointer_; - bool add = property == "add"; - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 1, - [bridge, pointer, add](Runtime& runtime, const Value&, - const Value* args, size_t count) -> Value { - if (count < 1 || !args[0].isNumber()) { - throw JSError(runtime, "Pointer offset must be a number."); - } - intptr_t offset = static_cast(args[0].getNumber()); - intptr_t base = reinterpret_cast(pointer); - void* result = reinterpret_cast(add ? base + offset : base - offset); - return createPointer(runtime, bridge, result); - }); - } - if (property == "toNumber") { - void* pointer = pointer_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toNumber"), 0, - [pointer](Runtime&, const Value&, const Value*, size_t) -> Value { - return static_cast(reinterpret_cast(pointer)); - }); - } - if (property == "toBigInt") { - void* pointer = pointer_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toBigInt"), 0, - [pointer](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return BigInt::fromUint64( - runtime, - static_cast(reinterpret_cast(pointer))); - }); - } - if (property == "toHexString" || property == "toDecimalString") { - void* pointer = pointer_; - bool hex = property == "toHexString"; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [pointer, hex](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - if (hex) { - char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; - snprintf(text, sizeof(text), "0x%llx", - static_cast( - reinterpret_cast(pointer))); - return makeString(runtime, text); - } else { - char text[32] = {}; - snprintf(text, sizeof(text), "%lld", - static_cast(reinterpret_cast(pointer))); - return makeString(runtime, text); - } - }); - } - if (property == "toString") { - void* pointer = pointer_; - std::string kind = kind_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [pointer, kind](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { - char address[32] = {}; - snprintf(address, sizeof(address), "%p", pointer); - if (kind == "pointer") { - return makeString(runtime, - ""); - } - return makeString(runtime, "[NativeApi " + kind + " " + - std::string(address) + "]"); - }); - } - return Value::undefined(); - } - - std::vector getPropertyNames(Runtime& runtime) override { - std::vector names; - names.reserve(3); - addPropertyName(runtime, names, "kind"); - addPropertyName(runtime, names, "address"); - addPropertyName(runtime, names, "adopted"); - addPropertyName(runtime, names, "takeRetainedValue"); - addPropertyName(runtime, names, "takeUnretainedValue"); - addPropertyName(runtime, names, "add"); - addPropertyName(runtime, names, "subtract"); - addPropertyName(runtime, names, "toNumber"); - addPropertyName(runtime, names, "toBigInt"); - addPropertyName(runtime, names, "toHexString"); - addPropertyName(runtime, names, "toDecimalString"); - addPropertyName(runtime, names, "toString"); - return names; - } +#include "host_objects/Interop.mm" - private: - 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, - NativeApiType type, void* data, bool ownsData, - size_t byteLength = 0, - std::shared_ptr pendingValue = nullptr, - std::shared_ptr backingValue = nullptr) - : bridge_(std::move(bridge)), - type_(std::move(type)), - data_(data), - ownsData_(ownsData), - byteLength_(byteLength), - pendingValue_(std::move(pendingValue)), - backingValue_(std::move(backingValue)) {} - - ~NativeApiReferenceHostObject() override { - for (id object : retainedObjects_) { - [object release]; - } - if (ownsData_ && data_ != nullptr) { - free(data_); - data_ = nullptr; - } - } - - void* data() const { return data_; } - 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); - void retainObjectSlot(size_t index, id object); +#include "host_objects/Struct.mm" - Value get(Runtime& runtime, const PropNameID& name) 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"); - addPropertyName(runtime, names, "value"); - addPropertyName(runtime, names, "address"); - addPropertyName(runtime, names, "toString"); - return names; - } +#include "host_objects/Object.mm" - private: - std::shared_ptr bridge_; - NativeApiType type_; - void* data_ = nullptr; - bool ownsData_ = false; - size_t byteLength_ = 0; - std::shared_ptr pendingValue_; - std::shared_ptr backingValue_; - std::vector retainedObjects_; -}; +#include "host_objects/Class.mm" -class NativeApiStructObjectHostObject final : public HostObject { - public: - NativeApiStructObjectHostObject( - std::shared_ptr bridge, - std::shared_ptr info, - const void* data = nullptr, bool ownsData = true, - std::shared_ptr> storageOwner = nullptr, - std::shared_ptr backingValue = nullptr) - : bridge_(std::move(bridge)), - info_(std::move(info)), - ownedData_(std::move(storageOwner)), - backingValue_(std::move(backingValue)), - ownsData_(ownsData) { - size_t size = info_ != nullptr ? info_->size : 0; - if (ownedData_ != nullptr) { - data_ = const_cast(data); - ownsData_ = false; - } else if (ownsData_) { - ownedData_ = std::make_shared>(size, 0); - if (data != nullptr && size > 0) { - std::memcpy(ownedData_->data(), data, size); - } - data_ = ownedData_->empty() ? nullptr : ownedData_->data(); - } else { - data_ = const_cast(data); - } - } - - void* data() const { return data_; } - std::shared_ptr info() const { return info_; } - std::shared_ptr> storageOwner() const { - return ownedData_; - } - std::shared_ptr backingValue() const { return backingValue_; } - - Value get(Runtime& runtime, const PropNameID& name) override; - 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> ownedData_; - std::shared_ptr backingValue_; - void* data_ = nullptr; - bool ownsData_ = true; -}; - -class NativeApiFastEnumerationIteratorHostObject final : public HostObject { - public: - NativeApiFastEnumerationIteratorHostObject( - std::shared_ptr bridge, id collection) - : bridge_(std::move(bridge)), collection_(collection) { - [(id)collection_ retain]; - } - - ~NativeApiFastEnumerationIteratorHostObject() override { - [(id)collection_ release]; - collection_ = nil; - } - - Value get(Runtime& runtime, const PropNameID& name) override { - std::string property = name.utf8(runtime); - if (property == "next") { - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "next"), 0, - [this](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return next(runtime); - }); - } - return Value::undefined(); - } - - std::vector getPropertyNames(Runtime& runtime) override { - std::vector names; - addPropertyName(runtime, names, "next"); - return names; - } - - private: - Value next(Runtime& runtime) { - Object result(runtime); - if (done_ || collection_ == nil) { - result.setProperty(runtime, "done", true); - return result; - } - - if (stackIndex_ >= stackLength_) { - stackLength_ = [collection_ countByEnumeratingWithState:&state_ - objects:stack_ - count:16]; - stackIndex_ = 0; - if (stackLength_ == 0) { - done_ = true; - result.setProperty(runtime, "done", true); - return result; - } - } - - id value = state_.itemsPtr[stackIndex_++]; - 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_; - id collection_ = nil; - NSFastEnumerationState state_ = {}; - id __unsafe_unretained stack_[16] = {}; - NSUInteger stackLength_ = 0; - NSUInteger stackIndex_ = 0; - bool done_ = false; -}; - -NativeApiSymbol nativeApiSymbolForRuntimeClass( - const std::shared_ptr& bridge, Class cls) { - const char* name = cls != Nil ? class_getName(cls) : ""; - if (bridge != nullptr) { - if (const NativeApiSymbol* symbol = bridge->findClassForRuntimePointer(cls)) { - return *symbol; - } - if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { - return *symbol; - } - if (name != nullptr) { - if (const NativeApiSymbol* symbol = bridge->findClass(name)) { - return *symbol; - } - } - } - - return NativeApiSymbol{ - .kind = NativeApiSymbolKind::Class, - .offset = MD_SECTION_OFFSET_NULL, - .name = name != nullptr ? name : "", - .runtimeName = name != nullptr ? name : "", - }; -} - -std::optional runtimeWritablePropertySetter(id object, - const std::string& property) { - if (object == nil || property.empty()) { - return std::nullopt; - } - - Class current = object_getClass(object); - while (current != Nil) { - objc_property_t prop = class_getProperty(current, property.c_str()); - if (prop != nullptr) { - if (char* readonly = property_copyAttributeValue(prop, "R")) { - free(readonly); - return std::nullopt; - } - - std::string setter = setterSelectorForProperty(property); - if (char* customSetter = property_copyAttributeValue(prop, "S")) { - setter = customSetter; - free(customSetter); - } - - SEL selector = sel_getUid(setter.c_str()); - if ([object respondsToSelector:selector]) { - return setter; - } - } - - current = class_getSuperclass(current); - } - - std::string setter = setterSelectorForProperty(property); - SEL selector = sel_getUid(setter.c_str()); - if ([object respondsToSelector:selector]) { - return setter; - } - - return std::nullopt; -} - -std::optional runtimeReadablePropertyGetter(id object, - const std::string& property) { - if (object == nil || property.empty()) { - return std::nullopt; - } - - Class current = object_getClass(object); - while (current != Nil) { - objc_property_t prop = class_getProperty(current, property.c_str()); - if (prop != nullptr) { - std::string getter = property; - if (char* customGetter = property_copyAttributeValue(prop, "G")) { - getter = customGetter; - free(customGetter); - } - - if (auto selector = - respondingPropertyGetterSelector(object, property, getter)) { - return selector; - } - } - - current = class_getSuperclass(current); - } - - return respondingPropertyGetterSelector(object, property, property); -} - -class NativeApiSuperHostObject final : public HostObject { - public: - NativeApiSuperHostObject(std::shared_ptr bridge, - id receiver, Class dispatchClass) - : bridge_(std::move(bridge)), - receiver_(receiver), - dispatchClass_(dispatchClass) { - if (receiver_ != nil) { - [receiver_ retain]; - } - } - - ~NativeApiSuperHostObject() override { - if (receiver_ != nil) { - [receiver_ release]; - receiver_ = nil; - } - } - - Value get(Runtime& runtime, const PropNameID& name) override { - std::string property = name.utf8(runtime); - if (property == "kind") { - return makeString(runtime, "super"); - } - if (property == "toString") { - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return makeString(runtime, "[NativeApiSuper]"); - }); - } - if (receiver_ == nil || dispatchClass_ == Nil) { - return Value::undefined(); - } - - if (const NativeApiSymbol* symbol = - bridge_->findClassForRuntimeClass(dispatchClass_)) { - const auto& members = bridge_->membersForClass(*symbol); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, false)) { - SEL selector = sel_getUid(propertyMember->selectorName.c_str()); - if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { - return callObjCSelector(runtime, bridge_, receiver_, false, - propertyMember->selectorName, propertyMember, - nullptr, 0, dispatchClass_); - } - } - - if (hasMethodMember(members, property, false)) { - auto bridge = bridge_; - id receiver = receiver_; - Class dispatchClass = dispatchClass_; - std::string memberName = property; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [bridge, receiver, dispatchClass, memberName]( - Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - const NativeApiSymbol* symbol = - bridge->findClassForRuntimeClass(dispatchClass); - if (symbol == nullptr) { - throw JSError( - runtime, "Objective-C metadata is not available for super."); - } - const NativeApiMember* selected = selectMethodMember( - bridge->membersForClass(*symbol), memberName, false, count); - if (selected == nullptr) { - throw JSError( - runtime, "Objective-C super selector is not available: " + - memberName); - } - return callObjCSelector(runtime, bridge, receiver, false, - selected->selectorName, selected, args, - count, dispatchClass); - }); - } - } - - return Value::undefined(); - } - - NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { - std::string property = name.utf8(runtime); - if (receiver_ == nil || dispatchClass_ == Nil) { - throw JSError(runtime, "Cannot set property on nil super."); - } - - if (const NativeApiSymbol* symbol = - bridge_->findClassForRuntimeClass(dispatchClass_)) { - const auto& members = bridge_->membersForClass(*symbol); - if (const NativeApiMember* propertyMember = - selectWritablePropertyMember(members, property, false)) { - if (propertyMember->readonly || - propertyMember->setterSelectorName.empty()) { - throw JSError( - runtime, "Attempted to assign to readonly property."); - } - NativeApiMember setterMember = *propertyMember; - setterMember.selectorName = propertyMember->setterSelectorName; - setterMember.signatureOffset = propertyMember->setterSignatureOffset; - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, receiver_, false, - setterMember.selectorName, &setterMember, args, 1, - dispatchClass_); - NATIVE_API_SET_RETURN(true); - } - } - - std::string setterSelectorName = setterSelectorForProperty(property); - SEL selector = sel_getUid(setterSelectorName.c_str()); - if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, receiver_, false, setterSelectorName, - nullptr, args, 1, dispatchClass_); - NATIVE_API_SET_RETURN(true); - } - - throw JSError(runtime, - "No writable native super property: " + - property); - } - - std::vector getPropertyNames(Runtime& runtime) override { - std::vector names; - addPropertyName(runtime, names, "kind"); - addPropertyName(runtime, names, "toString"); - return names; - } - - private: - std::shared_ptr bridge_; - id receiver_ = nil; - Class dispatchClass_ = Nil; -}; - -struct NativeApiRuntimeMember { - std::string name; - std::string selectorName; - size_t argumentCount = 0; -}; - -using NativeApiRuntimeMembers = std::vector; - -struct NativeApiRuntimeMemberIndex { - NativeApiRuntimeMembers members; - std::unordered_set memberNames; - std::unordered_map> - selectorsByNameAndCount; -}; - -struct NativeApiRuntimeMembersCacheKey { - Class cls = Nil; - bool staticMembers = false; - - bool operator==(const NativeApiRuntimeMembersCacheKey& other) const { - return cls == other.cls && staticMembers == other.staticMembers; - } -}; - -struct NativeApiRuntimeMembersCacheKeyHash { - size_t operator()(const NativeApiRuntimeMembersCacheKey& key) const { - size_t classHash = std::hash{}(reinterpret_cast(key.cls)); - return classHash ^ (key.staticMembers ? 0x9e3779b97f4a7c15ULL : 0); - } -}; - -std::mutex& runtimeMembersCacheMutex() { - static std::mutex mutex; - return mutex; -} - -std::unordered_map, - NativeApiRuntimeMembersCacheKeyHash>& -runtimeMembersCache() { - static std::unordered_map, - NativeApiRuntimeMembersCacheKeyHash> - cache; - return cache; -} - -std::shared_ptr emptyRuntimeMembers() { - static auto empty = std::make_shared(); - return empty; -} - -NativeApiRuntimeMemberIndex buildRuntimeMembersForClass(Class cls, - bool staticMembers) { - NativeApiRuntimeMemberIndex index; - if (cls == Nil) { - return index; - } - - std::unordered_set seen; - Class current = staticMembers ? object_getClass(cls) : cls; - while (current != Nil) { - unsigned int methodCount = 0; - Method* methods = class_copyMethodList(current, &methodCount); - for (unsigned int i = 0; i < methodCount; i++) { - SEL selector = method_getName(methods[i]); - const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; - if (selectorName == nullptr || selectorName[0] == '\0') { - continue; - } - - std::string selectorString(selectorName); - std::string name = jsifySelector(selectorString.c_str()); - if (name.empty()) { - continue; - } - - size_t argumentCount = selectorArgumentCount(selectorString); - std::string key = name + "\x1f" + std::to_string(argumentCount); - if (!seen.insert(key).second) { - continue; - } - - index.memberNames.insert(name); - index.selectorsByNameAndCount[name].emplace(argumentCount, selectorString); - index.members.push_back(NativeApiRuntimeMember{ - .name = std::move(name), - .selectorName = std::move(selectorString), - .argumentCount = argumentCount, - }); - } - if (methods != nullptr) { - free(methods); - } - current = class_getSuperclass(current); - } - - return index; -} - -std::shared_ptr runtimeMembersForClass( - Class cls, bool staticMembers) { - if (cls == Nil) { - return emptyRuntimeMembers(); - } - - NativeApiRuntimeMembersCacheKey key{.cls = cls, - .staticMembers = staticMembers}; - - { - std::lock_guard lock(runtimeMembersCacheMutex()); - auto& cache = runtimeMembersCache(); - auto cached = cache.find(key); - if (cached != cache.end()) { - return cached->second; - } - } - - auto members = - std::make_shared( - buildRuntimeMembersForClass(cls, staticMembers)); - - { - std::lock_guard lock(runtimeMembersCacheMutex()); - auto& cache = runtimeMembersCache(); - auto [cached, inserted] = cache.emplace(key, members); - return inserted ? members : cached->second; - } -} - -bool hasRuntimeMemberForName(Class cls, bool staticMembers, - const std::string& name) { - auto index = runtimeMembersForClass(cls, staticMembers); - return index->memberNames.find(name) != index->memberNames.end(); -} - -std::optional selectRuntimeSelectorForName( - Class cls, bool staticMembers, const std::string& name, size_t count) { - auto index = runtimeMembersForClass(cls, staticMembers); - auto selectorsForName = index->selectorsByNameAndCount.find(name); - if (selectorsForName == index->selectorsByNameAndCount.end()) { - return std::nullopt; - } - auto selector = selectorsForName->second.find(count); - if (selector == selectorsForName->second.end()) { - return std::nullopt; - } - return selector->second; -} - -Array runtimeMembersArray(Runtime& runtime, Class cls, bool staticMembers) { - auto index = runtimeMembersForClass(cls, staticMembers); - Array result(runtime, index->members.size()); - for (size_t i = 0; i < index->members.size(); i++) { - const auto& member = index->members[i]; - Object descriptor(runtime); - descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); - descriptor.setProperty(runtime, "selectorName", - makeString(runtime, member.selectorName)); - descriptor.setProperty(runtime, "argumentCount", - static_cast(member.argumentCount)); - descriptor.setProperty(runtime, "property", false); - descriptor.setProperty(runtime, "readonly", false); - descriptor.setProperty(runtime, "setterSelectorName", makeString(runtime, "")); - result.setValueAtIndex(runtime, i, descriptor); - } - return result; -} - -class NativeApiObjectHostObject final - : public HostObject, - public std::enable_shared_from_this { - public: - NativeApiObjectHostObject(std::shared_ptr bridge, - id object, bool ownsObject) - : bridge_(std::move(bridge)), - object_(object), - ownsObject_(ownsObject), - lifetimeState_(std::make_shared(object)) { - if (bridge_ != nullptr && object_ != nil) { - bridge_->retainObjectExpandoOwner(object_); - } - if (object_ != nil && !ownsObject_) { - [object_ retain]; - ownsObject_ = true; - wrapperRetainedObject_ = true; - } - } - - ~NativeApiObjectHostObject() override { - if (bridge_ != nullptr && object_ != nil) { - bridge_->forgetRoundTripValue(object_); - bridge_->releaseObjectExpandoOwner( - object_, class_conformsToProtocol(object_getClass(object_), - @protocol(NativeApiClassBuilderProtocol))); - } - if (lifetimeState_ != nullptr) { - lifetimeState_->clear(); - } - if (ownsObject_ && object_ != nil) { - [object_ release]; - object_ = nil; - } - } - - 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, bool preserveExpandos = false) { - if (object_ == expected) { - if (bridge_ != nullptr && expected != nil) { - bridge_->forgetRoundTripValue(expected); - bridge_->releaseObjectExpandoOwner(expected, preserveExpandos); - } - ownsObject_ = false; - wrapperRetainedObject_ = false; - object_ = nil; - if (lifetimeState_ != nullptr) { - lifetimeState_->clear(); - } - } - } - - static bool isInitializerSelector(const std::string& selectorName) { - return selectorName.rfind("init", 0) == 0; - } - - static id nativeObjectFromValue(Runtime& runtime, const Value& value) { - if (!value.isObject()) { - return nil; - } - Object object = value.asObject(runtime); - if (!object.isHostObject(runtime)) { - return nil; - } - return object.getHostObject(runtime)->object(); - } - - 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 JSError(runtime, - "Cannot send Objective-C selector to nil."); - } - - const bool initializer = isInitializerSelector(selectorName); - std::optional classWrapper; - if (initializer) { - Value classWrapperValue = bridge_->findObjectExpando( - runtime, receiver, "__nativeApiClassWrapper"); - if (classWrapperValue.isObject()) { - classWrapper.emplace(classWrapperValue.asObject(runtime)); - } - bridge_->forgetRoundTripValue(runtime, receiver); - } - - Value result = - callObjCSelector(runtime, bridge_, receiver, false, selectorName, member, - args, count, dispatchSuperClass); - if (initializer) { - id resultObject = nativeObjectFromValue(runtime, result); - disownObject(receiver, resultObject == 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 (bridge_ != nullptr) { - bridge_->retainObjectExpandoOwner(object_); - } - 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); - } - - Value result = callPreparedObjCSelector( - runtime, bridge_, receiver, false, prepared, args, count, - dispatchSuperClass); - if (initializer) { - id resultObject = nativeObjectFromValue(runtime, result); - disownObject(receiver, resultObject == 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 (bridge_ != nullptr) { - bridge_->retainObjectExpandoOwner(object_); - } - 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 classPrototypeForObject(Runtime& runtime) { - if (object_ == nil) { - 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()) { - Object classWrapper = classWrapperValue.asObject(runtime); - Value prototypeValue = classWrapper.getProperty(runtime, "prototype"); - if (prototypeValue.isObject()) { - return prototypeValue; - } - } - return bridge_->findClassPrototype(runtime, object_getClass(object_)); - } - - Value engineThisValueForObject(Runtime& runtime) { - Value thisValue = bridge_->findRoundTripValue(runtime, object_, - nullptr, true); - if (thisValue.isObject()) { - return thisValue; - } - return makeNativeObjectValue(runtime, bridge_, object_, false); - } - - Value prototypeFunctionForProperty(Runtime& runtime, - const std::string& property) { - if (property.empty()) { - return Value::undefined(); - } - - Value prototypeValue = classPrototypeForObject(runtime); - 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 prototypeValue = classPrototypeForObject(runtime); - 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 = engineThisValueForObject(runtime); - 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 prototypeValue = classPrototypeForObject(runtime); - 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 = engineThisValueForObject(runtime); - 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"); - } - if (property == "className") { - return makeString(runtime, object_ != nil ? object_getClassName(object_) : ""); - } - if (property == "nativeAddress") { - char address[32] = {}; - snprintf(address, sizeof(address), "%p", object_); - return makeString(runtime, address); - } - if (property == "class") { - auto bridge = bridge_; - id object = object_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "class"), 0, - [bridge, object](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { - if (object == nil) { - return Value::undefined(); - } - Value classWrapper = bridge->findObjectExpando( - runtime, object, "__nativeApiClassWrapper"); - if (classWrapper.isObject()) { - return classWrapper; - } - NativeApiSymbol symbol = - nativeApiSymbolForRuntimeClass(bridge, object_getClass(object)); - return makeNativeClassValue(runtime, bridge, std::move(symbol)); - }); - } - if (property == "constructor") { - if (object_ == nil) { - return Value::undefined(); - } - // 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_, 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") { - if (object_ == nil) { - return Value::undefined(); - } - Class superclass = class_getSuperclass(object_getClass(object_)); - 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)); - } - if (property == "super") { - Class dispatchClass = - object_ != nil ? class_getSuperclass(object_getClass(object_)) : Nil; - return Object::createFromHostObject( - runtime, - std::make_shared(bridge_, object_, - dispatchClass)); - } - if (property == "invoke" || property == "send") { - auto bridge = bridge_; - id object = object_; - std::weak_ptr weakSelf = shared_from_this(); - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 1, - [bridge, object, weakSelf](Runtime& runtime, const Value&, - const Value* args, - size_t count) -> Value { - std::string selectorName = - readStringArg(runtime, args, count, 0, "selector"); - if (auto self = weakSelf.lock()) { - return self->callObjectSelector(runtime, selectorName, nullptr, - args + 1, count - 1); - } - return callObjCSelector(runtime, bridge, object, false, selectorName, - nullptr, args + 1, count - 1); - }); - } - if (property == "takeRetainedValue" || property == "takeUnretainedValue") { - bool retained = property == "takeRetainedValue"; - std::weak_ptr weakSelf = shared_from_this(); - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 0, - [weakSelf, retained](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { - auto self = weakSelf.lock(); - if (!self || self->object_ == nil || self->consumed_) { - throw 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(runtime, object); - self->bridge_->releaseObjectExpandoOwner(object); - } - self->object_ = nil; - self->ownsObject_ = false; - self->wrapperRetainedObject_ = false; - if (self->lifetimeState_ != nullptr) { - self->lifetimeState_->clear(); - } - self->consumed_ = true; - 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") { - id object = object_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [object](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return NativeApiObjectHostObject::descriptionString(runtime, object); - }); - } - if (property == "description") { - return descriptionString(runtime, object_); - } - if (property == "URL" && object_ != nil && - [object_ respondsToSelector:@selector(URL)]) { - return callObjectSelector(runtime, "URL", nullptr, nullptr, 0); - } - if (property == "Symbol.iterator" || - property == "Symbol(Symbol.iterator)" || - property == "@@iterator") { - auto bridge = bridge_; - id object = object_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "Symbol.iterator"), 0, - [bridge, object](Runtime& runtime, const Value&, const Value*, - size_t) -> Value { - if (object == nil || - ![object conformsToProtocol:@protocol(NSFastEnumeration)]) { - throw JSError( - runtime, "Object does not conform to NSFastEnumeration."); - } - return Object::createFromHostObject( - runtime, - std::make_shared( - bridge, static_cast>(object))); - }); - } - -#if TARGET_OS_OSX - if (property == "initWithRedGreenBlueAlpha") { - Class nsColorClass = NSClassFromString(@"NSColor"); - if (object_ != nil && nsColorClass != Nil && - [object_ isKindOfClass:nsColorClass]) { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 4, - [bridge, nsColorClass](Runtime& runtime, const Value&, - const Value* args, size_t count) -> Value { - const char* selectors[] = { - "colorWithSRGBRed:green:blue:alpha:", - "colorWithCalibratedRed:green:blue:alpha:", - "colorWithDeviceRed:green:blue:alpha:", - }; - for (const char* selectorName : selectors) { - if (class_getClassMethod(nsColorClass, - sel_getUid(selectorName)) != nullptr) { - return callObjCSelector(runtime, bridge, - static_cast(nsColorClass), true, - selectorName, nullptr, args, count); - } - } - throw JSError( - runtime, "NSColor RGB initializer is not available."); - }); - } - } -#endif - - if (property == "initWithFireDateIntervalTargetSelectorUserInfoRepeats") { - Class timerClass = NSClassFromString(@"NSTimer"); - if (object_ != nil && timerClass != Nil && - [object_ isKindOfClass:timerClass]) { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 6, - [bridge, timerClass](Runtime& runtime, const Value&, - const Value* args, size_t count) -> Value { - if (count < 6) { - throw JSError( - runtime, "NSTimer initializer expects six arguments."); - } - return callObjCSelector( - runtime, bridge, static_cast(timerClass), true, - "timerWithTimeInterval:target:selector:userInfo:repeats:", - nullptr, args + 1, count - 1); - }); - } - } - - if (object_ != nil && [object_ isKindOfClass:[NSArray class]]) { - NSArray* array = static_cast(object_); - if (property == "length") { - return static_cast(array.count); - } - if (auto index = parseArrayIndexProperty(property)) { - if (*index >= array.count) { - return Value::undefined(); - } - id element = [array objectAtIndex:*index]; - NativeApiType elementType = nativeObjectReturnType(); - return convertNativeReturnValue(runtime, bridge_, elementType, &element); - } - } - - 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)) { - 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); - } - } - - // 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; - } - } - } - } - - Value prototypeFunction = prototypeFunctionForProperty(runtime, property); - if (!prototypeFunction.isUndefined()) { - return prototypeFunction; - } - - // JS-subclassed instances own their members in JS (prototype accessors and - // methods); defer so the engine resolves them instead of the bridge - // returning a registered getter IMP as a raw callable. - if (isEngineExtendedInstance) { -#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE - // Engines whose exotic property handler invokes prototype accessors with - // the wrong receiver need the JS-prototype getter resolved here with this - // instance as the receiver. - bool found = false; - Value resolved = resolveEnginePrototypeGetter(runtime, property, &found); - if (found) { - return resolved; - } -#endif - if (auto selector = - runtimeReadablePropertyGetter(object_, property)) { - return callObjectSelector(runtime, *selector, nullptr, nullptr, 0); - } - return Value::undefined(); - } - - if (object_ != 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(); - } - - NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { - std::string property = name.utf8(runtime); - if (object_ == nil) { - throw JSError(runtime, "Cannot set property on nil object."); - } - - if (const NativeApiSymbol* symbol = - bridge_->findClassForRuntimeClass(object_getClass(object_))) { - const auto& members = bridge_->membersForClass(*symbol); - if (const NativeApiMember* propertyMember = - selectWritablePropertyMember(members, property, false)) { - if (propertyMember->readonly) { - throw JSError( - runtime, "Attempted to assign to readonly property."); - } - NativeApiMember setterMember = *propertyMember; - setterMember.selectorName = propertyMember->setterSelectorName; - setterMember.signatureOffset = propertyMember->setterSignatureOffset; - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, object_, false, - setterMember.selectorName, &setterMember, args, 1); - NATIVE_API_SET_RETURN(true); - } - } - - if (auto setterSelectorName = - runtimeWritablePropertySetter(object_, property)) { - Value args[] = {Value(runtime, value)}; - callObjCSelector(runtime, bridge_, object_, false, - *setterSelectorName, nullptr, args, 1); - NATIVE_API_SET_RETURN(true); - } - - // For JS-subclassed instances, an unknown property is owned by the JS - // prototype (e.g. a JS-defined accessor); defer so the engine runs it instead of - // shadowing it with a bridge expando. - if (class_conformsToProtocol(object_getClass(object_), - @protocol(NativeApiClassBuilderProtocol))) { -#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE - // Engines whose exotic property storage doesn't fall back to own - // properties need the JS-owned set resolved here: invoke a JS-prototype - // setter if present, otherwise store the value as a bridge expando. - bool invokedPrototypeSetter = - invokeEnginePrototypeSetter(runtime, property, value); - if (!invokedPrototypeSetter) { - 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 { - std::vector names; - names.reserve(6); - addPropertyName(runtime, names, "kind"); - addPropertyName(runtime, names, "className"); - addPropertyName(runtime, names, "nativeAddress"); - addPropertyName(runtime, names, "constructor"); - addPropertyName(runtime, names, "superclass"); - addPropertyName(runtime, names, "super"); - addPropertyName(runtime, names, "invoke"); - addPropertyName(runtime, names, "send"); - addPropertyName(runtime, names, "takeRetainedValue"); - addPropertyName(runtime, names, "takeUnretainedValue"); - addPropertyName(runtime, names, "toString"); - return names; - } - - private: - std::shared_ptr bridge_; - id object_ = nil; - bool ownsObject_ = false; - bool wrapperRetainedObject_ = false; - bool consumed_ = false; - std::shared_ptr lifetimeState_; -}; - -class NativeApiClassHostObject final : public HostObject { - public: - NativeApiClassHostObject(std::shared_ptr bridge, - NativeApiSymbol symbol) - : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} - - Class nativeClass() const { - return objc_lookUpClass(symbol_.runtimeName.c_str()); - } - - 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") { - return makeString(runtime, "class"); - } - if (property == "name") { - return makeString(runtime, symbol_.name); - } - if (property == "runtimeName") { - return makeString(runtime, symbol_.runtimeName); - } - if (property == "available") { - return objc_lookUpClass(symbol_.runtimeName.c_str()) != nil; - } - if (property == "metadataOffset") { - return static_cast(symbol_.offset); - } - if (property == "__superclass") { - if (symbol_.superclassOffset == MD_SECTION_OFFSET_NULL) { - return Value::undefined(); - } - const NativeApiSymbol* superclass = - bridge_->findClassByOffset(symbol_.superclassOffset); - if (superclass == nullptr) { - return Value::undefined(); - } - return makeNativeClassValue(runtime, bridge_, *superclass); - } - if (property == "__runtimeStaticMembers" || - property == "__runtimeInstanceMembers") { - return runtimeMembersArray(runtime, nativeClass(), - property == "__runtimeStaticMembers"); - } - if (property == "__staticMembers" || property == "__instanceMembers") { - bool staticMembers = property == "__staticMembers"; - const auto& members = bridge_->surfaceMembersForClass(symbol_); - Array result(runtime, members.size()); - size_t index = 0; - for (const auto& member : members) { - bool memberIsStatic = - (member.flags & metagen::mdMemberStatic) != 0; - if (memberIsStatic != staticMembers) { - continue; - } - Object descriptor(runtime); - descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); - descriptor.setProperty(runtime, "selectorName", - makeString(runtime, member.selectorName)); - descriptor.setProperty( - runtime, "argumentCount", - static_cast(selectorArgumentCount(member.selectorName))); - descriptor.setProperty(runtime, "property", member.property); - descriptor.setProperty(runtime, "readonly", member.readonly); - 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); - } - Array compact(runtime, index); - for (size_t i = 0; i < index; i++) { - compact.setValueAtIndex(runtime, i, result.getValueAtIndex(runtime, i)); - } - return compact; - } - if (property == "toString") { - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [symbol = symbol_](Runtime& runtime, const Value&, - const Value*, size_t) -> Value { - return makeString(runtime, - "[NativeApiClass " + symbol.name + "]"); - }); - } - if (property == "construct" || property == "alloc" || property == "new") { - auto bridge = bridge_; - auto symbol = symbol_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property), 0, - [bridge, symbol, property](Runtime& runtime, const Value&, - const Value* args, size_t count) -> Value { - Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); - if (cls == nil) { - throw JSError( - runtime, "Objective-C class is not available: " + symbol.name); - } - - id result = nil; - if (property == "construct" && count == 1) { - void* pointer = nullptr; - if (args[0].isNumber()) { - pointer = reinterpret_cast( - static_cast(args[0].getNumber())); - } else if (args[0].isObject()) { - Object object = args[0].asObject(runtime); - if (object.isHostObject(runtime)) { - 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)) { - 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 - .getHostObject( - runtime) - ->object(); - } - } - return makeNativeObjectValue(runtime, bridge, - static_cast(pointer), false); - } - - if (property == "new") { - if (count != 0) { - throw JSError( - runtime, "new does not take arguments; use invoke for an " - "explicit Objective-C selector."); - } - performDirectObjCInvocation(runtime, - [&]() { result = [[cls alloc] init]; }); - } else { - if (count != 0) { - throw JSError( - runtime, "alloc does not take arguments; call invoke on the " - "allocated object for an explicit init selector."); - } - performDirectObjCInvocation(runtime, - [&]() { result = [cls alloc]; }); - } - - return makeNativeObjectValue(runtime, bridge, result, true); - }); - } - if (property == "invoke" || property == "send") { - auto bridge = bridge_; - auto symbol = symbol_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, property.c_str()), 1, - [bridge, symbol](Runtime& runtime, const Value&, const Value* args, - size_t count) -> Value { - std::string selectorName = - readStringArg(runtime, args, count, 0, "selector"); - Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); - if (cls == nil) { - throw JSError( - runtime, "Objective-C class is not available: " + symbol.name); - } - return callObjCSelector(runtime, bridge, static_cast(cls), true, - selectorName, nullptr, args + 1, - count - 1); - }); - } - - 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)) { - auto bridge = bridge_; - auto symbol = symbol_; - Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); - if (cls == nil) { - throw JSError( - runtime, "Objective-C class is not available: " + symbol.name); - } - SEL selector = sel_getUid(propertyMember->selectorName.c_str()); - Class dispatchClass = classRespondingToClassSelector(cls, selector); - if (dispatchClass != Nil) { - return callObjCSelector(runtime, bridge, static_cast(dispatchClass), true, - propertyMember->selectorName, propertyMember, - nullptr, 0); - } - } - - 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(); - } - - 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 JSError( - runtime, "Objective-C class is not available: " + symbol_.name); - } - - const auto& members = bridge_->membersForClass(symbol_); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, true)) { - if (propertyMember->readonly) { - throw 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(dispatchClass), true, - setterMember.selectorName, &setterMember, args, 1); - NATIVE_API_SET_RETURN(true); - } - - throw JSError(runtime, - "No writable native property: " + property); - } - - std::vector getPropertyNames(Runtime& runtime) override { - std::vector names; - names.reserve(8); - addPropertyName(runtime, names, "kind"); - addPropertyName(runtime, names, "name"); - addPropertyName(runtime, names, "runtimeName"); - addPropertyName(runtime, names, "available"); - addPropertyName(runtime, names, "metadataOffset"); - addPropertyName(runtime, names, "toString"); - addPropertyName(runtime, names, "construct"); - addPropertyName(runtime, names, "alloc"); - addPropertyName(runtime, names, "new"); - addPropertyName(runtime, names, "invoke"); - addPropertyName(runtime, names, "send"); - return names; - } - - private: - std::shared_ptr bridge_; - NativeApiSymbol symbol_; -}; - -Value makeNativeObjectValue(Runtime& runtime, - const std::shared_ptr& bridge, - id object, bool ownsObject) { - if (object == nil) { - return Value::null(); - } - - Value cached = bridge->findRoundTripValue(runtime, object, nullptr, true); - if (!cached.isUndefined()) { - // 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; - } - bridge->forgetRoundTripValue(runtime, object); - } - - Object result = createNativeInstanceHostObject( - runtime, - std::make_shared(bridge, object, ownsObject)); - 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; -} - -Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, - const char* expectedKind) { - Object global = runtime.global(); - Value cacheValue = global.getProperty( - runtime, "__nativeScriptNativeApiGlobalCache"); - if (!cacheValue.isObject()) { - return Value::undefined(); - } - - Object cache = cacheValue.asObject(runtime); - auto readCache = [&](const std::string& name) -> Value { - if (name.empty()) { - return Value::undefined(); - } - - Value value = cache.getProperty(runtime, name.c_str()); - if (!value.isObject()) { - return Value::undefined(); - } - - try { - Object object = value.asObject(runtime); - Value kindValue = object.getProperty(runtime, "kind"); - if (kindValue.isString() && - kindValue.asString(runtime).utf8(runtime) == expectedKind) { - return value; - } - } catch (const std::exception&) { - } - - return Value::undefined(); - }; - - Value value = readCache(symbol.name); - if (!value.isUndefined()) { - return value; - } - if (symbol.runtimeName != symbol.name) { - value = readCache(symbol.runtimeName); - if (!value.isUndefined()) { - return value; - } - } - - try { - if (std::strcmp(expectedKind, "class") == 0) { - Value classResolverValue = global.getProperty( - runtime, "__nativeScriptResolveNativeApiClassWrapper"); - if (classResolverValue.isObject() && - classResolverValue.asObject(runtime).isFunction(runtime)) { - Function classResolver = - classResolverValue.asObject(runtime).asFunction(runtime); - auto resolveClassWrapper = [&](const std::string& name) -> Value { - if (name.empty()) { - return Value::undefined(); - } - Value resolved = classResolver.call(runtime, makeString(runtime, name)); - return resolved.isObject() ? std::move(resolved) : Value::undefined(); - }; - - value = resolveClassWrapper(symbol.name); - if (!value.isUndefined()) { - return value; - } - if (symbol.runtimeName != symbol.name) { - value = resolveClassWrapper(symbol.runtimeName); - if (!value.isUndefined()) { - return value; - } - } - } - } - - Value resolverValue = - global.getProperty(runtime, "__nativeScriptResolveNativeApiGlobal"); - if (resolverValue.isObject() && - resolverValue.asObject(runtime).isFunction(runtime)) { - Function resolver = resolverValue.asObject(runtime).asFunction(runtime); - auto resolveGlobal = [&](const std::string& name) -> Value { - if (name.empty()) { - return Value::undefined(); - } - Value resolved = resolver.call(runtime, makeString(runtime, name), - makeString(runtime, expectedKind)); - if (resolved.isObject()) { - return resolved; - } - return Value::undefined(); - }; - - value = resolveGlobal(symbol.name); - if (!value.isUndefined()) { - return value; - } - if (symbol.runtimeName != symbol.name) { - value = resolveGlobal(symbol.runtimeName); - if (!value.isUndefined()) { - return value; - } - } - } - } catch (const std::exception&) { - } - - return Value::undefined(); -} - -Value makeNativeClassValue(Runtime& runtime, - const std::shared_ptr& bridge, - NativeApiSymbol symbol) { - Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); - Value cachedClass = bridge->findClassValue(runtime, cls); - if (!cachedClass.isUndefined()) { - return cachedClass; - } - Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); - if (!globalValue.isUndefined()) { - return globalValue; - } - return Object::createFromHostObject( - runtime, - std::make_shared(bridge, std::move(symbol))); -} - -Protocol* lookupProtocolByNativeName(const std::string& name) { - Protocol* protocol = objc_getProtocol(name.c_str()); - if (protocol != nullptr) { - return protocol; - } - constexpr const char* suffix = "Protocol"; - size_t suffixLength = std::strlen(suffix); - if (name.size() > suffixLength && - name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { - protocol = objc_getProtocol( - name.substr(0, name.size() - suffixLength).c_str()); - } - return protocol; -} - -class NativeApiProtocolHostObject final : public HostObject { - public: - NativeApiProtocolHostObject(std::shared_ptr bridge, - NativeApiSymbol symbol) - : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} - - Protocol* nativeProtocol() const { - Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); - if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { - protocol = lookupProtocolByNativeName(symbol_.name); - } - return protocol; - } - - const NativeApiSymbol& symbol() const { return symbol_; } - - Value get(Runtime& runtime, const PropNameID& name) override { - std::string property = name.utf8(runtime); - if (property == "kind") { - return makeString(runtime, "protocol"); - } - if (property == "name") { - return makeString(runtime, symbol_.name); - } - if (property == "runtimeName") { - return makeString(runtime, symbol_.runtimeName); - } - if (property == "available") { - return nativeProtocol() != nullptr; - } - if (property == "metadataOffset") { - return static_cast(symbol_.offset); - } - if (property == "nativeAddress") { - return static_cast( - reinterpret_cast(nativeProtocol())); - } - if (property == "prototype") { - Object prototype(runtime); - for (const auto& member : bridge_->membersForProtocol(symbol_)) { - if (prototype.hasProperty(runtime, member.name.c_str())) { - continue; - } - if (member.property) { - defineProtocolProperty(runtime, prototype, member, false); - } else { - prototype.setProperty(runtime, member.name.c_str(), - makeProtocolMemberFunction(runtime, member, - false)); - } - } - return prototype; - } - if (property == "toString") { - auto symbol = symbol_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, "toString"), 0, - [symbol](Runtime& runtime, const Value&, const Value*, size_t) -> Value { - return makeString(runtime, - "[NativeApiProtocol " + symbol.name + "]"); - }); - } - const auto& members = bridge_->membersForProtocol(symbol_); - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, true)) { - return makeProtocolPropertyGetter(runtime, *propertyMember, true); - } - if (const NativeApiMember* propertyMember = - selectPropertyMember(members, property, false)) { - return makeProtocolPropertyGetter(runtime, *propertyMember, true); - } - 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); - } - } - 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(); - } - - std::vector getPropertyNames(Runtime& runtime) override { - std::vector names; - addPropertyName(runtime, names, "kind"); - addPropertyName(runtime, names, "name"); - addPropertyName(runtime, names, "runtimeName"); - addPropertyName(runtime, names, "available"); - addPropertyName(runtime, names, "metadataOffset"); - addPropertyName(runtime, names, "nativeAddress"); - addPropertyName(runtime, names, "prototype"); - addPropertyName(runtime, names, "toString"); - for (const auto& member : bridge_->membersForProtocol(symbol_)) { - addPropertyName(runtime, names, member.name.c_str()); - } - return names; - } - - private: - static Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) { - if (!thisValue.isObject()) { - return Nil; - } - - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); - } - - Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject(runtime)) { - return wrappedObject.getHostObject(runtime) - ->nativeClass(); - } - } - - Value kindValue = object.getProperty(runtime, "kind"); - if (kindValue.isString() && - kindValue.asString(runtime).utf8(runtime) == "class") { - Value runtimeNameValue = object.getProperty(runtime, "runtimeName"); - if (!runtimeNameValue.isString()) { - runtimeNameValue = object.getProperty(runtime, "name"); - } - if (runtimeNameValue.isString()) { - std::string runtimeName = - runtimeNameValue.asString(runtime).utf8(runtime); - return objc_lookUpClass(runtimeName.c_str()); - } - } - - return Nil; - } - - id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { - if (!thisValue.isObject()) { - return nil; - } - - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->object(); - } - - return nil; - } - - Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value* args, - size_t count) -> Value { - id receiver = nil; - if (receiverIsClass) { - receiver = static_cast( - classReceiverFromThis(runtime, thisValue)); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw JSError( - runtime, "Protocol member requires a native receiver."); - } - return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - member.selectorName, &member, args, count); - }); - } - - Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value*, size_t) -> Value { - id receiver = nil; - if (receiverIsClass) { - receiver = static_cast( - classReceiverFromThis(runtime, thisValue)); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw 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, - getterMember.selectorName, &getterMember, - nullptr, 0); - }); - } - - Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, - bool receiverIsClass) const { - auto bridge = bridge_; - return Function::createFromHostFunction( - runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), - 1, - [bridge, member, receiverIsClass](Runtime& runtime, - const Value& thisValue, - const Value* args, - size_t count) -> Value { - id receiver = nil; - if (receiverIsClass) { - receiver = static_cast( - classReceiverFromThis(runtime, thisValue)); - } else if (thisValue.isObject()) { - Object object = thisValue.asObject(runtime); - if (object.isHostObject(runtime)) { - receiver = object.getHostObject(runtime) - ->object(); - } - } - - if (receiver == nil) { - throw JSError( - runtime, "Protocol property requires a native receiver."); - } - if (count < 1) { - throw JSError( - runtime, "Protocol property setter expects a value."); - } - - NativeApiMember setterMember = member; - setterMember.selectorName = member.setterSelectorName; - setterMember.signatureOffset = member.setterSignatureOffset; - return callObjCSelector(runtime, bridge, receiver, receiverIsClass, - setterMember.selectorName, &setterMember, - args, 1); - }); - } - - void defineProtocolProperty(Runtime& runtime, Object& target, - const NativeApiMember& member, - bool receiverIsClass) const { - try { - Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); - Function defineProperty = - objectCtor.getPropertyAsFunction(runtime, "defineProperty"); - Object descriptor(runtime); - descriptor.setProperty(runtime, "configurable", true); - descriptor.setProperty(runtime, "enumerable", true); - descriptor.setProperty(runtime, "get", - makeProtocolPropertyGetter(runtime, member, - receiverIsClass)); - if (!member.readonly && !member.setterSelectorName.empty()) { - descriptor.setProperty(runtime, "set", - makeProtocolPropertySetter(runtime, member, - receiverIsClass)); - } - defineProperty.call(runtime, target, makeString(runtime, member.name), - descriptor); - } catch (const std::exception&) { - } - } - - std::shared_ptr bridge_; - NativeApiSymbol symbol_; -}; - -Value makeNativeProtocolValue(Runtime& runtime, - const std::shared_ptr& bridge, - NativeApiSymbol symbol) { - Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); - if (!globalValue.isUndefined()) { - return globalValue; - } - return Object::createFromHostObject( - runtime, - std::make_shared(bridge, std::move(symbol))); -} - -Class nativeClassFromEngineObject(Runtime& runtime, const Object& object) { - if (object.isHostObject(runtime)) { - return object.getHostObject(runtime)->nativeClass(); - } - - Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); - if (wrappedClass.isObject()) { - Object wrappedObject = wrappedClass.asObject(runtime); - if (wrappedObject.isHostObject(runtime)) { - return wrappedObject.getHostObject(runtime) - ->nativeClass(); - } - } - return Nil; -} +#include "host_objects/Protocol.mm" diff --git a/NativeScript/ffi/objc/shared/bridge/TypeConv.mm b/NativeScript/ffi/objc/shared/bridge/TypeConv.mm index 24d6be0cc..670b38bfe 100644 --- a/NativeScript/ffi/objc/shared/bridge/TypeConv.mm +++ b/NativeScript/ffi/objc/shared/bridge/TypeConv.mm @@ -1,8 +1,50 @@ std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, const char* name); void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object); -id objectFromEngineValue(Runtime& runtime, const std::shared_ptr& bridge, - const Value& value, NativeApiArgumentFrame& frame, bool mutableString) { +class NativeApiObjectConversionStack final { + public: + bool contains(Runtime& runtime, const Value& value) const { + for (const auto& active : activeValues_) { + if (Value::strictEquals(runtime, active, value)) { + return true; + } + } + return false; + } + + void push(Runtime& runtime, const Value& value) { + activeValues_.emplace_back(runtime, value); + } + + void pop() { activeValues_.pop_back(); } + + private: + std::vector activeValues_; +}; + +class NativeApiObjectConversionGuard final { + public: + NativeApiObjectConversionGuard(Runtime& runtime, const Value& value, + NativeApiObjectConversionStack& stack) + : stack_(stack) { + if (stack_.contains(runtime, value)) { + throw JSError( + runtime, + "Circular JavaScript object graphs cannot be converted to Objective-C collections."); + } + stack_.push(runtime, value); + } + + ~NativeApiObjectConversionGuard() { stack_.pop(); } + + private: + NativeApiObjectConversionStack& stack_; +}; + +id objectFromEngineValueImpl( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value, NativeApiArgumentFrame& frame, bool mutableString, + NativeApiObjectConversionStack& conversionStack) { if (value.isNull() || value.isUndefined()) { return nil; } @@ -67,7 +109,8 @@ id objectFromEngineValue(Runtime& runtime, const std::shared_ptrrememberScopedRoundTripValue(runtime, nativeArray, value, false, false); @@ -98,8 +146,9 @@ id objectFromEngineValue(Runtime& runtime, const std::shared_ptrrememberScopedRoundTripValue(runtime, nativeArray, value, false, false); @@ -130,10 +179,12 @@ id objectFromEngineValue(Runtime& runtime, const std::shared_ptr& bridge, + const Value& value, NativeApiArgumentFrame& frame, + bool mutableString) { + NativeApiObjectConversionStack conversionStack; + return objectFromEngineValueImpl(runtime, bridge, value, frame, + mutableString, conversionStack); +} + std::string utf8StringFromNSString(NSString* string) { if (string == nil) { return ""; diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm new file mode 100644 index 000000000..e74c4032b --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm @@ -0,0 +1,501 @@ +class NativeApiClassHostObject final : public HostObject { + public: + NativeApiClassHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Class nativeClass() const { + return objc_lookUpClass(symbol_.runtimeName.c_str()); + } + + 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") { + return makeString(runtime, "class"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return objc_lookUpClass(symbol_.runtimeName.c_str()) != nil; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "__superclass") { + if (symbol_.superclassOffset == MD_SECTION_OFFSET_NULL) { + return Value::undefined(); + } + const NativeApiSymbol* superclass = + bridge_->findClassByOffset(symbol_.superclassOffset); + if (superclass == nullptr) { + return Value::undefined(); + } + return makeNativeClassValue(runtime, bridge_, *superclass); + } + if (property == "__runtimeStaticMembers" || + property == "__runtimeInstanceMembers") { + return runtimeMembersArray(runtime, nativeClass(), + property == "__runtimeStaticMembers"); + } + if (property == "__staticMembers" || property == "__instanceMembers") { + bool staticMembers = property == "__staticMembers"; + const auto& members = bridge_->surfaceMembersForClass(symbol_); + Array result(runtime, members.size()); + size_t index = 0; + for (const auto& member : members) { + bool memberIsStatic = + (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMembers) { + continue; + } + Object descriptor(runtime); + descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); + descriptor.setProperty(runtime, "selectorName", + makeString(runtime, member.selectorName)); + descriptor.setProperty( + runtime, "argumentCount", + static_cast(selectorArgumentCount(member.selectorName))); + descriptor.setProperty(runtime, "property", member.property); + descriptor.setProperty(runtime, "readonly", member.readonly); + 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); + } + Array compact(runtime, index); + for (size_t i = 0; i < index; i++) { + compact.setValueAtIndex(runtime, i, result.getValueAtIndex(runtime, i)); + } + return compact; + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol = symbol_](Runtime& runtime, const Value&, + const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiClass " + symbol.name + "]"); + }); + } + if (property == "construct" || property == "alloc" || property == "new") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property), 0, + [bridge, symbol, property](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + + id result = nil; + if (property == "construct" && count == 1) { + void* pointer = nullptr; + if (args[0].isNumber()) { + pointer = reinterpret_cast( + static_cast(args[0].getNumber())); + } else if (args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + 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)) { + 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 + .getHostObject( + runtime) + ->object(); + } + } + return makeNativeObjectValue(runtime, bridge, + static_cast(pointer), false); + } + + if (property == "new") { + if (count != 0) { + throw JSError( + runtime, "new does not take arguments; use invoke for an " + "explicit Objective-C selector."); + } + performDirectObjCInvocation(runtime, + [&]() { result = [[cls alloc] init]; }); + } else { + if (count != 0) { + throw JSError( + runtime, "alloc does not take arguments; call invoke on the " + "allocated object for an explicit init selector."); + } + performDirectObjCInvocation(runtime, + [&]() { result = [cls alloc]; }); + } + + return makeNativeObjectValue(runtime, bridge, result, true); + }); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, symbol](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + selectorName, nullptr, args + 1, + count - 1); + }); + } + + 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)) { + auto bridge = bridge_; + auto symbol = symbol_; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + Class dispatchClass = classRespondingToClassSelector(cls, selector); + if (dispatchClass != Nil) { + return callObjCSelector(runtime, bridge, static_cast(dispatchClass), true, + propertyMember->selectorName, propertyMember, + nullptr, 0); + } + } + + 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(); + } + + 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 JSError( + runtime, "Objective-C class is not available: " + symbol_.name); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + if (propertyMember->readonly) { + throw 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(dispatchClass), true, + setterMember.selectorName, &setterMember, args, 1); + NATIVE_API_SET_RETURN(true); + } + + throw JSError(runtime, + "No writable native property: " + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(8); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "toString"); + addPropertyName(runtime, names, "construct"); + addPropertyName(runtime, names, "alloc"); + addPropertyName(runtime, names, "new"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject) { + if (object == nil) { + return Value::null(); + } + + Value cached = bridge->findRoundTripValue(runtime, object, nullptr, true); + if (!cached.isUndefined()) { + // 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; + } + bridge->forgetRoundTripValue(runtime, object); + } + + Object result = createNativeInstanceHostObject( + runtime, + std::make_shared(bridge, object, ownsObject)); + 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; +} + +Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, + const char* expectedKind) { + Object global = runtime.global(); + Value cacheValue = global.getProperty( + runtime, "__nativeScriptNativeApiGlobalCache"); + if (!cacheValue.isObject()) { + return Value::undefined(); + } + + Object cache = cacheValue.asObject(runtime); + auto readCache = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + + Value value = cache.getProperty(runtime, name.c_str()); + if (!value.isObject()) { + return Value::undefined(); + } + + try { + Object object = value.asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == expectedKind) { + return value; + } + } catch (const std::exception&) { + } + + return Value::undefined(); + }; + + Value value = readCache(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = readCache(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + + try { + if (std::strcmp(expectedKind, "class") == 0) { + Value classResolverValue = global.getProperty( + runtime, "__nativeScriptResolveNativeApiClassWrapper"); + if (classResolverValue.isObject() && + classResolverValue.asObject(runtime).isFunction(runtime)) { + Function classResolver = + classResolverValue.asObject(runtime).asFunction(runtime); + auto resolveClassWrapper = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = classResolver.call(runtime, makeString(runtime, name)); + return resolved.isObject() ? std::move(resolved) : Value::undefined(); + }; + + value = resolveClassWrapper(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveClassWrapper(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } + + Value resolverValue = + global.getProperty(runtime, "__nativeScriptResolveNativeApiGlobal"); + if (resolverValue.isObject() && + resolverValue.asObject(runtime).isFunction(runtime)) { + Function resolver = resolverValue.asObject(runtime).asFunction(runtime); + auto resolveGlobal = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = resolver.call(runtime, makeString(runtime, name), + makeString(runtime, expectedKind)); + if (resolved.isObject()) { + return resolved; + } + return Value::undefined(); + }; + + value = resolveGlobal(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveGlobal(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } catch (const std::exception&) { + } + + return Value::undefined(); +} + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + Value cachedClass = bridge->findClassValue(runtime, cls); + if (!cachedClass.isUndefined()) { + return cachedClass; + } + Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Protocol* lookupProtocolByNativeName(const std::string& name) { + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol != nullptr) { + return protocol; + } + constexpr const char* suffix = "Protocol"; + size_t suffixLength = std::strlen(suffix); + if (name.size() > suffixLength && + name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - suffixLength).c_str()); + } + return protocol; +} diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm new file mode 100644 index 000000000..5913da68d --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm @@ -0,0 +1,225 @@ +class NativeApiPointerHostObject final + : public HostObject, + public std::enable_shared_from_this { + public: + NativeApiPointerHostObject(std::shared_ptr bridge, + void* pointer, std::string kind = "pointer", + bool adopted = false, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + pointer_(pointer), + kind_(std::move(kind)), + adopted_(adopted), + backingValue_(std::move(backingValue)) {} + + ~NativeApiPointerHostObject() override { + if (adopted_ && pointer_ != nullptr) { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + free(pointer_); + pointer_ = nullptr; + } + } + + 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() { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + pointer_ = nullptr; + adopted_ = false; + backingValue_.reset(); + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, kind_); + } + if (property == "address") { + return static_cast(reinterpret_cast(pointer_)); + } + if (property == "adopted") { + return adopted_; + } + if (property == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->pointer_ == nullptr || self->consumed_) { + throw 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); + }); + } + if (property == "add" || property == "subtract") { + void* pointer = pointer_; + bool add = property == "add"; + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, pointer, add](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 1 || !args[0].isNumber()) { + throw JSError(runtime, "Pointer offset must be a number."); + } + intptr_t offset = static_cast(args[0].getNumber()); + intptr_t base = reinterpret_cast(pointer); + void* result = reinterpret_cast(add ? base + offset : base - offset); + return createPointer(runtime, bridge, result); + }); + } + if (property == "toNumber") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toNumber"), 0, + [pointer](Runtime&, const Value&, const Value*, size_t) -> Value { + return static_cast(reinterpret_cast(pointer)); + }); + } + if (property == "toBigInt") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toBigInt"), 0, + [pointer](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return BigInt::fromUint64( + runtime, + static_cast(reinterpret_cast(pointer))); + }); + } + if (property == "toHexString" || property == "toDecimalString") { + void* pointer = pointer_; + bool hex = property == "toHexString"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [pointer, hex](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + if (hex) { + char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; + snprintf(text, sizeof(text), "0x%llx", + static_cast( + reinterpret_cast(pointer))); + return makeString(runtime, text); + } else { + char text[32] = {}; + snprintf(text, sizeof(text), "%lld", + static_cast(reinterpret_cast(pointer))); + return makeString(runtime, text); + } + }); + } + if (property == "toString") { + void* pointer = pointer_; + std::string kind = kind_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [pointer, kind](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", pointer); + if (kind == "pointer") { + return makeString(runtime, + ""); + } + return makeString(runtime, "[NativeApi " + kind + " " + + std::string(address) + "]"); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(3); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "adopted"); + addPropertyName(runtime, names, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + addPropertyName(runtime, names, "add"); + addPropertyName(runtime, names, "subtract"); + addPropertyName(runtime, names, "toNumber"); + addPropertyName(runtime, names, "toBigInt"); + addPropertyName(runtime, names, "toHexString"); + addPropertyName(runtime, names, "toDecimalString"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + void* pointer_ = nullptr; + std::string kind_; + bool adopted_ = false; + bool consumed_ = false; + std::shared_ptr backingValue_; +}; + +class NativeApiReferenceHostObject final : public HostObject { + public: + 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) + : bridge_(std::move(bridge)), + type_(std::move(type)), + data_(data), + ownsData_(ownsData), + byteLength_(byteLength), + pendingValue_(std::move(pendingValue)), + backingValue_(std::move(backingValue)) {} + + ~NativeApiReferenceHostObject() override { + for (id object : retainedObjects_) { + [object release]; + } + if (ownsData_ && data_ != nullptr) { + free(data_); + data_ = nullptr; + } + } + + void* data() const { return data_; } + 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); + void retainObjectSlot(size_t index, id object); + + Value get(Runtime& runtime, const PropNameID& name) 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"); + addPropertyName(runtime, names, "value"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiType type_; + void* data_ = nullptr; + bool ownsData_ = false; + size_t byteLength_ = 0; + std::shared_ptr pendingValue_; + std::shared_ptr backingValue_; + std::vector retainedObjects_; +}; diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm new file mode 100644 index 000000000..2fd5b80be --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm @@ -0,0 +1,1380 @@ +class NativeApiFastEnumerationIteratorHostObject final : public HostObject { + public: + NativeApiFastEnumerationIteratorHostObject( + std::shared_ptr bridge, id collection) + : bridge_(std::move(bridge)), collection_(collection) { + [(id)collection_ retain]; + } + + ~NativeApiFastEnumerationIteratorHostObject() override { + [(id)collection_ release]; + collection_ = nil; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "next") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "next"), 0, + [this](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return next(runtime); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "next"); + return names; + } + + private: + Value next(Runtime& runtime) { + Object result(runtime); + if (done_ || collection_ == nil) { + result.setProperty(runtime, "done", true); + return result; + } + + if (stackIndex_ >= stackLength_) { + stackLength_ = [collection_ countByEnumeratingWithState:&state_ + objects:stack_ + count:16]; + stackIndex_ = 0; + if (stackLength_ == 0) { + done_ = true; + result.setProperty(runtime, "done", true); + return result; + } + } + + id value = state_.itemsPtr[stackIndex_++]; + 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_; + id collection_ = nil; + NSFastEnumerationState state_ = {}; + id __unsafe_unretained stack_[16] = {}; + NSUInteger stackLength_ = 0; + NSUInteger stackIndex_ = 0; + bool done_ = false; +}; + +NativeApiSymbol nativeApiSymbolForRuntimeClass( + const std::shared_ptr& bridge, Class cls) { + const char* name = cls != Nil ? class_getName(cls) : ""; + if (bridge != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimePointer(cls)) { + return *symbol; + } + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { + return *symbol; + } + if (name != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClass(name)) { + return *symbol; + } + } + } + + return NativeApiSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; +} + +std::optional runtimeWritablePropertySetter(id object, + const std::string& property) { + if (object == nil || property.empty()) { + return std::nullopt; + } + + Class current = object_getClass(object); + while (current != Nil) { + objc_property_t prop = class_getProperty(current, property.c_str()); + if (prop != nullptr) { + if (char* readonly = property_copyAttributeValue(prop, "R")) { + free(readonly); + return std::nullopt; + } + + std::string setter = setterSelectorForProperty(property); + if (char* customSetter = property_copyAttributeValue(prop, "S")) { + setter = customSetter; + free(customSetter); + } + + SEL selector = sel_getUid(setter.c_str()); + if ([object respondsToSelector:selector]) { + return setter; + } + } + + current = class_getSuperclass(current); + } + + std::string setter = setterSelectorForProperty(property); + SEL selector = sel_getUid(setter.c_str()); + if ([object respondsToSelector:selector]) { + return setter; + } + + return std::nullopt; +} + +std::optional runtimeReadablePropertyGetter(id object, + const std::string& property) { + if (object == nil || property.empty()) { + return std::nullopt; + } + + Class current = object_getClass(object); + while (current != Nil) { + objc_property_t prop = class_getProperty(current, property.c_str()); + if (prop != nullptr) { + std::string getter = property; + if (char* customGetter = property_copyAttributeValue(prop, "G")) { + getter = customGetter; + free(customGetter); + } + + if (auto selector = + respondingPropertyGetterSelector(object, property, getter)) { + return selector; + } + } + + current = class_getSuperclass(current); + } + + return respondingPropertyGetterSelector(object, property, property); +} + +class NativeApiSuperHostObject final : public HostObject { + public: + NativeApiSuperHostObject(std::shared_ptr bridge, + id receiver, Class dispatchClass) + : bridge_(std::move(bridge)), + receiver_(receiver), + dispatchClass_(dispatchClass) { + if (receiver_ != nil) { + [receiver_ retain]; + } + } + + ~NativeApiSuperHostObject() override { + if (receiver_ != nil) { + [receiver_ release]; + receiver_ = nil; + } + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "super"); + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, "[NativeApiSuper]"); + }); + } + if (receiver_ == nil || dispatchClass_ == Nil) { + return Value::undefined(); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { + return callObjCSelector(runtime, bridge_, receiver_, false, + propertyMember->selectorName, propertyMember, + nullptr, 0, dispatchClass_); + } + } + + if (hasMethodMember(members, property, false)) { + auto bridge = bridge_; + id receiver = receiver_; + Class dispatchClass = dispatchClass_; + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, receiver, dispatchClass, memberName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(dispatchClass); + if (symbol == nullptr) { + throw JSError( + runtime, "Objective-C metadata is not available for super."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw JSError( + runtime, "Objective-C super selector is not available: " + + memberName); + } + return callObjCSelector(runtime, bridge, receiver, false, + selected->selectorName, selected, args, + count, dispatchClass); + }); + } + } + + return Value::undefined(); + } + + NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (receiver_ == nil || dispatchClass_ == Nil) { + throw JSError(runtime, "Cannot set property on nil super."); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly || + propertyMember->setterSelectorName.empty()) { + throw JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, + setterMember.selectorName, &setterMember, args, 1, + dispatchClass_); + NATIVE_API_SET_RETURN(true); + } + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, setterSelectorName, + nullptr, args, 1, dispatchClass_); + NATIVE_API_SET_RETURN(true); + } + + throw JSError(runtime, + "No writable native super property: " + + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id receiver_ = nil; + Class dispatchClass_ = Nil; +}; + +struct NativeApiRuntimeMember { + std::string name; + std::string selectorName; + size_t argumentCount = 0; +}; + +using NativeApiRuntimeMembers = std::vector; + +struct NativeApiRuntimeMemberIndex { + NativeApiRuntimeMembers members; + std::unordered_set memberNames; + std::unordered_map> + selectorsByNameAndCount; +}; + +struct NativeApiRuntimeMembersCacheKey { + Class cls = Nil; + bool staticMembers = false; + + bool operator==(const NativeApiRuntimeMembersCacheKey& other) const { + return cls == other.cls && staticMembers == other.staticMembers; + } +}; + +struct NativeApiRuntimeMembersCacheKeyHash { + size_t operator()(const NativeApiRuntimeMembersCacheKey& key) const { + size_t classHash = std::hash{}(reinterpret_cast(key.cls)); + return classHash ^ (key.staticMembers ? 0x9e3779b97f4a7c15ULL : 0); + } +}; + +std::mutex& runtimeMembersCacheMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map, + NativeApiRuntimeMembersCacheKeyHash>& +runtimeMembersCache() { + static std::unordered_map, + NativeApiRuntimeMembersCacheKeyHash> + cache; + return cache; +} + +std::shared_ptr emptyRuntimeMembers() { + static auto empty = std::make_shared(); + return empty; +} + +NativeApiRuntimeMemberIndex buildRuntimeMembersForClass(Class cls, + bool staticMembers) { + NativeApiRuntimeMemberIndex index; + if (cls == Nil) { + return index; + } + + std::unordered_set seen; + Class current = staticMembers ? object_getClass(cls) : cls; + while (current != Nil) { + unsigned int methodCount = 0; + Method* methods = class_copyMethodList(current, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + SEL selector = method_getName(methods[i]); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName == nullptr || selectorName[0] == '\0') { + continue; + } + + std::string selectorString(selectorName); + std::string name = jsifySelector(selectorString.c_str()); + if (name.empty()) { + continue; + } + + size_t argumentCount = selectorArgumentCount(selectorString); + std::string key = name + "\x1f" + std::to_string(argumentCount); + if (!seen.insert(key).second) { + continue; + } + + index.memberNames.insert(name); + index.selectorsByNameAndCount[name].emplace(argumentCount, selectorString); + index.members.push_back(NativeApiRuntimeMember{ + .name = std::move(name), + .selectorName = std::move(selectorString), + .argumentCount = argumentCount, + }); + } + if (methods != nullptr) { + free(methods); + } + current = class_getSuperclass(current); + } + + return index; +} + +std::shared_ptr runtimeMembersForClass( + Class cls, bool staticMembers) { + if (cls == Nil) { + return emptyRuntimeMembers(); + } + + NativeApiRuntimeMembersCacheKey key{.cls = cls, + .staticMembers = staticMembers}; + + { + std::lock_guard lock(runtimeMembersCacheMutex()); + auto& cache = runtimeMembersCache(); + auto cached = cache.find(key); + if (cached != cache.end()) { + return cached->second; + } + } + + auto members = + std::make_shared( + buildRuntimeMembersForClass(cls, staticMembers)); + + { + std::lock_guard lock(runtimeMembersCacheMutex()); + auto& cache = runtimeMembersCache(); + auto [cached, inserted] = cache.emplace(key, members); + return inserted ? members : cached->second; + } +} + +bool hasRuntimeMemberForName(Class cls, bool staticMembers, + const std::string& name) { + auto index = runtimeMembersForClass(cls, staticMembers); + return index->memberNames.find(name) != index->memberNames.end(); +} + +std::optional selectRuntimeSelectorForName( + Class cls, bool staticMembers, const std::string& name, size_t count) { + auto index = runtimeMembersForClass(cls, staticMembers); + auto selectorsForName = index->selectorsByNameAndCount.find(name); + if (selectorsForName == index->selectorsByNameAndCount.end()) { + return std::nullopt; + } + auto selector = selectorsForName->second.find(count); + if (selector == selectorsForName->second.end()) { + return std::nullopt; + } + return selector->second; +} + +Array runtimeMembersArray(Runtime& runtime, Class cls, bool staticMembers) { + auto index = runtimeMembersForClass(cls, staticMembers); + Array result(runtime, index->members.size()); + for (size_t i = 0; i < index->members.size(); i++) { + const auto& member = index->members[i]; + Object descriptor(runtime); + descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); + descriptor.setProperty(runtime, "selectorName", + makeString(runtime, member.selectorName)); + descriptor.setProperty(runtime, "argumentCount", + static_cast(member.argumentCount)); + descriptor.setProperty(runtime, "property", false); + descriptor.setProperty(runtime, "readonly", false); + descriptor.setProperty(runtime, "setterSelectorName", makeString(runtime, "")); + result.setValueAtIndex(runtime, i, descriptor); + } + return result; +} + +class NativeApiObjectHostObject final + : public HostObject, + public std::enable_shared_from_this { + public: + NativeApiObjectHostObject(std::shared_ptr bridge, + id object, bool ownsObject) + : bridge_(std::move(bridge)), + object_(object), + ownsObject_(ownsObject), + lifetimeState_(std::make_shared(object)) { + if (bridge_ != nullptr && object_ != nil) { + bridge_->retainObjectExpandoOwner(object_); + } + if (object_ != nil && !ownsObject_) { + [object_ retain]; + ownsObject_ = true; + wrapperRetainedObject_ = true; + } + } + + ~NativeApiObjectHostObject() override { + if (bridge_ != nullptr && object_ != nil) { + bridge_->forgetRoundTripValue(object_); + bridge_->releaseObjectExpandoOwner( + object_, class_conformsToProtocol(object_getClass(object_), + @protocol(NativeApiClassBuilderProtocol))); + } + if (lifetimeState_ != nullptr) { + lifetimeState_->clear(); + } + if (ownsObject_ && object_ != nil) { + [object_ release]; + object_ = nil; + } + } + + 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, bool preserveExpandos = false) { + if (object_ == expected) { + if (bridge_ != nullptr && expected != nil) { + bridge_->forgetRoundTripValue(expected); + bridge_->releaseObjectExpandoOwner(expected, preserveExpandos); + } + ownsObject_ = false; + wrapperRetainedObject_ = false; + object_ = nil; + if (lifetimeState_ != nullptr) { + lifetimeState_->clear(); + } + } + } + + static bool isInitializerSelector(const std::string& selectorName) { + return selectorName.rfind("init", 0) == 0; + } + + static id nativeObjectFromValue(Runtime& runtime, const Value& value) { + if (!value.isObject()) { + return nil; + } + Object object = value.asObject(runtime); + if (!object.isHostObject(runtime)) { + return nil; + } + return object.getHostObject(runtime)->object(); + } + + 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 JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + const bool initializer = isInitializerSelector(selectorName); + std::optional classWrapper; + if (initializer) { + Value classWrapperValue = bridge_->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + classWrapper.emplace(classWrapperValue.asObject(runtime)); + } + bridge_->forgetRoundTripValue(runtime, receiver); + } + + Value result = + callObjCSelector(runtime, bridge_, receiver, false, selectorName, member, + args, count, dispatchSuperClass); + if (initializer) { + id resultObject = nativeObjectFromValue(runtime, result); + disownObject(receiver, resultObject == 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 (bridge_ != nullptr) { + bridge_->retainObjectExpandoOwner(object_); + } + 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); + } + + Value result = callPreparedObjCSelector( + runtime, bridge_, receiver, false, prepared, args, count, + dispatchSuperClass); + if (initializer) { + id resultObject = nativeObjectFromValue(runtime, result); + disownObject(receiver, resultObject == 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 (bridge_ != nullptr) { + bridge_->retainObjectExpandoOwner(object_); + } + 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 classPrototypeForObject(Runtime& runtime) { + if (object_ == nil) { + 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()) { + Object classWrapper = classWrapperValue.asObject(runtime); + Value prototypeValue = classWrapper.getProperty(runtime, "prototype"); + if (prototypeValue.isObject()) { + return prototypeValue; + } + } + return bridge_->findClassPrototype(runtime, object_getClass(object_)); + } + + Value engineThisValueForObject(Runtime& runtime) { + Value thisValue = bridge_->findRoundTripValue(runtime, object_, + nullptr, true); + if (thisValue.isObject()) { + return thisValue; + } + return makeNativeObjectValue(runtime, bridge_, object_, false); + } + + Value prototypeFunctionForProperty(Runtime& runtime, + const std::string& property) { + if (property.empty()) { + return Value::undefined(); + } + + Value prototypeValue = classPrototypeForObject(runtime); + 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 prototypeValue = classPrototypeForObject(runtime); + 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 = engineThisValueForObject(runtime); + 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 prototypeValue = classPrototypeForObject(runtime); + 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 = engineThisValueForObject(runtime); + 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"); + } + if (property == "className") { + return makeString(runtime, object_ != nil ? object_getClassName(object_) : ""); + } + if (property == "nativeAddress") { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", object_); + return makeString(runtime, address); + } + if (property == "class") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "class"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil) { + return Value::undefined(); + } + Value classWrapper = bridge->findObjectExpando( + runtime, object, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + return classWrapper; + } + NativeApiSymbol symbol = + nativeApiSymbolForRuntimeClass(bridge, object_getClass(object)); + return makeNativeClassValue(runtime, bridge, std::move(symbol)); + }); + } + if (property == "constructor") { + if (object_ == nil) { + return Value::undefined(); + } + // 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_, 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") { + if (object_ == nil) { + return Value::undefined(); + } + Class superclass = class_getSuperclass(object_getClass(object_)); + 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)); + } + if (property == "super") { + Class dispatchClass = + object_ != nil ? class_getSuperclass(object_getClass(object_)) : Nil; + return Object::createFromHostObject( + runtime, + std::make_shared(bridge_, object_, + dispatchClass)); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, object, weakSelf](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + if (auto self = weakSelf.lock()) { + return self->callObjectSelector(runtime, selectorName, nullptr, + args + 1, count - 1); + } + return callObjCSelector(runtime, bridge, object, false, selectorName, + nullptr, args + 1, count - 1); + }); + } + if (property == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->object_ == nil || self->consumed_) { + throw 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(runtime, object); + self->bridge_->releaseObjectExpandoOwner(object); + } + self->object_ = nil; + self->ownsObject_ = false; + self->wrapperRetainedObject_ = false; + if (self->lifetimeState_ != nullptr) { + self->lifetimeState_->clear(); + } + self->consumed_ = true; + 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") { + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [object](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return NativeApiObjectHostObject::descriptionString(runtime, object); + }); + } + if (property == "description") { + return descriptionString(runtime, object_); + } + if (property == "URL" && object_ != nil && + [object_ respondsToSelector:@selector(URL)]) { + return callObjectSelector(runtime, "URL", nullptr, nullptr, 0); + } + if (property == "Symbol.iterator" || + property == "Symbol(Symbol.iterator)" || + property == "@@iterator") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.iterator"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil || + ![object conformsToProtocol:@protocol(NSFastEnumeration)]) { + throw JSError( + runtime, "Object does not conform to NSFastEnumeration."); + } + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, static_cast>(object))); + }); + } + +#if TARGET_OS_OSX + if (property == "initWithRedGreenBlueAlpha") { + Class nsColorClass = NSClassFromString(@"NSColor"); + if (object_ != nil && nsColorClass != Nil && + [object_ isKindOfClass:nsColorClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 4, + [bridge, nsColorClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + const char* selectors[] = { + "colorWithSRGBRed:green:blue:alpha:", + "colorWithCalibratedRed:green:blue:alpha:", + "colorWithDeviceRed:green:blue:alpha:", + }; + for (const char* selectorName : selectors) { + if (class_getClassMethod(nsColorClass, + sel_getUid(selectorName)) != nullptr) { + return callObjCSelector(runtime, bridge, + static_cast(nsColorClass), true, + selectorName, nullptr, args, count); + } + } + throw JSError( + runtime, "NSColor RGB initializer is not available."); + }); + } + } +#endif + + if (property == "initWithFireDateIntervalTargetSelectorUserInfoRepeats") { + Class timerClass = NSClassFromString(@"NSTimer"); + if (object_ != nil && timerClass != Nil && + [object_ isKindOfClass:timerClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 6, + [bridge, timerClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 6) { + throw JSError( + runtime, "NSTimer initializer expects six arguments."); + } + return callObjCSelector( + runtime, bridge, static_cast(timerClass), true, + "timerWithTimeInterval:target:selector:userInfo:repeats:", + nullptr, args + 1, count - 1); + }); + } + } + + if (object_ != nil && [object_ isKindOfClass:[NSArray class]]) { + NSArray* array = static_cast(object_); + if (property == "length") { + return static_cast(array.count); + } + if (auto index = parseArrayIndexProperty(property)) { + if (*index >= array.count) { + return Value::undefined(); + } + id element = [array objectAtIndex:*index]; + NativeApiType elementType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, elementType, &element); + } + } + + 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)) { + 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); + } + } + + // 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; + } + } + } + } + + Value prototypeFunction = prototypeFunctionForProperty(runtime, property); + if (!prototypeFunction.isUndefined()) { + return prototypeFunction; + } + + // JS-subclassed instances own their members in JS (prototype accessors and + // methods); defer so the engine resolves them instead of the bridge + // returning a registered getter IMP as a raw callable. + if (isEngineExtendedInstance) { +#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE + // Engines whose exotic property handler invokes prototype accessors with + // the wrong receiver need the JS-prototype getter resolved here with this + // instance as the receiver. + bool found = false; + Value resolved = resolveEnginePrototypeGetter(runtime, property, &found); + if (found) { + return resolved; + } +#endif + if (auto selector = + runtimeReadablePropertyGetter(object_, property)) { + return callObjectSelector(runtime, *selector, nullptr, nullptr, 0); + } + return Value::undefined(); + } + + if (object_ != 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(); + } + + NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (object_ == nil) { + throw JSError(runtime, "Cannot set property on nil object."); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly) { + throw JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, + setterMember.selectorName, &setterMember, args, 1); + NATIVE_API_SET_RETURN(true); + } + } + + if (auto setterSelectorName = + runtimeWritablePropertySetter(object_, property)) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, + *setterSelectorName, nullptr, args, 1); + NATIVE_API_SET_RETURN(true); + } + + // For JS-subclassed instances, an unknown property is owned by the JS + // prototype (e.g. a JS-defined accessor); defer so the engine runs it instead of + // shadowing it with a bridge expando. + if (class_conformsToProtocol(object_getClass(object_), + @protocol(NativeApiClassBuilderProtocol))) { +#ifdef NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE + // Engines whose exotic property storage doesn't fall back to own + // properties need the JS-owned set resolved here: invoke a JS-prototype + // setter if present, otherwise store the value as a bridge expando. + bool invokedPrototypeSetter = + invokeEnginePrototypeSetter(runtime, property, value); + if (!invokedPrototypeSetter) { + 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 { + std::vector names; + names.reserve(6); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "className"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "constructor"); + addPropertyName(runtime, names, "superclass"); + addPropertyName(runtime, names, "super"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + addPropertyName(runtime, names, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id object_ = nil; + bool ownsObject_ = false; + bool wrapperRetainedObject_ = false; + bool consumed_ = false; + std::shared_ptr lifetimeState_; +}; diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm new file mode 100644 index 000000000..5c689f627 --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm @@ -0,0 +1,318 @@ +class NativeApiProtocolHostObject final : public HostObject { + public: + NativeApiProtocolHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Protocol* nativeProtocol() const { + Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); + if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { + protocol = lookupProtocolByNativeName(symbol_.name); + } + return protocol; + } + + const NativeApiSymbol& symbol() const { return symbol_; } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "protocol"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return nativeProtocol() != nullptr; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "nativeAddress") { + return static_cast( + reinterpret_cast(nativeProtocol())); + } + if (property == "prototype") { + Object prototype(runtime); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + if (prototype.hasProperty(runtime, member.name.c_str())) { + continue; + } + if (member.property) { + defineProtocolProperty(runtime, prototype, member, false); + } else { + prototype.setProperty(runtime, member.name.c_str(), + makeProtocolMemberFunction(runtime, member, + false)); + } + } + return prototype; + } + if (property == "toString") { + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiProtocol " + symbol.name + "]"); + }); + } + const auto& members = bridge_->membersForProtocol(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + 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); + } + } + 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(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "prototype"); + addPropertyName(runtime, names, "toString"); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + addPropertyName(runtime, names, member.name.c_str()); + } + return names; + } + + private: + static Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) { + if (!thisValue.isObject()) { + return Nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == "class") { + Value runtimeNameValue = object.getProperty(runtime, "runtimeName"); + if (!runtimeNameValue.isString()) { + runtimeNameValue = object.getProperty(runtime, "name"); + } + if (runtimeNameValue.isString()) { + std::string runtimeName = + runtimeNameValue.asString(runtime).utf8(runtime); + return objc_lookUpClass(runtimeName.c_str()); + } + } + + return Nil; + } + + id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { + if (!thisValue.isObject()) { + return nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + + return nil; + } + + Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw JSError( + runtime, "Protocol member requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, args, count); + }); + } + + Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value*, size_t) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw 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, + getterMember.selectorName, &getterMember, + nullptr, 0); + }); + } + + Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), + 1, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw JSError( + runtime, "Protocol property requires a native receiver."); + } + if (count < 1) { + throw JSError( + runtime, "Protocol property setter expects a value."); + } + + NativeApiMember setterMember = member; + setterMember.selectorName = member.setterSelectorName; + setterMember.signatureOffset = member.setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + setterMember.selectorName, &setterMember, + args, 1); + }); + } + + void defineProtocolProperty(Runtime& runtime, Object& target, + const NativeApiMember& member, + bool receiverIsClass) const { + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty(runtime, "enumerable", true); + descriptor.setProperty(runtime, "get", + makeProtocolPropertyGetter(runtime, member, + receiverIsClass)); + if (!member.readonly && !member.setterSelectorName.empty()) { + descriptor.setProperty(runtime, "set", + makeProtocolPropertySetter(runtime, member, + receiverIsClass)); + } + defineProperty.call(runtime, target, makeString(runtime, member.name), + descriptor); + } catch (const std::exception&) { + } + } + + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeProtocolValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Class nativeClassFromEngineObject(Runtime& runtime, const Object& object) { + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + return Nil; +} diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm new file mode 100644 index 000000000..a71cea169 --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm @@ -0,0 +1,47 @@ +class NativeApiStructObjectHostObject final : public HostObject { + public: + NativeApiStructObjectHostObject( + std::shared_ptr bridge, + std::shared_ptr info, + const void* data = nullptr, bool ownsData = true, + std::shared_ptr> storageOwner = nullptr, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + info_(std::move(info)), + ownedData_(std::move(storageOwner)), + backingValue_(std::move(backingValue)), + ownsData_(ownsData) { + size_t size = info_ != nullptr ? info_->size : 0; + if (ownedData_ != nullptr) { + data_ = const_cast(data); + ownsData_ = false; + } else if (ownsData_) { + ownedData_ = std::make_shared>(size, 0); + if (data != nullptr && size > 0) { + std::memcpy(ownedData_->data(), data, size); + } + data_ = ownedData_->empty() ? nullptr : ownedData_->data(); + } else { + data_ = const_cast(data); + } + } + + void* data() const { return data_; } + std::shared_ptr info() const { return info_; } + std::shared_ptr> storageOwner() const { + return ownedData_; + } + std::shared_ptr backingValue() const { return backingValue_; } + + Value get(Runtime& runtime, const PropNameID& name) override; + 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> ownedData_; + std::shared_ptr backingValue_; + void* data_ = nullptr; + bool ownsData_ = true; +}; diff --git a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h b/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h index 0d43fed55..7800c230e 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h +++ b/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h @@ -354,6 +354,11 @@ class Value { return value; } + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + return lhs.local(runtime)->StrictEquals(rhs.local(runtime)); + } + bool isUndefined() const; bool isNull() const; bool isBool() const; diff --git a/platforms/apple/test/cli/benchmark/run_foundation_bench.js b/platforms/apple/test/cli/benchmark/run_foundation_bench.js index 46b832a28..d1570f054 100644 --- a/platforms/apple/test/cli/benchmark/run_foundation_bench.js +++ b/platforms/apple/test/cli/benchmark/run_foundation_bench.js @@ -168,16 +168,12 @@ function printComparison(labelA, dataA, labelB, dataB) { async function main() { const opts = parseArgs(process.argv); - const repoRoot = path.resolve(__dirname, "..", ".."); + const repoRoot = path.resolve(__dirname, "..", "..", "..", "..", ".."); const benchScriptPath = path.resolve(__dirname, "foundation_calls.js"); - const gsdPath = path.join(repoRoot, "dist", "nsr-gsd"); - const nonGsdPath = path.join(repoRoot, "dist", "nsr"); + const runtimePath = path.join(repoRoot, "dist", "nsr"); - if (!fs.existsSync(gsdPath)) { - throw new Error(`Missing runtime: ${gsdPath}`); - } - if (!fs.existsSync(nonGsdPath)) { - throw new Error(`Missing runtime: ${nonGsdPath}`); + if (!fs.existsSync(runtimePath)) { + throw new Error(`Missing runtime: ${runtimePath}`); } const gsdRuns = []; @@ -188,18 +184,18 @@ async function main() { const runGsdFirst = (i & 1) === 0; if (runGsdFirst) { console.log(`Running iteration ${iteration}/${opts.repeat} with GSD runtime...`); - gsdRuns.push(await runOnce(gsdPath, benchScriptPath, repoRoot)); + gsdRuns.push(await runOnce(runtimePath, benchScriptPath, repoRoot)); console.log(`Running iteration ${iteration}/${opts.repeat} with non-GSD runtime...`); nonGsdRuns.push( - await runOnce(nonGsdPath, benchScriptPath, repoRoot, { NS_DISABLE_GSD: "1" }), + await runOnce(runtimePath, benchScriptPath, repoRoot, { NS_DISABLE_GSD: "1" }), ); } else { console.log(`Running iteration ${iteration}/${opts.repeat} with non-GSD runtime...`); nonGsdRuns.push( - await runOnce(nonGsdPath, benchScriptPath, repoRoot, { NS_DISABLE_GSD: "1" }), + await runOnce(runtimePath, benchScriptPath, repoRoot, { NS_DISABLE_GSD: "1" }), ); console.log(`Running iteration ${iteration}/${opts.repeat} with GSD runtime...`); - gsdRuns.push(await runOnce(gsdPath, benchScriptPath, repoRoot)); + gsdRuns.push(await runOnce(runtimePath, benchScriptPath, repoRoot)); } } diff --git a/platforms/apple/test/cli/memory/run_memory_semantics_tests.js b/platforms/apple/test/cli/memory/run_memory_semantics_tests.js index 6a20fb2f8..09266299d 100644 --- a/platforms/apple/test/cli/memory/run_memory_semantics_tests.js +++ b/platforms/apple/test/cli/memory/run_memory_semantics_tests.js @@ -19,6 +19,8 @@ const semanticsTests = [ "test_block_completion_safety.js", "test_block_callback_finalization.js", "test_c_function_pointer_semantics.js", + "test_circular_native_wrapper_finalization.js", + "test_circular_js_to_native_conversion.js", ]; function resolveSemanticsTests(memoryDir, grep) { @@ -118,7 +120,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/platforms/apple/test/cli/memory/run_memory_tests.js b/platforms/apple/test/cli/memory/run_memory_tests.js index 511e71b52..f4a130a4b 100644 --- a/platforms/apple/test/cli/memory/run_memory_tests.js +++ b/platforms/apple/test/cli/memory/run_memory_tests.js @@ -25,6 +25,8 @@ const memoryThresholdsKB = { "reference-lifecycle": 40 * 1024, "block-callback-finalization": 40 * 1024, "c-function-pointer-semantics": 40 * 1024, + "circular-native-wrapper-finalization": 60 * 1024, + "circular-js-to-native-conversion": 60 * 1024, }; const kMinValidRssKB = 4 * 1024; @@ -32,7 +34,7 @@ const kMinValidRssKB = 4 * 1024; function parseArgs(argv) { const args = argv.slice(2); const parsed = { - timeoutMs: 45_000, + timeoutMs: 120_000, repeat: 2, grep: null, runtime: null, @@ -59,7 +61,7 @@ function parseArgs(argv) { } if (!Number.isFinite(parsed.timeoutMs) || parsed.timeoutMs <= 0) { - parsed.timeoutMs = 45_000; + parsed.timeoutMs = 120_000; } if (!Number.isFinite(parsed.repeat) || parsed.repeat <= 0) { parsed.repeat = 1; @@ -213,7 +215,9 @@ async function runSingleTest({ nsrPath, cwd, testFile, timeoutMs }) { const logicalName = parsedResult && parsedResult.name ? parsedResult.name - : path.basename(testFile, ".js"); + : path.basename(testFile, ".js") + .replace(/^test_/, "") + .replace(/_/g, "-"); const driftThresholdKB = memoryThresholdsKB[logicalName] ?? (80 * 1024); const memoryPass = driftKB == null ? false : driftKB <= driftThresholdKB; @@ -273,7 +277,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/platforms/apple/test/cli/memory/test_circular_js_to_native_conversion.js b/platforms/apple/test/cli/memory/test_circular_js_to_native_conversion.js new file mode 100644 index 000000000..3b0170fe0 --- /dev/null +++ b/platforms/apple/test/cli/memory/test_circular_js_to_native_conversion.js @@ -0,0 +1,103 @@ +"use strict"; + +const { runPlainMemoryTest } = require("./_plain_harness"); + +runPlainMemoryTest("circular-js-to-native-conversion", async (t) => { + const iterations = 160; + const roots = []; + const wrappers = []; + const nativeObjects = NSHashTable.weakObjectsHashTable(); + let rejected = 0; + + function makeCircularGraph(index, native) { + switch (index % 5) { + case 0: { + const array = [native]; + array.push(array); + return array; + } + case 1: { + const object = { native }; + object.self = object; + return object; + } + case 2: { + const array = [native]; + const object = { array }; + array.push(object); + return object; + } + case 3: { + const map = new Map([["native", native]]); + map.set("self", map); + return map; + } + default: { + const arrayLike = { 0: native, length: 2 }; + arrayLike[1] = arrayLike; + return arrayLike; + } + } + } + + for (let i = 0; i < iterations; i++) { + (function attemptCircularConversion() { + const native = NSMutableDictionary.dictionary(); + native.setObjectForKey(NSNumber.numberWithInt(i), "index"); + nativeObjects.addObject(native); + + const root = makeCircularGraph(i, native); + roots.push(new WeakRef(root)); + wrappers.push(new WeakRef(native)); + + try { + NSArray.arrayWithArray(root); + } catch (error) { + t.assert( + String(error).includes("Circular JavaScript object graphs"), + `unexpected circular conversion error: ${error}`, + ); + rejected++; + return; + } + + throw new Error("circular JS-to-native conversion was accepted"); + })(); + + if ((i + 1) % 20 === 0) { + await t.forceGC(2, 8 * 1024 * 1024, 3); + } + } + + t.assert(rejected === iterations, `rejected ${rejected}/${iterations}`); + + const collected = await t.forceCollectUntil(() => { + return t.countAliveWeakRefs(roots) <= 2 && + t.countAliveWeakRefs(wrappers) <= 2 && + t.weakTableCount(nativeObjects) <= 2; + }, { + timeoutMs: 14_000, + intervalMs: 25, + gcRounds: 2, + pressureBytes: 16 * 1024 * 1024, + pauseMs: 4, + }); + + const rootsAlive = t.countAliveWeakRefs(roots); + const wrappersAlive = t.countAliveWeakRefs(wrappers); + const nativeAlive = t.weakTableCount(nativeObjects); + + t.assert( + collected, + `rejected circular conversions retained values roots=${rootsAlive} wrappers=${wrappersAlive} native=${nativeAlive}`, + ); + + return { + iterations, + rejected, + rootsAlive, + wrappersAlive, + nativeAlive, + engine: t.engine, + }; +}, { timeoutMs: 24_000 }); diff --git a/platforms/apple/test/cli/memory/test_circular_native_wrapper_finalization.js b/platforms/apple/test/cli/memory/test_circular_native_wrapper_finalization.js new file mode 100644 index 000000000..93d9f31d8 --- /dev/null +++ b/platforms/apple/test/cli/memory/test_circular_native_wrapper_finalization.js @@ -0,0 +1,115 @@ +"use strict"; + +const vm = require("node:vm"); +const { runPlainMemoryTest } = require("./_plain_harness"); + +runPlainMemoryTest("circular-native-wrapper-finalization", async (t) => { + const rounds = 8; + const objectsPerRound = 80; + const total = rounds * objectsPerRound; + const rootWeakRefs = []; + const wrapperWeakRefs = []; + const nativeWeakObjects = NSHashTable.weakObjectsHashTable(); + + async function sampleHeap() { + await t.forceGC(2, 8 * 1024 * 1024, 4); + const usage = process.memoryUsage(); + const measured = await vm.measureMemory({ mode: "summary" }); + return { + heapUsed: Number(usage.heapUsed) || 0, + estimate: + Number(measured && measured.total && measured.total.jsMemoryEstimate) || 0, + }; + } + + const before = await sampleHeap(); + + for (let round = 0; round < rounds; round++) { + (function createCircularGraphs() { + for (let i = 0; i < objectsPerRound; i++) { + const index = round * objectsPerRound + i; + const native = NSMutableDictionary.dictionary(); + native.setObjectForKey(NSNumber.numberWithInt(index), "index"); + nativeWeakObjects.addObject(native); + + const root = { + index, + native, + payload: new Uint8Array(4 * 1024), + }; + const peer = { root, native }; + const array = [root, peer, native]; + const map = new Map(); + const set = new Set(); + + root.self = root; + root.peer = peer; + root.array = array; + root.map = map; + root.set = set; + peer.peer = peer; + array.push(array); + map.set(root, peer); + map.set(map, native); + set.add(root); + set.add(set); + + rootWeakRefs.push(new WeakRef(root)); + wrapperWeakRefs.push(new WeakRef(native)); + } + })(); + + await t.forceGC(2, 8 * 1024 * 1024, 4); + } + + const collected = await t.forceCollectUntil(() => { + return t.countAliveWeakRefs(rootWeakRefs) <= 2 && + t.countAliveWeakRefs(wrapperWeakRefs) <= 2 && + t.weakTableCount(nativeWeakObjects) <= 2; + }, { + timeoutMs: 14_000, + intervalMs: 25, + gcRounds: 2, + pressureBytes: 16 * 1024 * 1024, + pauseMs: 4, + }); + + const afterSamples = []; + for (let i = 0; i < 3; i++) { + afterSamples.push(await sampleHeap()); + } + const after = afterSamples.reduce((best, sample) => { + return sample.heapUsed < best.heapUsed ? sample : best; + }); + + const rootsAlive = t.countAliveWeakRefs(rootWeakRefs); + const wrappersAlive = t.countAliveWeakRefs(wrapperWeakRefs); + const nativeAlive = t.weakTableCount(nativeWeakObjects); + const heapDrift = after.heapUsed - before.heapUsed; + const heapDriftLimit = 8 * 1024 * 1024; + + t.assert( + collected, + `circular native wrapper graphs were retained roots=${rootsAlive} wrappers=${wrappersAlive} native=${nativeAlive}`, + ); + if (before.heapUsed > 0 && after.heapUsed > 0) { + t.assert( + heapDrift <= heapDriftLimit, + `JS heap retained circular wrapper graphs drift=${heapDrift} limit=${heapDriftLimit}`, + ); + } + + return { + rounds, + objectsPerRound, + total, + rootsAlive, + wrappersAlive, + nativeAlive, + heapBefore: before, + heapAfter: after, + heapDrift, + heapDriftLimit, + engine: t.engine, + }; +}, { timeoutMs: 24_000 }); From 3de5177e1fd5a471fd8a1f0ddc52ff3cb02c6432 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 2 Aug 2026 19:14:10 -0400 Subject: [PATCH 2/2] refactor(runtime): consolidate engine selector dispatch --- NativeScript/ffi/objc/hermes/NativeApiJsi.mm | 207 +++----------- .../ffi/objc/jsc/NativeApiJSCHostObjects.mm | 41 +-- .../objc/jsc/NativeApiJSCSelectorGroups.mm | 254 +++--------------- .../quickjs/NativeApiQuickJSHostObjects.mm | 41 +-- .../quickjs/NativeApiQuickJSSelectorGroups.mm | 226 ++-------------- .../objc/shared/NativeApiStackValueArray.h | 47 ++++ .../ffi/objc/shared/bridge/Callbacks.mm | 13 - .../ffi/objc/shared/bridge/ClassBuilder.mm | 1 - .../ffi/objc/shared/bridge/HostObject.mm | 8 - .../ffi/objc/shared/bridge/ObjCBridge.mm | 20 -- .../objc/shared/bridge/SelectorGroupCall.h | 184 +++++++++++++ .../objc/shared/bridge/SelectorGroupData.h | 24 ++ .../objc/shared/bridge/SelectorGroupState.h | 32 +++ .../ffi/objc/v8/NativeApiV8HostObjects.mm | 41 +-- .../ffi/objc/v8/NativeApiV8SelectorGroups.mm | 251 +++-------------- 15 files changed, 432 insertions(+), 958 deletions(-) create mode 100644 NativeScript/ffi/objc/shared/NativeApiStackValueArray.h create mode 100644 NativeScript/ffi/objc/shared/bridge/SelectorGroupCall.h create mode 100644 NativeScript/ffi/objc/shared/bridge/SelectorGroupData.h create mode 100644 NativeScript/ffi/objc/shared/bridge/SelectorGroupState.h diff --git a/NativeScript/ffi/objc/hermes/NativeApiJsi.mm b/NativeScript/ffi/objc/hermes/NativeApiJsi.mm index e7f508d7a..6039fb8a4 100644 --- a/NativeScript/ffi/objc/hermes/NativeApiJsi.mm +++ b/NativeScript/ffi/objc/hermes/NativeApiJsi.mm @@ -91,6 +91,8 @@ void SetNativeApiObjectPrototype(Runtime& runtime, Object& object, #include "NativeApiJsiGsd.mm" +#include "../shared/bridge/SelectorGroupCall.h" + void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); @@ -123,40 +125,25 @@ Function CreateNativeApiSelectorGroupFunctionImpl( std::shared_ptr< std::vector>> preparedInvocations, - std::weak_ptr boundReceiver = {}, - std::shared_ptr boundReceiverState = - nullptr) { + std::weak_ptr boundReceiver, + std::shared_ptr boundReceiverState) { + NativeApiSelectorGroupState state( + std::move(bridge), lookupClass, receiverIsClass, std::move(selectors), + std::move(preparedInvocations), std::move(boundReceiver), + std::move(boundReceiverState)); 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)]( + [state = std::move(state)]( 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; + NativeApiRoundTripCacheFrameGuard roundTripFrame(state.bridge); 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."); - } + auto resolveReceiverHost = [&]() { + if (receiverHostObject) { + return receiverHostObject; + } + if (state.boundReceiverState != nullptr) { + receiverHostObject = state.boundReceiver.lock(); } else if (thisValue.isObject()) { Object receiverObject = thisValue.asObject(runtime); if (receiverObject.isHostObject( @@ -164,166 +151,58 @@ throw JSError(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); } } + return receiverHostObject; + }; + auto call = resolveNativeApiSelectorGroupCall( + runtime, state, count, + [&]() -> id { + auto host = resolveReceiverHost(); + return host != nullptr ? host->object() : nil; + }, + resolveReceiverHost, + [](uint64_t dispatchId) { + return lookupObjCGsdInvoker(dispatchId); + }); + if (call.hasImmediateResult) { + return std::move(call.immediateResult); } - // Memoized dispatch-superclass resolution (pure function of the - // receiver's class + lookupClass) — avoids a per-call - // class_conformsToProtocol probe. - Class gsdDispatchClass = Nil; - if (!receiverIsClass) { - Class receiverClass = object_getClass(receiver); - if (receiverClass == cachedReceiverClass) { - gsdDispatchClass = cachedDispatchClass; - } else { - gsdDispatchClass = - dispatchSuperclassForEngineDerivedReceiver(receiver, lookupClass); - cachedReceiverClass = receiverClass; - cachedDispatchClass = gsdDispatchClass; - } - } // GSD fast path: read jsi args directly, call objc_msgSend with a // typed cast, produce the jsi return value — bypassing all generic // marshalling. Only engages for plain calls (no super dispatch, init // disown handling, or implicit NSError-out argument). - if (prepared->gsdEngineCallable && gsdDispatchClass == Nil && - count == prepared->gsdEngineArgumentCount && - !(!receiverIsClass && prepared->isInitMethod)) { + if (call.prepared->gsdEngineCallable && call.dispatchClass == Nil && + count == call.prepared->gsdEngineArgumentCount && + !(!state.receiverIsClass && call.prepared->isInitMethod)) { auto invoker = - reinterpret_cast(prepared->engineInvoker); - GsdObjCContext ctx{runtime, bridge, receiver, prepared->selector, - args, prepared->signature.returnType}; + reinterpret_cast(call.prepared->engineInvoker); + GsdObjCContext ctx{runtime, state.bridge, call.receiver, + call.prepared->selector, args, + call.prepared->signature.returnType}; if (invoker(ctx)) { return std::move(ctx.result); } } - if (receiverIsClass) { - return callPreparedObjCSelector(runtime, bridge, receiver, true, - *prepared, args, count, Nil); + if (state.receiverIsClass) { + return callPreparedObjCSelector(runtime, state.bridge, call.receiver, + true, *call.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); - } - } + receiverHostObject = resolveReceiverHost(); } if (!receiverHostObject) { throw JSError(runtime, "Objective-C selector requires a native receiver."); } return receiverHostObject->callPreparedObjectSelector( - runtime, *prepared, args, count, gsdDispatchClass); + runtime, *call.prepared, args, count, call.dispatchClass); }); } -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" diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm b/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm index 7af17b3e4..8c9f03a7a 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm +++ b/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm @@ -1,4 +1,5 @@ #include "NativeApiJSCRuntime.h" +#include "../shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_JSC @@ -15,44 +16,6 @@ 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") || @@ -159,7 +122,7 @@ JSValueRef functionCall(JSContextRef context, JSObjectRef function, JSObjectRef return JSValueMakeUndefined(context); } Runtime runtime(holder->state); - StackValueArray<8> args(argumentCount); + StackValueArray args(argumentCount); for (size_t i = 0; i < argumentCount; i++) { args.emplace(i, Value::borrowed(runtime, arguments[i])); } diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCSelectorGroups.mm b/NativeScript/ffi/objc/jsc/NativeApiJSCSelectorGroups.mm index 52f775102..1020e8cf2 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCSelectorGroups.mm +++ b/NativeScript/ffi/objc/jsc/NativeApiJSCSelectorGroups.mm @@ -1,49 +1,13 @@ // Included by NativeApiJSC.mm inside the NativeScript anonymous namespace. -struct NativeApiSelectorGroupData { - NativeApiSelectorGroupData( - std::shared_ptr state, - std::shared_ptr bridge, Class lookupClass, - bool receiverIsClass, - std::shared_ptr> - selectors, - std::shared_ptr< - std::vector>> - preparedInvocations, - std::weak_ptr boundReceiver = {}, - std::shared_ptr boundReceiverState = - nullptr) - : state(state), - bridge(std::move(bridge)), - lookupClass(lookupClass), - receiverIsClass(receiverIsClass), - selectors(std::move(selectors)), - preparedInvocations(std::move(preparedInvocations)), - boundReceiver(std::move(boundReceiver)), - boundReceiverState(std::move(boundReceiverState)), - runtime(state) {} - - std::shared_ptr state; - std::shared_ptr bridge; - Class lookupClass = Nil; - bool receiverIsClass = false; - std::shared_ptr> selectors; - std::shared_ptr< - std::vector>> - preparedInvocations; - std::weak_ptr boundReceiver; - std::shared_ptr boundReceiverState; - // Reused per call (avoids per-call shared_ptr refcount + dispatch-superclass - // probe on the hot path). - Runtime runtime; - Class cachedReceiverClass = Nil; - Class cachedDispatchClass = Nil; -}; +#include "../shared/bridge/SelectorGroupData.h" #include "NativeApiJSCMarshalling.mm" #include "NativeApiJSCGsd.mm" +#include "../shared/bridge/SelectorGroupCall.h" + void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); @@ -226,156 +190,47 @@ JSValueRef NativeApiSelectorGroupCall( 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); - } + auto resolveObjectHost = [&]() + -> engine::jscengine::HostObjectHolder* { + if (thisObject == nullptr) { + return nullptr; } - } - - 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); + auto* holder = static_cast( + JSObjectGetPrivate(thisObject)); + return holder != nullptr && + holder->typeToken == + engine::jscengine::hostObjectTypeToken< + NativeApiObjectHostObject>() + ? holder + : nullptr; + }; + auto call = resolveNativeApiSelectorGroupCall( + runtime, *data, argumentCount, + [&]() -> id { + auto* holder = resolveObjectHost(); + return holder != nullptr + ? static_cast( + holder->hostObject.get())->object() + : nil; + }, + [&]() -> std::shared_ptr { + if (data->boundReceiverState != nullptr) { + return nullptr; } - } - } - 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; - } + auto* holder = resolveObjectHost(); + return holder != nullptr + ? std::static_pointer_cast( + holder->hostObject) + : nullptr; + }, + [](uint64_t dispatchId) { return lookupObjCGsdInvoker(dispatchId); }); + if (call.hasImmediateResult) { + return call.immediateResult.local(runtime); } return setJSCEnginePreparedObjCResult( - runtime, data->bridge, receiver, *prepared, receiverHostObject, - initializerClassWrapper, argumentCount, arguments, dispatchClass); + runtime, data->bridge, call.receiver, *call.prepared, + call.receiverHostObject, call.initializerClassWrapper, argumentCount, + arguments, call.dispatchClass); } catch (const std::exception& error) { engine::jscengine::setException(context, exception, error); return JSValueMakeUndefined(context); @@ -407,8 +262,7 @@ Function CreateNativeApiSelectorGroupFunctionImpl( std::vector>> preparedInvocations, std::weak_ptr boundReceiver, - std::shared_ptr boundReceiverState = - nullptr) { + std::shared_ptr boundReceiverState) { auto* data = new NativeApiSelectorGroupData( runtime.state(), std::move(bridge), lookupClass, receiverIsClass, std::move(selectors), std::move(preparedInvocations), @@ -430,29 +284,3 @@ Function CreateNativeApiSelectorGroupFunctionImpl( Value functionValue(runtime, function); return functionValue.asObject(runtime).asFunction(runtime); } - -Function CreateNativeApiSelectorGroupFunction( - Runtime& runtime, std::shared_ptr bridge, - Class lookupClass, bool receiverIsClass, - std::shared_ptr> selectors, - std::shared_ptr< - std::vector>> - preparedInvocations) { - return CreateNativeApiSelectorGroupFunctionImpl( - runtime, std::move(bridge), lookupClass, receiverIsClass, - std::move(selectors), std::move(preparedInvocations), {}, nullptr); -} - -Function CreateNativeApiBoundSelectorGroupFunction( - Runtime& runtime, std::shared_ptr bridge, Class lookupClass, - std::shared_ptr receiverHostObject, - std::shared_ptr> selectors, - std::shared_ptr< - std::vector>> - preparedInvocations) { - return CreateNativeApiSelectorGroupFunctionImpl( - runtime, std::move(bridge), lookupClass, false, std::move(selectors), - std::move(preparedInvocations), receiverHostObject, - receiverHostObject != nullptr ? receiverHostObject->lifetimeState() - : nullptr); -} diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm index 5917fd686..15532e17f 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm +++ b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm @@ -1,4 +1,5 @@ #include "NativeApiQuickJSRuntime.h" +#include "../shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_QUICKJS @@ -26,44 +27,6 @@ } } // 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(); @@ -261,7 +224,7 @@ static JSValue invokeFunctionHolder(JSContext* ctx, FunctionHolder* holder, JSVa if (holder == nullptr || !holder->callback) { return JS_UNDEFINED; } - StackValueArray<8> args(static_cast(argc)); + StackValueArray args(static_cast(argc)); for (int i = 0; i < argc; i++) { args.emplace(static_cast(i), Value::borrowed(runtime, argv[i])); } diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSSelectorGroups.mm b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSSelectorGroups.mm index 26d0f1415..bb9fe56c2 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSSelectorGroups.mm +++ b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSSelectorGroups.mm @@ -1,47 +1,13 @@ // Included by NativeApiQuickJS.mm inside the NativeScript anonymous namespace. -struct NativeApiSelectorGroupData { - NativeApiSelectorGroupData( - std::shared_ptr state, - std::shared_ptr bridge, Class lookupClass, - bool receiverIsClass, - std::shared_ptr> - selectors, - std::shared_ptr< - std::vector>> - preparedInvocations, - std::weak_ptr boundReceiver = {}, - std::shared_ptr boundReceiverState = - nullptr) - : state(state), - bridge(std::move(bridge)), - lookupClass(lookupClass), - receiverIsClass(receiverIsClass), - selectors(std::move(selectors)), - preparedInvocations(std::move(preparedInvocations)), - boundReceiver(std::move(boundReceiver)), - boundReceiverState(std::move(boundReceiverState)), - runtime(state) {} - - std::shared_ptr state; - std::shared_ptr bridge; - Class lookupClass = Nil; - bool receiverIsClass = false; - std::shared_ptr> selectors; - std::shared_ptr< - std::vector>> - preparedInvocations; - std::weak_ptr boundReceiver; - std::shared_ptr boundReceiverState; - Runtime runtime; - Class cachedReceiverClass = Nil; - Class cachedDispatchClass = Nil; -}; +#include "../shared/bridge/SelectorGroupData.h" #include "NativeApiQuickJSMarshalling.mm" #include "NativeApiQuickJSGsd.mm" +#include "../shared/bridge/SelectorGroupCall.h" + void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); @@ -240,8 +206,8 @@ void EnsureNativeApiSelectorGroupClass(Runtime& runtime) { } JSValue NativeApiSelectorGroupCall(JSContext* context, JSValue thisValue, - int argc, JSValue* argv, int, - JSValue* dataValues) { + int argc, JSValue* argv, int, + JSValue* dataValues) { auto* data = static_cast( JS_GetOpaque(dataValues[0], gNativeApiSelectorGroupDataClassId)); if (data == nullptr || data->selectors == nullptr || @@ -253,142 +219,27 @@ JSValue NativeApiSelectorGroupCall(JSContext* context, JSValue thisValue, 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; - } + auto call = resolveNativeApiSelectorGroupCall( + runtime, *data, count, + [&]() -> id { + auto* host = quickJSHostObjectRaw( + runtime, thisValue); + return host != nullptr ? host->object() : nil; + }, + [&]() { + return data->boundReceiverState == nullptr + ? quickJSHostObject(runtime, + thisValue) + : nullptr; + }, + [](uint64_t dispatchId) { return lookupObjCGsdInvoker(dispatchId); }); + if (call.hasImmediateResult) { + return call.immediateResult.local(runtime); } return setQuickJSEnginePreparedObjCResult( - runtime, data->bridge, receiver, *prepared, receiverHostObject, - initializerClassWrapper, count, argv, dispatchClass); + runtime, data->bridge, call.receiver, *call.prepared, + call.receiverHostObject, call.initializerClassWrapper, count, argv, + call.dispatchClass); } catch (const std::exception& error) { return engine::quickjsengine::throwError(context, error); } @@ -402,8 +253,7 @@ Function CreateNativeApiSelectorGroupFunctionImpl( std::vector>> preparedInvocations, std::weak_ptr boundReceiver, - std::shared_ptr boundReceiverState = - nullptr) { + std::shared_ptr boundReceiverState) { EnsureNativeApiSelectorGroupClass(runtime); auto* data = new NativeApiSelectorGroupData( runtime.state(), std::move(bridge), lookupClass, receiverIsClass, @@ -436,29 +286,3 @@ Function CreateNativeApiSelectorGroupFunctionImpl( JS_FreeValue(runtime.context(), function); return result; } - -Function CreateNativeApiSelectorGroupFunction( - Runtime& runtime, std::shared_ptr bridge, - Class lookupClass, bool receiverIsClass, - std::shared_ptr> selectors, - std::shared_ptr< - std::vector>> - preparedInvocations) { - return CreateNativeApiSelectorGroupFunctionImpl( - runtime, std::move(bridge), lookupClass, receiverIsClass, - std::move(selectors), std::move(preparedInvocations), {}, nullptr); -} - -Function CreateNativeApiBoundSelectorGroupFunction( - Runtime& runtime, std::shared_ptr bridge, Class lookupClass, - std::shared_ptr receiverHostObject, - std::shared_ptr> selectors, - std::shared_ptr< - std::vector>> - preparedInvocations) { - return CreateNativeApiSelectorGroupFunctionImpl( - runtime, std::move(bridge), lookupClass, false, std::move(selectors), - std::move(preparedInvocations), receiverHostObject, - receiverHostObject != nullptr ? receiverHostObject->lifetimeState() - : nullptr); -} diff --git a/NativeScript/ffi/objc/shared/NativeApiStackValueArray.h b/NativeScript/ffi/objc/shared/NativeApiStackValueArray.h new file mode 100644 index 000000000..b91091ddd --- /dev/null +++ b/NativeScript/ffi/objc/shared/NativeApiStackValueArray.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +namespace nativescript::engine { + +template +class StackValueArray { + public: + explicit StackValueArray(size_t count) : count_(count) { + values_ = + count_ > InlineCount + ? static_cast( + ::operator new(sizeof(ValueType) * count_)) + : reinterpret_cast(inlineStorage_); + } + + ~StackValueArray() { + for (size_t i = 0; i < constructed_; i++) { + values_[i].~ValueType(); + } + if (count_ > InlineCount) { + ::operator delete(values_); + } + } + + StackValueArray(const StackValueArray&) = delete; + StackValueArray& operator=(const StackValueArray&) = delete; + + void emplace(size_t index, ValueType&& value) { + new (&values_[index]) ValueType(std::move(value)); + constructed_++; + } + + ValueType* data() { return count_ == 0 ? nullptr : values_; } + size_t size() const { return count_; } + + private: + size_t count_ = 0; + size_t constructed_ = 0; + ValueType* values_ = nullptr; + alignas(ValueType) unsigned char inlineStorage_[sizeof(ValueType) * InlineCount]; +}; + +} // namespace nativescript::engine diff --git a/NativeScript/ffi/objc/shared/bridge/Callbacks.mm b/NativeScript/ffi/objc/shared/bridge/Callbacks.mm index 9f9d2c0cd..179fbdf9b 100644 --- a/NativeScript/ffi/objc/shared/bridge/Callbacks.mm +++ b/NativeScript/ffi/objc/shared/bridge/Callbacks.mm @@ -2082,19 +2082,6 @@ bool prepareEngineMethodSignature(NativeApiSignature* signature) { return signature->prepared; } -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) { diff --git a/NativeScript/ffi/objc/shared/bridge/ClassBuilder.mm b/NativeScript/ffi/objc/shared/bridge/ClassBuilder.mm index 9c816f6e4..8141e7c41 100644 --- a/NativeScript/ffi/objc/shared/bridge/ClassBuilder.mm +++ b/NativeScript/ffi/objc/shared/bridge/ClassBuilder.mm @@ -556,7 +556,6 @@ throw JSError(runtime, throw JSError(runtime, "Failed to allocate Objective-C class."); } - markNativeApiExtendedClass(nativeClass); class_addProtocol(nativeClass, @protocol(NativeApiClassBuilderProtocol)); rememberNativeApiClassBuilder(runtime, bridge, nativeClass); diff --git a/NativeScript/ffi/objc/shared/bridge/HostObject.mm b/NativeScript/ffi/objc/shared/bridge/HostObject.mm index 2e671ed32..d16a91f96 100644 --- a/NativeScript/ffi/objc/shared/bridge/HostObject.mm +++ b/NativeScript/ffi/objc/shared/bridge/HostObject.mm @@ -18,14 +18,6 @@ inline bool InstallNativeApiLazyGlobal( #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) diff --git a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm index 80688018c..5baecf609 100644 --- a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm +++ b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm @@ -2,26 +2,6 @@ thread_local int gNativeCallerThreadEngineCallbackDepth = 0; thread_local std::vector gNativeCallbackExceptionCaptureStack; std::atomic gActiveSynchronousNativeInvocationDepth{0}; -static char gNativeApiExtendedClassKey; - -void markNativeApiExtendedClass(Class cls) { - if (cls == Nil) { - return; - } - objc_setAssociatedObject(cls, &gNativeApiExtendedClassKey, @YES, - OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -bool isNativeApiExtendedClass(Class cls) { - Class current = cls; - while (current != Nil) { - if (objc_getAssociatedObject(current, &gNativeApiExtendedClassKey) != nil) { - return true; - } - current = class_getSuperclass(current); - } - return false; -} class ScopedNativeApiSynchronousInvocation final { public: diff --git a/NativeScript/ffi/objc/shared/bridge/SelectorGroupCall.h b/NativeScript/ffi/objc/shared/bridge/SelectorGroupCall.h new file mode 100644 index 000000000..ba7f3887b --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/SelectorGroupCall.h @@ -0,0 +1,184 @@ +#pragma once + +#include "SelectorGroupState.h" + +struct NativeApiResolvedSelectorGroupCall { + id receiver = nil; + NativeApiPreparedObjCInvocation* prepared = nullptr; + std::shared_ptr receiverHostObject; + std::optional initializerClassWrapper; + Class dispatchClass = Nil; + bool hasImmediateResult = false; + Value immediateResult; +}; + +template +inline NativeApiResolvedSelectorGroupCall resolveNativeApiSelectorGroupCall( + Runtime& runtime, NativeApiSelectorGroupState& data, size_t argumentCount, + ResolveReceiver&& resolveReceiver, + ResolveReceiverHost&& resolveReceiverHost, + LookupGsdInvoker&& lookupGsdInvoker) { + if (argumentCount >= data.selectors->size() || + (*data.selectors)[argumentCount].selectorName.empty()) { + throw JSError( + runtime, + "Objective-C selector is not available for the provided arguments count."); + } + + NativeApiResolvedSelectorGroupCall result; + NativeApiSelectorGroupEntry& entry = (*data.selectors)[argumentCount]; + auto& prepared = (*data.preparedInvocations)[argumentCount]; + Class selectorLookupClass = data.lookupClass; + result.receiver = + data.receiverIsClass ? static_cast(data.lookupClass) : nil; + if (!data.receiverIsClass) { + result.receiver = data.boundReceiverState != nullptr + ? data.boundReceiverState->object() + : resolveReceiver(); + } + if (result.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( + result.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; + result.receiver = static_cast(methodClass); + } + if (propertyGetterCall && !callTargetCanPrepare) { + result.immediateResult = + callObjCSelector(runtime, data.bridge, result.receiver, + data.receiverIsClass, selectorName, selectedMember, + nullptr, 0); + result.hasImmediateResult = true; + return result; + } + + if (prepared == nullptr) { + if (!data.receiverIsClass) { + SEL selector = sel_registerName(selectorName.c_str()); + if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { + Class receiverClass = object_getClass(result.receiver); + if (class_getInstanceMethod(receiverClass, selector) != nullptr) { + selectorLookupClass = receiverClass; + } + } + } + prepared = prepareNativeApiObjCInvocation( + runtime, data.bridge, selectorLookupClass, data.receiverIsClass, + selectorName, selectedMember); + if (prepared->engineInvoker == nullptr) { + uint64_t dispatchId = dispatchIdForEngineSignature( + prepared->signature, SignatureCallKind::ObjCMethod); + if (auto gsdInvoker = lookupGsdInvoker(dispatchId)) { + prepared->engineInvoker = reinterpret_cast(gsdInvoker); + configureGeneratedEngineObjCInvocation(*prepared); + } + } + } + result.prepared = prepared.get(); + + if constexpr (PrepareInitializer) { + if (!data.receiverIsClass && prepared->isInitMethod) { + if (data.boundReceiverState != nullptr) { + result.receiverHostObject = data.boundReceiver.lock(); + } + if (!result.receiverHostObject) { + result.receiverHostObject = resolveReceiverHost(); + } + Value classWrapperValue = data.bridge->findObjectExpando( + runtime, result.receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + result.initializerClassWrapper.emplace( + classWrapperValue.asObject(runtime)); + } + data.bridge->forgetRoundTripValue(result.receiver); + data.bridge->forgetObjectExpandos(result.receiver); + } + } + + if (!data.receiverIsClass) { + Class receiverClass = object_getClass(result.receiver); + if (receiverClass == data.cachedReceiverClass) { + result.dispatchClass = data.cachedDispatchClass; + } else { + result.dispatchClass = dispatchSuperclassForEngineDerivedReceiver( + result.receiver, data.lookupClass); + data.cachedReceiverClass = receiverClass; + data.cachedDispatchClass = result.dispatchClass; + } + } + return result; +} + +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); + +inline 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); +} + +inline Function CreateNativeApiBoundSelectorGroupFunction( + Runtime& runtime, std::shared_ptr bridge, + Class lookupClass, + std::shared_ptr receiverHostObject, + std::shared_ptr> selectors, + std::shared_ptr< + std::vector>> + preparedInvocations) { + return CreateNativeApiSelectorGroupFunctionImpl( + runtime, std::move(bridge), lookupClass, false, std::move(selectors), + std::move(preparedInvocations), receiverHostObject, + receiverHostObject != nullptr ? receiverHostObject->lifetimeState() + : nullptr); +} diff --git a/NativeScript/ffi/objc/shared/bridge/SelectorGroupData.h b/NativeScript/ffi/objc/shared/bridge/SelectorGroupData.h new file mode 100644 index 000000000..d82878aa8 --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/SelectorGroupData.h @@ -0,0 +1,24 @@ +#pragma once + +#include "SelectorGroupState.h" + +struct NativeApiSelectorGroupData : NativeApiSelectorGroupState { + template + 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) + : NativeApiSelectorGroupState( + std::move(bridge), lookupClass, receiverIsClass, + std::move(selectors), std::move(preparedInvocations), + std::move(boundReceiver), std::move(boundReceiverState)), + runtime(std::move(state)) {} + + Runtime runtime; +}; diff --git a/NativeScript/ffi/objc/shared/bridge/SelectorGroupState.h b/NativeScript/ffi/objc/shared/bridge/SelectorGroupState.h new file mode 100644 index 000000000..175988210 --- /dev/null +++ b/NativeScript/ffi/objc/shared/bridge/SelectorGroupState.h @@ -0,0 +1,32 @@ +#pragma once + +struct NativeApiSelectorGroupState { + NativeApiSelectorGroupState( + 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) + : bridge(std::move(bridge)), + lookupClass(lookupClass), + receiverIsClass(receiverIsClass), + selectors(std::move(selectors)), + preparedInvocations(std::move(preparedInvocations)), + boundReceiver(std::move(boundReceiver)), + boundReceiverState(std::move(boundReceiverState)) {} + + 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; + Class cachedReceiverClass = Nil; + Class cachedDispatchClass = Nil; +}; diff --git a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm b/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm index 6064601ea..b71245931 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm +++ b/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm @@ -1,4 +1,5 @@ #include "NativeApiV8Runtime.h" +#include "../shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_V8 @@ -9,44 +10,6 @@ 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()) { @@ -379,7 +342,7 @@ void functionWeakCallback(const v8::WeakCallbackInfo& info) { auto* holder = static_cast(info.Data().As()->Value()); Runtime runtime(holder->state); - v8engine::StackValueArray<8> args(static_cast(info.Length())); + StackValueArray args(static_cast(info.Length())); for (int i = 0; i < info.Length(); i++) { args.emplace(static_cast(i), Value::borrowed(runtime, info[i])); } diff --git a/NativeScript/ffi/objc/v8/NativeApiV8SelectorGroups.mm b/NativeScript/ffi/objc/v8/NativeApiV8SelectorGroups.mm index 5b77573bd..66e621304 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8SelectorGroups.mm +++ b/NativeScript/ffi/objc/v8/NativeApiV8SelectorGroups.mm @@ -1,50 +1,13 @@ // Included by NativeApiV8.mm inside the NativeScript anonymous namespace. -struct NativeApiSelectorGroupData { - NativeApiSelectorGroupData( - std::shared_ptr state, - std::shared_ptr bridge, Class lookupClass, - bool receiverIsClass, - std::shared_ptr> - selectors, - std::shared_ptr< - std::vector>> - preparedInvocations, - std::weak_ptr boundReceiver = {}, - std::shared_ptr boundReceiverState = - nullptr) - : state(state), - bridge(std::move(bridge)), - lookupClass(lookupClass), - receiverIsClass(receiverIsClass), - selectors(std::move(selectors)), - preparedInvocations(std::move(preparedInvocations)), - boundReceiver(std::move(boundReceiver)), - boundReceiverState(std::move(boundReceiverState)), - runtime(state) {} - - std::shared_ptr state; - std::shared_ptr bridge; - Class lookupClass = Nil; - bool receiverIsClass = false; - std::shared_ptr> selectors; - std::shared_ptr< - std::vector>> - preparedInvocations; - std::weak_ptr boundReceiver; - std::shared_ptr boundReceiverState; - // Cached Runtime wrapper reused per call (avoids per-call shared_ptr - // atomic refcount on the hot dispatch path). - Runtime runtime; - // 1-entry memo for dispatchSuperclassForEngineDerivedReceiver. - Class cachedReceiverClass = Nil; - Class cachedDispatchClass = Nil; -}; +#include "../shared/bridge/SelectorGroupData.h" #include "NativeApiV8Marshalling.mm" #include "NativeApiV8Gsd.mm" +#include "../shared/bridge/SelectorGroupCall.h" + void* lookupGeneratedEngineObjCGsdInvoker(uint64_t dispatchId) { return reinterpret_cast(lookupObjCGsdInvoker(dispatchId)); @@ -222,173 +185,46 @@ void NativeApiSelectorGroupCallback( 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)); + auto call = resolveNativeApiSelectorGroupCall( + runtime, *data, count, + [&]() -> id { + // The V8 handle keeps this raw host object alive for the call. + auto* host = + v8HostObjectRaw(info.This()); + return host != nullptr ? host->object() : nil; + }, + [&]() { + return v8HostObject(runtime, info.This()); + }, + [](uint64_t dispatchId) { return lookupObjCGsdInvoker(dispatchId); }); + if (call.hasImmediateResult) { + info.GetReturnValue().Set(call.immediateResult.local(runtime)); return; } - - if (prepared == nullptr) { - // First call: resolve the method and cache the prepared invocation. - if (!data->receiverIsClass) { - SEL selector = sel_registerName(selectorName.c_str()); - if (class_getInstanceMethod(selectorLookupClass, selector) == nullptr) { - Class receiverClass = object_getClass(receiver); - if (class_getInstanceMethod(receiverClass, selector) != nullptr) { - selectorLookupClass = receiverClass; - } - } - } - prepared = prepareNativeApiObjCInvocation( - runtime, data->bridge, selectorLookupClass, data->receiverIsClass, - selectorName, selectedMember); - // Look up the engine-neutral GSD invoker for this signature. - if (prepared->engineInvoker == nullptr) { - uint64_t dispatchId = dispatchIdForEngineSignature( - prepared->signature, SignatureCallKind::ObjCMethod); - if (auto gsdInvoker = lookupObjCGsdInvoker(dispatchId)) { - prepared->engineInvoker = reinterpret_cast(gsdInvoker); - configureGeneratedEngineObjCInvocation(*prepared); - } - } - } - - std::optional initializerClassWrapper; - if (!data->receiverIsClass && prepared->isInitMethod) { - // Init methods need the shared_ptr for disown handling. - if (!receiverHostObject) { - if (data->boundReceiverState != nullptr) { - if (auto boundReceiver = data->boundReceiver.lock()) { - receiverHostObject = std::move(boundReceiver); - } - } - } - if (!receiverHostObject) { - receiverHostObject = - v8HostObject(runtime, info.This()); - } - Value classWrapperValue = data->bridge->findObjectExpando( - runtime, receiver, "__nativeApiClassWrapper"); - if (classWrapperValue.isObject()) { - initializerClassWrapper.emplace(classWrapperValue.asObject(runtime)); - } - data->bridge->forgetRoundTripValue(receiver); - data->bridge->forgetObjectExpandos(receiver); - } - - // For JS-extended receivers, dispatch from the immediate native - // superclass so native-derived overrides are honored (not the method's - // defining ancestor, which would skip intermediate native overrides). - // dispatchSuperclassForEngineDerivedReceiver is a pure function of the - // receiver's class + lookupClass, so memoize it (1-entry cache) to avoid a - // per-call class_conformsToProtocol on the hot path. - Class dispatchClass = Nil; - if (!data->receiverIsClass) { - Class receiverClass = object_getClass(receiver); - if (receiverClass == data->cachedReceiverClass) { - dispatchClass = data->cachedDispatchClass; - } else { - dispatchClass = dispatchSuperclassForEngineDerivedReceiver( - receiver, data->lookupClass); - data->cachedReceiverClass = receiverClass; - data->cachedDispatchClass = dispatchClass; - } - } // Inline GSD fast path: skip the setV8EnginePreparedObjCResult call and its // argument-count/NSError preamble entirely for the common case. The // generated invoker reads args, calls objc_msgSend, and sets the return. - if (prepared->gsdEngineCallable && dispatchClass == Nil && - !prepared->isInitMethod && - count == prepared->gsdEngineArgumentCount) { - auto invoker = reinterpret_cast(prepared->engineInvoker); + if (call.prepared->gsdEngineCallable && call.dispatchClass == Nil && + !call.prepared->isInitMethod && + count == call.prepared->gsdEngineArgumentCount) { + auto invoker = + reinterpret_cast(call.prepared->engineInvoker); GsdObjCContext ctx{runtime, data->bridge, - receiver, - prepared->selector, + call.receiver, + call.prepared->selector, info, runtime.isolate(), runtime.context(), - prepared->signature.returnType}; + call.prepared->signature.returnType}; if (invoker(ctx)) { return; } } - setV8EnginePreparedObjCResult(runtime, data->bridge, receiver, *prepared, - receiverHostObject, initializerClassWrapper, - info, dispatchClass); + setV8EnginePreparedObjCResult( + runtime, data->bridge, call.receiver, *call.prepared, + call.receiverHostObject, call.initializerClassWrapper, info, + call.dispatchClass); } catch (const std::exception& exception) { engine::v8engine::throwV8Exception(info.GetIsolate(), exception); } @@ -402,8 +238,7 @@ Function CreateNativeApiSelectorGroupFunctionImpl( std::vector>> preparedInvocations, std::weak_ptr boundReceiver, - std::shared_ptr boundReceiverState = - nullptr) { + std::shared_ptr boundReceiverState) { auto data = std::make_shared( runtime.state(), std::move(bridge), lookupClass, receiverIsClass, std::move(selectors), std::move(preparedInvocations), @@ -423,29 +258,3 @@ Function CreateNativeApiSelectorGroupFunctionImpl( 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); -}