Repository: swiftlang/swift
Pull request #79662: [cxx-interop] Interpret Self as a static shorthand for FRTs
Author: login: Xazax-hun
Labels: name: c++ interop
description: Feature: Interoperability with C++
Created: 2025-02-27T16:01:37Z | Merged: 2025-03-20T17:54:35Z

We do not have dynamic self metadata for foreign reference types.

rdar://145066864



Diff:
diff --git a/lib/ClangImporter/ImportDecl.cpp b/lib/ClangImporter/ImportDecl.cpp
index dbd9e46c029a1..ae9214ac1f2b6 100644
--- a/lib/ClangImporter/ImportDecl.cpp
+++ b/lib/ClangImporter/ImportDecl.cpp
@@ -3017,6 +3017,10 @@ namespace {
               /*Obsoleted=*/{});
           classDecl->getAttrs().add(AvAttr);
         }
+
+        if (decl->isEffectivelyFinal())
+          classDecl->getAttrs().add(new (Impl.SwiftContext)
+                                    FinalAttr(/*IsImplicit=*/true));
       }
 
       // If this module is declared as a C++ module, try to synthesize
diff --git a/lib/Sema/PreCheckTarget.cpp b/lib/Sema/PreCheckTarget.cpp
index 70c20492a2297..1f07041da2f61 100644
--- a/lib/Sema/PreCheckTarget.cpp
+++ b/lib/Sema/PreCheckTarget.cpp
@@ -709,7 +709,8 @@ Expr *TypeChecker::resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE,
         if (DeclContext *typeContext = DC->getInnermostTypeContext()){
           Type SelfType = typeContext->getSelfInterfaceType();
 
-          if (typeContext->getSelfClassDecl())
+          if (typeContext->getSelfClassDecl() &&
+              !typeContext->getSelfClassDecl()->isForeignReferenceType())
             SelfType = DynamicSelfType::get(SelfType, Context);
           return new (Context)
               TypeExpr(new (Context) SelfTypeRepr(SelfType, Loc));
diff --git a/lib/Sema/TypeCheckType.cpp b/lib/Sema/TypeCheckType.cpp
index 86ca6b6d176e1..db70f10e349ef 100644
--- a/lib/Sema/TypeCheckType.cpp
+++ b/lib/Sema/TypeCheckType.cpp
@@ -1679,6 +1679,10 @@ static SelfTypeKind getSelfTypeKind(DeclContext *dc,
   if (!typeDC->getSelfClassDecl())
     return SelfTypeKind::StaticSelf;
 
+  // For foreign reference types `Self` is a shorthand for the nominal type.
+  if (typeDC->getSelfClassDecl()->isForeignReferenceType())
+    return SelfTypeKind::StaticSelf;
+
   // In class methods, 'Self' is the DynamicSelfType and can only appear in
   // the return type.
   switch (options.getBaseContext()) {
diff --git a/test/Interop/Cxx/foreign-reference/Inputs/inheritance.h b/test/Interop/Cxx/foreign-reference/Inputs/inheritance.h
index d70e90f0a001b..5192ef2dd4ec8 100644
--- a/test/Interop/Cxx/foreign-reference/Inputs/inheritance.h
+++ b/test/Interop/Cxx/foreign-reference/Inputs/inheritance.h
@@ -109,7 +109,7 @@ DerivedFromValueTypeAndAnnotated *returnDerivedFromValueTypeAndAnnotated() { //
     return new DerivedFromValueTypeAndAnnotated();
 }
 
-struct DerivedFromRefType : RefType {};
+struct DerivedFromRefType final : RefType {};
 DerivedFromRefType *returnDerivedFromRefType() {
     return new DerivedFromRefType();
 }
@@ -180,7 +180,7 @@ __attribute__((swift_attr("release:RCRelease"))) RefType {};
 
 RefType *returnRefType() { return new RefType(); }; // expected-warning {{'returnRefType' should be annotated with either SWIFT_RETURNS_RETAINED or SWIFT_RETURNS_UNRETAINED as it is returning a SWIFT_SHARED_REFERENC}}
 
-struct DerivedFromRefType : RefType {};
+struct DerivedFromRefType final : RefType {};
 DerivedFromRefType *returnDerivedFromRefType() { // TODO: rdar://145098078 Missing SWIFT_RETURNS_(UN)RETAINED annotation warning should trigger for inferred foreeign reference types as well
   return new DerivedFromRefType();
 };
diff --git a/test/Interop/Cxx/foreign-reference/Inputs/reference-counted.h b/test/Interop/Cxx/foreign-reference/Inputs/reference-counted.h
index a6d361c0b466e..1ed4c3512d8d5 100644
--- a/test/Interop/Cxx/foreign-reference/Inputs/reference-counted.h
+++ b/test/Interop/Cxx/foreign-reference/Inputs/reference-counted.h
@@ -14,7 +14,7 @@ namespace NS {
 
 struct __attribute__((swift_attr("import_as_ref")))
 __attribute__((swift_attr("retain:LCRetain")))
-__attribute__((swift_attr("release:LCRelease"))) LocalCount {
+__attribute__((swift_attr("release:LCRelease"))) LocalCount final {
   int value = 0;
 
   static LocalCount *create() {
diff --git a/test/Interop/Cxx/foreign-reference/extensions.swift b/test/Interop/Cxx/foreign-reference/extensions.swift
new file mode 100644
index 0000000000000..818b648be3981
--- /dev/null
+++ b/test/Interop/Cxx/foreign-reference/extensions.swift
@@ -0,0 +1,50 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -cxx-interoperability-mode=default -Xfrontend -disable-availability-checking -Onone) | %FileCheck %s
+// REQUIRES: executable_test
+
+// Metadata for foreign reference types is not supported on Windows.
+// UNSUPPORTED: OS=windows-msvc
+
+import ReferenceCounted
+
+protocol MyProto {
+    static func g()
+    func foo() -> Self
+    func bar(_ x: Self)
+    func baz()
+}
+
+extension NS.LocalCount: MyProto {
+    static func g() {
+        print("Static method g called")
+    }
+
+    public func foo() -> Self {
+        Self.g()
+        return self
+    }
+
+    public func bar(_ x: Self) {
+    }
+}
+
+extension MyProto {
+    func baz() {
+        Self.g()
+    }
+}
+
+extension NS.LocalCount {
+    public func f() {
+        Self.g()
+    }
+}
+
+
+let x = NS.LocalCount.create()
+x.f()
+// CHECK: Static method g called
+let _ = x.foo()
+// CHECK-NEXT: Static method g called
+let _ = x.baz()
+// CHECK-NEXT: Static method g called
+x.bar(x)


Reviews:
user: login: AnthonyLatsis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T16:16:55Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T16:18:58Z
user: login: egorzhdan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T16:27:02Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T17:20:18Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:10:29Z
user: login: egorzhdan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:38:15Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:41:52Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:52:14Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:17:52Z
user: login: j-hui
body: Overall this LGTM. Left a nit and a question.

I'm also curious to know what's going on with the Windows CI, and why that only triggers on Windows.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:31:09Z
user: login: Xazax-hun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:06:45Z

Review comments:
diff_hunk: @@ -1684,6 +1684,11 @@ static SelfTypeKind getSelfTypeKind(DeclContext *dc,
   if (!typeDC->getSelfClassDecl())
     return SelfTypeKind::StaticSelf;
 
+  // For foreign reference types `Self` is a shorthand for the nominal type.
+  if (typeDC->getSelfClassDecl() &&
+      typeDC->getSelfClassDecl()->isForeignReferenceType())
path: lib/Sema/TypeCheckType.cpp
user: login: AnthonyLatsis
body: ```suggestion
  if (!typeDC->getSelfClassDecl())
    return SelfTypeKind::StaticSelf;

  // For foreign reference types `Self` is a shorthand for the nominal type.
  if (typeDC->getSelfClassDecl()->isForeignReferenceType())
```
created_at: 2025-02-27T16:16:55Z
author_association: COLLABORATOR
diff_hunk: @@ -1684,6 +1684,11 @@ static SelfTypeKind getSelfTypeKind(DeclContext *dc,
   if (!typeDC->getSelfClassDecl())
     return SelfTypeKind::StaticSelf;
 
+  // For foreign reference types `Self` is a shorthand for the nominal type.
+  if (typeDC->getSelfClassDecl() &&
+      typeDC->getSelfClassDecl()->isForeignReferenceType())
path: lib/Sema/TypeCheckType.cpp
user: login: Xazax-hun
body: Whoops, good point :)
created_at: 2025-02-27T16:18:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: egorzhdan
body: Could you please add a test where an FRT is conformed to a Swift protocol that uses `Self` in its requirements? e.g.
```swift
protocol MyProto {
  func foo() -> Self
}
```
created_at: 2025-02-27T16:25:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: egorzhdan
body: Since this test doesn't have any executable code, is it effectively a typechecking test? Do you think it would be worth trying to verify, for instance, that a value can be returned from a static function via this mechanism?
created_at: 2025-02-27T16:26:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Ah my bad! I wanted to call some code to make sure the binary does not actually crash at runtime. Will update the test. 
created_at: 2025-02-27T17:20:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Added. 
created_at: 2025-03-03T12:10:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Test updated.
created_at: 2025-03-03T12:10:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: egorzhdan
body: Is there a conformance to this protocol?
created_at: 2025-03-03T12:38:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Whoops, I left it out by accident. Good catch! 
created_at: 2025-03-03T12:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Hmm, actually, I am running into some trouble constructing a case like this with foreign reference types.

```
extension NS.LocalCount: MyProto {
    public func foo() -> Self {
        return self
    }
}
```

Will trigger the compiler diagnostic:
```
error: method 'foo()' in non-final class 'NS.LocalCount' must return 'Self' to conform to protocol 'MyProto'
```

This makes sense, the implementation does not guarantee that we produce the most-derived type.

I tried:
```
extension NS.LocalCount: MyProto {
    public func foo() -> Self {
        return Self()
    }
}
```

This does not work as we do not really create any initializers for foreign reference types. And even when we will, probably those will not support creating the most-derived type as C++ does not have virtual constructors.

I tried marking `NS.LocalCount`  as `final` on the C++ side, but it looks like this information is lost in the importer.

Do you have a code snippet in mind that would be valid and conform to a protocol like that? I could fix importing the `final` specifier and add a test here after that. But wanted to double check if you had something else in mind. 
created_at: 2025-03-03T13:52:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Made the class final and fixed the importing of final. Did not create a separate PR for the `final` fix because I had no idea how to test it as we don't support subclassing foreign reference types in Swift and ide-tests don't seem to print the final attribute for classes. So this might be the only place at the moment where this attribute actually matters in Swift. 
created_at: 2025-03-03T15:17:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,26 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: j-hui
body: nit:

```suggestion
// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -cxx-interoperability-mode=default -Xfrontend -disable-availability-checking -Onone)
```
created_at: 2025-03-04T06:19:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: j-hui
body: What happens if you use `Self` in the argument position rather than in the return position? Would `final` matter then?

Or even both, which should give you a "free" conformance:

```swift
protocol Free {
    func id(x: Self) -> Self
}
```
created_at: 2025-03-04T06:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+// RUN: %target-run-simple-swift(-I %S/Inputs/ -Xfrontend -enable-experimental-cxx-interop -Xfrontend -disable-availability-checking -Onone)
+// REQUIRES: executable_test
+
+import ReferenceCounted
+
+extension NS.LocalCount {
+    static func g() {}
+
+    public func f() {
+        Self.g()
+    }
+}
path: test/Interop/Cxx/foreign-reference/extensions.swift
user: login: Xazax-hun
body: Added test when Self is in a parameter position. Also added FileCheck lines to make sure the static method is actually called. Added another case where the static method is called from a default protocol implementation. 
created_at: 2025-03-05T16:06:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: Xazax-hun
created_at: 2025-03-20T12:36:23Z
body: @swift-ci please smoke test 
author_association: CONTRIBUTOR
user: login: Xazax-hun
created_at: 2025-03-20T12:37:38Z
body: > I'm also curious to know what's going on with the Windows CI, and why that only triggers on Windows.

It looks like the root cause of the problem is that we do not support emitting metadata for some foreign types on Windows. This is a known limitation and the crash is not related to my patch, the code crashes the compiler even before my changes on Windows. Disabling the test on Windows for now. 
author_association: CONTRIBUTOR