Skip to content
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

Passing class-constrained type as AnyObject instance behavior is not matched on Darwin and non-Darwin platform #70645

Open
Kyle-Ye opened this issue Dec 28, 2023 · 15 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. good first issue Good for newcomers swift 5.9 type checker Area → compiler: Semantic analysis

Comments

@Kyle-Ye
Copy link
Contributor

Kyle-Ye commented Dec 28, 2023

Description

Passing class-constrained type as AnyObject instance behavior is not matched on Darwin and non-Darwin platform

On Darwin platform, we can pass it directly.

On non-Darwin platform, a fatalError will be emitted - "error: argument type 'Self.Type' expected to be an instance of a class or class-constrained type". And we can workaround it by manually adding "as AnyObject"

test1(Self.self as AnyObject)

Reproduction

class X {
    func test1(_ o: AnyObject) {}
    func test12() { test1(Self.self) }
}

Expected behavior

The behavior is matched. (The current Darwin platform behavior is preferred.)

Environment

Linux:
swift --version
Swift version 5.9.2 (swift-5.9.2-RELEASE)
Target: aarch64-unknown-linux-gnu

macOS:
swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
Target: arm64-apple-macosx14.0

Additional information

Forum Post about the issue: https://forums.swift.org/t/69190

@Kyle-Ye Kyle-Ye added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels Dec 28, 2023
@slavapestov
Copy link
Contributor

The dynamic Self type inside a class is always convertible to AnyObject, regardless of Objective-C interop. This is a type checker bug.

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Dec 28, 2023

The dynamic Self type inside a class is always convertible to AnyObject, regardless of Objective-C interop. This is a type checker bug.

Thanks for the explanation. And I think this should be a "Starter Bug" easy to be fixed.

I have not explored with Swift's "Type Checker" code before, can you help point out the relevant code folder/scope so that I can keep investigating and hopefully fix it myself?

@Kyle-Ye Kyle-Ye added type checker Area → compiler: Semantic analysis good first issue Good for newcomers swift 5.9 and removed triage needed This issue needs more specific labels labels Dec 28, 2023
@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Dec 29, 2023

expected to be an instance of a class or class-constrained type

Swift have such check here. But the test is marked as "REQUIRES: objc_interop"

https://github.com/apple/swift/blob/f08f86c71617bacbc61f69ce842e284b27036598/test/Parse/metatype_object_conversion.swift#L14

See the 10 years ago commit: cf9e0d2 cc @jckarter

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Dec 29, 2023

The dynamic Self type inside a class is always convertible to AnyObject, regardless of Objective-C interop. This is a type checker bug.

https://github.com/apple/swift/blob/f08f86c71617bacbc61f69ce842e284b27036598/lib/Sema/CSSimplify.cpp#L7556-L7561

The conversion code is under a if check of getASTContext().LangOpts.EnableObjCInterop.

Maybe the solution is we just move this part out of the EnableObjCInterop check

@slavapestov
Copy link
Contributor

I'm sorry, I misunderstood the original bug report. You're not converting an instance of a class to AnyObject -- that's allowed -- you're converting a metatype value to AnyObject. That is indeed not supported without an Objective-C runtime, because metatypes do not have a Swift reference counting header, so allowing the conversion here would lead to a runtime crash.

Can you change your code to traffic in Any.Type instead of AnyObject? Any.Type is also a single pointer, but we don't attempt to retain/release it, hence it can store a metatype even without an Objective-C runtime.

@slavapestov
Copy link
Contributor

And we can workaround it by manually adding "as AnyObject"

I believe this is actually a bug and should not be allowed on non-Objective-C platforms. @jckarter what do you think?

@slavapestov slavapestov reopened this Dec 29, 2023
@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Dec 29, 2023

I'm also curious about the runtime behavior for this on non-Objective-C platform. I'll give it a try and report later.

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Dec 30, 2023

Test it via the following snippets

