Skip to content

[cxx-interop] Zero-initialize C++ structs when calling their default constructors #72903

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
Apr 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
41 changes: 33 additions & 8 deletions lib/IRGen/GenDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3476,7 +3476,12 @@ llvm::Constant *swift::irgen::emitCXXConstructorThunkIfNeeded(
llvm::FunctionType *ctorFnType =
cast<llvm::FunctionType>(clangFunc->getValueType());

if (assumedFnType == ctorFnType) {
// Only need a thunk if either:
// 1. The calling conventions do not match, and we need to pass arguments
// differently.
// 2. This is a default constructor, and we need to zero the backing memory of
// the struct.
if (assumedFnType == ctorFnType && !ctor->isDefaultConstructor()) {
return ctorAddress;
}

Expand Down Expand Up @@ -3510,14 +3515,34 @@ llvm::Constant *swift::irgen::emitCXXConstructorThunkIfNeeded(
Args.push_back(i);
}

clang::CodeGen::ImplicitCXXConstructorArgs implicitArgs =
clang::CodeGen::getImplicitCXXConstructorArgs(IGM.ClangCodeGen->CGM(),
ctor);
for (size_t i = 0; i < implicitArgs.Prefix.size(); ++i) {
Args.insert(Args.begin() + 1 + i, implicitArgs.Prefix[i]);
if (assumedFnType != ctorFnType) {
clang::CodeGen::ImplicitCXXConstructorArgs implicitArgs =
clang::CodeGen::getImplicitCXXConstructorArgs(IGM.ClangCodeGen->CGM(),
ctor);
for (size_t i = 0; i < implicitArgs.Prefix.size(); ++i) {
Args.insert(Args.begin() + 1 + i, implicitArgs.Prefix[i]);
}
for (const auto &arg : implicitArgs.Suffix) {
Args.push_back(arg);
}
}
for (const auto &arg : implicitArgs.Suffix) {
Args.push_back(arg);

if (ctor->isDefaultConstructor()) {
assert(Args.size() > 0 && "expected at least 1 argument (result address) "
"for default constructor");

// Zero out the backing memory of the struct.
// This makes default initializers for C++ structs behave consistently with
// the synthesized empty initializers for C structs. When C++ interop is
// enabled in a project, all imported C structs are treated as C++ structs,
// which sometimes means that Clang will synthesize a default constructor
// for the C++ struct that does not zero out trivial fields of a struct.
auto cxxRecord = ctor->getParent();
clang::ASTContext &ctx = cxxRecord->getASTContext();
auto typeSize = ctx.getTypeSizeInChars(ctx.getRecordType(cxxRecord));
subIGF.Builder.CreateMemSet(Args[0],
llvm::ConstantInt::get(subIGF.IGM.Int8Ty, 0),
typeSize.getQuantity(), llvm::MaybeAlign());
}

auto *call =
Expand Down
3 changes: 3 additions & 0 deletions test/Interop/Cxx/class/Inputs/constructors.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ struct ConstructorWithParam {
struct CopyAndMoveConstructor {
CopyAndMoveConstructor(const CopyAndMoveConstructor &) = default;
CopyAndMoveConstructor(CopyAndMoveConstructor &&) = default;

int value = 123;
int *ptr = nullptr;
};

struct Base {};
Expand Down
19 changes: 19 additions & 0 deletions test/Interop/Cxx/class/constructors-executable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,23 @@ CxxConstructorTestSuite.test("TemplatedConstructor") {
expectEqual(2, instance.value.i)
}

CxxConstructorTestSuite.test("implicit default ctor") {
// Make sure that fields of C++ structs are zeroed out.

let instance1 = ConstructorWithParam()
expectEqual(0, instance1.x)

let instance2 = IntWrapper()
expectEqual(0, instance2.x)

// CopyAndMoveConstructor is not default-initializable in C++, however, Swift
// generates an implicit deprecated default constructor for C++ structs for
// compatibility with C. This constructor will zero out the entire backing
// memory of the struct, including fields that have an init expression.
// See `SwiftDeclSynthesizer::createDefaultConstructor`.
let instance3 = CopyAndMoveConstructor()
expectEqual(0, instance3.value)
expectNil(instance3.ptr)
}

runAllTests()
1 change: 1 addition & 0 deletions test/Interop/Cxx/class/constructors-irgen-android.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public func createImplicitDefaultConstructor() -> ImplicitDefaultConstructor {
// ITANIUM_ARM: define protected swiftcc i32 @"$s7MySwift32createImplicitDefaultConstructorSo0deF0VyF"()
// ITANIUM_ARM-NOT: define
// Note `this` return type.
// ITANIUM_ARM: call void @llvm.memset.p0.i64
// ITANIUM_ARM: call void @_ZN26ImplicitDefaultConstructorC2Ev(ptr %{{[0-9]+}})
return ImplicitDefaultConstructor()
}
Expand Down
1 change: 1 addition & 0 deletions test/Interop/Cxx/class/constructors-irgen-macosx.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public func createHasVirtualBase() -> HasVirtualBase {
public func createImplicitDefaultConstructor() -> ImplicitDefaultConstructor {
// ITANIUM_X64: define swiftcc i32 @"$s7MySwift32createImplicitDefaultConstructorSo0deF0VyF"()
// ITANIUM_X64-NOT: define
// ITANIUM_X64: call void @llvm.memset.p0.i64
// ITANIUM_X64: call void @_ZN26ImplicitDefaultConstructorC1Ev(ptr %{{[0-9]+}})
return ImplicitDefaultConstructor()
}
Expand Down
1 change: 1 addition & 0 deletions test/Interop/Cxx/class/constructors-irgen-windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public func createImplicitDefaultConstructor() -> ImplicitDefaultConstructor {
// MICROSOFT_X64: define dllexport swiftcc i32 @"$s7MySwift32createImplicitDefaultConstructorSo0{{bcD0VyF|deF0VyF}}"()
// MICROSOFT_X64-NOT: define
// Note `this` return type but no implicit "most derived" argument.
// MICROSOFT_X64: call void @llvm.memset.p0.i64
// MICROSOFT_X64: call ptr @"??0ImplicitDefaultConstructor@@QEAA@XZ"(ptr %{{[0-9]+}})
return ImplicitDefaultConstructor()
}
Expand Down
3 changes: 3 additions & 0 deletions test/Interop/Cxx/class/constructors-module-interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
// CHECK-NEXT: struct CopyAndMoveConstructor {
// CHECK-NEXT: @available(*, deprecated, message
// CHECK-NEXT: init()
// CHECK-NEXT: init(value: Int32, ptr: UnsafeMutablePointer<Int32>!)
// CHECK-NEXT: var value: Int32
// CHECK-NEXT: var ptr: UnsafeMutablePointer<Int32>!
// CHECK-NEXT: }
// CHECK-NEXT: struct Base {
// CHECK-NEXT: init()
Expand Down
1 change: 1 addition & 0 deletions test/Interop/Cxx/class/constructors-typechecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let explicit = ExplicitDefaultConstructor()
let implicit = ImplicitDefaultConstructor()

let deletedImplicitly = ConstructorWithParam() // expected-warning {{'init()' is deprecated}}
let onlyCopyAndMove = CopyAndMoveConstructor() // expected-warning {{'init()' is deprecated}}

let deletedExplicitly = DefaultConstructorDeleted() // expected-error {{missing argument for parameter 'a' in call}}

Expand Down