Skip to content

Commit 76a1742

Browse files
authored
[cxx-interop] Use formal C++ interop mode to fix name lookup in module interfaces (#79984)
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
1 parent 93f9db6 commit 76a1742

10 files changed

+235
-13
lines changed

include/swift/AST/DiagnosticsFrontend.def

+1-1
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ ERROR(dont_enable_interop_and_compat,none,
573573
"'-cxx-interoperability-mode'; remove '-enable-experimental-cxx-interop'", ())
574574

575575
NOTE(valid_cxx_interop_modes,none,
576-
"valid arguments to '-cxx-interoperability-mode=' are %0", (StringRef))
576+
"valid arguments to '%0' are %1", (StringRef, StringRef))
577577
NOTE(swift_will_maintain_compat,none,
578578
"Swift will maintain source compatibility for imported APIs based on the "
579579
"selected compatibility mode, so updating the Swift compiler will not "

include/swift/Basic/LangOptions.h

+4
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ namespace swift {
329329
/// to the Swift language version.
330330
version::Version cxxInteropCompatVersion;
331331

332+
/// What version of C++ interoperability a textual interface was originally
333+
/// generated with (if at all).
334+
std::optional<version::Version> FormalCxxInteropMode;
335+
332336
void setCxxInteropFromArgs(llvm::opt::ArgList &Args,
333337
swift::DiagnosticEngine &Diags);
334338

include/swift/ClangImporter/ClangImporter.h

+9
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,15 @@ AccessLevel convertClangAccess(clang::AccessSpecifier access);
757757
/// and should be parsed using swift::SourceFile::FileIDStr::parse().
758758
SmallVector<std::pair<StringRef, clang::SourceLocation>, 1>
759759
getPrivateFileIDAttrs(const clang::Decl *decl);
760+
761+
/// Use some heuristics to determine whether the clang::Decl associated with
762+
/// \a decl would not exist without C++ interop.
763+
///
764+
/// For instance, a namespace is C++-only, but a plain struct is valid in both
765+
/// C and C++.
766+
///
767+
/// Returns false if \a decl was not imported by ClangImporter.
768+
bool declIsCxxOnly(const Decl *decl);
760769
} // namespace importer
761770

762771
struct ClangInvocationFileMapping {

include/swift/Option/FrontendOptions.td

+4
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ let Flags = [FrontendOption, NoDriverOption, HelpHidden, ModuleInterfaceOptionIg
315315
Joined<["-"], "enable-destroy-hoisting=">,
316316
HelpText<"Whether to enable destroy hoisting">,
317317
MetaVarName<"true|false">;
318+
def formal_cxx_interoperability_mode :
319+
Joined<["-"], "formal-cxx-interoperability-mode=">,
320+
HelpText<"What version of C++ interoperability a textual interface was originally generated with">,
321+
MetaVarName<"<cxx-interop-version>|off">;
318322
}
319323

320324
// Flags that are saved into module interfaces

lib/AST/UnqualifiedLookup.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,18 @@ void UnqualifiedLookupFactory::addImportedResults(const DeclContext *const dc) {
487487
NLKind::UnqualifiedLookup, resolutionKind, dc,
488488
Loc, nlOptions);
489489

490+
if (dc->isInSwiftinterface() &&
491+
!dc->getASTContext().LangOpts.FormalCxxInteropMode) {
492+
// It's possible that the textual interface was originally compiled without
493+
// C++ interop enabled, but is now being imported in another compilation
494+
// instance with C++ interop enabled. In that case, we filter out any decls
495+
// that only exist due to C++ interop, e.g., namespace.
496+
CurModuleResults.erase(std::remove_if(CurModuleResults.begin(),
497+
CurModuleResults.end(),
498+
importer::declIsCxxOnly),
499+
CurModuleResults.end());
500+
}
501+
490502
// Always perform name shadowing for type lookup.
491503
if (options.contains(Flags::TypeLookup)) {
492504
removeShadowedDecls(CurModuleResults, dc);

lib/ClangImporter/ClangImporter.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
#include "llvm/ADT/SmallVector.h"
9595
#include "llvm/ADT/StringExtras.h"
9696
#include "llvm/ADT/StringRef.h"
97+
#include "llvm/ADT/TypeSwitch.h"
9798
#include "llvm/CAS/CASReference.h"
9899
#include "llvm/CAS/ObjectStore.h"
99100
#include "llvm/Support/Casting.h"
@@ -8670,3 +8671,22 @@ importer::getPrivateFileIDAttrs(const clang::Decl *decl) {
86708671

86718672
return files;
86728673
}
8674+
8675+
bool importer::declIsCxxOnly(const Decl *decl) {
8676+
if (auto *clangDecl = decl->getClangDecl()) {
8677+
return llvm::TypeSwitch<const clang::Decl *, bool>(clangDecl)
8678+
.template Case<const clang::NamespaceAliasDecl>(
8679+
[](auto) { return true; })
8680+
.template Case<const clang::NamespaceDecl>([](auto) { return true; })
8681+
// For the issues this filter function was trying to resolve at its
8682+
// time of writing, it suffices to only filter out namespaces. But
8683+
// there are many other kinds of clang::Decls that only appear in C++.
8684+
// This is obvious for some decls, e.g., templates, using directives,
8685+
// non-trivial structs, and scoped enums; but it is not obvious for
8686+
// other kinds of decls, e.g., an enum member or some variable.
8687+
//
8688+
// TODO: enumerate those kinds in a more precise and robust way
8689+
.Default([](auto) { return false; });
8690+
}
8691+
return false;
8692+
}

lib/Frontend/CompilerInvocation.cpp

+56-2
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,8 @@ static void diagnoseCxxInteropCompatMode(Arg *verArg, ArgList &Args,
644644
auto validVers = {llvm::StringRef("off"), llvm::StringRef("default"),
645645
llvm::StringRef("swift-6"), llvm::StringRef("swift-5.9")};
646646
auto versStr = "'" + llvm::join(validVers, "', '") + "'";
647-
diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes, versStr);
647+
diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes,
648+
verArg->getSpelling(), versStr);
648649
}
649650

650651
void LangOptions::setCxxInteropFromArgs(ArgList &Args,
@@ -679,6 +680,54 @@ void LangOptions::setCxxInteropFromArgs(ArgList &Args,
679680
cxxInteropCompatVersion =
680681
validateCxxInteropCompatibilityMode("swift-5.9").second;
681682
}
683+
684+
if (Arg *A = Args.getLastArg(options::OPT_formal_cxx_interoperability_mode)) {
685+
// Take formal version from explicitly specified formal version flag
686+
StringRef version = A->getValue();
687+
688+
// FIXME: the only valid modes are 'off' and 'swift-6'; see below.
689+
if (version == "off") {
690+
FormalCxxInteropMode = std::nullopt;
691+
} else if (version == "swift-6") {
692+
FormalCxxInteropMode = {6};
693+
} else {
694+
Diags.diagnose(SourceLoc(), diag::error_invalid_arg_value,
695+
A->getAsString(Args), A->getValue());
696+
Diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes,
697+
A->getSpelling(), "'off', 'swift-6'");
698+
}
699+
} else {
700+
// In the absence of a formal mode flag, we capture it from the current
701+
// C++ compat version (if C++ interop is enabled).
702+
//
703+
// FIXME: cxxInteropCompatVersion is computed based on the Swift language
704+
// version, and is either 4, 5, 6, or 7 (even though only 5.9 and 6.* make
705+
// any sense). For now, we don't actually care about the version, so we'll
706+
// just use version 6 (i.e., 'swift-6') to mean that C++ interop mode is on.
707+
if (EnableCXXInterop)
708+
FormalCxxInteropMode = {6};
709+
else
710+
FormalCxxInteropMode = std::nullopt;
711+
}
712+
}
713+
714+
static std::string printFormalCxxInteropVersion(const LangOptions &Opts) {
715+
std::string str;
716+
llvm::raw_string_ostream OS(str);
717+
718+
OS << "-formal-cxx-interoperability-mode=";
719+
720+
// We must print a 'stable' C++ interop version here, which cannot be
721+
// 'default' and 'upcoming-swift' (since those are relative to the current
722+
// version, which may change in the future).
723+
if (!Opts.FormalCxxInteropMode) {
724+
OS << "off";
725+
} else {
726+
// FIXME: FormalCxxInteropMode will always be 6 (or nullopt); see above
727+
OS << "swift-6";
728+
}
729+
730+
return str;
682731
}
683732

