Skip to content

Remove stdlib and runtime dependencies on Foundation and CF #26630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 24, 2019
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
5 changes: 5 additions & 0 deletions include/swift/Runtime/ObjCBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ namespace swift {
SWIFT_RUNTIME_EXPORT
void swift_rootObjCDealloc(HeapObject *self);

// Uses Swift bridging to box a C string into an NSString without introducing
// a link-time dependency on NSString.
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_API
id swift_stdlib_NSStringFromUTF8(const char *cstr, int len);

}

#endif // SWIFT_OBJC_INTEROP
Expand Down
16 changes: 16 additions & 0 deletions stdlib/public/core/BridgeObjectiveC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ public protocol _ObjectiveCBridgeable {

#if _runtime(_ObjC)

@_silgen_name("swift_stdlib_connectNSBaseClasses")
internal func _connectNSBaseClasses() -> Bool


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any mechanism or way to enforce this? Requiring maintainers to just remember to bind a let variable whenever they bridge seems very error prone and would lead to difficult to understand bugs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only needed for the set of classes that we use the reparenting mechanism for, not bridging in general. So it shouldn't need to expand much beyond this, if any.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a while brainstorming ways to enforce this but haven't come up with much so far. The problem is that it's an easy property to check, but once it's set, you lose the ability to check if other code paths set it. So we'd need to somehow have tests that exercised each possible future bridging function before bridging anything else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly more practical might be debug preconditions that check the superclass at a funnel point in each class, but I'm not sure there are good funnel points

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a horrible idea (I can probably stop qualifying my statements this way, my username is sufficient warning by now) but how about, say, overriding retain on these classes in debug builds and checking there? That comes pretty close to a funnel point that only happens when bridging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

objectAtIndex: or count might be a slightly less horrible funnel point, at the cost of "if someone calls something else implemented on NSArray first it will just crash".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only needed for the set of classes that we use the reparenting mechanism for

That's not the main issue with this approach. This patch makes it so that anyone who wants to instantiate a type, such as __SwiftEmptyNSEnumerator, has to somehow know the unwritten convention that they need to call _connectOrphanedFoundationSubclassesIfNeeded() first, or else there will be obscure corner case bugs discovered in production.

For example, a more structured approach would be for e.g. __SwiftNativeNSEnumerator's initializer to call _connectOrphanedFoundationSubclassesIfNeeded(). That way, at least for that type family, it is an enforced invariant rather than an ad-hoc use-site convention. Is there something preventing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially thought moving it to init would require adding an override of init, which would add an extra objc_msgSend to every initialization of these, but it looks like we have an empty passthrough override anyway.

Even better though: Jordan points out in another comment that you can't get to this except via another bridging call, which will do this initialization, so we can just delete the enumerator calls entirely.

private let _bridgeInitializedSuccessfully = _connectNSBaseClasses()
internal var _orphanedFoundationSubclassesReparented: Bool = false

/// Reparents the SwiftNativeNS*Base classes to be subclasses of their respective
/// Foundation types, or is false if they couldn't be reparented. Must be run
/// in order to bridge Swift Strings, Arrays, Dictionarys, Sets, or Enumerators to ObjC.
internal func _connectOrphanedFoundationSubclassesIfNeeded() -> Void {
let bridgeWorks = _bridgeInitializedSuccessfully
_debugPrecondition(bridgeWorks)
_orphanedFoundationSubclassesReparented = true
}

//===--- Bridging for metatypes -------------------------------------------===//

/// A stand-in for a value of metatype type.
Expand Down
2 changes: 0 additions & 2 deletions stdlib/public/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,6 @@ set(swift_core_private_link_libraries)
set(swift_stdlib_compile_flags "${SWIFT_RUNTIME_SWIFT_COMPILE_FLAGS}")
if(SWIFT_PRIMARY_VARIANT_SDK IN_LIST SWIFT_APPLE_PLATFORMS)
list(APPEND swift_core_link_flags "-all_load")
list(APPEND swift_core_framework_depends Foundation)
list(APPEND swift_core_framework_depends CoreFoundation)
list(APPEND swift_core_private_link_libraries icucore)
else()
# With the GNU linker the equivalent of -all_load is to tell the linker
Expand Down
16 changes: 15 additions & 1 deletion stdlib/public/core/ContiguousArrayBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,27 @@ internal struct _ContiguousArrayBuffer<Element>: _ArrayBufferProtocol {
}

#if _runtime(_ObjC)

/// Convert to an NSArray.
///
/// - Precondition: `Element` is bridged to Objective-C.
///
/// - Complexity: O(1).
@inlinable
@usableFromInline
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, isn't this an ABI change?

internal __consuming func _asCocoaArray() -> AnyObject {
// _asCocoaArray was @inlinable in Swift 5.0 and 5.1, which means that there
// are existing apps out there that effectively have the old implementation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: missing "."

// Be careful with future changes to this function. Here be dragons!
// The old implementation was
// if count == 0 {
// return _emptyArrayStorage
// }
// if _isBridgedVerbatimToObjectiveC(Element.self) {
// return _storage
// }
// return __SwiftDeferredNSArray(_nativeStorage: _storage)

_connectOrphanedFoundationSubclassesIfNeeded()
if count == 0 {
return _emptyArrayStorage
}
Expand Down
3 changes: 3 additions & 0 deletions stdlib/public/core/DictionaryBridging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal func _stdlib_NSDictionary_allKeys(
extension _NativeDictionary { // Bridging
@usableFromInline
__consuming internal func bridged() -> AnyObject {
_connectOrphanedFoundationSubclassesIfNeeded()
// We can zero-cost bridge if our keys are verbatim
// or if we're the empty singleton.

Expand Down Expand Up @@ -67,6 +68,7 @@ final internal class _SwiftDictionaryNSEnumerator<Key: Hashable, Value>

internal init(_ base: __owned _NativeDictionary<Key, Value>) {
_internalInvariant(_isBridgedVerbatimToObjectiveC(Key.self))
_internalInvariant(_orphanedFoundationSubclassesReparented)
self.base = base
self.bridgedKeys = nil
self.nextBucket = base.hashTable.startBucket
Expand All @@ -77,6 +79,7 @@ final internal class _SwiftDictionaryNSEnumerator<Key: Hashable, Value>
@nonobjc
internal init(_ deferred: __owned _SwiftDeferredNSDictionary<Key, Value>) {
_internalInvariant(!_isBridgedVerbatimToObjectiveC(Key.self))
_internalInvariant(_orphanedFoundationSubclassesReparented)
self.base = deferred.native
self.bridgedKeys = deferred.bridgeKeys()
self.nextBucket = base.hashTable.startBucket
Expand Down
5 changes: 4 additions & 1 deletion stdlib/public/core/Hashing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ internal struct _UnmanagedAnyObjectArray {
// renamed. The old name must not be used in the new runtime.
final internal class __SwiftEmptyNSEnumerator
: __SwiftNativeNSEnumerator, _NSEnumerator {
internal override required init() { super.init() }
internal override required init() {
super.init()
_internalInvariant(_orphanedFoundationSubclassesReparented)
}

@objc
internal func nextObject() -> AnyObject? {
Expand Down
6 changes: 4 additions & 2 deletions stdlib/public/core/ReflectionMirror.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ internal func getChild<T>(of value: T, type: Any.Type, index: Int) -> (label: St
internal func _getQuickLookObject<T>(_: T) -> AnyObject?

@_silgen_name("_swift_stdlib_NSObject_isKindOfClass")
internal func _isImpl(_ object: AnyObject, kindOf: AnyObject) -> Bool
internal func _isImpl(_ object: AnyObject, kindOf: UnsafePointer<CChar>) -> Bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrose-apple I'm assuming silgen_name entities are not automatically usableFromInline, is that so?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not, but it is sufficient to prevent dead code stripping (finally). Since this is only called from the runtime, which is in the same binary, this should be okay.


internal func _is(_ object: AnyObject, kindOf `class`: String) -> Bool {
return _isImpl(object, kindOf: `class` as AnyObject)
return `class`.withCString {
return _isImpl(object, kindOf: $0)
}
}

internal func _getClassPlaygroundQuickLook(
Expand Down
15 changes: 7 additions & 8 deletions stdlib/public/core/SetBridging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,21 @@

import SwiftShims

@_silgen_name("swift_stdlib_CFSetGetValues")
internal
func _stdlib_CFSetGetValues(
_ nss: AnyObject,
_: UnsafeMutablePointer<AnyObject>)

/// Equivalent to `NSSet.allObjects`, but does not leave objects on the
/// autorelease pool.
internal func _stdlib_NSSet_allObjects(_ object: AnyObject) -> _BridgingBuffer {
let nss = unsafeBitCast(object, to: _NSSet.self)
let storage = _BridgingBuffer(nss.count)
_stdlib_CFSetGetValues(nss, storage.baseAddress)
let count = nss.count
let storage = _BridgingBuffer(count)
nss.getObjects(storage.baseAddress, count: count)
return storage
}

extension _NativeSet { // Bridging
@usableFromInline
internal __consuming func bridged() -> AnyObject {
_connectOrphanedFoundationSubclassesIfNeeded()

// We can zero-cost bridge if our keys are verbatim
// or if we're the empty singleton.

Expand Down Expand Up @@ -70,6 +67,7 @@ final internal class _SwiftSetNSEnumerator<Element: Hashable>

internal init(_ base: __owned _NativeSet<Element>) {
_internalInvariant(_isBridgedVerbatimToObjectiveC(Element.self))
_internalInvariant(_orphanedFoundationSubclassesReparented)
self.base = base
self.bridgedElements = nil
self.nextBucket = base.hashTable.startBucket
Expand All @@ -80,6 +78,7 @@ final internal class _SwiftSetNSEnumerator<Element: Hashable>
@nonobjc
internal init(_ deferred: __owned _SwiftDeferredNSSet<Element>) {
_internalInvariant(!_isBridgedVerbatimToObjectiveC(Element.self))
_internalInvariant(_orphanedFoundationSubclassesReparented)
self.base = deferred.native
self.bridgedElements = deferred.bridgeElements()
self.nextBucket = base.hashTable.startBucket
Expand Down
4 changes: 4 additions & 0 deletions stdlib/public/core/ShadowProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ internal protocol _NSSetCore: _NSCopying, _NSFastEnumeration {
/// supplies.
@unsafe_no_objc_tagged_pointer @objc
internal protocol _NSSet: _NSSetCore {
@objc(getObjects:count:) func getObjects(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a small speedup relative to the CF call :)

_ buffer: UnsafeMutablePointer<AnyObject>,
count: Int
)
}

/// A shadow for the API of NSNumber we will use in the core
Expand Down
15 changes: 15 additions & 0 deletions stdlib/public/core/StringBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ extension String {
@_effects(releasenone)
public // SPI(Foundation)
func _bridgeToObjectiveCImpl() -> AnyObject {

_connectOrphanedFoundationSubclassesIfNeeded()

// Smol ASCII a) may bridge to tagged pointers, b) can't contain a BOM
if _guts.isSmallASCII {
let maybeTagged = _guts.asSmall.withUTF8 { bufPtr in
Expand All @@ -453,6 +456,7 @@ extension String {
}
if let tagged = maybeTagged { return tagged }
}

if _guts.isSmall {
// We can't form a tagged pointer String, so grow to a non-small String,
// and bridge that instead. Also avoids CF deleting any BOM that may be
Expand Down Expand Up @@ -497,6 +501,17 @@ public func _getDescription<T>(_ x: T) -> AnyObject {
return String(reflecting: x)._bridgeToObjectiveCImpl()
}

@_silgen_name("swift_stdlib_NSStringFromUTF8")
@usableFromInline //this makes the symbol available to the runtime :(
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
internal func _NSStringFromUTF8(_ s: UnsafePointer<UInt8>, _ len: Int)
-> AnyObject {
return String(
decoding: UnsafeBufferPointer(start: s, count: len),
as: UTF8.self
)._bridgeToObjectiveCImpl()
}

#else // !_runtime(_ObjC)

internal class __SwiftNativeNSString {
Expand Down
67 changes: 47 additions & 20 deletions stdlib/public/runtime/ErrorObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,28 @@
using namespace swift;
using namespace swift::hashable_support;

// Mimic the memory layout of NSError so things don't go haywire when we
// switch superclasses to the real thing.
@interface __SwiftNSErrorLayoutStandin : NSObject {
@private
void *_reserved;
NSInteger _code;
id _domain;
id _userInfo;
}
@end

@implementation __SwiftNSErrorLayoutStandin
@end

/// A subclass of NSError used to represent bridged native Swift errors.
/// This type cannot be subclassed, and should not ever be instantiated
/// except by the Swift runtime.
///
/// NOTE: older runtimes called this _SwiftNativeNSError. The two must
/// coexist, so it was renamed. The old name must not be used in the new
/// runtime.
@interface __SwiftNativeNSError : NSError
@interface __SwiftNativeNSError : __SwiftNSErrorLayoutStandin
@end

@implementation __SwiftNativeNSError
Expand All @@ -71,29 +85,29 @@ - (void)dealloc {
// layout. This gives us a buffer in case NSError decides to change its stored
// property order.

- (NSString*)domain {
- (id /* NSString */)domain {
auto error = (const SwiftError*)self;
// The domain string should not be nil; if it is, then this error box hasn't
// been initialized yet as an NSError.
auto domain = error->domain.load(SWIFT_MEMORY_ORDER_CONSUME);
assert(domain
&& "Error box used as NSError before initialization");
// Don't need to .retain.autorelease since it's immutable.
return cf_const_cast<NSString*>(domain);
return cf_const_cast<id>(domain);
}

- (NSInteger)code {
auto error = (const SwiftError*)self;
return error->code.load(SWIFT_MEMORY_ORDER_CONSUME);
}

- (NSDictionary*)userInfo {
- (id /* NSDictionary */)userInfo {
auto error = (const SwiftError*)self;
auto userInfo = error->userInfo.load(SWIFT_MEMORY_ORDER_CONSUME);
assert(userInfo
&& "Error box used as NSError before initialization");
// Don't need to .retain.autorelease since it's immutable.
return cf_const_cast<NSDictionary*>(userInfo);
return cf_const_cast<id>(userInfo);
}

- (id)copyWithZone:(NSZone *)zone {
Expand Down Expand Up @@ -153,16 +167,29 @@ - (BOOL)isEqual:(id)other {
@end

Class swift::getNSErrorClass() {
return SWIFT_LAZY_CONSTANT([NSError class]);
return SWIFT_LAZY_CONSTANT(objc_lookUpClass("NSError"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there still a difference between this and objc_getClass in the modern runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small difference: they pass different values to the third, unused parameter of the internal function that implements the functionality. Don't ask me why.

}

const Metadata *swift::getNSErrorMetadata() {
return SWIFT_LAZY_CONSTANT(
swift_getObjCClassMetadata((const ClassMetadata *)getNSErrorClass()));
}

static Class getAndBridgeSwiftNativeNSErrorClass() {
Class nsErrorClass = swift::getNSErrorClass();
Class ourClass = [__SwiftNativeNSError class];
// We want "err as AnyObject" to do *something* even without Foundation
if (nsErrorClass) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
class_setSuperclass(ourClass, nsErrorClass);
#pragma clang diagnostic pop
}
return ourClass;
}

static Class getSwiftNativeNSErrorClass() {
return SWIFT_LAZY_CONSTANT([__SwiftNativeNSError class]);
return SWIFT_LAZY_CONSTANT(getAndBridgeSwiftNativeNSErrorClass());
}

/// Allocate a catchable error object.
Expand Down Expand Up @@ -274,7 +301,7 @@ static ErrorBridgingInfo getErrorBridgingInfo() {

const Metadata *SwiftError::getType() const {
if (isPureNSError()) {
auto asError = reinterpret_cast<NSError *>(const_cast<SwiftError *>(this));
id asError = reinterpret_cast<id>(const_cast<SwiftError *>(this));
return swift_getObjCClassMetadata((ClassMetadata*)[asError class]);
}
return type;
Expand Down Expand Up @@ -363,9 +390,9 @@ static ErrorBridgingInfo getErrorBridgingInfo() {
#define getErrorDomainNSString \
MANGLE_SYM(s23_getErrorDomainNSStringyyXlSPyxGs0B0RzlF)
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERNAL
NSString *getErrorDomainNSString(const OpaqueValue *error,
const Metadata *T,
const WitnessTable *Error);
id getErrorDomainNSString(const OpaqueValue *error,
const Metadata *T,
const WitnessTable *Error);

// internal func _getErrorCode<T : Error>(_ x: UnsafePointer<T>) -> Int
#define getErrorCode \
Expand All @@ -379,17 +406,17 @@ NSInteger getErrorCode(const OpaqueValue *error,
#define getErrorUserInfoNSDictionary \
MANGLE_SYM(s29_getErrorUserInfoNSDictionaryyyXlSgSPyxGs0B0RzlF)
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERNAL
NSDictionary *getErrorUserInfoNSDictionary(
id getErrorUserInfoNSDictionary(
const OpaqueValue *error,
const Metadata *T,
const WitnessTable *Error);

// @_silgen_name("_swift_stdlib_getErrorDefaultUserInfo")
// internal func _getErrorDefaultUserInfo<T : Error>(_ x: T) -> AnyObject
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERNAL
NSDictionary *_swift_stdlib_getErrorDefaultUserInfo(OpaqueValue *error,
const Metadata *T,
const WitnessTable *Error) {
id _swift_stdlib_getErrorDefaultUserInfo(OpaqueValue *error,
const Metadata *T,
const WitnessTable *Error) {
// public func Foundation._getErrorDefaultUserInfo<T: Error>(_ error: T)
// -> AnyObject?
auto foundationGetDefaultUserInfo = getErrorBridgingInfo().GetErrorDefaultUserInfo;
Expand All @@ -407,7 +434,7 @@ NSInteger getErrorCode(const OpaqueValue *error,
/// at +1.
id
swift::_swift_stdlib_bridgeErrorToNSError(SwiftError *errorObject) {
auto ns = reinterpret_cast<NSError *>(errorObject);
id ns = reinterpret_cast<id>(errorObject);

// If we already have a domain set, then we've already initialized.
// If this is a real NSError, then Cocoa and Core Foundation's initializers
Expand All @@ -431,9 +458,9 @@ NSInteger getErrorCode(const OpaqueValue *error,
auto type = errorObject->getType();
auto witness = errorObject->getErrorConformance();

NSString *domain = getErrorDomainNSString(value, type, witness);
id domain = getErrorDomainNSString(value, type, witness);
NSInteger code = getErrorCode(value, type, witness);
NSDictionary *userInfo = getErrorUserInfoNSDictionary(value, type, witness);
id userInfo = getErrorUserInfoNSDictionary(value, type, witness);

// Never produce an empty userInfo dictionary.
if (!userInfo)
Expand Down Expand Up @@ -480,7 +507,7 @@ NSInteger getErrorCode(const OpaqueValue *error,
if (![reinterpret_cast<id>(object) isKindOfClass: NSErrorClass])
return false;

NSError *srcInstance = reinterpret_cast<NSError *>(object);
id srcInstance = reinterpret_cast<id>(object);

// A __SwiftNativeNSError box can always be unwrapped to cast the value back
// out as an Error existential.
Expand Down Expand Up @@ -523,7 +550,7 @@ ProtocolDescriptorRef theErrorProtocol(&PROTOCOL_DESCR_SYM(s5Error),
auto *destTypeExistential = dyn_cast<ExistentialTypeMetadata>(destType);
if (destTypeExistential &&
destTypeExistential->getRepresentation() == ExistentialTypeRepresentation::Error) {
auto destBoxAddr = reinterpret_cast<NSError**>(dest);
auto destBoxAddr = reinterpret_cast<id*>(dest);
*destBoxAddr = objc_retain(srcInstance);
return true;
}
Expand Down
Loading