Skip to content

Commit 3ed3774

Browse files
committed
Override ObjC's class_getImageName to handle Swift classes
This not only restores the correct behavior for classes with generic ancestry, but also handles actual generic classes as well. (This is the function that backs Foundation's Bundle.init(for: AnyClass) initializer.) https://bugs.swift.org/browse/SR-1917 rdar://problem/33450609&40367300
1 parent b02d554 commit 3ed3774

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed

stdlib/public/runtime/Metadata.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "swift/Runtime/ExistentialContainer.h"
2525
#include "swift/Runtime/HeapObject.h"
2626
#include "swift/Runtime/Mutex.h"
27+
#include "swift/Runtime/Once.h"
2728
#include "swift/Strings.h"
2829
#include "llvm/Support/MathExtras.h"
2930
#include "llvm/Support/PointerLikeTypeTraits.h"
@@ -54,6 +55,7 @@
5455
#endif
5556

5657
#if SWIFT_OBJC_INTEROP
58+
#include <dlfcn.h>
5759
#include <objc/runtime.h>
5860
#endif
5961

@@ -1974,6 +1976,45 @@ swift::swift_relocateClassMetadata(ClassMetadata *self,
19741976
return self;
19751977
}
19761978

1979+
#if SWIFT_OBJC_INTEROP
1980+
1981+
// FIXME: This is from a later version of <objc/runtime.h>. Once the declaration
1982+
// is available in SDKs, we can remove this typedef.
1983+
typedef BOOL (*objc_hook_getImageName)(
1984+
Class _Nonnull cls, const char * _Nullable * _Nonnull outImageName);
1985+
1986+
/// \see customGetImageNameFromClass
1987+
static objc_hook_getImageName defaultGetImageNameFromClass = nullptr;
1988+
1989+
/// A custom implementation of Objective-C's class_getImageName for Swift
1990+
/// classes, which knows how to handle dynamically-initialized class metadata.
1991+
///
1992+
/// Per the documentation for objc_setHook_getImageName, any non-Swift classes
1993+
/// will still go through the normal implementation of class_getImageName,
1994+
/// which is stored in defaultGetImageNameFromClass.
1995+
static BOOL
1996+
customGetImageNameFromClass(Class _Nonnull objcClass,
1997+
const char * _Nullable * _Nonnull outImageName) {
1998+
auto *classAsMetadata = reinterpret_cast<const ClassMetadata *>(objcClass);
1999+
2000+
// Is this a Swift class?
2001+
if (classAsMetadata->isTypeMetadata() &&
2002+
!classAsMetadata->isArtificialSubclass()) {
2003+
const void *descriptor = classAsMetadata->getDescription();
2004+
assert(descriptor &&
2005+
"all non-artificial Swift classes should have a descriptor");
2006+
Dl_info imageInfo = {};
2007+
if (!dladdr(descriptor, &imageInfo))
2008+
return NO;
2009+
*outImageName = imageInfo.dli_fname;
2010+
return imageInfo.dli_fname != nullptr;
2011+
}
2012+
2013+
// If not, fall back to the default implementation.
2014+
return defaultGetImageNameFromClass(objcClass, outImageName);
2015+
}
2016+
#endif
2017+
19772018
/// Initialize the field offset vector for a dependent-layout class, using the
19782019
/// "Universal" layout strategy.
19792020
void
@@ -1982,6 +2023,23 @@ swift::swift_initClassMetadata(ClassMetadata *self,
19822023
size_t numFields,
19832024
const TypeLayout * const *fieldTypes,
19842025
size_t *fieldOffsets) {
2026+
#if SWIFT_OBJC_INTEROP
2027+
// Register our custom implementation of class_getImageName.
2028+
static swift_once_t onceToken;
2029+
swift_once(&onceToken, [](void *unused) {
2030+
(void)unused;
2031+
// FIXME: This is from a later version of <objc/runtime.h>. Once the
2032+
// declaration is available in SDKs, we can access this directly instead of
2033+
// using dlsym.
2034+
if (void *setHookPtr = dlsym(RTLD_DEFAULT, "objc_setHook_getImageName")) {
2035+
auto setHook = reinterpret_cast<
2036+
void(*)(objc_hook_getImageName _Nonnull,
2037+
objc_hook_getImageName _Nullable * _Nonnull)>(setHookPtr);
2038+
setHook(customGetImageNameFromClass, &defaultGetImageNameFromClass);
2039+
}
2040+
}, nullptr);
2041+
#endif
2042+
19852043
_swift_initializeSuperclass(self);
19862044

19872045
// Start layout by appending to a standard heap object header.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Foundation
2+
3+
public class SimpleSubclass: NSObject {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
3+
public class SimpleSwiftObject {}
4+
public class SimpleNSObject: NSObject {
5+
@objc public dynamic var observableName: String = ""
6+
}
7+
8+
public class GenericSwiftObject<T> {}
9+
public class GenericNSObject<T>: NSObject {}
10+
11+
public class GenericAncestrySwiftObject: GenericSwiftObject<AnyObject> {}
12+
public class GenericAncestryNSObject: GenericNSObject<AnyObject> {
13+
@objc public dynamic var observableName: String = ""
14+
}
15+
16+
public class ResilientFieldSwiftObject {
17+
public var url: URL?
18+
public var data: Data?
19+
}
20+
public class ResilientFieldNSObject: NSObject {
21+
public var url: URL?
22+
public var data: Data?
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
static inline const char *getNameOfClassToFind() {
2+
return "SimpleNSObjectSubclass.SimpleSubclass";
3+
}
4+
5+
static inline const char *getHookName() {
6+
return "objc_setHook_getImageName";
7+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -emit-library -o %t/libSimpleNSObjectSubclass.dylib %S/Inputs/SimpleNSObjectSubclass.swift
3+
// RUN: %target-codesign %t/libSimpleNSObjectSubclass.dylib
4+
5+
// RUN: %target-build-swift %s -o %t/main -lSimpleNSObjectSubclass -L%t -import-objc-header %S/Inputs/class_getImageName-static-helper.h
6+
// RUN: %target-run %t/main %t/libSimpleNSObjectSubclass.dylib
7+
8+
// REQUIRES: executable_test
9+
// REQUIRES: objc_interop
10+
11+
import Darwin
12+
import ObjectiveC
13+
// import SimpleNSObjectSubclass // Deliberately omitted in favor of dynamic loads.
14+
15+
// Note: The following typealias uses AnyObject instead of AnyClass so that the
16+
// function type is trivially bridgeable to Objective-C. (The representation of
17+
// AnyClass is not the same as Objective-C's 'Class' type.)
18+
typealias GetImageHook = @convention(c) (AnyObject, UnsafeMutablePointer<UnsafePointer<CChar>?>) -> ObjCBool
19+
var hook: GetImageHook?
20+
21+
func checkThatSwiftHookWasNotInstalled() {
22+
// Check that the Swift hook did not get installed.
23+
guard let setHookPtr = dlsym(UnsafeMutableRawPointer(bitPattern: -2),
24+
getHookName()) else {
25+
// If the version of the ObjC runtime we're using doesn't have the hook,
26+
// we're good.
27+
return
28+
}
29+
30+
let setHook = unsafeBitCast(setHookPtr, to: (@convention(c) (GetImageHook, UnsafeMutablePointer<GetImageHook?>) -> Void).self)
31+
setHook({ hook!($0, $1) }, &hook)
32+
33+
var info: Dl_info = .init()
34+
guard 0 != dladdr(unsafeBitCast(hook, to: UnsafeRawPointer.self), &info) else {
35+
fatalError("could not get dladdr info for objc_hook_getImageName")
36+
}
37+
38+
precondition(String(cString: info.dli_fname).hasSuffix("libobjc.A.dylib"),
39+
"hook was replaced")
40+
}
41+
42+
// It's important that this test does not register any Swift classes with the
43+
// Objective-C runtime---that's where Swift sets up its custom hook, and we want
44+
// to check the behavior /without/ that hook. That includes the buffer types for
45+
// String and Array. Therefore, we get C strings directly from a bridging
46+
// header.
47+
48+
guard let theClass = objc_getClass(getNameOfClassToFind()) as! AnyClass? else {
49+
fatalError("could not find class")
50+
}
51+
52+
guard let imageName = class_getImageName(theClass) else {
53+
fatalError("could not find image")
54+
}
55+
56+
checkThatSwiftHookWasNotInstalled()
57+
58+
// Okay, now we can use String.
59+
60+
precondition(String(cString: imageName).hasSuffix("libSimpleNSObjectSubclass.dylib"),
61+
"found wrong image")
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -emit-library -o %t/libGetImageNameHelper.dylib -emit-module %S/Inputs/class_getImageName-helper.swift
3+
// RUN: %target-codesign %t/libGetImageNameHelper.dylib
4+
5+
// RUN: %target-build-swift -g %s -I %t -o %t/main -L %t -lGetImageNameHelper
6+
// RUN: %target-run %t/main %t/libGetImageNameHelper.dylib
7+
8+
// REQUIRES: executable_test
9+
// REQUIRES: objc_interop
10+
11+
import Darwin
12+
import ObjectiveC
13+
import GetImageNameHelper
14+
import StdlibUnittest
15+
16+
func check(_ cls: AnyClass, in library: String) {
17+
guard let imageName = class_getImageName(cls) else {
18+
expectUnreachable("could not find image for \(cls)")
19+
return
20+
}
21+
expectTrue(String(cString: imageName).hasSuffix(library),
22+
"wrong library for \(cls)")
23+
}
24+
25+
let isMissingObjCRuntimeHook =
26+
(nil == dlsym(UnsafeMutableRawPointer(bitPattern: -2),
27+
"objc_setHook_getImageName"))
28+
29+
var testSuite = TestSuite("class_getImageName")
30+
31+
testSuite.test("Simple") {
32+
check(SimpleSwiftObject.self, in: "libGetImageNameHelper.dylib")
33+
check(SimpleNSObject.self, in: "libGetImageNameHelper.dylib")
34+
}
35+
36+
testSuite.test("Generic")
37+
.xfail(.custom({ isMissingObjCRuntimeHook },
38+
reason: "hook for class_getImageName not present"))
39+
.code {
40+
check(GenericSwiftObject<Int>.self, in: "libGetImageNameHelper.dylib")
41+
check(GenericSwiftObject<NSObject>.self, in: "libGetImageNameHelper.dylib")
42+
43+
check(GenericNSObject<Int>.self, in: "libGetImageNameHelper.dylib")
44+
check(GenericNSObject<NSObject>.self, in: "libGetImageNameHelper.dylib")
45+
}
46+
47+
testSuite.test("GenericAncestry")
48+
.xfail(.custom({ isMissingObjCRuntimeHook },
49+
reason: "hook for class_getImageName not present"))
50+
.code {
51+
check(GenericAncestrySwiftObject.self, in: "libGetImageNameHelper.dylib")
52+
check(GenericAncestryNSObject.self, in: "libGetImageNameHelper.dylib")
53+
}
54+
55+
testSuite.test("Resilient") {
56+
check(ResilientFieldSwiftObject.self, in: "libGetImageNameHelper.dylib")
57+
check(ResilientFieldNSObject.self, in: "libGetImageNameHelper.dylib")
58+
}
59+
60+
testSuite.test("ObjC") {
61+
check(NSObject.self, in: "libobjc.A.dylib")
62+
}
63+
64+
testSuite.test("KVO/Simple") {
65+
// We use object_getClass in this test to not look through KVO's artificial
66+
// subclass.
67+
let obj = SimpleNSObject()
68+
let observation = obj.observe(\.observableName) { _, _ in }
69+
withExtendedLifetime(observation) {
70+
let theClass = object_getClass(obj)
71+
precondition(theClass !== SimpleNSObject.self, "no KVO subclass?")
72+
expectNil(class_getImageName(theClass),
73+
"should match what happens with NSObject (below)")
74+
}
75+
}
76+
77+
testSuite.test("KVO/GenericAncestry") {
78+
// We use object_getClass in this test to not look through KVO's artificial
79+
// subclass.
80+
let obj = GenericAncestryNSObject()
81+
let observation = obj.observe(\.observableName) { _, _ in }
82+
withExtendedLifetime(observation) {
83+
let theClass = object_getClass(obj)
84+
precondition(theClass !== GenericAncestryNSObject.self, "no KVO subclass?")
85+
expectNil(class_getImageName(theClass),
86+
"should match what happens with NSObject (below)")
87+
}
88+
}
89+
90+
testSuite.test("KVO/ObjC") {
91+
// We use object_getClass in this test to not look through KVO's artificial
92+
// subclass.
93+
let obj = NSObject()
94+
let observation = obj.observe(\.description) { _, _ in }
95+
withExtendedLifetime(observation) {
96+
let theClass = object_getClass(obj)
97+
precondition(theClass !== NSObject.self, "no KVO subclass?")
98+
expectNil(class_getImageName(theClass),
99+
"should match what happens with the Swift objects (above)")
100+
}
101+
}
102+
103+
testSuite.test("dynamic") {
104+
let newClass: AnyClass = objc_allocateClassPair(/*superclass*/nil,
105+
"CompletelyDynamic",
106+
/*extraBytes*/0)!
107+
objc_registerClassPair(newClass)
108+
109+
// We don't actually care what the result is; we just need to not crash.
110+
_ = class_getImageName(newClass)
111+
}
112+
113+
testSuite.test("nil") {
114+
// The ObjC runtime should handle this before it even gets to Swift's custom
115+
// implementation, but just in case.
116+
expectNil(class_getImageName(nil))
117+
}
118+
119+
runAllTests()

0 commit comments

Comments
 (0)