Skip to content

Commit db4ceae

Browse files
authored
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
2 parents 979cb9c + df974b2 commit db4ceae

File tree

8 files changed

+306
-12
lines changed

8 files changed

+306
-12
lines changed

lib/Driver/DarwinToolChains.cpp

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,26 @@ static void addVersionString(const ArgList &inputArgs, ArgStringList &arguments,
198198
arguments.push_back(inputArgs.MakeArgString(os.str()));
199199
}
200200

201+
/// Returns true if the compiler depends on features provided by the ObjC
202+
/// runtime that are not present on the deployment target indicated by
203+
/// \p triple.
204+
static bool wantsObjCRuntime(const llvm::Triple &triple) {
205+
assert((!triple.isTvOS() || triple.isiOS()) &&
206+
"tvOS is considered a kind of iOS");
207+
208+
// When updating the versions listed here, please record the most recent
209+
// feature being depended on and when it was introduced:
210+
//
211+
// - The hook to override class_getImageName (macOS 10.14 and equivalent)
212+
if (triple.isiOS())
213+
return triple.isOSVersionLT(12);
214+
if (triple.isMacOSX())
215+
return triple.isMacOSXVersionLT(10, 14);
216+
if (triple.isWatchOS())
217+
return triple.isOSVersionLT(5);
218+
llvm_unreachable("unknown Darwin OS");
219+
}
220+
201221
ToolChain::InvocationInfo
202222
toolchains::Darwin::constructInvocation(const LinkJobAction &job,
203223
const JobContext &context) const {
@@ -282,15 +302,9 @@ toolchains::Darwin::constructInvocation(const LinkJobAction &job,
282302
if (llvm::sys::fs::exists(CompilerRTPath))
283303
Arguments.push_back(context.Args.MakeArgString(CompilerRTPath));
284304

285-
bool wantsObjCRuntime = false;
286-
if (Triple.isiOS())
287-
wantsObjCRuntime = Triple.isOSVersionLT(9);
288-
else if (Triple.isMacOSX())
289-
wantsObjCRuntime = Triple.isMacOSXVersionLT(10, 11);
290-
291305
if (context.Args.hasFlag(options::OPT_link_objc_runtime,
292306
options::OPT_no_link_objc_runtime,
293-
/*Default=*/wantsObjCRuntime)) {
307+
/*Default=*/wantsObjCRuntime(Triple))) {
294308
llvm::SmallString<128> ARCLiteLib(D.getSwiftProgramPath());
295309
llvm::sys::path::remove_filename(ARCLiteLib); // 'swift'
296310
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

@@ -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.

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)