Skip to content

[cxx-interop] Generate safe overloads for non-escapable spans #78422

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 1 commit into from
Jan 9, 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
3 changes: 2 additions & 1 deletion lib/ClangImporter/ClangImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,8 @@ void importer::getNormalInvocationArguments(
}
}

if (LangOpts.hasFeature(Feature::SafeInteropWrappers))
if (LangOpts.hasFeature(Feature::SafeInteropWrappers) &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw some crashes in Clang when this option was used in C++ mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine for now, but we should dig into these crashes to see what's going on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-fbounds-safety support for C++ in Clang is currently experimental, but -fexperimental-bounds-safety-attributes is used for C/C++ interop also, so that one shouldn't crash. It is a relatively new flag though, so there might be some gaps. CC @patrykstefanski for awareness.

!LangOpts.EnableCXXInterop)
invocationArgStrs.push_back("-fexperimental-bounds-safety-attributes");

// Set C language options.
Expand Down
77 changes: 72 additions & 5 deletions lib/ClangImporter/ImportDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclObjCCommon.h"
#include "clang/AST/PrettyPrinter.h"
#include "clang/AST/Type.h"
#include "clang/Basic/Specifiers.h"
#include "clang/Basic/TargetInfo.h"
Expand All @@ -71,10 +72,12 @@
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/Support/Path.h"

#include <algorithm>
#include <utility>

#define DEBUG_TYPE "Clang module importer"

Expand Down Expand Up @@ -130,6 +133,7 @@ createFuncOrAccessor(ClangImporter::Implementation &impl, SourceLoc funcLoc,
impl.importSwiftAttrAttributes(decl);
if (hasBoundsAnnotation)
impl.importBoundsAttributes(decl);
impl.importSpanAttributes(decl);

return decl;
}
Expand Down Expand Up @@ -8685,11 +8689,7 @@ class SwiftifyInfoPrinter {

void printCountedBy(const clang::CountAttributedType *CAT,
size_t pointerIndex) {
if (!firstParam) {
out << ", ";
} else {
firstParam = false;
}
printSeparator();
clang::Expr *countExpr = CAT->getCountExpr();
bool isSizedBy = CAT->isCountInBytes();
out << ".";
Expand All @@ -8707,9 +8707,76 @@ class SwiftifyInfoPrinter {
out, {}, {ctx.getLangOpts()}); // TODO: map clang::Expr to Swift Expr
out << "\")";
}

void printNonEscaping(int idx) {
printSeparator();
out << ".nonescaping(pointer: " << idx << ")";
}

void printTypeMapping(const llvm::StringMap<std::string> &mapping) {
printSeparator();
out << "typeMappings: [";
if (mapping.empty()) {
out << ":]";
return;
}
llvm::interleaveComma(mapping, out, [&](const auto &entry) {
out << '"' << entry.getKey() << "\" : \"" << entry.getValue() << '"';
});
out << "]";
}

private:
void printSeparator() {
if (!firstParam) {
out << ", ";
} else {
firstParam = false;
}
}
};
} // namespace

void ClangImporter::Implementation::importSpanAttributes(FuncDecl *MappedDecl) {
if (!SwiftContext.LangOpts.hasFeature(Feature::SafeInteropWrappers))
return;
auto ClangDecl =
dyn_cast_or_null<clang::FunctionDecl>(MappedDecl->getClangDecl());
if (!ClangDecl)
return;

llvm::SmallString<128> MacroString;
bool attachMacro = false;
{
llvm::raw_svector_ostream out(MacroString);
llvm::StringMap<std::string> typeMapping;

SwiftifyInfoPrinter printer(getClangASTContext(), out);
for (auto [index, param] : llvm::enumerate(ClangDecl->parameters())) {
auto paramTy = param->getType();
const auto *decl = paramTy->getAsTagDecl();
if (!decl || !decl->isInStdNamespace())
continue;
if (decl->getName() != "span")
continue;
if (param->hasAttr<clang::NoEscapeAttr>()) {
printer.printNonEscaping(index + 1);
clang::PrintingPolicy policy(param->getASTContext().getLangOpts());
policy.SuppressTagKeyword = true;
auto param = MappedDecl->getParameters()->get(index);
typeMapping.insert(std::make_pair(
paramTy.getAsString(policy),
param->getInterfaceType()->getDesugaredType()->getString()));
attachMacro = true;
}
}
printer.printTypeMapping(typeMapping);
}

if (attachMacro)
importNontrivialAttribute(MappedDecl, MacroString);
}

void ClangImporter::Implementation::importBoundsAttributes(
FuncDecl *MappedDecl) {
assert(SwiftContext.LangOpts.hasFeature(Feature::SafeInteropWrappers));
Expand Down
1 change: 1 addition & 0 deletions lib/ClangImporter/ImporterImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,7 @@ class LLVM_LIBRARY_VISIBILITY ClangImporter::Implementation

void importSwiftAttrAttributes(Decl *decl);
void importBoundsAttributes(FuncDecl *MappedDecl);
void importSpanAttributes(FuncDecl *MappedDecl);

/// Find the lookup table that corresponds to the given Clang module.
///
Expand Down
27 changes: 21 additions & 6 deletions lib/Macros/Sources/SwiftMacros/SwiftifyImportMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ func isRawPointerType(text: String) -> Bool {
}
}