684733
static std::optional<swift::StrictConcurrency>
@@ -925,6 +974,7 @@ static bool ParseEnabledFeatureArgs(LangOptions &Opts, ArgList &Args,
925974

926975
static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
927976
DiagnosticEngine &Diags,
977+
ModuleInterfaceOptions &ModuleInterfaceOpts,
928978
const FrontendOptions &FrontendOpts) {
929979
using namespace options;
930980
bool buildingFromInterface =
@@ -1478,6 +1528,9 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
14781528
Opts.ClangTargetVariant = llvm::Triple(A->getValue());
14791529

14801530
Opts.setCxxInteropFromArgs(Args, Diags);
1531+
if (!Args.hasArg(options::OPT_formal_cxx_interoperability_mode))
1532+
ModuleInterfaceOpts.PublicFlags.IgnorableFlags +=
1533+
" " + printFormalCxxInteropVersion(Opts);
14811534

14821535
Opts.EnableObjCInterop =
14831536
Args.hasFlag(OPT_enable_objc_interop, OPT_disable_objc_interop,
@@ -3893,7 +3946,8 @@ bool CompilerInvocation::parseArgs(
38933946
return true;
38943947
}
38953948

3896-
if (ParseLangArgs(LangOpts, ParsedArgs, Diags, FrontendOpts)) {
3949+
if (ParseLangArgs(LangOpts, ParsedArgs, Diags, ModuleInterfaceOpts,
3950+
FrontendOpts)) {
38973951
return true;
38983952
}
38993953

test/Interop/Cxx/modules/emit-module-interface.swift

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
// RUN: %empty-directory(%t)
22

3-
// Check if fragile Swift interface with struct
4-
// extensions can be reparsed:
5-
// RUN: %target-swift-frontend -swift-version 5 -typecheck -emit-module-interface-path %t/UsesCxxStruct.swiftinterface %s -I %S/Inputs -swift-version 5 -enable-experimental-cxx-interop %S/Inputs/namespace-extension-lib.swift
6-
// RUN: %target-swift-frontend -swift-version 5 -typecheck-module-from-interface %t/UsesCxxStruct.swiftinterface -I %S/Inputs -enable-experimental-cxx-interop
3+
// Check if fragile Swift interface with struct extensions can be reparsed:
4+
// RUN: %target-swift-frontend -swift-version 5 -typecheck -emit-module-interface-path %t/UsesCxxStruct.swiftinterface %s -I %S/Inputs -swift-version 5 -cxx-interoperability-mode=default %S/Inputs/namespace-extension-lib.swift
5+
// RUN: %target-swift-frontend -swift-version 5 -typecheck-module-from-interface %t/UsesCxxStruct.swiftinterface -I %S/Inputs -cxx-interoperability-mode=default
76
// RUN: %FileCheck --input-file=%t/UsesCxxStruct.swiftinterface %s
87

9-
// The textual module interface should not contain the C++ interop flag.
8+
// The textual module interface should not contain the C++ interop flag, but it
9+
// should record the C++ interop version it was built with (the formal version):
1010
// CHECK-NOT: -enable-experimental-cxx-interop
1111
// CHECK-NOT: -cxx-interoperability-mode
12+
// CHECK: -formal-cxx-interoperability-mode=
1213

13-
14-
// Check if resilient Swift interface with builtin
15-
// type extensions can be reparsed:
16-
// RUN: %target-swift-emit-module-interface(%t/ResilientStruct.swiftinterface) %s -I %S/Inputs -enable-library-evolution -swift-version 5 -enable-experimental-cxx-interop %S/Inputs/namespace-extension-lib.swift -DRESILIENT
17-
// RUN: %target-swift-typecheck-module-from-interface(%t/ResilientStruct.swiftinterface) -I %S/Inputs -DRESILIENT -enable-experimental-cxx-interop
14+
// Check if resilient Swift interface with builtin type extensions can be reparsed:
15+
// RUN: %target-swift-emit-module-interface(%t/ResilientStruct.swiftinterface) %s -I %S/Inputs -enable-library-evolution -swift-version 5 -cxx-interoperability-mode=default %S/Inputs/namespace-extension-lib.swift -DRESILIENT
16+
// RUN: %target-swift-typecheck-module-from-interface(%t/ResilientStruct.swiftinterface) -I %S/Inputs -DRESILIENT -cxx-interoperability-mode=default
1817
// RUN: %FileCheck --input-file=%t/ResilientStruct.swiftinterface %s
1918

2019
import Namespaces
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// XFAIL: *
2+
// This test currently fails because there's no way to explicitly refer to
3+
// a module that has been shadowed by another declaration, e.g., a namespace.
4+
// Unlike in the prevent-module-shadowed-by-namespace.swift test, the textual
5+
// interface is generated with C++ interop enabled, which means namespaces are
6+
// not filtered out during name lookup when the interface is recompiled later.
7+
8+
// RUN: %empty-directory(%t)
9+
// RUN: %empty-directory(%t/include)
10+
// RUN: split-file %s %t
11+
12+
// Compile shim.swift with C++ interop, check that its interface is usable
13+
// (despite using a mix of C/C++ decls):
14+
//
15+
// RUN: %empty-directory(%t/lib)
16+
// RUN: %target-swift-emit-module-interface(%t/lib/shim.swiftinterface) %t/shim.swift -cxx-interoperability-mode=default -module-name shim -I %t/include
17+
// RUN: %FileCheck %t/shim.swift < %t/lib/shim.swiftinterface
18+
// RUN: %target-swift-frontend %t/program.swift -typecheck -verify -cxx-interoperability-mode=default -I %t/include -I %t/lib
19+
20+
//--- include/module.modulemap
21+
// A Clang module which will first be compiled in C mode, and then later compiled in C++ mode
22+
module c2cxx {
23+
header "c2cxx.h"
24+
export *
25+
}
26+
27+
//--- include/c2cxx.h
28+
// A header file that defines a namespace with the same name as the module,
29+
// a common C++ idiom. We want to make sure that it does not shadow the module
30+
// in textual interfaces generated with C++ interop disabled, but later compiled
31+
// with C++ interop enabled.
32+
#ifndef __C2CXX_NAMESPACE_H
33+
#define __C2CXX_NAMESPACE_H
34+
typedef int c2cxx_number; // always available and resilient
35+
#ifdef __cplusplus
36+
namespace c2cxx { typedef c2cxx_number number; }; // only available in C++
37+
#endif // __cplusplus
38+
#endif // __C2CXX_NAMESPACE_H
39+
40+
//--- shim.swift
41+
// A shim around c2cxx that refers to a mixture of namespaced (C++) and
42+
// top-level (C) decls; requires cxx-interoperability-mode
43+
import c2cxx
44+
public func shimId(_ n: c2cxx.number) -> c2cxx_number { return n }
45+
// ^^^^^`- refers to the namespace
46+
// CHECK: public func shimId(_ n: c2cxx.c2cxx.number) -> c2cxx.c2cxx_number
47+
// ^^^^^\^^^^^`-namespace ^^^^^`-module
48+
// `-module
49+
50+
@inlinable public func shimIdInline(_ n: c2cxx_number) -> c2cxx.number {
51+
// CHECK: public func shimIdInline(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx.number
52+
// ^^^^^`-module ^^^^^\^^^^^`-namespace
53+
// `-module
54+
let m: c2cxx.number = n
55+
// CHECK: let m: c2cxx.number = n
56+
// ^^^^^`-namespace
57+
return m
58+
}
59+
60+
//--- program.swift
61+
// Uses the shim and causes it to be (re)built from its interface
62+
import shim
63+
func numberwang() { let _ = shimId(42) + shimIdInline(24) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %empty-directory(%t/include)
3+
// RUN: split-file %s %t
4+
5+
// Generate textual interface for shim.swift without C++ interop enabled, then
6+
// re-compile it with C++ interop enabled (because it is used by program.swift,
7+
// which is compiled with C++ interop enabled).
8+
//
9+
// RUN: %empty-directory(%t/lib)
10+
// RUN: %target-swift-emit-module-interface(%t/lib/shim.swiftinterface) %t/shim.swift -module-name shim -I %t/include
11+
// RUN: %FileCheck %t/shim.swift < %t/lib/shim.swiftinterface
12+
// RUN: %target-swift-frontend %t/program.swift -typecheck -verify -cxx-interoperability-mode=default -I %t/include -I %t/lib
13+
14+
//--- include/module.modulemap
15+
// A Clang module which will first be compiled in C mode, and then later compiled in C++ mode
16+
module c2cxx {
17+
header "c2cxx.h"
18+
export *
19+
}
20+
21+
//--- include/c2cxx.h
22+
// A header file that defines a namespace with the same name as the module,
23+
// a common C++ idiom. We want to make sure that it does not shadow the module
24+
// in textual interfaces generated with C++ interop disabled, but later compiled
25+
// with C++ interop enabled.
26+
#ifndef __C2CXX_NAMESPACE_H
27+
#define __C2CXX_NAMESPACE_H
28+
typedef int c2cxx_number; // always available and resilient
29+
#ifdef __cplusplus
30+
namespace c2cxx { typedef c2cxx_number number; }; // only available in C++
31+
#endif // __cplusplus
32+
#endif // __C2CXX_NAMESPACE_H
33+
34+
//--- shim.swift
35+
// A shim around c2cxx that exposes a c2cxx decl in its module interface
36+
import c2cxx
37+
38+
// Exposes a (fully-qualified) c2cxx decl in its module interface.
39+
public func shimId(_ n: c2cxx_number) -> c2cxx_number { return n }
40+
// CHECK: public func shimId(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx_number
41+
// ^^^^^`- refers to the module
42+
43+
// @inlinable functions have their bodies splatted in the module interface file;
44+
// those verbatim bodies may contain unqualified names.
45+
@inlinable public func shimIdInline(_ n: c2cxx_number) -> c2cxx_number {
46+
// CHECK: public func shimIdInline(_ n: c2cxx.c2cxx_number) -> c2cxx.c2cxx_number
47+
// ^^^^^`- refers to the module
48+
let m: c2cxx_number = n
49+
// CHECK: let m: c2cxx_number = n
50+
// ^^^^^^^^^^^^`- verbatim (unqualified)
51+
return m
52+
}
53+
54+
//--- program.swift
55+
// Uses the shim and causes it to be (re)built from its interface
56+
import shim
57+
func numberwang() { let _ = shimId(42) + shimIdInline(24) }

0 commit comments

Comments
 (0)