Skip to content

🍒[cxx-interop] Fix metadata mismatch regarding fields of structs #81740

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
May 27, 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
8 changes: 8 additions & 0 deletions lib/IRGen/Field.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,20 @@ struct Field {
bool operator!=(Field other) const { return declOrKind != other.declOrKind; }
};

// Don't export private C++ fields that were imported as private Swift fields.
// The type of a private field might not have all the type witness operations
// that Swift requires, for instance, `std::unique_ptr<IncompleteType>` would
// not have a destructor.
bool isExportableField(Field field);

/// Iterate all the fields of the given struct or class type, including
/// any implicit fields that might be accounted for in
/// getFieldVectorLength.
void forEachField(IRGenModule &IGM, const NominalTypeDecl *typeDecl,
llvm::function_ref<void(Field field)> fn);

unsigned countExportableFields(IRGenModule &IGM, const NominalTypeDecl *type);

} // end namespace irgen
} // end namespace swift

Expand Down
2 changes: 1 addition & 1 deletion lib/IRGen/GenMeta.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1841,7 +1841,7 @@ namespace {

void addLayoutInfo() {
// uint32_t NumFields;
B.addInt32(getNumFields(getType()));
B.addInt32(countExportableFields(IGM, getType()));

// uint32_t FieldOffsetVectorOffset;
B.addInt32(FieldVectorOffset / IGM.getPointerSize());
Expand Down
30 changes: 4 additions & 26 deletions lib/IRGen/GenReflection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -946,35 +946,13 @@ class FieldTypeMetadataBuilder : public ReflectionMetadataBuilder {
B.addInt16(uint16_t(kind));
B.addInt16(FieldRecordSize);

// Filter to select which fields we'll export FieldDescriptors for.
auto exportable_field =
[](Field field) {
// Don't export private C++ fields that were imported as private Swift fields.
// The type of a private field might not have all the type witness
// operations that Swift requires, for instance,
// `std::unique_ptr<IncompleteType>` would not have a destructor.
if (field.getKind() == Field::Kind::Var &&
field.getVarDecl()->getClangDecl() &&
field.getVarDecl()->getFormalAccess() == AccessLevel::Private)
return false;
// All other fields are exportable
return true;
};

// Count exportable fields
int exportableFieldCount = 0;
forEachField(IGM, NTD, [&](Field field) {
if (exportable_field(field)) {
++exportableFieldCount;
}
});

// Emit exportable fields, prefixed with a count
B.addInt32(exportableFieldCount);
B.addInt32(countExportableFields(IGM, NTD));

// Filter to select which fields we'll export FieldDescriptor for.
forEachField(IGM, NTD, [&](Field field) {
if (exportable_field(field)) {
if (isExportableField(field))
addField(field);
}
});
}

Expand Down
20 changes: 20 additions & 0 deletions lib/IRGen/StructLayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,15 @@ unsigned irgen::getNumFields(const NominalTypeDecl *target) {
return numFields;
}

bool irgen::isExportableField(Field field) {
if (field.getKind() == Field::Kind::Var &&
field.getVarDecl()->getClangDecl() &&
field.getVarDecl()->getFormalAccess() == AccessLevel::Private)
return false;
// All other fields are exportable
return true;
}

void irgen::forEachField(IRGenModule &IGM, const NominalTypeDecl *typeDecl,
llvm::function_ref<void(Field field)> fn) {
auto classDecl = dyn_cast<ClassDecl>(typeDecl);
Expand All @@ -610,6 +619,17 @@ void irgen::forEachField(IRGenModule &IGM, const NominalTypeDecl *typeDecl,
}
}

unsigned irgen::countExportableFields(IRGenModule &IGM,
const NominalTypeDecl *type) {
// Don't count private C++ fields that were imported as private Swift fields
unsigned exportableFieldCount = 0;
forEachField(IGM, type, [&](Field field) {
if (isExportableField(field))
++exportableFieldCount;
});
return exportableFieldCount;
}

SILType Field::getType(IRGenModule &IGM, SILType baseType) const {
switch (getKind()) {
case Field::Var:
Expand Down
7 changes: 5 additions & 2 deletions lib/IRGen/StructMetadataVisitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ template <class Impl> class StructMetadataVisitor

// Struct field offsets.
asImpl().noteStartOfFieldOffsets();
for (VarDecl *prop : Target->getStoredProperties())
asImpl().addFieldOffset(prop);
for (VarDecl *prop : Target->getStoredProperties()) {
if (!(prop->getClangDecl() &&
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer if this check went through the same function as isExportableField to determine whether the particular VarDecl is exported. It doesn't matter so much on the 6.2 branch, but I can imagine us tuning this predicate in the future, and it would be best to have it in a single place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I'll make this change

prop->getFormalAccess() == AccessLevel::Private))
asImpl().addFieldOffset(prop);
}

asImpl().noteEndOfFieldOffsets();

Expand Down
6 changes: 6 additions & 0 deletions test/Interop/Cxx/class/Inputs/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,9 @@ module PIMPL {
requires cplusplus
export *
}

module SimpleStructs {
header "simple-structs.h"
requires cplusplus
export *
}
55 changes: 55 additions & 0 deletions test/Interop/Cxx/class/Inputs/simple-structs.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#ifndef TEST_INTEROP_CXX_CLASS_INPUTS_SIMPLE_STRUCTS_H
#define TEST_INTEROP_CXX_CLASS_INPUTS_SIMPLE_STRUCTS_H

struct HasPrivateFieldsOnly {
private:
int priv1;
int priv2;

public:
HasPrivateFieldsOnly(int i1, int i2) : priv1(i1), priv2(i2) {}
};

struct HasPublicFieldsOnly {
int publ1;
int publ2;

HasPublicFieldsOnly(int i1, int i2) : publ1(i1), publ2(i2) {}
};

struct HasPrivatePublicProtectedFields {
private:
int priv1;

public:
int publ1;

protected:
int prot1;

protected:
int prot2;

private:
int priv2;

public:
int publ2;

HasPrivatePublicProtectedFields(int i1, int i2, int i3, int i4, int i5,
int i6)
: priv1(i1), publ1(i2), prot1(i3), prot2(i4), priv2(i5),
publ2(i6) {}
};

struct Outer {
private:
HasPrivatePublicProtectedFields privStruct;

public:
HasPrivatePublicProtectedFields publStruct;

Outer() : privStruct(1, 2, 3, 4, 5, 6), publStruct(7, 8, 9, 10, 11, 12) {}
};

#endif
76 changes: 76 additions & 0 deletions test/Interop/Cxx/class/for-each-field.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// RUN: %target-run-simple-swift(-cxx-interoperability-mode=default -Xfrontend -disable-availability-checking -I %S/Inputs)

// REQUIRES: executable_test
// REQUIRES: reflection

@_spi(Reflection) import Swift
import SimpleStructs
import StdlibUnittest

func checkFieldsWithKeyPath<T>(
of type: T.Type,
options: _EachFieldOptions = [],
fields: [String: PartialKeyPath<T>]
) {
var count = 0

_forEachFieldWithKeyPath(of: T.self, options: options) {
charPtr, keyPath in
count += 1

let fieldName = String(cString: charPtr)
if fieldName == "" {
expectTrue(false, "Empty field name")
return true
}
guard let checkKeyPath = fields[fieldName] else {
expectTrue(false, "Unexpected field '\(fieldName)'")
return true
}

expectTrue(checkKeyPath == keyPath)
return true
}

expectEqual(fields.count, count)
}

var ForEachFieldTestSuite = TestSuite("ForEachField")

ForEachFieldTestSuite.test("HasPrivateFieldsOnly") {
checkFieldsWithKeyPath(
of: HasPrivateFieldsOnly.self,
fields: [:]
)
}

ForEachFieldTestSuite.test("HasPublicFieldsOnly") {
checkFieldsWithKeyPath(
of: HasPublicFieldsOnly.self,
fields: [
"publ1": \HasPublicFieldsOnly.publ1,
"publ2": \HasPublicFieldsOnly.publ2
]
)
}

ForEachFieldTestSuite.test("HasPrivatePublicProtectedFields") {
checkFieldsWithKeyPath(
of: HasPrivatePublicProtectedFields.self,
fields: [
"publ1": \HasPrivatePublicProtectedFields.publ1,
"publ2": \HasPrivatePublicProtectedFields.publ2
]
)
}

ForEachFieldTestSuite.test("Outer") {
checkFieldsWithKeyPath(
of: Outer.self,
fields: [
"publStruct": \Outer.publStruct
]
)
}

runAllTests()
37 changes: 37 additions & 0 deletions test/Interop/Cxx/class/print-simple-structs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// RUN: %target-run-simple-swift(-cxx-interoperability-mode=default -Xfrontend -disable-availability-checking -I %S/Inputs) | %FileCheck %s

// REQUIRES: executable_test

import SimpleStructs

func printCxxStructPrivateFields() {
let s = HasPrivateFieldsOnly(1, 2)
print(s)
}

func printCxxStructPublicFields() {
let s = HasPublicFieldsOnly(1, 2)
print(s)
}

func printCxxStructPrivatePublicProtectedFields() {
let s = HasPrivatePublicProtectedFields(1, 2, 3, 4, 5, 6)
print(s)
}

func printCxxStructNested() {
let s = Outer()
print(s)
}

printCxxStructPrivateFields()
// CHECK: HasPrivateFieldsOnly()

printCxxStructPublicFields()
// CHECK: HasPublicFieldsOnly(publ1: 1, publ2: 2)

printCxxStructPrivatePublicProtectedFields()
// CHECK: HasPrivatePublicProtectedFields(publ1: 2, publ2: 6)

printCxxStructNested()
// CHECK: Outer(publStruct: {{.*}}.HasPrivatePublicProtectedFields(publ1: 8, publ2: 12))
25 changes: 25 additions & 0 deletions test/Interop/Cxx/stdlib/Inputs/std-string-and-vector.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,28 @@ struct Item {
inline Item get_item() {
return {};
}

std::vector<int> makeVecOfInt() { return {1, 2, 3}; }
std::vector<std::string> makeVecOfString() { return {"Hello", "World"}; }

struct S {
private:
std::string privStr;
std::vector<std::string> privVec;

public:
std::string pubStr;
std::vector<std::string> pubVec;

protected:
std::string protStr;
std::vector<std::string> protVec;

public:
S() : privStr("private"), privVec({"private", "vector"}),
pubStr("public"), pubVec({"a", "public", "vector"}),
protStr("protected"), protVec({"protected", "vector"}) {}

std::vector<std::string> getPrivVec() const { return privVec; }
std::string getProtStr() const { return protStr; }
};
11 changes: 11 additions & 0 deletions test/Interop/Cxx/stdlib/Inputs/std-unique-ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define TEST_INTEROP_CXX_STDLIB_INPUTS_STD_UNIQUE_PTR_H

#include <memory>
#include <string>

struct NonCopyable {
NonCopyable(int x) : x(x) {}
Expand Down Expand Up @@ -29,6 +30,10 @@ std::unique_ptr<int> makeInt() {
return std::make_unique<int>(42);
}

std::unique_ptr<std::string> makeString() {
return std::make_unique<std::string>("Unique string");
}

std::unique_ptr<int[]> makeArray() {
int *array = new int[3];
array[0] = 1;
Expand All @@ -55,4 +60,10 @@ std::unique_ptr<HasDtor> makeDtor() {
return std::make_unique<HasDtor>();
}

std::shared_ptr<int> makeIntShared() { return std::make_unique<int>(42); }

std::shared_ptr<std::string> makeStringShared() {
return std::make_unique<std::string>("Shared string");
}

#endif // TEST_INTEROP_CXX_STDLIB_INPUTS_STD_UNIQUE_PTR_H
Loading