Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 161 additions & 1 deletion NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,162 @@ explicit NativeApiBridge(const NativeApiConfig& config)
}
current = class_getSuperclass(current);
}
return nullptr;

// The class-name walk above found nothing at all: not even a distant
// ancestor (e.g. NSObject) is known to metadata. As a last resort,
// identify the object by the most specific conformed protocol metadata
// does know about (see forEachConformedProtocol for the deterministic
// traversal/tie-break order), so callers that just need *some* symbol to
// key off (building a class-identity/prototype value, for instance)
// still get one instead of nullptr.
//
// This is the rarer half of the protocol-method-metadata fallback. The
// common real-world case -- a class walk that *does* resolve to a known
// symbol (the object's own class, or an ancestor), but whose
// metadata-baked member table doesn't carry a method declared solely on
// a protocol the object conforms to only at the ObjC runtime level (a
// category/class-extension conformance the metadata generator never
// parsed, or a fully private concrete class such as the one behind
// UIViewControllerTransitionCoordinator) -- is handled by
// protocolMembersForRuntimeClass below, which callers consult once a
// normal member lookup on this symbol comes up empty.
const NativeApiSymbol* protocolSymbol = nullptr;
forEachConformedProtocol(cls, [&](Protocol* protocol) {
auto it = protocolSymbolsByRuntimePointer_.find(
normalizeRuntimePointer(reinterpret_cast<uintptr_t>(protocol)));
if (it != protocolSymbolsByRuntimePointer_.end()) {
protocolSymbol = &it->second;
return true;
}
return false;
});
return protocolSymbol;
}

// Invokes `visit` for every protocol that `cls` (or an ancestor in its
// *runtime* class hierarchy) conforms to, until `visit` returns true.
// class_copyProtocolList only reports protocols adopted directly at the
// queried class level -- it does not walk superclasses -- so this walks
// class_getSuperclass itself (mirroring findClassForRuntimeClass's own
// walk) to collect the full picture.
//
// Traversal order (this is the deterministic tie-break rule for a class
// that conforms to more than one protocol declaring the same selector):
// the most-derived class in cls's hierarchy is visited first, then each
// ancestor in turn; within one class's own adopted-protocol list, in the
// order class_copyProtocolList returns them (stable per compiled binary --
// it reflects the order protocols were listed in that class's
// @interface/category/extension); each protocol's own inherited protocols
// are expanded depth-first immediately after it, ahead of its next
// sibling. The first protocol in this order that satisfies the caller's
// `visit` wins. This mirrors the "closest/most-specific declaration wins"
// rule the class-hierarchy member composition elsewhere in this file
// already follows (see appendSurfaceMember).
template <typename Visitor>
void forEachConformedProtocol(Class cls, Visitor&& visit) const {
std::unordered_set<Protocol*> visited;
for (Class current = cls; current != Nil;
current = class_getSuperclass(current)) {
unsigned int count = 0;
Protocol* __unsafe_unretained* protocols =
class_copyProtocolList(current, &count);
if (protocols == nullptr) {
continue;
}
bool stop = false;
for (unsigned int i = 0; i < count && !stop; i++) {
stop = visitProtocolDepthFirst(protocols[i], visited, visit);
}
free(protocols);
if (stop) {
return;
}
}
}

template <typename Visitor>
bool visitProtocolDepthFirst(Protocol* protocol,
std::unordered_set<Protocol*>& visited,
Visitor&& visit) const {
if (protocol == nullptr || !visited.insert(protocol).second) {
return false;
}
if (visit(protocol)) {
return true;
}

unsigned int count = 0;
Protocol* __unsafe_unretained* inherited =
protocol_copyProtocolList(protocol, &count);
if (inherited == nullptr) {
return false;
}
bool stop = false;
for (unsigned int i = 0; i < count && !stop; i++) {
stop = visitProtocolDepthFirst(inherited[i], visited, visit);
}
free(inherited);
return stop;
}

