Skip to content

Commit 40104ba

Browse files
Link against the C++ standard library when C++ interop is enabled (#30914)
This doesn't yet allow including C++ headers on platforms where libc++ isn't the default; see comments in UnixToolChains.cpp for details. However, it does, for example, allow throwing and catching exceptions in C++ code used through interop, unblocking https://github.com/apple/swift/pull/30674/files. The flags (-enable-experimental-cxx-interop and -experimental-cxx-stdlib) carry "experimental" in the name to emphasize that C++ interop is still an experimental feature. Co-authored-by: Michael Forster <[email protected]>
1 parent a689dd2 commit 40104ba

File tree

9 files changed

+142
-50
lines changed

9 files changed

+142
-50
lines changed

include/swift/AST/DiagnosticsDriver.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ ERROR(cannot_find_migration_script, none,
186186
ERROR(error_darwin_static_stdlib_not_supported, none,
187187
"-static-stdlib is no longer supported on Apple platforms", ())
188188

189+
ERROR(error_darwin_only_supports_libcxx, none,
190+
"The only C++ standard library supported on Apple platforms is libc++",
191+
())
192+
189193
WARNING(warn_drv_darwin_sdk_invalid_settings, none,
190194
"SDK settings were ignored because 'SDKSettings.json' could not be parsed",
191195
())

include/swift/Driver/ToolChain.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ class ToolChain {
294294
void getClangLibraryPath(const llvm::opt::ArgList &Args,
295295
SmallString<128> &LibPath) const;
296296

297+
// Returns the Clang driver executable to use for linking.
298+
const char *getClangLinkerDriver(const llvm::opt::ArgList &Args) const;
299+
297300
/// Returns the name the clang library for a given sanitizer would have on
298301
/// the current toolchain.
299302
///

include/swift/Option/Options.td

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,14 @@ def disable_direct_intramodule_dependencies : Flag<["-"],
556556
Flags<[FrontendOption, HelpHidden]>,
557557
HelpText<"Disable experimental dependency tracking that never cascades">;
558558

559+
def enable_experimental_cxx_interop :
560+
Flag<["-"], "enable-experimental-cxx-interop">,
561+
HelpText<"Allow importing C++ modules into Swift (experimental feature)">;
562+
563+
def experimental_cxx_stdlib :
564+
Separate<["-"], "experimental-cxx-stdlib">,
565+
HelpText<"C++ standard library to use; forwarded to Clang's -stdlib flag">;
566+
559567

560568
// Diagnostic control options
561569
def suppress_warnings : Flag<["-"], "suppress-warnings">,

lib/Driver/DarwinToolChains.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,11 @@ toolchains::Darwin::constructInvocation(const DynamicLinkJobAction &job,
795795
Arguments.push_back("-arch");
796796
Arguments.push_back(context.Args.MakeArgString(getTriple().getArchName()));
797797

798+
// On Darwin, we only support libc++.
799+
if (context.Args.hasArg(options::OPT_enable_experimental_cxx_interop)) {
800+
Arguments.push_back("-lc++");
801+
}
802+
798803
addArgsToLinkStdlib(Arguments, job, context);
799804

800805
addProfileGenerationArgs(Arguments, context);
@@ -938,6 +943,13 @@ toolchains::Darwin::validateArguments(DiagnosticEngine &diags,
938943
if (args.hasArg(options::OPT_static_stdlib)) {
939944
diags.diagnose(SourceLoc(), diag::error_darwin_static_stdlib_not_supported);
940945
}
946+
947+
// If a C++ standard library is specified, it has to be libc++.
948+
if (auto arg = args.getLastArg(options::OPT_experimental_cxx_stdlib)) {
949+
if (StringRef(arg->getValue()) != "libc++") {
950+
diags.diagnose(SourceLoc(), diag::error_darwin_only_supports_libcxx);
951+
}
952+
}
941953
}
942954

943955
void

lib/Driver/ToolChains.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ void ToolChain::addCommonFrontendArgs(const OutputInfo &OI,
167167
arguments.push_back("-disable-objc-interop");
168168
}
169169

170+
// Add flags for C++ interop.
171+
if (inputArgs.hasArg(options::OPT_enable_experimental_cxx_interop)) {
172+
arguments.push_back("-enable-cxx-interop");
173+
}
174+
if (const Arg *arg =
175+
inputArgs.getLastArg(options::OPT_experimental_cxx_stdlib)) {
176+
arguments.push_back("-Xcc");
177+
arguments.push_back(
178+
inputArgs.MakeArgString(Twine("-stdlib=") + arg->getValue()));
179+
}
180+
170181
// Handle the CPU and its preferences.
171182
inputArgs.AddLastArg(arguments, options::OPT_target_cpu);
172183

@@ -1330,6 +1341,26 @@ void ToolChain::getRuntimeLibraryPaths(SmallVectorImpl<std::string> &runtimeLibP
13301341
}
13311342
}
13321343

1344+
const char *ToolChain::getClangLinkerDriver(
1345+
const llvm::opt::ArgList &Args) const {
1346+
// We don't use `clang++` unconditionally because we want to avoid pulling in
1347+
// a C++ standard library if it's not needed, in particular because the
1348+
// standard library that `clang++` selects by default may not be the one that
1349+
// is desired.
1350+
const char *LinkerDriver =
1351+
Args.hasArg(options::OPT_enable_experimental_cxx_interop) ? "clang++"
1352+
: "clang";
1353+
if (const Arg *A = Args.getLastArg(options::OPT_tools_directory)) {
1354+
StringRef toolchainPath(A->getValue());
1355+
1356+
// If there is a linker driver in the toolchain folder, use that instead.
1357+
if (auto tool = llvm::sys::findProgramByName(LinkerDriver, {toolchainPath}))
1358+
LinkerDriver = Args.MakeArgString(tool.get());
1359+
}
1360+
1361+
return LinkerDriver;
1362+
}
1363+
13331364
bool ToolChain::sanitizerRuntimeLibExists(const ArgList &args,
13341365
StringRef sanitizerName,
13351366
bool shared) const {

lib/Driver/UnixToolChains.cpp

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -181,31 +181,9 @@ toolchains::GenericUnix::constructInvocation(const DynamicLinkJobAction &job,
181181
}
182182

183183
// Configure the toolchain.
184-
//
185-
// By default use the system `clang` to perform the link. We use `clang` for
186-
// the driver here because we do not wish to select a particular C++ runtime.
187-
// Furthermore, until C++ interop is enabled, we cannot have a dependency on
188-
// C++ code from pure Swift code. If linked libraries are C++ based, they
189-
// should properly link C++. In the case of static linking, the user can
190-
// explicitly specify the C++ runtime to link against. This is particularly
191-
// important for platforms like android where as it is a Linux platform, the
192-
// default C++ runtime is `libstdc++` which is unsupported on the target but
193-
// as the builds are usually cross-compiled from Linux, libstdc++ is going to
194-
// be present. This results in linking the wrong version of libstdc++
195-
// generating invalid binaries. It is also possible to use different C++
196-
// runtimes than the default C++ runtime for the platform (e.g. libc++ on
197-
// Windows rather than msvcprt). When C++ interop is enabled, we will need to
198-
// surface this via a driver flag. For now, opt for the simpler approach of
199-
// just using `clang` and avoid a dependency on the C++ runtime.
200-
const char *Clang = "clang";
201184
if (const Arg *A = context.Args.getLastArg(options::OPT_tools_directory)) {
202185
StringRef toolchainPath(A->getValue());
203186

204-
// If there is a clang in the toolchain folder, use that instead.
205-
if (auto tool = llvm::sys::findProgramByName("clang", {toolchainPath})) {
206-
Clang = context.Args.MakeArgString(tool.get());
207-
}
208-
209187
// Look for binutils in the toolchain folder.
210188
Arguments.push_back("-B");
211189
Arguments.push_back(context.Args.MakeArgString(A->getValue()));
@@ -307,6 +285,13 @@ toolchains::GenericUnix::constructInvocation(const DynamicLinkJobAction &job,
307285
}
308286
}
309287

288+
// Link against the desired C++ standard library.
289+
if (const Arg *A =
290+
context.Args.getLastArg(options::OPT_experimental_cxx_stdlib)) {
291+
Arguments.push_back(
292+
context.Args.MakeArgString(Twine("-stdlib=") + A->getValue()));
293+
}
294+
310295
// Explicitly pass the target to the linker
311296
Arguments.push_back(
312297
context.Args.MakeArgString("--target=" + getTriple().str()));
@@ -352,7 +337,7 @@ toolchains::GenericUnix::constructInvocation(const DynamicLinkJobAction &job,
352337
Arguments.push_back(
353338
context.Args.MakeArgString(context.Output.getPrimaryOutputFilename()));
354339

355-
InvocationInfo II{Clang, Arguments};
340+
InvocationInfo II{getClangLinkerDriver(context.Args), Arguments};
356341
II.allowsResponseFiles = true;
357342

358343
return II;

lib/Driver/WindowsToolChains.cpp

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,32 +84,6 @@ toolchains::Windows::constructInvocation(const DynamicLinkJobAction &job,
8484
Arguments.push_back("/DEBUG");
8585
}
8686

87-
// Configure the toolchain.
88-
//
89-
// By default use the system `clang` to perform the link. We use `clang` for
90-
// the driver here because we do not wish to select a particular C++ runtime.
91-
// Furthermore, until C++ interop is enabled, we cannot have a dependency on
92-
// C++ code from pure Swift code. If linked libraries are C++ based, they
93-
// should properly link C++. In the case of static linking, the user can
94-
// explicitly specify the C++ runtime to link against. This is particularly
95-
// important for platforms like android where as it is a Linux platform, the
96-
// default C++ runtime is `libstdc++` which is unsupported on the target but
97-
// as the builds are usually cross-compiled from Linux, libstdc++ is going to
98-
// be present. This results in linking the wrong version of libstdc++
99-
// generating invalid binaries. It is also possible to use different C++
100-
// runtimes than the default C++ runtime for the platform (e.g. libc++ on
101-
// Windows rather than msvcprt). When C++ interop is enabled, we will need to
102-
// surface this via a driver flag. For now, opt for the simpler approach of
103-
// just using `clang` and avoid a dependency on the C++ runtime.
104-
const char *Clang = "clang";
105-
if (const Arg *A = context.Args.getLastArg(options::OPT_tools_directory)) {
106-
StringRef toolchainPath(A->getValue());
107-
108-
// If there is a clang in the toolchain folder, use that instead.
109-
if (auto tool = llvm::sys::findProgramByName("clang", {toolchainPath}))
110-
Clang = context.Args.MakeArgString(tool.get());
111-
}
112-
11387
// Rely on `-libc` to correctly identify the MSVC Runtime Library. We use
11488
// `-nostartfiles` as that limits the difference to just the
11589
// `-defaultlib:libcmt` which is passed unconditionally with the `clang`
@@ -159,6 +133,13 @@ toolchains::Windows::constructInvocation(const DynamicLinkJobAction &job,
159133
Arguments.push_back(context.Args.MakeArgString(context.OI.SDKPath));
160134
}
161135

136+
// Link against the desired C++ standard library.
137+
if (const Arg *A =
138+
context.Args.getLastArg(options::OPT_experimental_cxx_stdlib)) {
139+
Arguments.push_back(context.Args.MakeArgString(
140+
Twine("-stdlib=") + A->getValue()));
141+
}
142+
162143
if (job.getKind() == LinkKind::Executable) {
163144
if (context.OI.SelectedSanitizers & SanitizerKind::Address)
164145
addLinkRuntimeLib(context.Args, Arguments,
@@ -196,7 +177,7 @@ toolchains::Windows::constructInvocation(const DynamicLinkJobAction &job,
196177
Arguments.push_back(
197178
context.Args.MakeArgString(context.Output.getPrimaryOutputFilename()));
198179

199-
InvocationInfo II{Clang, Arguments};
180+
InvocationInfo II{getClangLinkerDriver(context.Args), Arguments};
200181
II.allowsResponseFiles = true;
201182

202183
return II;

test/Driver/cxx_interop.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-macosx10.9 %s -enable-experimental-cxx-interop 2>^1 | %FileCheck -check-prefix ENABLE %s
2+
3+
// RUN: %swiftc_driver -driver-print-jobs -target x86_64-apple-macosx10.9 %s -enable-experimental-cxx-interop -experimental-cxx-stdlib libc++ 2>^1 | %FileCheck -check-prefix STDLIB %s
4+
5+
// ENABLE: swift
6+
// ENABLE: -enable-cxx-interop
7+
8+
// STDLIB: swift
9+
// STDLIB-DAG: -enable-cxx-interop
10+
// STDLIB-DAG: -Xcc -stdlib=libc++

test/Driver/linker.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@
101101
// INFERRED_NAMED_DARWIN tests above: 'libLINKER.dylib'.
102102
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-macosx10.9 -emit-library %s -o libLINKER.dylib | %FileCheck -check-prefix INFERRED_NAME_DARWIN %s
103103

104+
// On Darwin, when C++ interop is turned on, we link against libc++ explicitly
105+
// regardless of whether -experimental-cxx-stdlib is specified or not. So also
106+
// run a test where C++ interop is turned off to make sure we don't link
107+
// against libc++ in this case.
108+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-ios7.1 %s 2>&1 | %FileCheck -check-prefix IOS-no-cxx-interop %s
109+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-ios7.1 -enable-experimental-cxx-interop %s 2>&1 | %FileCheck -check-prefix IOS-cxx-interop-libcxx %s
110+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-ios7.1 -enable-experimental-cxx-interop -experimental-cxx-stdlib libc++ %s 2>&1 | %FileCheck -check-prefix IOS-cxx-interop-libcxx %s
111+
// RUN: not %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-ios7.1 -enable-experimental-cxx-interop -experimental-cxx-stdlib libstdc++ %s 2>&1 | %FileCheck -check-prefix IOS-cxx-interop-libstdcxx %s
112+
113+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-unknown-linux-gnu -enable-experimental-cxx-interop %s 2>&1 | %FileCheck -check-prefix LINUX-cxx-interop %s
114+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-unknown-linux-gnu -enable-experimental-cxx-interop -experimental-cxx-stdlib libc++ %s 2>&1 | %FileCheck -check-prefix LINUX-cxx-interop-libcxx %s
115+
116+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-unknown-windows-msvc -enable-experimental-cxx-interop %s 2>&1 | %FileCheck -check-prefix WINDOWS-cxx-interop %s
117+
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-unknown-windows-msvc -enable-experimental-cxx-interop -experimental-cxx-stdlib libc++ %s 2>&1 | %FileCheck -check-prefix WINDOWS-cxx-interop-libcxx %s
118+
104119
// Check reading the SDKSettings.json from an SDK
105120
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-macosx10.9 -sdk %S/Inputs/MacOSX10.15.versioned.sdk %s 2>&1 | %FileCheck -check-prefix MACOS_10_15 %s
106121
// RUN: %swiftc_driver -sdk "" -driver-print-jobs -target x86_64-apple-macosx10.9 -sdk %S/Inputs/MacOSX10.15.4.versioned.sdk %s 2>&1 | %FileCheck -check-prefix MACOS_10_15_4 %s
@@ -424,6 +439,49 @@
424439
// INFERRED_NAME_WINDOWS: -o LINKER.dll
425440
// INFERRED_NAME_WASI: -o libLINKER.so
426441

442+
// Instead of a single "NOT" check for this run, we would really want to check
443+
// for all of the driver arguments that we _do_ expect, and then use an
444+
// --implicit-check-not to check that -lc++ doesn't occur.
445+
// However, --implicit-check-not has a bug where it fails to flag the
446+
// unexpected text when it occurs after text matched by a CHECK-DAG; see
447+
// https://bugs.llvm.org/show_bug.cgi?id=45629
448+
// For this reason, we use a single "NOT" check for the time being here.
449+
// The same consideration applies to the Linux and Windows cases below.
450+
// IOS-no-cxx-interop-NOT: -lc++
451+
452+
// IOS-cxx-interop-libcxx: swift
453+
// IOS-cxx-interop-libcxx-DAG: -enable-cxx-interop
454+
// IOS-cxx-interop-libcxx-DAG: -o [[OBJECTFILE:.*]]
455+
456+
// IOS-cxx-interop-libcxx: {{(bin/)?}}ld{{"? }}
457+
// IOS-cxx-interop-libcxx-DAG: [[OBJECTFILE]]
458+
// IOS-cxx-interop-libcxx-DAG: -lc++
459+
// IOS-cxx-interop-libcxx: -o linker
460+
461+
// IOS-cxx-interop-libstdcxx: error: The only C++ standard library supported on Apple platforms is libc++
462+
463+
// LINUX-cxx-interop-NOT: -stdlib
464+
465+
// LINUX-cxx-interop-libcxx: swift
466+
// LINUX-cxx-interop-libcxx-DAG: -enable-cxx-interop
467+
// LINUX-cxx-interop-libcxx-DAG: -o [[OBJECTFILE:.*]]
468+
469+
// LINUX-cxx-interop-libcxx: clang++{{(\.exe)?"? }}
470+
// LINUX-cxx-interop-libcxx-DAG: [[OBJECTFILE]]
471+
// LINUX-cxx-interop-libcxx-DAG: -stdlib=libc++
472+
// LINUX-cxx-interop-libcxx: -o linker
473+
474+
// WINDOWS-cxx-interop-NOT: -stdlib
475+
476+
// WINDOWS-cxx-interop-libcxx: swift
477+
// WINDOWS-cxx-interop-libcxx-DAG: -enable-cxx-interop
478+
// WINDOWS-cxx-interop-libcxx-DAG: -o [[OBJECTFILE:.*]]
479+
480+
// WINDOWS-cxx-interop-libcxx: clang++{{(\.exe)?"? }}
481+
// WINDOWS-cxx-interop-libcxx-DAG: [[OBJECTFILE]]
482+
// WINDOWS-cxx-interop-libcxx-DAG: -stdlib=libc++
483+
// WINDOWS-cxx-interop-libcxx: -o linker
484+
427485
// Test ld detection. We use hard links to make sure
428486
// the Swift driver really thinks it's been moved.
429487

0 commit comments

Comments
 (0)