Skip to content

Commit bdf2294

Browse files
authored
[cxx-interop] Allow Swift to access non-public C++ members (#79093)
This patch introduces an a C++ class annotation, SWIFT_PRIVATE_FILEID, which will specify where Swift extensions of that class will be allowed to access its non-public members, e.g.: class SWIFT_PRIVATE_FILEID("MyModule/MyFile.swift") Foo { ... }; The goal of this feature is to help C++ developers incrementally migrate the implementation of their C++ classes to Swift, without breaking encapsulation and indiscriminately exposing those classes' private and protected fields. As an implementation detail of this feature, this patch introduces an abstraction for file ID strings, FileIDStr, which represent a parsed pair of module name/file name. rdar://137764620
1 parent 7aa1970 commit bdf2294

File tree

14 files changed

+827
-4
lines changed

14 files changed

+827
-4
lines changed

include/swift/AST/DiagnosticsClangImporter.def

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ NOTE(iterator_method_unavailable, none, "C++ method '%0' that returns an "
212212
NOTE(iterator_potentially_unsafe, none, "C++ methods that return iterators "
213213
"are potentially unsafe; try using Swift collection APIs instead", ())
214214

215+
ERROR(private_fileid_attr_repeated, none,
216+
"multiple SWIFT_PRIVATE_FILEID annotations were found on '%0'",
217+
(StringRef))
218+
219+
ERROR(private_fileid_attr_on_incomplete_type, none,
220+
"SWIFT_PRIVATE_FILEID cannot be applied to incomplete type, '%0'",
221+
(StringRef))
222+
NOTE(private_fileid_attr_here, none,
223+
"SWIFT_PRIVATE_FILEID annotation found here", ())
224+
225+
WARNING(private_fileid_attr_format_invalid, none,
226+
"SWIFT_PRIVATE_FILEID annotation on '%0' does not have a valid file ID",
227+
(StringRef))
228+
REMARK(private_fileid_attr_format_specification, none,
229+
"file IDs have the following format: 'ModuleName/FileName.swift'", ())
230+
NOTE(private_fileid_attr_format_suggestion, none,
231+
"did you mean '%0'?", (StringRef))
232+
215233
ERROR(reference_type_must_have_retain_release_attr, none,
216234
"reference type '%1' must have %select{'retain:'|'release:'}0 Swift "
217235
"attribute",

include/swift/AST/SourceFile.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,22 @@ class SourceFile final : public FileUnit {
825825

826826
ArrayRef<TypeDecl *> getLocalTypeDecls() const;
827827

828+
/// Uniquely identifies a source file without exposing its full file path.
829+
///
830+
/// A valid file ID should always be of the format "modulename/filename.swift"
831+
struct FileIDStr {
832+
StringRef moduleName;
833+
StringRef fileName;
834+
835+
/// Parse a string as a SourceFile::FileIDStr.
836+
///
837+
/// Returns \c nullopt if \param fileID could not be parsed.
838+
static std::optional<FileIDStr> parse(StringRef fileID);
839+
840+
/// Whether this SourceFile::FileID matches that of the given \param file.
841+
bool matches(const SourceFile *file) const;
842+
};
843+
828844
private:
829845

830846
/// If not \c None, the underlying vector contains the parsed tokens of this

include/swift/ClangImporter/ClangImporter.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,11 +738,23 @@ llvm::StringRef getCFTypeName(const clang::TypedefNameDecl *decl);
738738
ValueDecl *getImportedMemberOperator(const DeclBaseName &name,
739739
NominalTypeDecl *selfType,
740740
std::optional<Type> parameterType);
741+
741742
/// Map the access specifier of a Clang record member to a Swift access level.
742743
///
743744
/// This mapping is conservative: the resulting Swift access should be at _most_
744745
/// as permissive as the input C++ access.
745746
AccessLevel convertClangAccess(clang::AccessSpecifier access);
747+
748+
/// Read file IDs from 'private_fileid' Swift attributes on a Clang decl.
749+
///
750+
/// May return >1 fileID when a decl is annotated more than once, which should
751+
/// be treated as an error and appropriately diagnosed (using the included
752+
/// SourceLocation).
753+
///
754+
/// The returned fileIDs may not be of a valid format (e.g., missing a '/'),
755+
/// and should be parsed using swift::SourceFile::FileIDStr::parse().
756+
SmallVector<std::pair<StringRef, clang::SourceLocation>, 1>
757+
getPrivateFileIDAttrs(const clang::Decl *decl);
746758
} // namespace importer
747759

748760
struct ClangInvocationFileMapping {

lib/AST/DeclContext.cpp

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "swift/Basic/Assertions.h"
3232
#include "swift/Basic/SourceManager.h"
3333
#include "swift/Basic/Statistic.h"
34+
#include "swift/ClangImporter/ClangImporter.h"
3435
#include "clang/AST/ASTContext.h"
3536
#include "llvm/ADT/DenseMap.h"
3637
#include "llvm/ADT/Statistic.h"
@@ -1480,16 +1481,37 @@ bool AccessScope::allowsPrivateAccess(const DeclContext *useDC, const DeclContex
14801481
return usePkg->isSamePackageAs(srcPkg);
14811482
}
14821483
}
1483-
// Do not allow access if the sourceDC is in a different file
1484-
auto useSF = useDC->getOutermostParentSourceFile();
1485-
if (useSF != sourceDC->getOutermostParentSourceFile())
1486-
return false;
14871484

14881485
// Do not allow access if the sourceDC does not represent a type.
14891486
auto sourceNTD = sourceDC->getSelfNominalTypeDecl();
14901487
if (!sourceNTD)
14911488
return false;
14921489

1490+
// Do not allow access if the sourceDC is in a different file
1491+
auto *useSF = useDC->getOutermostParentSourceFile();
1492+
if (useSF != sourceDC->getOutermostParentSourceFile()) {
1493+
// This might be a C++ declaration with a SWIFT_PRIVATE_FILEID
1494+
// attribute, which asks us to treat it as if it were defined in the file
1495+
// with the specified FileID.
1496+
1497+
auto clangDecl = sourceNTD->getDecl()->getClangDecl();
1498+
if (!clangDecl)
1499+
return false;
1500+
1501+
// Diagnostics should enforce that there is at most SWIFT_PRIVATE_FILEID,
1502+
// but this handles the case where there is more than anyway (whether that
1503+
// is a feature or a bug). Allow access check to proceed if useSF is blessed
1504+
// by any of the SWIFT_PRIVATE_FILEID annotations (i.e., disallow private
1505+
// access if none of them bless useSF).
1506+
if (!llvm::any_of(
1507+
importer::getPrivateFileIDAttrs(clangDecl), [&](auto &blessed) {
1508+
auto blessedFileID = SourceFile::FileIDStr::parse(blessed.first);
1509+
return blessedFileID && blessedFileID->matches(useSF);
1510+
})) {
1511+
return false;
1512+
}
1513+
}
1514+
14931515
// Compare the private scopes and iterate over the parent types.
14941516
sourceDC = getPrivateDeclContext(sourceDC, useSF);
14951517
while (!useDC->isModuleContext()) {

lib/AST/Module.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3783,6 +3783,29 @@ ArrayRef<TypeDecl *> SourceFile::getLocalTypeDecls() const {
37833783
LocalTypeDeclsRequest{mutableThis}, {});
37843784
}
37853785

3786+
std::optional<SourceFile::FileIDStr>
3787+
SourceFile::FileIDStr::parse(StringRef fileID) {
3788+
auto names = fileID.split('/');
3789+
auto moduleName = names.first;
3790+
auto fileName = names.second;
3791+
3792+
if (moduleName.empty() || fileName.empty() || !fileName.ends_with(".swift") ||
3793+
fileName.contains('/'))
3794+
return {};
3795+
3796+
return {SourceFile::FileIDStr{/*.moduleName=*/moduleName,
3797+
/*.fileName=*/fileName}};
3798+
}
3799+
3800+
bool SourceFile::FileIDStr::matches(const SourceFile *file) const {
3801+
// Never match with SourceFiles that do not correpond to a file on disk
3802+
if (file->getFilename().empty())
3803+
return false;
3804+
3805+
return moduleName == file->getParentModule()->getNameStr() &&
3806+
fileName == llvm::sys::path::filename(file->getFilename());
3807+
}
3808+
37863809
namespace {
37873810
class LocalTypeDeclCollector : public ASTWalker {
37883811
SmallVectorImpl<TypeDecl *> &results;

lib/ClangImporter/ClangImporter.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8450,3 +8450,21 @@ AccessLevel importer::convertClangAccess(clang::AccessSpecifier access) {
84508450
return AccessLevel::Public;
84518451
}
84528452
}
8453+
8454+
SmallVector<std::pair<StringRef, clang::SourceLocation>, 1>
8455+
importer::getPrivateFileIDAttrs(const clang::Decl *decl) {
8456+
llvm::SmallVector<std::pair<StringRef, clang::SourceLocation>, 1> files;
8457+
8458+
constexpr auto prefix = StringRef("private_fileid:");
8459+
8460+
if (decl->hasAttrs()) {
8461+
for (const auto *attr : decl->getAttrs()) {
8462+
const auto *swiftAttr = dyn_cast<clang::SwiftAttrAttr>(attr);
8463+
if (swiftAttr && swiftAttr->getAttribute().starts_with(prefix))
8464+
files.push_back({swiftAttr->getAttribute().drop_front(prefix.size()),
8465+
attr->getLocation()});
8466+
}
8467+
}
8468+
8469+
return files;
8470+
}

lib/ClangImporter/ImportDecl.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,44 @@ namespace {
26022602
return result;
26032603
}
26042604

2605+
void validatePrivateFileIDAttributes(const clang::CXXRecordDecl *decl) {
2606+
auto anns = importer::getPrivateFileIDAttrs(decl);
2607+
2608+
if (anns.size() > 1) {
2609+
Impl.diagnose(HeaderLoc(decl->getLocation()),
2610+
diag::private_fileid_attr_repeated, decl->getName());
2611+
for (auto ann : anns)
2612+
Impl.diagnose(HeaderLoc(ann.second), diag::private_fileid_attr_here);
2613+
} else if (anns.size() == 1) {
2614+
auto ann = anns[0];
2615+
if (!SourceFile::FileIDStr::parse(ann.first)) {
2616+
Impl.diagnose(HeaderLoc(ann.second),
2617+
diag::private_fileid_attr_format_invalid,
2618+
decl->getName());
2619+
Impl.diagnose({}, diag::private_fileid_attr_format_specification);
2620+
2621+
if (ann.first.count('/') > 1) {
2622+
// Try to construct a suggestion from predictable mistakes.
2623+
SmallString<32> suggestion;
2624+
2625+
// Mistake #1: confusing fileID for filePath => writing too many
2626+
// '/'s
2627+
suggestion.append(ann.first.split('/').first);
2628+
suggestion.push_back('/');
2629+
suggestion.append(ann.first.rsplit('/').second);
2630+
2631+
// Mistake #2: forgetting to use filename with .swift extension
2632+
if (!suggestion.ends_with(".swift"))
2633+
suggestion.append(".swift");
2634+
2635+
if (SourceFile::FileIDStr::parse(suggestion))
2636+
Impl.diagnose({}, diag::private_fileid_attr_format_suggestion,
2637+
suggestion);
2638+
}
2639+
}
2640+
}
2641+
}
2642+
26052643
void validateForeignReferenceType(const clang::CXXRecordDecl *decl,
26062644
ClassDecl *classDecl) {
26072645

@@ -2804,6 +2842,16 @@ namespace {
28042842
Diagnostic(diag::incomplete_record, Impl.SwiftContext.AllocateCopy(
28052843
decl->getNameAsString())),
28062844
decl->getLocation());
2845+
2846+
auto attrs = importer::getPrivateFileIDAttrs(decl);
2847+
if (!attrs.empty()) {
2848+
Impl.diagnose(HeaderLoc(decl->getLocation()),
2849+
diag::private_fileid_attr_on_incomplete_type,
2850+
decl->getName());
2851+
for (auto attr : attrs)
2852+
Impl.diagnose(HeaderLoc(attr.second),
2853+
diag::private_fileid_attr_here);
2854+
}
28072855
}
28082856

28092857
decl = decl->getDefinition();
@@ -2958,6 +3006,8 @@ namespace {
29583006
"are not yet available in Swift");
29593007
}
29603008

3009+
validatePrivateFileIDAttributes(decl);
3010+
29613011
if (auto classDecl = dyn_cast<ClassDecl>(result)) {
29623012
validateForeignReferenceType(decl, classDecl);
29633013

lib/ClangImporter/SwiftBridging/swift/bridging

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,42 @@
191191
#define SWIFT_RETURNS_UNRETAINED \
192192
__attribute__((swift_attr("returns_unretained")))
193193

194+
/// Specifies that the non-public members of a C++ class, struct, or union can
195+
/// be accessed from extensions of that type, in the given file ID.
196+
///
197+
/// In other words, Swift's access controls will behave as if the non-public
198+
/// members of the annotated C++ class were privated declared in the specified
199+
/// Swift source file, rather than in a C++ header file/Clang module.
200+
///
201+
/// For example, we can annotate a C++ class definition like this:
202+
///
203+
/// ```c++
204+
/// class SWIFT_PRIVATE_FILEID("MySwiftModule/MySwiftFile.swift")
205+
/// MyCxxClass {
206+
/// private:
207+
/// void privateMethod();
208+
/// int privateStorage;
209+
/// };
210+
/// ```
211+
///
212+
/// Then, Swift extensions of `MyCxxClass` in `MySwiftModule/MySwiftFile.swift`
213+
/// are allowed to access `privateMethod()` and `privateStorage`:
214+
///
215+
/// ```swift
216+
/// //-- MySwiftModule/SwiftFile.swift
217+
/// extension MyCxxClass {
218+
/// func ext() {
219+
/// privateMethod()
220+
/// print("\(privateStorage)")
221+
/// }
222+
/// }
223+
/// ```
224+
///
225+
/// Non-public access is still forbidden outside of extensions and outside of
226+
/// the designated file ID.
227+
#define SWIFT_PRIVATE_FILEID(_fileID) \
228+
__attribute__((swift_attr("private_fileid:" _fileID)))
229+
194230
#else // #if _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)
195231

196232
// Empty defines for compilers that don't support `attribute(swift_attr)`.
@@ -210,6 +246,7 @@
210246
#define SWIFT_ESCAPABLE_IF(...)
211247
#define SWIFT_RETURNS_RETAINED
212248
#define SWIFT_RETURNS_UNRETAINED
249+
#define SWIFT_PRIVATE_FILEID(_fileID)
213250

214251
#endif // #if _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)
215252

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module NonPublic {
2+
requires cplusplus
3+
header "non-public.h"
4+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#ifndef NON_PUBLIC_H
2+
#define NON_PUBLIC_H
3+
4+
// Override this to test structs
5+
#ifndef TEST_CLASS
6+
#define TEST_CLASS class
7+
#endif
8+
9+
// Override this to test protected
10+
#ifndef TEST_PRIVATE
11+
#define TEST_PRIVATE private
12+
#endif
13+
14+
/// A C++ class with various kinds of public and non-public fields, all of which
15+
/// should be imported. Non-public fields should only be accessible inside
16+
/// MyClass extensions in blessed.swift.
17+
TEST_CLASS
18+
__attribute__((__swift_attr__("private_fileid:main/blessed.swift"))) MyClass {
19+
20+
public:
21+
void publMethod(void) const {}
22+
void publMutatingMethod(void) {}
23+
int publVar;
24+
static void publStaticFunc(void);
25+
static int publStaticVar;
26+
27+
typedef int publTypedef;
28+
struct publStruct {};
29+
30+
enum publEnum { publEnumValue1 };
31+
enum class publEnumClass { publEnumClassValue1 };
32+
enum { publEnumAnonValue1 };
33+
enum publEnumClosed {
34+
publEnumClosedValue1
35+
} __attribute__((enum_extensibility(closed)));
36+
enum publEnumOpen {
37+
publEnumOpenValue1
38+
} __attribute__((enum_extensibility(open)));
39+
enum publEnumFlag {} __attribute__((flag_enum));
40+
41+
TEST_PRIVATE:
42+
void privMethod(void) const {}
43+
void privMutatingMethod(void) {}
44+
int privVar;
45+
static void privStaticFunc(void);
46+
static int privStaticVar;
47+
48+
typedef int privTypedef;
49+
struct privStruct {};
50+
51+
enum privEnum { privEnumValue1 };
52+
enum class privEnumClass { privEnumClassValue1 };
53+
enum { privEnumAnonValue1 };
54+
enum privEnumClosed {
55+
privEnumClosedValue1
56+
} __attribute__((enum_extensibility(closed)));
57+
enum privEnumOpen {
58+
privEnumOpenValue1
59+
} __attribute__((enum_extensibility(open)));
60+
enum privEnumFlag {} __attribute__((flag_enum));
61+
};
62+
63+
/// A C++ templated class, whose non-public fields should be accessible in
64+
/// extensions of the (instantiated) class in blessed.swift.
65+
template <typename T>
66+
TEST_CLASS __attribute__((
67+
__swift_attr__("private_fileid:main/blessed.swift"))) MyClassTemplate {
68+
public:
69+
T publMethodT(T t) const { return t; }
70+
T publVarT;
71+
typedef T publTypedefT;
72+
73+
void publMethod(void) const {}
74+
int publVar;
75+
typedef int publTypedef;
76+
TEST_PRIVATE:
77+
T privMethodT(T t) const { return t; }
78+
T privVarT;
79+
typedef T privTypedefT;
80+
81+
void privMethod(void) const {}
82+
int privVar;
83+
typedef int privTypedef;
84+
};
85+
86+
typedef MyClassTemplate<float> MyFloatyClass;
87+
typedef MyClassTemplate<MyClass> MyClassyClass;
88+
89+
#endif /* NON_PUBLIC_H */

0 commit comments

Comments
 (0)