// Members contributed by protocols `cls` conforms to at the Objective-C
// runtime level, restricted to protocols metadata knows about. This is
// the fallback for a selector declared solely on a protocol -- e.g. every
// method of UIViewControllerTransitionCoordinator -- where
// findClassForRuntimeClass's plain class_getSuperclass walk resolves to a
// real, known class symbol (the object's own class, or some ancestor),
// but that symbol's own member table doesn't carry the method, because
// metadata bakes conformance from what each class's *own* processed
// header declares, not from what the object conforms to at runtime.
//
// Cached per runtime Class (not per metadata symbol offset, the way
// membersForClass/surfaceMembersForClass are): two different runtime
// classes can both resolve to the same metadata symbol while conforming
// to different runtime-only protocols, so the metadata-symbol cache would
// be the wrong granularity here and could leak members across unrelated
// classes.
//
// Callers are expected to consult this only once an ordinary
// membersForClass/surfaceMembersForClass lookup has already missed, so
// classes with complete metadata (the common case) pay nothing extra.
const std::vector<NativeApiMember>& protocolMembersForRuntimeClass(
Class cls) const {
static const std::vector<NativeApiMember> kEmptyMembers;
if (cls == Nil) {
return kEmptyMembers;
}

uintptr_t key = normalizeRuntimePointer(reinterpret_cast<uintptr_t>(cls));
auto cached = protocolMembersByRuntimeClass_.find(key);
if (cached != protocolMembersByRuntimeClass_.end()) {
return cached->second;
}

std::vector<NativeApiMember> members;
forEachConformedProtocol(cls, [&](Protocol* protocol) {
auto it = protocolSymbolsByRuntimePointer_.find(
normalizeRuntimePointer(reinterpret_cast<uintptr_t>(protocol)));
if (it != protocolSymbolsByRuntimePointer_.end()) {
for (const auto& member : membersForProtocol(it->second)) {
bool exists = false;
for (const auto& existing : members) {
if (sameMemberSlot(existing, member)) {
exists = true;
break;
}
}
if (!exists) {
members.push_back(member);
}
}
}
return false; // Keep visiting: collect members from every conformed
// protocol, not just the first.
});

auto inserted = protocolMembersByRuntimeClass_.emplace(
key, std::move(members));
return inserted.first->second;
}

const NativeApiSymbol* findClassForRuntimePointer(void* pointer) const {
Expand Down Expand Up @@ -2251,6 +2406,11 @@ static void appendSurfaceMember(
surfaceMembersByClassOffset_;
mutable std::unordered_map<MDSectionOffset, std::vector<NativeApiMember>>
membersByProtocolOffset_;
// Keyed by runtime Class pointer (not metadata offset -- see
// protocolMembersForRuntimeClass for why). Caches the protocol-conformance
// method-metadata fallback.
mutable std::unordered_map<uintptr_t, std::vector<NativeApiMember>>
protocolMembersByRuntimeClass_;
std::unordered_map<MDSectionOffset, NativeApiSymbol> structSymbolsByOffset_;
std::unordered_map<MDSectionOffset, NativeApiSymbol> unionSymbolsByOffset_;
std::unordered_map<MDSectionOffset, std::shared_ptr<NativeApiAggregateInfo>>
Expand Down
85 changes: 67 additions & 18 deletions NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1285,9 +1285,13 @@ throw JSError(
@protocol(NativeApiClassBuilderProtocol));

if (object_ != nil && !isEngineExtendedInstance) {
if (const NativeApiSymbol* symbol =
bridge_->findClassForRuntimeClass(object_getClass(object_))) {
const auto& members = bridge_->membersForClass(*symbol);
// Tries to resolve `property` as a metadata getter or method against
// one member table, returning the resolved JS value if it does.
// Shared by the ordinary class-hierarchy lookup below and the
// protocol-conformance fallback, so both paths behave identically.
auto tryResolveFromMembers =
[&](const std::vector<NativeApiMember>& members)
-> std::optional<Value> {
if (const NativeApiMember* propertyMember =
selectPropertyMember(members, property, false)) {
if (auto getter = respondingPropertyGetterSelector(
Expand Down Expand Up @@ -1335,6 +1339,30 @@ throw JSError(
return methodFunction;
}
}
return std::nullopt;
};

if (const NativeApiSymbol* symbol =
bridge_->findClassForRuntimeClass(object_getClass(object_))) {
if (auto result =
tryResolveFromMembers(bridge_->membersForClass(*symbol))) {
return std::move(*result);
}
}

// Fallback for a selector declared solely on a protocol the object
// conforms to only at the ObjC runtime level (a category/class-
// extension conformance the metadata generator never parsed, or a
// fully private concrete class such as the one behind
// UIViewControllerTransitionCoordinator). Only reached once the
// ordinary class-hierarchy lookup above has already missed, so
// classes with complete metadata pay nothing extra.
const auto& protocolMembers = bridge_->protocolMembersForRuntimeClass(
object_getClass(object_));
if (!protocolMembers.empty()) {
if (auto result = tryResolveFromMembers(protocolMembers)) {
return std::move(*result);
}
}
}

Expand Down Expand Up @@ -1493,27 +1521,48 @@ throw JSError(
}
}

// Tries to resolve `property` as a writable metadata property against
// one member table. Returns true (and performs the assignment) if it
// does. Shared by the ordinary class-hierarchy lookup and the
// protocol-conformance fallback below.
auto tryAssignFromMembers =
[&](const std::vector<NativeApiMember>& members) -> bool {
const NativeApiMember* propertyMember =
selectWritablePropertyMember(members, property, false);
if (propertyMember == nullptr) {
return 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);
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
value);
return true;
};

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);
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
value);
if (tryAssignFromMembers(bridge_->membersForClass(*symbol))) {
NATIVE_API_SET_RETURN(true);
}
}

