Skip to content

[cxx-interop] Allow initializing std::function from Swift closures #71396

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 2 commits into from
Jun 17, 2024
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
87 changes: 87 additions & 0 deletions lib/ClangImporter/ClangDerivedConformances.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1019,3 +1019,90 @@ void swift::conformToCxxVectorIfNeeded(ClangImporter::Implementation &impl,
rawIteratorTy);
impl.addSynthesizedProtocolAttrs(decl, {KnownProtocolKind::CxxVector});
}

void swift::conformToCxxFunctionIfNeeded(
ClangImporter::Implementation &impl, NominalTypeDecl *decl,
const clang::CXXRecordDecl *clangDecl) {
PrettyStackTraceDecl trace("conforming to CxxFunction", decl);

assert(decl);
assert(clangDecl);
ASTContext &ctx = decl->getASTContext();
clang::ASTContext &clangCtx = impl.getClangASTContext();
clang::Sema &clangSema = impl.getClangSema();

// Only auto-conform types from the C++ standard library. Custom user types
// might have a similar interface but different semantics.
if (!isStdDecl(clangDecl, {"function"}))
return;

// There is no typealias for the argument types on the C++ side, so to
// retrieve the argument types we look at the overload of `operator()` that
// got imported into Swift.

auto callAsFunctionDecl = lookupDirectSingleWithoutExtensions<FuncDecl>(
decl, ctx.getIdentifier("callAsFunction"));
if (!callAsFunctionDecl)
return;

auto operatorCallDecl = dyn_cast_or_null<clang::CXXMethodDecl>(
callAsFunctionDecl->getClangDecl());
if (!operatorCallDecl)
return;

std::vector<clang::QualType> operatorCallParamTypes;
llvm::transform(
operatorCallDecl->parameters(),
std::back_inserter(operatorCallParamTypes),
[](const clang::ParmVarDecl *paramDecl) { return paramDecl->getType(); });

auto funcPointerType = clangCtx.getPointerType(clangCtx.getFunctionType(
operatorCallDecl->getReturnType(), operatorCallParamTypes,
clang::FunctionProtoType::ExtProtoInfo()));

// Create a fake variable with a function type that matches the type of
// `operator()`.
auto fakeFuncPointerVarDecl = clang::VarDecl::Create(
clangCtx, /*DC*/ clangCtx.getTranslationUnitDecl(),
clang::SourceLocation(), clang::SourceLocation(), /*Id*/ nullptr,
funcPointerType, clangCtx.getTrivialTypeSourceInfo(funcPointerType),
clang::StorageClass::SC_None);
auto fakeFuncPointerRefExpr = new (clangCtx) clang::DeclRefExpr(
clangCtx, fakeFuncPointerVarDecl, false, funcPointerType,
clang::ExprValueKind::VK_LValue, clang::SourceLocation());

auto clangDeclTyInfo = clangCtx.getTrivialTypeSourceInfo(
clang::QualType(clangDecl->getTypeForDecl(), 0));
SmallVector<clang::Expr *, 1> constructExprArgs = {fakeFuncPointerRefExpr};

// Instantiate the templated constructor that would accept this fake variable.
auto constructExprResult = clangSema.BuildCXXTypeConstructExpr(
clangDeclTyInfo, clangDecl->getLocation(), constructExprArgs,
clangDecl->getLocation(), /*ListInitialization*/ false);
if (!constructExprResult.isUsable())
return;

auto castExpr = dyn_cast_or_null<clang::CastExpr>(constructExprResult.get());
if (!castExpr)
return;

auto bindTempExpr =
dyn_cast_or_null<clang::CXXBindTemporaryExpr>(castExpr->getSubExpr());
if (!bindTempExpr)
return;

auto constructExpr =
dyn_cast_or_null<clang::CXXConstructExpr>(bindTempExpr->getSubExpr());
if (!constructExpr)
return;

auto constructorDecl = constructExpr->getConstructor();

auto importedConstructor =
impl.importDecl(constructorDecl, impl.CurrentVersion);
if (!importedConstructor)
return;
decl->addMember(importedConstructor);

// TODO: actually conform to some form of CxxFunction protocol
}
6 changes: 6 additions & 0 deletions lib/ClangImporter/ClangDerivedConformances.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ void conformToCxxVectorIfNeeded(ClangImporter::Implementation &impl,
NominalTypeDecl *decl,
const clang::CXXRecordDecl *clangDecl);