// DemoKit's main.swift
import Foundation
class X {}
func test1(_ obj: AnyObject) -> UInt64 {
    UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
func test2() {
    print("RawValue: \(test1(X.self))")
}
test2()

We are expecting a stable result here. And macOS is fine on this

./DemoKit
RawValue: 4295033832
./DemoKit
RawValue: 4295033832
./DemoKit
RawValue: 4295033832
./DemoKit
RawValue: 4295033832

On Linux platform we need some patch here to make it work

-    print("RawValue: \(test1(X.self))")
+    print("RawValue: \(test1(X.self as AnyObject))")

And no runtime crash happen. We did successfully get a result.

But the result is surprisingly unstable on different launch.

./DemoKit
RawValue: 187651124585888
./DemoKit
RawValue: 187651270174112
./DemoKit
RawValue: 187650465600928
...

And if we add 3 more test2() call here, we will find it will give different results even on the same launch.

./DemoKit
RawValue: 187650637530528
RawValue: 187650637531488
RawValue: 187650637531488
RawValue: 187650637531488
./DemoKit
RawValue: 187651094341024
RawValue: 187651094341984
RawValue: 187651094341984
RawValue: 187651094341984

IMO, such behavior means there is something wrong. Thus we should forbids "X.self as AnyObject" on non-ObjC Platform and always return false for "X.self as? AnyObject".

This may be a breaking change for non-ObjC Platform user, but it is worth to fix it on next release (5.10 or 5.10+1) IMO. (Or do we need to postpone the fix to landing on Swift 6?)

@slavapestov
Copy link
Contributor

I believe what's happening is that the explicit as AnyObject performs the fake "bridging" that the Linux compiler supports in service of Foundation, where an arbitrary Swift value (which can include a metatype) is wrapped inside an opaque _SwiftValue class instance.

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Jan 1, 2024

I believe what's happening is that the explicit as AnyObject performs the fake "bridging" that the Linux compiler supports in service of Foundation, where an arbitrary Swift value (which can include a metatype) is wrapped inside an opaque _SwiftValue class instance.

🤔 So is this expected behavior or should we somehow add a patch to ban this behavior?

@slavapestov
Copy link
Contributor

🤔 So is this expected behavior or should we somehow add a patch to ban this behavior?

It's expected behavior, unfortunately.

@tbkka
Copy link
Contributor

tbkka commented Jan 3, 2024

Seems like we should be able to change the metatype representation so it did look like a Swift object in memory. That would be a step towards resolving a number of other related problems with metatypes on Linux.

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Jan 4, 2024

🤔 So is this expected behavior or should we somehow add a patch to ban this behavior?

It's expected behavior, unfortunately.

I assume this is the logic you are talking about https://github.com/apple/swift/blob/a4e99b315cb3b4a39c7bf1686b907573a91f957a/stdlib/public/core/BridgeObjectiveC.swift#L796-L828

Can we at least return a stable identifier just as what it did on ObjC-supported platform? (Maybe a Hash table to track it?)

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Jan 4, 2024

Checking the compiled code by running swift build -c release on Ubuntu 22-04 and decompile it

We can see it was indeed calling a Swift._bridgeAnythingToObjectiveC<A>(A) -> Swift.AnyObject here

0000000000004078         sub        sp, sp, #0x40                               ; End of unwind block (FDE at 0x8ba4), Begin of unwind block (FDE at 0x8bb8), CODE XREF=$s11OpenCombine08AbstractB6Latest33_A5305C650BAF9A75262972B685046861LLC4SideVyxq_q0__qd__GAA06CustomB21IdentifierConvertibleA2aHP07combineL0AA0bL0VvgTW+16
000000000000407c         stp        fp, lr, [sp, #0x20]
0000000000004080         str        x19, [sp, #0x20 + saved_regs_10]
0000000000004084         add        fp, sp, #0x20
0000000000004088         stp        x1, x2, [sp, #0x8]
000000000000408c         mov        x1, sp
0000000000004090         str        x0, [sp, #0x20 + var_20]
0000000000004094         mov        x0, xzr
0000000000004098         str        x4, [sp, #0x20 + var_8]
000000000000409c         bl         $s11OpenCombine08AbstractB6Latest33_A5305C650BAF9A75262972B685046861LLCMa ; type metadata accessor for OpenCombine.(AbstractCombineLatest in _A5305C650BAF9A75262972B685046861)
00000000000040a0         str        x0, [sp, #0x20 + var_20]
00000000000040a4         bl         swift_getMetatypeMetadata                   ; swift_getMetatypeMetadata
00000000000040a8         mov        x1, x0
00000000000040ac         mov        x0, sp
00000000000040b0         bl         $ss27_bridgeAnythingToObjectiveCyyXlxlF     ; Swift._bridgeAnythingToObjectiveC<A>(A) -> Swift.AnyObject
00000000000040b4         mov        x19, x0
00000000000040b8         bl         swift_release                               ; swift_release
00000000000040bc         ldp        fp, lr, [sp, #0x20]
00000000000040c0         mov        x0, x19
00000000000040c4         ldr        x19, [sp, #0x20 + saved_regs_10]
00000000000040c8         add        sp, sp, #0x40
00000000000040cc         ret
                        ; endp

@Kyle-Ye
Copy link
Contributor Author

Kyle-Ye commented Jan 4, 2024

import Foundation
class X {}
func test1(_ obj: AnyObject) -> UInt64 {
    UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
func test2() {
    print("RawValue: \(test1(X.self))")
}
test2()

More test case on Linux platform

import Foundation
class X {}
func test1(_ obj: AnyObject) -> UInt64 {
    UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
func test2() {
    print("RawValue: \(test1(X.self as AnyObject))")
}
test2()
test2()
test2()
test2()
// Result will be x y y y
import Foundation
class X {}
func test1(_ obj: AnyObject) -> UInt64 {
    UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
func test2() {
	let a = X.self as AnyObject
    print("RawValue: \(test1(a))")
}
test2()
test2()
test2()
test2()
// Result will be x x x x
import Foundation
class X {}
func test1(_ obj: AnyObject) -> UInt64 {
    UInt64(UInt(bitPattern: ObjectIdentifier(obj)))
}
func test2() {
	let a = X.self
    print("RawValue: \(test1(a as AnyObject))")
}
test2()
test2()
test2()
test2()
// Result will be x y y y

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. good first issue Good for newcomers swift 5.9 type checker Area → compiler: Semantic analysis
Projects
None yet
Development

No branches or pull requests

3 participants