Skip to content

[cxx-interop] Allow Swift to access non-public C++ members #79093

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 9 commits into from
Feb 18, 2025
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
18 changes: 18 additions & 0 deletions include/swift/AST/DiagnosticsClangImporter.def
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,24 @@ NOTE(iterator_method_unavailable, none, "C++ method '%0' that returns an "
NOTE(iterator_potentially_unsafe, none, "C++ methods that return iterators "
"are potentially unsafe; try using Swift collection APIs instead", ())

ERROR(private_fileid_attr_repeated, none,
"multiple SWIFT_PRIVATE_FILEID annotations were found on '%0'",
(StringRef))

ERROR(private_fileid_attr_on_incomplete_type, none,
"SWIFT_PRIVATE_FILEID cannot be applied to incomplete type, '%0'",
(StringRef))
NOTE(private_fileid_attr_here, none,
"SWIFT_PRIVATE_FILEID annotation found here", ())

WARNING(private_fileid_attr_format_invalid, none,
"SWIFT_PRIVATE_FILEID annotation on '%0' does not have a valid file ID",
(StringRef))
REMARK(private_fileid_attr_format_specification, none,
"file IDs have the following format: 'ModuleName/FileName.swift'", ())
NOTE(private_fileid_attr_format_suggestion, none,
"did you mean '%0'?", (StringRef))

ERROR(reference_type_must_have_retain_release_attr, none,
"reference type '%1' must have %select{'retain:'|'release:'}0 Swift "
"attribute",
Expand Down
16 changes: 16 additions & 0 deletions include/swift/AST/SourceFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,22 @@ class SourceFile final : public FileUnit {

ArrayRef<TypeDecl *> getLocalTypeDecls() const;

/// Uniquely identifies a source file without exposing its full file path.
///
/// A valid file ID should always be of the format "modulename/filename.swift"
struct FileIDStr {
StringRef moduleName;
StringRef fileName;

/// Parse a string as a SourceFile::FileIDStr.
///
/// Returns \c nullopt if \param fileID could not be parsed.
static std::optional<FileIDStr> parse(StringRef fileID);

/// Whether this SourceFile::FileID matches that of the given \param file.
bool matches(const SourceFile *file) const;
};

private:

/// If not \c None, the underlying vector contains the parsed tokens of this
Expand Down
12 changes: 12 additions & 0 deletions include/swift/ClangImporter/ClangImporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -738,11 +738,23 @@ llvm::StringRef getCFTypeName(const clang::TypedefNameDecl *decl);
ValueDecl *getImportedMemberOperator(const DeclBaseName &name,
NominalTypeDecl *selfType,
std::optional<Type> parameterType);

/// Map the access specifier of a Clang record member to a Swift access level.
///
/// This mapping is conservative: the resulting Swift access should be at _most_
/// as permissive as the input C++ access.
AccessLevel convertClangAccess(clang::AccessSpecifier access);

/// Read file IDs from 'private_fileid' Swift attributes on a Clang decl.
///
/// May return >1 fileID when a decl is annotated more than once, which should
/// be treated as an error and appropriately diagnosed (using the included
/// SourceLocation).
///
/// The returned fileIDs may not be of a valid format (e.g., missing a '/'),
/// and should be parsed using swift::SourceFile::FileIDStr::parse().
SmallVector<std::pair<StringRef, clang::SourceLocation>, 1>
getPrivateFileIDAttrs(const clang::Decl *decl);
} // namespace importer

struct ClangInvocationFileMapping {
Expand Down
32 changes: 27 additions & 5 deletions lib/AST/DeclContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "swift/AST/AccessScope.h"
#include "swift/AST/AvailabilityInference.h"
#include "swift/AST/ClangModuleLoader.h"
#include "swift/AST/DiagnosticsSema.h"
#include "swift/AST/Expr.h"
#include "swift/AST/FileUnit.h"
#include "swift/AST/GenericEnvironment.h"
Expand All @@ -26,10 +27,10 @@
#include "swift/AST/SourceFile.h"
#include "swift/AST/TypeCheckRequests.h"
#include "swift/AST/Types.h"
#include "swift/AST/DiagnosticsSema.h"
#include "swift/Basic/Assertions.h"
#include "swift/Basic/SourceManager.h"
#include "swift/Basic/Statistic.h"
#include "swift/ClangImporter/ClangImporter.h"
#include "clang/AST/ASTContext.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/Statistic.h"
Expand Down Expand Up @@ -1467,16 +1468,37 @@ bool AccessScope::allowsPrivateAccess(const DeclContext *useDC, const DeclContex
return usePkg->isSamePackageAs(srcPkg);
}
}
// Do not allow access if the sourceDC is in a different file
auto useSF = useDC->getOutermostParentSourceFile();
if (useSF != sourceDC->getOutermostParentSourceFile())
return false;

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

// Do not allow access if the sourceDC is in a different file
auto *useSF = useDC->getOutermostParentSourceFile();
if (useSF != sourceDC->getOutermostParentSourceFile()) {
// This might be a C++ declaration with a SWIFT_PRIVATE_FILEID
// attribute, which asks us to treat it as if it were defined in the file
// with the specified FileID.

auto clangDecl = sourceNTD->getDecl()->getClangDecl();
if (!clangDecl)
return false;

// Diagnostics should enforce that there is at most SWIFT_PRIVATE_FILEID,
// but this handles the case where there is more than anyway (whether that
// is a feature or a bug). Allow access check to proceed if useSF is blessed
// by any of the SWIFT_PRIVATE_FILEID annotations (i.e., disallow private
// access if none of them bless useSF).
if (!llvm::any_of(
importer::getPrivateFileIDAttrs(clangDecl), [&](auto &blessed) {
auto blessedFileID = SourceFile::FileIDStr::parse(blessed.first);
return blessedFileID && blessedFileID->matches(useSF);
})) {
return false;
}
}

// Compare the private scopes and iterate over the parent types.
sourceDC = getPrivateDeclContext(sourceDC, useSF);
while (!useDC->isModuleContext()) {
Expand Down
23 changes: 23 additions & 0 deletions lib/AST/Module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3783,6 +3783,29 @@ ArrayRef<TypeDecl *> SourceFile::getLocalTypeDecls() const {
LocalTypeDeclsRequest{mutableThis}, {});
}

std::optional<SourceFile::FileIDStr>
SourceFile::FileIDStr::parse(StringRef fileID) {
auto names = fileID.split('/');
auto moduleName = names.first;
auto fileName = names.second;

if (moduleName.empty() || fileName.empty() || !fileName.ends_with(".swift") ||
fileName.contains('/'))
return {};

return {SourceFile::FileIDStr{/*.moduleName=*/moduleName,
/*.fileName=*/fileName}};
}

bool SourceFile::FileIDStr::matches(const SourceFile *file) const {
// Never match with SourceFiles that do not correpond to a file on disk
if (file->getFilename().empty())
return false;

return moduleName == file->getParentModule()->getNameStr() &&
fileName == llvm::sys::path::filename(file->getFilename());
}

namespace {
class LocalTypeDeclCollector : public ASTWalker {
SmallVectorImpl<TypeDecl *> &results;
Expand Down
18 changes: 18 additions & 0 deletions lib/ClangImporter/ClangImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8348,3 +8348,21 @@ AccessLevel importer::convertClangAccess(clang::AccessSpecifier access) {
return AccessLevel::Public;
}
}

SmallVector<std::pair<StringRef, clang::SourceLocation>, 1>
importer::getPrivateFileIDAttrs(const clang::Decl *decl) {
llvm::SmallVector<std::pair<StringRef, clang::SourceLocation>, 1> files;

constexpr auto prefix = StringRef("private_fileid:");

if (decl->hasAttrs()) {
for (const auto *attr : decl->getAttrs()) {
const auto *swiftAttr = dyn_cast<clang::SwiftAttrAttr>(attr);
if (swiftAttr && swiftAttr->getAttribute().starts_with(prefix))
files.push_back({swiftAttr->getAttribute().drop_front(prefix.size()),
attr->getLocation()});
}
}

return files;
}
50 changes: 50 additions & 0 deletions lib/ClangImporter/ImportDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2604,6 +2604,44 @@ namespace {
return result;
}

void validatePrivateFileIDAttributes(const clang::CXXRecordDecl *decl) {
auto anns = importer::getPrivateFileIDAttrs(decl);

if (anns.size() > 1) {
Impl.diagnose(HeaderLoc(decl->getLocation()),
diag::private_fileid_attr_repeated, decl->getName());
for (auto ann : anns)
Impl.diagnose(HeaderLoc(ann.second), diag::private_fileid_attr_here);
} else if (anns.size() == 1) {
auto ann = anns[0];
if (!SourceFile::FileIDStr::parse(ann.first)) {
Impl.diagnose(HeaderLoc(ann.second),
diag::private_fileid_attr_format_invalid,
decl->getName());
Impl.diagnose({}, diag::private_fileid_attr_format_specification);

if (ann.first.count('/') > 1) {
// Try to construct a suggestion from predictable mistakes.
SmallString<32> suggestion;

// Mistake #1: confusing fileID for filePath => writing too many
// '/'s
suggestion.append(ann.first.split('/').first);
suggestion.push_back('/');
suggestion.append(ann.first.rsplit('/').second);

// Mistake #2: forgetting to use filename with .swift extension
if (!suggestion.ends_with(".swift"))
suggestion.append(".swift");

if (SourceFile::FileIDStr::parse(suggestion))
Impl.diagnose({}, diag::private_fileid_attr_format_suggestion,
suggestion);
}
}
}
}

void validateForeignReferenceType(const clang::CXXRecordDecl *decl,
ClassDecl *classDecl) {

Expand Down Expand Up @@ -2806,6 +2844,16 @@ namespace {
Diagnostic(diag::incomplete_record, Impl.SwiftContext.AllocateCopy(
decl->getNameAsString())),
decl->getLocation());

auto attrs = importer::getPrivateFileIDAttrs(decl);
if (!attrs.empty()) {
Impl.diagnose(HeaderLoc(decl->getLocation()),
diag::private_fileid_attr_on_incomplete_type,
decl->getName());
for (auto attr : attrs)
Impl.diagnose(HeaderLoc(attr.second),
diag::private_fileid_attr_here);
}
}

decl = decl->getDefinition();
Expand Down Expand Up @@ -2960,6 +3008,8 @@ namespace {
"are not yet available in Swift");
}

validatePrivateFileIDAttributes(decl);

if (auto classDecl = dyn_cast<ClassDecl>(result)) {
validateForeignReferenceType(decl, classDecl);

Expand Down
37 changes: 37 additions & 0 deletions lib/ClangImporter/SwiftBridging/swift/bridging
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,42 @@
#define SWIFT_RETURNS_UNRETAINED \
__attribute__((swift_attr("returns_unretained")))

/// Specifies that the non-public members of a C++ class, struct, or union can
/// be accessed from extensions of that type, in the given file ID.
///
/// In other words, Swift's access controls will behave as if the non-public
/// members of the annotated C++ class were privated declared in the specified
/// Swift source file, rather than in a C++ header file/Clang module.
///
/// For example, we can annotate a C++ class definition like this:
///
/// ```c++
/// class SWIFT_PRIVATE_FILEID("MySwiftModule/MySwiftFile.swift")
/// MyCxxClass {
/// private:
/// void privateMethod();
/// int privateStorage;
/// };
/// ```
///
/// Then, Swift extensions of `MyCxxClass` in `MySwiftModule/MySwiftFile.swift`
/// are allowed to access `privateMethod()` and `privateStorage`:
///
/// ```swift
/// //-- MySwiftModule/SwiftFile.swift
/// extension MyCxxClass {
/// func ext() {
/// privateMethod()
/// print("\(privateStorage)")
/// }
/// }
/// ```
///
/// Non-public access is still forbidden outside of extensions and outside of
/// the designated file ID.
#define SWIFT_PRIVATE_FILEID(_fileID) \
__attribute__((swift_attr("private_fileid:" _fileID)))

#else // #if _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)

// Empty defines for compilers that don't support `attribute(swift_attr)`.
Expand All @@ -210,6 +246,7 @@
#define SWIFT_ESCAPABLE_IF(...)
#define SWIFT_RETURNS_RETAINED
#define SWIFT_RETURNS_UNRETAINED
#define SWIFT_PRIVATE_FILEID(_fileID)

#endif // #if _CXX_INTEROP_HAS_ATTRIBUTE(swift_attr)

Expand Down
4 changes: 4 additions & 0 deletions test/Interop/Cxx/class/access/Inputs/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module NonPublic {
requires cplusplus
header "non-public.h"
}
Loading