/// If the decl is an instantiation of C++ `std::function`, synthesize a
/// conformance to CxxFunction, which is defined in the Cxx module.
void conformToCxxFunctionIfNeeded(ClangImporter::Implementation &impl,
NominalTypeDecl *decl,
const clang::CXXRecordDecl *clangDecl);

} // namespace swift

#endif // SWIFT_CLANG_DERIVED_CONFORMANCES_H
1 change: 1 addition & 0 deletions lib/ClangImporter/ImportDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2815,6 +2815,7 @@ namespace {
conformToCxxPairIfNeeded(Impl, nominalDecl, decl);
conformToCxxOptionalIfNeeded(Impl, nominalDecl, decl);
conformToCxxVectorIfNeeded(Impl, nominalDecl, decl);
conformToCxxFunctionIfNeeded(Impl, nominalDecl, decl);
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/IRGen/GenClangDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ void IRGenModule::emitClangDecl(const clang::Decl *decl) {
// Unfortunately, implicitly defined CXXDestructorDecls don't have a real
// body, so we need to traverse these manually.
if (auto *dtor = dyn_cast<clang::CXXDestructorDecl>(next)) {
if (dtor->isImplicit() && dtor->isDefaulted() && !dtor->isDeleted() &&
!dtor->doesThisDeclarationHaveABody())
clangSema.DefineImplicitDestructor(dtor->getLocation(), dtor);

if (dtor->isImplicit() || dtor->hasBody()) {
auto cxxRecord = dtor->getParent();

Expand Down
6 changes: 6 additions & 0 deletions test/Interop/Cxx/stdlib/Inputs/std-function.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
#define TEST_INTEROP_CXX_STDLIB_INPUTS_STD_FUNCTION_H

#include <functional>
#include <string>

using FunctionIntToInt = std::function<int(int)>;
using FunctionStringToString = std::function<std::string(const std::string&)>;

inline FunctionIntToInt getIdentityFunction() {
return [](int x) { return x; };
Expand All @@ -13,4 +15,8 @@ inline bool isEmptyFunction(FunctionIntToInt f) { return !(bool)f; }

inline int invokeFunction(FunctionIntToInt f, int x) { return f(x); }

std::string invokeFunctionTwice(FunctionStringToString f, std::string s) {
return f(f(s));
}

#endif // TEST_INTEROP_CXX_STDLIB_INPUTS_STD_FUNCTION_H
33 changes: 30 additions & 3 deletions test/Interop/Cxx/stdlib/use-std-function.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,52 @@

import StdlibUnittest
import StdFunction
import CxxStdlib

var StdFunctionTestSuite = TestSuite("StdFunction")

StdFunctionTestSuite.test("init empty") {
StdFunctionTestSuite.test("FunctionIntToInt.init()") {
let f = FunctionIntToInt()
expectTrue(isEmptyFunction(f))

let copied = f
expectTrue(isEmptyFunction(copied))
}

StdFunctionTestSuite.test("call") {
StdFunctionTestSuite.test("FunctionIntToInt.callAsFunction") {
let f = getIdentityFunction()
expectEqual(123, f(123))
}

StdFunctionTestSuite.test("retrieve and pass back as parameter") {
StdFunctionTestSuite.test("FunctionIntToInt retrieve and pass back as parameter") {
let res = invokeFunction(getIdentityFunction(), 456)
expectEqual(456, res)
}

#if !os(Windows) // FIXME: rdar://103979602
StdFunctionTestSuite.test("FunctionIntToInt init from closure and call") {
let cClosure: @convention(c) (Int32) -> Int32 = { $0 + 1 }
let f = FunctionIntToInt(cClosure)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to have a test case where the closure actually captures some local state? Or is it not yet supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's not supported yet. @convention(c) closures (aka the ones that can be bridged to C function pointers) cannot capture context in Swift. In the future we'll need to add support for converting @convention(thick) closures to std::function, but it requires more work.

expectEqual(1, f(0))
expectEqual(124, f(123))
expectEqual(0, f(-1))

let f2 = FunctionIntToInt({ $0 * 2 })
expectEqual(0, f2(0))
expectEqual(246, f2(123))
}

StdFunctionTestSuite.test("FunctionIntToInt init from closure and pass as parameter") {
let res = invokeFunction(.init({ $0 * 2 }), 111)
expectEqual(222, res)
}

// FIXME: assertion for address-only closure params (rdar://124501345)
//StdFunctionTestSuite.test("FunctionStringToString init from closure and pass as parameter") {
// let res = invokeFunctionTwice(.init({ $0 + std.string("abc") }),
// std.string("prefix"))
// expectEqual(std.string("prefixabcabc"), res)
//}
#endif

runAllTests()