Skip to content

Commit 5ea6e17

Browse files
committed
Merge pull request #17910 from jrose-apple/better-class_getImageName
Override ObjC's class_getImageName to handle Swift classes https://bugs.swift.org/browse/SR-1917 (cherry picked from commit db4ceae)
1 parent f467fa6 commit 5ea6e17

File tree

8 files changed

+306
-12
lines changed

8 files changed

+306
-12
lines changed

lib/Driver/ToolChains.cpp

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,26 @@ static void addLinkSanitizerLibArgsForLinux(const ArgList &Args,
12931293
Arguments.push_back("-ldl");
12941294
}
12951295

1296+
/// Returns true if the compiler depends on features provided by the ObjC
1297+
/// runtime that are not present on the deployment target indicated by
1298+
/// \p triple.
1299+
static bool wantsObjCRuntime(const llvm::Triple &triple) {
1300+
assert((!triple.isTvOS() || triple.isiOS()) &&
1301+
"tvOS is considered a kind of iOS");
1302+
1303+
// When updating the versions listed here, please record the most recent
1304+
// feature being depended on and when it was introduced:
1305+
//
1306+
// - The hook to override class_getImageName (macOS 10.14 and equivalent)
1307+
if (triple.isiOS())
1308+
return triple.isOSVersionLT(12);
1309+
if (triple.isMacOSX())
1310+
return triple.isMacOSXVersionLT(10, 14);
1311+
if (triple.isWatchOS())
1312+
return triple.isOSVersionLT(5);
1313+
llvm_unreachable("unknown Darwin OS");
1314+
}
1315+
12961316
ToolChain::InvocationInfo
12971317
toolchains::Darwin::constructInvocation(const LinkJobAction &job,
12981318
const JobContext &context) const {
@@ -1377,15 +1397,9 @@ toolchains::Darwin::constructInvocation(const LinkJobAction &job,
13771397
if (llvm::sys::fs::exists(CompilerRTPath))
13781398
Arguments.push_back(context.Args.MakeArgString(CompilerRTPath));
13791399

1380-
bool wantsObjCRuntime = false;
1381-
if (Triple.isiOS())
1382-
wantsObjCRuntime = Triple.isOSVersionLT(9);
1383-
else if (Triple.isMacOSX())
1384-
wantsObjCRuntime = Triple.isMacOSXVersionLT(10, 11);
1385-
13861400
if (context.Args.hasFlag(options::OPT_link_objc_runtime,
13871401
options::OPT_no_link_objc_runtime,
1388-
/*Default=*/wantsObjCRuntime)) {
1402+
/*Default=*/wantsObjCRuntime(Triple))) {
13891403
llvm::SmallString<128> ARCLiteLib(D.getSwiftProgramPath());
13901404
llvm::sys::path::remove_filename(ARCLiteLib); // 'swift'
13911405
llvm::sys::path::remove_filename(ARCLiteLib); // 'bin'

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

@@ -2017,6 +2019,45 @@ swift::swift_relocateClassMetadata(ClassMetadata *self,
20172019
return self;
20182020
}
20192021

2022+
#if SWIFT_OBJC_INTEROP
2023+
2024+
// FIXME: This is from a later version of <objc/runtime.h>. Once the declaration
2025+
// is available in SDKs, we can remove this typedef.
2026+
typedef BOOL (*objc_hook_getImageName)(
2027+
Class _Nonnull cls, const char * _Nullable * _Nonnull outImageName);
2028+
2029+
/// \see customGetImageNameFromClass
2030+
static objc_hook_getImageName defaultGetImageNameFromClass = nullptr;
2031+
2032+
/// A custom implementation of Objective-C's class_getImageName for Swift
2033+
/// classes, which knows how to handle dynamically-initialized class metadata.
2034+
///
2035+
/// Per the documentation for objc_setHook_getImageName, any non-Swift classes
2036+
/// will still go through the normal implementation of class_getImageName,
2037+
/// which is stored in defaultGetImageNameFromClass.
2038+
static BOOL
2039+
customGetImageNameFromClass(Class _Nonnull objcClass,
2040+
const char * _Nullable * _Nonnull outImageName) {
2041+
auto *classAsMetadata = reinterpret_cast<const ClassMetadata *>(objcClass);
2042+
2043+
// Is this a Swift class?
2044+
if (classAsMetadata->isTypeMetadata() &&
2045+
!classAsMetadata->isArtificialSubclass()) {
2046+
const void *descriptor = classAsMetadata->getDescription();
2047+
assert(descriptor &&
2048+
"all non-artificial Swift classes should have a descriptor");
2049+
Dl_info imageInfo = {};
2050+
if (!dladdr(descriptor, &imageInfo))
2051+
return NO;
2052+
*outImageName = imageInfo.dli_fname;
2053+
return imageInfo.dli_fname != nullptr;
2054+
}
2055+
2056+
// If not, fall back to the default implementation.
2057+
return defaultGetImageNameFromClass(objcClass, outImageName);
2058+
}
2059+
#endif
2060+
20202061
/// Initialize the field offset vector for a dependent-layout class, using the
20212062
/// "Universal" layout strategy.
20222063
void
@@ -2025,6 +2066,23 @@ swift::swift_initClassMetadata(ClassMetadata *self,
20252066
size_t numFields,
20262067
const TypeLayout * const *fieldTypes,
20272068
size_t *fieldOffsets) {
2069+
#if SWIFT_OBJC_INTEROP
2070+
// Register our custom implementation of class_getImageName.
2071+
static swift_once_t onceToken;
2072+
swift_once(&onceToken, [](void *unused) {
2073+
(void)unused;
2074+
// FIXME: This is from a later version of <objc/runtime.h>. Once the
2075+
// declaration is available in SDKs, we can access this directly instead of
2076+
// using dlsym.
2077+
if (void *setHookPtr = dlsym(RTLD_DEFAULT, "objc_setHook_getImageName")) {
2078+
auto setHook = reinterpret_cast<
2079+
void(*)(objc_hook_getImageName _Nonnull,
2080+
objc_hook_getImageName _Nullable * _Nonnull)>(setHookPtr);
2081+
setHook(customGetImageNameFromClass, &defaultGetImageNameFromClass);
2082+
}
2083+
}, nullptr);
2084+
#endif
2085+
20282086
_swift_initializeSuperclass(self);
20292087

20302088
// Start layout by appending to a standard heap object header.

test/Driver/linker-arclite.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,24 @@
1111

1212
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-ios8.0 %S/../Inputs/empty.swift | %FileCheck -check-prefix IOS_ARCLITE %s
1313

14-
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-macosx10.11 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
15-
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-ios9.0 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
16-
// RUN: %swiftc_driver -driver-print-jobs -target arm64-apple-tvos9.0 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
17-
// RUN: %swiftc_driver -driver-print-jobs -target armv7k-apple-watchos2.0 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
18-
1914
// IOS_ARCLITE: bin/ld{{"? }}
2015
// IOS_ARCLITE: -force_load {{[^ ]+/lib/arc/libarclite_iphonesimulator.a}}
2116
// IOS_ARCLITE: -o {{[^ ]+}}
2217

18+
19+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-macosx10.14 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
20+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-macosx10.13 %S/../Inputs/empty.swift | %FileCheck -check-prefix ANY_ARCLITE %s
21+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-ios12 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
22+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-ios11 %S/../Inputs/empty.swift | %FileCheck -check-prefix ANY_ARCLITE %s
23+
// RUN: %swiftc_driver -driver-print-jobs -target arm64-apple-tvos12 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
24+
// RUN: %swiftc_driver -driver-print-jobs -target arm64-apple-tvos11 %S/../Inputs/empty.swift | %FileCheck -check-prefix ANY_ARCLITE %s
25+
// RUN: %swiftc_driver -driver-print-jobs -target armv7k-apple-watchos5 %S/../Inputs/empty.swift | %FileCheck -check-prefix NO_ARCLITE %s
26+
// RUN: %swiftc_driver -driver-print-jobs -target armv7k-apple-watchos4 %S/../Inputs/empty.swift | %FileCheck -check-prefix ANY_ARCLITE %s
27+
2328
// NO_ARCLITE: bin/ld{{"? }}
2429
// NO_ARCLITE-NOT: arclite
2530
// NO_ARCLITE: -o {{[^ ]+}}
31+
32+
// ANY_ARCLITE: bin/ld{{"? }}
33+
// ANY_ARCLITE: -force_load {{[^ ]+}}/lib/arc/libarclite_{{.+}}.a
34+
// ANY_ARCLITE: -o {{[^ ]+}}
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)