// Fallback for a writable property declared solely on a protocol the
// object conforms to only at the ObjC runtime level. See
// NativeApiBridge::protocolMembersForRuntimeClass. Only reached once the
// ordinary class-hierarchy lookup above has already missed.
const auto& protocolMembers =
bridge_->protocolMembersForRuntimeClass(object_getClass(object_));
if (!protocolMembers.empty() && tryAssignFromMembers(protocolMembers)) {
NATIVE_API_SET_RETURN(true);
}

if (auto setterSelectorName =
runtimeWritablePropertySetter(object_, property)) {
Value args[] = {Value(runtime, value)};
Expand Down
32 changes: 32 additions & 0 deletions platforms/apple/test/runtime/fixtures/TNSTestNativeCallbacks.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,35 @@ typedef UIView TNSPlatformView;
- (void (^)())getBlockFromNative;

@end

// Fixtures for the "protocol-only method metadata" resolution path: a
// selector declared solely on an Objective-C protocol, with the conforming
// class's *public* header (the only thing the metadata generator parses)
// never declaring that conformance. This mirrors
// UIViewControllerTransitionCoordinator, where every method lives on the
// protocol and the concrete class is private -- interop.Block(fn, encoding)
// is the documented workaround for exactly this shape of metadata gap.
@protocol TNSProtocolOnlyBlockProtocol <NSObject>
- (void)invokeBlockCallback:(void (^)(NSInteger value))callback;
- (NSInteger)invokeBlockCallbackReturningSum:(NSInteger (^)(NSInteger a, NSInteger b))callback;
@end

// Deliberately declared WITHOUT <TNSProtocolOnlyBlockProtocol> here: the
// metadata generator only ever sees this public interface. Conformance is
// added in the .m via a class extension, which is invisible to the
// metadata generator but real at the Objective-C runtime level -- the same
// gap as a private Apple class implementing a public protocol.
@interface TNSProtocolOnlyMembersImplementor : NSObject
@end

// Control case: the same protocol, but conformance IS declared on the
// public interface, so metadata already resolves it today. Used to prove
// the fix doesn't regress (or change the behavior of) the already-working
// path.
@interface TNSProtocolDeclaredMembersImplementor : NSObject <TNSProtocolOnlyBlockProtocol>
@end

@interface TNSProtocolOnlyMembersFactory : NSObject
+ (id<TNSProtocolOnlyBlockProtocol>)createImplementorWithHiddenConformance;
+ (id<TNSProtocolOnlyBlockProtocol>)createImplementorWithDeclaredConformance;
@end
43 changes: 43 additions & 0 deletions platforms/apple/test/runtime/fixtures/TNSTestNativeCallbacks.m
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,46 @@ + (NSString*)callOnThread:(NSString* (^)())block {
}

@end

// Conformance added here, in a class extension the metadata generator never
// parses as part of the public @interface -- this is what makes the
// selectors protocol-only from metadata's point of view while still being
// real, callable methods at the Objective-C runtime level.
@interface TNSProtocolOnlyMembersImplementor () <TNSProtocolOnlyBlockProtocol>
@end

@implementation TNSProtocolOnlyMembersImplementor

- (void)invokeBlockCallback:(void (^)(NSInteger value))callback {
callback(42);
}

- (NSInteger)invokeBlockCallbackReturningSum:(NSInteger (^)(NSInteger a, NSInteger b))callback {
return callback(3, 4);
}

@end

@implementation TNSProtocolDeclaredMembersImplementor

- (void)invokeBlockCallback:(void (^)(NSInteger value))callback {
callback(42);
}

- (NSInteger)invokeBlockCallbackReturningSum:(NSInteger (^)(NSInteger a, NSInteger b))callback {
return callback(3, 4);
}

@end

@implementation TNSProtocolOnlyMembersFactory

+ (id<TNSProtocolOnlyBlockProtocol>)createImplementorWithHiddenConformance {
return [[TNSProtocolOnlyMembersImplementor alloc] init];
}

+ (id<TNSProtocolOnlyBlockProtocol>)createImplementorWithDeclaredConformance {
return [[TNSProtocolDeclaredMembersImplementor alloc] init];
}

@end
Loading
Loading