// Remove std. or std.__1. prefix
func getUnqualifiedStdName(_ type: String) -> String? {
if (type.hasPrefix("std.")) {
var ty = type.dropFirst(4)
if (ty.hasPrefix("__1.")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it always be __1., or could it also be __2.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not anticipate a change in this, it is the same namespace for the last decade or so. But it is true that we are somewhat closely coupled to this implementation detail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this adjustment on the C++ side where we can reason about things like inline namespaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be reasonable to skip inline namespaces in the Swift names, especially since these names do leak into the diagnostics occasionally. It would be more user friendly. I am not sure if this could ever create name collisions, hopefully not. But I'd address this in a separate PR.

How closely coupled are the compiler and the standard library? In case this code supposed to work with older compiler versions, we'd want to keep the skipping logic here.

ty = ty.dropFirst(4)
}
return String(ty)
}
return nil
}

func getSafePointerName(mut: Mutability, generateSpan: Bool, isRaw: Bool) -> TokenSyntax {
switch (mut, generateSpan, isRaw) {
case (.Immutable, true, true): return "RawSpan"
Expand Down Expand Up @@ -314,9 +326,10 @@ struct CxxSpanThunkBuilder: BoundsCheckedThunkBuilder {
"unable to desugar type with name '\(typeName)'", node: node)
}

let parsedDesugaredType = TypeSyntax("\(raw: desugaredType)")
types[index] = TypeSyntax(IdentifierTypeSyntax(name: "Span",
genericArgumentClause: parsedDesugaredType.as(IdentifierTypeSyntax.self)!.genericArgumentClause))
let parsedDesugaredType = TypeSyntax("\(raw: getUnqualifiedStdName(desugaredType)!)")
let genericArg = TypeSyntax(parsedDesugaredType.as(IdentifierTypeSyntax.self)!
.genericArgumentClause!.arguments.first!.argument)!
types[index] = TypeSyntax("Span<\(raw: try getTypeName(genericArg).text)>")
return try base.buildFunctionSignature(types, variant)
}

Expand Down Expand Up @@ -696,9 +709,11 @@ public struct SwiftifyImportMacro: PeerMacro {
for (idx, param) in signature.parameterClause.parameters.enumerated() {
let typeName = try getTypeName(param.type).text;
if let desugaredType = typeMappings[typeName] {
if desugaredType.starts(with: "span") {
result.append(CxxSpan(pointerIndex: idx + 1, nonescaping: false,
original: param, typeMappings: typeMappings))
if let unqualifiedDesugaredType = getUnqualifiedStdName(desugaredType) {
if unqualifiedDesugaredType.starts(with: "span<") {
result.append(CxxSpan(pointerIndex: idx + 1, nonescaping: false,
original: param, typeMappings: typeMappings))
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/Interop/Cxx/stdlib/Inputs/std-span.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ inline SpanOfInt initSpan(int arr[], size_t size) {

inline struct SpanBox getStructSpanBox() { return {iarray, iarray, sarray, sarray}; }

void funcWithSafeWrapper(SpanOfInt s [[clang::noescape]]) {}

#endif // TEST_INTEROP_CXX_STDLIB_INPUTS_STD_SPAN_H
17 changes: 17 additions & 0 deletions test/Interop/Cxx/stdlib/std-span-interface.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-ide-test -plugin-path %swift-plugin-dir -I %S/Inputs -enable-experimental-feature Span -enable-experimental-feature SafeInteropWrappers -print-module -module-to-print=StdSpan -source-filename=x -enable-experimental-cxx-interop -Xcc -std=c++20 -module-cache-path %t > %t/interface.swift
// RUN: %FileCheck %s < %t/interface.swift

// REQUIRES: swift_feature_SafeInteropWrappers
// REQUIRES: swift_feature_Span

// FIXME swift-ci linux tests do not support std::span
// UNSUPPORTED: OS=linux-gnu

#if !BRIDGING_HEADER
import StdSpan
#endif
import CxxStdlib

// CHECK: func funcWithSafeWrapper(_ s: SpanOfInt)
// CHECK-NEXT: @_alwaysEmitIntoClient public func funcWithSafeWrapper(_ s: Span<CInt>)
2 changes: 1 addition & 1 deletion test/Macros/SwiftifyImport/CxxSpan/NoEscapeSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public struct SpanOfInt {
init(_ x: Span<CInt>) {}
}

@_SwiftifyImport(.nonescaping(pointer: 1), typeMappings: ["SpanOfInt" : "span<CInt>"])
@_SwiftifyImport(.nonescaping(pointer: 1), typeMappings: ["SpanOfInt" : "std.span<CInt>"])
func myFunc(_ span: SpanOfInt, _ secondSpan: SpanOfInt) {
}

Expand Down