-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[cxx-interop] Use formal C++ interop mode to fix name lookup in module interfaces #79984
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…e interfaces It is possible for a module interface (e.g., ModuleA) to be generated with C++ interop disabled, and then rebuilt with C++ interop enabled (e.g., because ModuleB, which imports ModuleA, has C++ interop enabled). This circumstance can lead to various issues when name lookup behaves differently depending on whether C++ interop is enabled, e.g., when a module name is shadowed by a namespace of the same name---this only happens in C++ because namespaces do not exist in C. Unfortunately, naming namespaces the same as a module is a common C++ convention, leading to many textual interfaces whose fully-qualified identifiers (e.g., c_module.c_member) cannot be correctly resolved when C++ interop is enabled (because c_module is shadowed by a namespace of the same name). This patch does two things. First, it introduces a new frontend flag, -formal-cxx-interoperability-mode, which records the C++ interop mode a module interface was originally compiled with. Doing so allows subsequent consumers of that interface to interpret it according to the formal C++ interop mode. Note that the actual "versioning" used by this flag is very crude: "off" means disabled, and "swift-6" means enabled. This is done to be compatible with C++ interop compat versioning scheme, which seems to produce some invalid (but unused) version numbers. The versioning scheme for both the formal and actual C++ interop modes should be clarified and fixed in a subsequent patch. The second thing this patch does is fix the module/namespace collision issue in module interface files. It uses the formal C++ interop mode to determine whether it should resolve C++-only decls during name lookup. For now, the fix is very minimal and conservative: it only filters out C++ namespaces during unqualified name lookup in an interface that was originally generated without C++ interop. Doing so should fix the issue while minimizing the chance for collateral breakge. More cases other than C++ namespaces should be added in subsequent patches, with sufficient testing and careful consideration. rdar://144566922
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -94,6 +94,7 @@ | |
#include "llvm/ADT/SmallVector.h" | ||
#include "llvm/ADT/StringExtras.h" | ||
#include "llvm/ADT/StringRef.h" | ||
#include "llvm/ADT/TypeSwitch.h" | ||
#include "llvm/CAS/CASReference.h" | ||
#include "llvm/CAS/ObjectStore.h" | ||
#include "llvm/Support/Casting.h" | ||
|
@@ -8665,3 +8666,22 @@ importer::getPrivateFileIDAttrs(const clang::Decl *decl) { | |
|
||
return files; | ||
} | ||
|
||
bool importer::declIsCxxOnly(const Decl *decl) { | ||
if (auto *clangDecl = decl->getClangDecl()) { | ||
return llvm::TypeSwitch<const clang::Decl *, bool>(clangDecl) | ||
.template Case<const clang::NamespaceAliasDecl>( | ||
[](auto _) { return true; }) | ||
.template Case<const clang::NamespaceDecl>([](auto _) { return true; }) | ||
// For the issues this filter function was trying to resolve at its | ||
// time of writing, it suffices to only filter out namespaces. But | ||
// there are many other kinds of clang::Decls that only appear in C++. | ||
// This is obvious for some decls, e.g., templates, using directives, | ||
// non-trivial structs, and scoped enums; but it is not obvious for | ||
// other kinds of decls, e.g., an enum member or some variable. | ||
// | ||
// TODO: enumerate those kinds in a precise and robust way | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we could ever be fully precise just by looking at the AST. People might have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this comment is mostly referring to other clearly C++ decls like template functions and using decls. As you point out, it's impossible to reliably warn users either because of ifdefs. |
||
.Default([&](auto _) { return false; }); | ||
} | ||
return false; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// XFAIL: * | ||
// This test currently fails because there's no way to explicitly refer to | ||
// a module that has been shadowed by another declaration, e.g., a namespace. | ||
// Unlike in the prevent-module-shadowed-by-namespace.swift test, the textual | ||
// interface is generated with C++ interop enabled, which means namespaces are | ||
// not filtered out during name lookup when the interface is recompiled later. | ||
|
||
// RUN: %empty-directory(%t) | ||
// RUN: %empty-directory(%t/include) | ||
// RUN: split-file %s %t | ||
|
||
// Compile shim.swift with C++ interop, check that its interface is usable | ||
// (despite using a mix of C/C++ decls): | ||
// | ||
// RUN: %empty-directory(%t/lib) | ||
// RUN: %target-swift-emit-module-interface(%t/lib/shim.swiftinterface) %t/shim.swift -cxx-interoperability-mode=default -module-name shim -I %t/include | ||
// RUN: %FileCheck %t/shim.swift < %t/lib/shim.swiftinterface | ||
// RUN: %swift-frontend %t/program.swift -typecheck -verify -cxx-interoperability-mode=default -I %t/include -I %t/lib | ||
|
||
//--- include/module.modulemap | ||
// A Clang module which will first be compiled in C mode, and then later compiled in C++ mode | ||
module c2cxx { | ||
header "c2cxx.h" | ||
export * | ||
} | ||
|
||
//--- include/c2cxx.h | ||
// A header file that defines a namespace with the same name as the module, | ||
// a common C++ idiom. We want to make sure that it does not shadow the module | ||
// in textual interfaces generated with C++ interop disabled, but later compiled | ||
// with C++ interop enabled. | ||
#ifndef __C2CXX_NAMESPACE_H | ||
#define __C2CXX_NAMESPACE_H | ||
typedef int c2cxx_number; // always available and resilient | ||
#ifdef __cplusplus | ||
namespace c2cxx { typedef c2cxx_number number; }; // only available in C++ | ||
#endif // __cplusplus | ||
#endif // __C2CXX_NAMESPACE_H | ||
|
||
//--- shim.swift | ||
// A shim around c2cxx that refers to a mixture of namespaced (C++) and | ||
// top-level (C) decls; requires cxx-interoperability-mode | ||
import c2cxx | ||
public func shimId(_ n: c2cxx.number) -> c2cxx_number { return n } | ||
// ^^^^^`- refers to the namespace | ||
// CHECK: public func shimId(_ n: c2cxx.c2cxx.number) -> c2cxx.c2cxx_number | ||
// ^^^^^\^^^^^`-namespace ^^^^^`-module | ||
// `-module | ||
|
||
@inlinable public func shimIdInline(_ n: c2cxx_number) -> c2cxx.number { | ||
// CHECK: public func shimIdInline(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx.number | ||
// ^^^^^`-module ^^^^^\^^^^^`-namespace | ||
// `-module | ||
let m: c2cxx.number = n | ||
// CHECK: let m: c2cxx.number = n | ||
// ^^^^^`-namespace | ||
return m | ||
} | ||
|
||
//--- program.swift | ||
// Uses the shim and causes it to be (re)built from its interface | ||
import shim | ||
func numberwang() { let _ = shimId(42) + shimIdInline(24) } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// RUN: %empty-directory(%t) | ||
// RUN: %empty-directory(%t/include) | ||
// RUN: split-file %s %t | ||
|
||
// Generate textual interface for shim.swift without C++ interop enabled, then | ||
// re-compile it with C++ interop enabled (because it is used by program.swift, | ||
// which is compiled with C++ interop enabled). | ||
// | ||
// RUN: %empty-directory(%t/lib) | ||
// RUN: %target-swift-emit-module-interface(%t/lib/shim.swiftinterface) %t/shim.swift -module-name shim -I %t/include | ||
// RUN: %FileCheck %t/shim.swift < %t/lib/shim.swiftinterface | ||
// RUN: %swift-frontend %t/program.swift -typecheck -verify -cxx-interoperability-mode=default -I %t/include -I %t/lib | ||
|
||
//--- include/module.modulemap | ||
// A Clang module which will first be compiled in C mode, and then later compiled in C++ mode | ||
module c2cxx { | ||
header "c2cxx.h" | ||
export * | ||
} | ||
|
||
//--- include/c2cxx.h | ||
// A header file that defines a namespace with the same name as the module, | ||
// a common C++ idiom. We want to make sure that it does not shadow the module | ||
// in textual interfaces generated with C++ interop disabled, but later compiled | ||
// with C++ interop enabled. | ||
#ifndef __C2CXX_NAMESPACE_H | ||
#define __C2CXX_NAMESPACE_H | ||
typedef int c2cxx_number; // always available and resilient | ||
#ifdef __cplusplus | ||
namespace c2cxx { typedef c2cxx_number number; }; // only available in C++ | ||
#endif // __cplusplus | ||
#endif // __C2CXX_NAMESPACE_H | ||
|
||
//--- shim.swift | ||
// A shim around c2cxx that exposes a c2cxx decl in its module interface | ||
import c2cxx | ||
|
||
// Exposes a (fully-qualified) c2cxx decl in its module interface. | ||
public func shimId(_ n: c2cxx_number) -> c2cxx_number { return n } | ||
// CHECK: public func shimId(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx_number | ||
// ^^^^^`- refers to the module | ||
|
||
// @inlinable functions have their bodies splatted in the module interface file; | ||
// those verbatim bodies may contain unqualified names. | ||
@inlinable public func shimIdInline(_ n: c2cxx_number) -> c2cxx_number { | ||
// CHECK: public func shimIdInline(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx_number | ||
// ^^^^^`- refers to the module | ||
let m: c2cxx_number = n | ||
// CHECK: let m: c2cxx_number = n | ||
// ^^^^^^^^^^^^`- verbatim (unqualified) | ||
return m | ||
} | ||
|
||
//--- program.swift | ||
// Uses the shim and causes it to be (re)built from its interface | ||
import shim | ||
func numberwang() { let _ = shimId(42) + shimIdInline(24) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: If we only have 2 options here, maybe a
%select{
would be a better way to take the arguments.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My argument in favor of taking a
StringRef
here is that, at the call site,arg->getSpelling()
means something at face-value, rather thantrue
/false
or1
/0
. But I don't feel very strongly about this. What do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Orthogonally I'm even wondering whether we should combine this with this other note:
In fact,
valid arguments to <flag> are <options>
seems like a very re-usable note and it would be nice to de-duplicate some of that.But I think that can also be done in a follow-up patch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I am fine with this as is then.