diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt index 8091792184d..c5b91cd2eee 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt @@ -26,15 +26,13 @@ private class StructDeclImpl(spelling: String, override val location: Location) } private class StructDefImpl( - size: Long, align: Int, decl: StructDecl, - override val kind: Kind -) : StructDef( - size, align, decl -) { - override val members = mutableListOf() - override val methods = mutableListOf() - override val staticFields = mutableListOf() -} + size: Long, + align: Int, + override val kind: Kind, + override val members: List, + override val methods: List, + override val staticFields: List +) : StructDef(size, align) private class EnumDefImpl(spelling: String, type: Type, override val location: Location) : EnumDef(spelling, type) { override val constants = mutableListOf() @@ -181,24 +179,21 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole ): StructDecl = structRegistry.getOrPut(cursor, { createStructDecl(cursor) }) { decl -> val definitionCursor = clang_getCursorDefinition(cursor) if (clang_Cursor_isNull(definitionCursor) == 0) { - assert(clang_isCursorDefinition(definitionCursor) != 0) - // TODO: is this a bug or this is a wrong thing to do? - // Otherwise c++ class definition is created from its forward declaration - // and hence is empty. - //createStructDef(decl, cursor) - createStructDef(decl, definitionCursor) + decl.def = createStructDef(definitionCursor, definitionCursor.type, decl) } } - private fun createStructDecl(cursor: CValue): StructDeclImpl { - val cursorType = clang_getCursorType(cursor) - val typeSpelling = clang_getTypeSpelling(cursorType).convertAndDispose() - return StructDeclImpl(typeSpelling, getLocation(cursor)) - } + private fun createStructDecl(cursor: CValue): StructDeclImpl = + StructDeclImpl(cursor.type.name, getLocation(cursor)) - private fun addCxxMembers(classCursor: CValue, clazz: StructDefImpl) { - if (library.language != Language.CPP) return + private data class CxxMembers(val methods: List = emptyList(), val staticFields: List = emptyList()) + + private fun collectCxxMembers(classCursor: CValue, receiverType: RecordType): CxxMembers { + assert(library.language == Language.CPP) + + val methods = mutableListOf() + val staticFields = mutableListOf() // TODO skip method (function) when encounter UnsupportedType in params or ret value. Otherwise all class methods will be lost due to exception (?) visitChildren(classCursor) { cursor, _ -> @@ -206,23 +201,23 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole // TODO If a kotlin class is _conceptually_ derived from its c++ counterpart, then it shall be able to override virtual private and access protected when (cursor.kind) { CXCursorKind.CXCursor_CXXMethod -> { - val isOperatorFunction = (clang_getCursorSpelling(cursor).convertAndDispose().take(8) == "operator") + val isOperatorFunction = cursor.spelling.startsWith("operator") // operators are Not Implemented Yet if (!isOperatorFunction) { if (clang_isFunctionTypeVariadic(clang_getCursorType(cursor)) == 0) // FIXME why it doesn't work??? - getFunction(cursor, clazz.decl)?.let { clazz.methods.add(it) } + getFunction(cursor, receiverType)?.let { methods.add(it) } } } CXCursorKind.CXCursor_Constructor, CXCursorKind.CXCursor_Destructor -> - getFunction(cursor, clazz.decl)?.let { clazz.methods.add(it) } + getFunction(cursor, receiverType)?.let { methods.add(it) } CXCursorKind.CXCursor_VarDecl -> { - clazz.staticFields.add(GlobalDecl( - name =getCursorSpelling(cursor), + staticFields.add(GlobalDecl( + name = getCursorSpelling(cursor), type = convertCursorType(cursor), isConst = clang_isConstQualifiedType(clang_getCursorType(cursor)) != 0, - parentName = clazz.decl.spelling) + parentName = receiverType.decl.spelling) ) } @@ -232,72 +227,90 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole } CXChildVisitResult.CXChildVisit_Continue } + return CxxMembers(methods, staticFields) } - private fun createStructDef(structDecl: StructDeclImpl, cursor: CValue) { + private fun createStructDef(cursor: CValue, structType: CValue, structDecl: StructDecl? = null): StructDefImpl { + assert(clang_isCursorDefinition(cursor) != 0) val type = clang_getCursorType(cursor) - - val fields = mutableListOf() - addDeclaredFields(fields, type, type) - val size = clang_Type_getSizeOf(type) val align = clang_Type_getAlignOf(type).toInt() + val members = getMembers(cursor, structType) + val cxxMembers = + if (library.language == Language.CPP && structDecl != null) collectCxxMembers(cursor, RecordType(structDecl)) + else CxxMembers() - val structDef = StructDefImpl( - size, align, structDecl, - when (cursor.kind) { - CXCursorKind.CXCursor_UnionDecl -> StructDef.Kind.UNION - CXCursorKind.CXCursor_StructDecl -> StructDef.Kind.STRUCT - CXCursorKind.CXCursor_ClassDecl -> StructDef.Kind.CLASS - else -> error(cursor.kind) - } - ) - - structDef.members += fields - addCxxMembers(cursor, structDef) - - structDecl.def = structDef + with(cxxMembers) { + return StructDefImpl( + size, align, + when (cursor.kind) { + CXCursorKind.CXCursor_UnionDecl -> StructDef.Kind.UNION + CXCursorKind.CXCursor_StructDecl -> StructDef.Kind.STRUCT + CXCursorKind.CXCursor_ClassDecl -> StructDef.Kind.CLASS + else -> error(cursor.kind) + }, + members, + methods, + staticFields + ) + } } - private fun addDeclaredFields(result: MutableList, structType: CValue, containerType: CValue) { - // TODO: We don't exactly preserve C++ layout here, but - // we don't allow general case C++ classes by value at the moment. - getFields(containerType).filter { library.language != Language.CPP || it.isCxxPublic }.forEach { fieldCursor -> - val name = getCursorSpelling(fieldCursor) - if (name.isNotEmpty()) { - val fieldType = convertCursorType(fieldCursor) - val offset = clang_Type_getOffsetOf(structType, name) - val member = if (offset < 0) { - IncompleteField(name, fieldType) - } else if (clang_Cursor_isBitField(fieldCursor) == 0) { - val canonicalFieldType = clang_getCanonicalType(clang_getCursorType(fieldCursor)) - Field( - name, - fieldType, - offset, - clang_Type_getSizeOf(canonicalFieldType), - clang_Type_getAlignOf(canonicalFieldType) - ) - } else { - val size = clang_getFieldDeclBitWidth(fieldCursor) - BitField(name, fieldType, offset, size) - } - result.add(member) - } else { - // Unnamed field. - val fieldType = clang_getCursorType(fieldCursor) - when (fieldType.kind) { - CXTypeKind.CXType_Record -> { - // Unnamed struct fields also contribute their fields: - addDeclaredFields(result, structType, fieldType) + // cursor may be at the root struct or at a inner anonymous struct or union, + // while structType is always the nearest named enclosing struct/union (i.e. root struct) + // All offsets are calculated relative to this named parent + private fun getMembers(cursor: CValue, structType: CValue): List = + // TODO: We don't exactly preserve C++ layout here, but we don't allow general case C++ classes by value at the moment. + getFields(cursor.type).filter { library.language != Language.CPP || it.isCxxPublic }.map { fieldCursor -> + + /* + * We want to identify anonymous struct/union member, according with definition (ISO/IEC 9899): + * "An unnamed member whose type specifier is a structure specifier with no tag is called an anonymous structure" + * `clang_Cursor_isAnonymous` intended to identify such entity and distinguish with cases alike: + * struct { + * struct {int x; } f; // named member of anonymous type "struct with no tag" + * int : 16; // anonymous bitfield + * typedef struct { int z; } foo; // c++ only; anon struct but the struct tag is implicitly assigned by the compiler; clang_getTypeSpelling still empty + * struct { int a; }; // this is the only one that we are looking for, i.e. anonymous struct member + * } + * `clang_Cursor_isAnonymous` implementation has been changed since LLVM 8 so we have to additionally check type.kind == CXType_Record + * Starting from LLVM 9 a new function `clang_Cursor_isAnonymousRecordDecl` provided specifically for that. + * Also, both `clang_Cursor_isAnonymous` and `clang_Cursor_isAnonymousRecordDecl` expect StructDecl cursor but we got + * FieldDecl cursor now, so we have to convert cursor to type declaration cursor first (e.g. StructDecl)) + */ + val declCursor = clang_getTypeDeclaration(fieldCursor.type) + + // Behavior of clang_Cursor_isAnonymous is changing starting from LLVM 8. + // Use lately introduced clang_Cursor_isAnonymousRecordDecl when available (LLVM 9) + val isAnonymousRecordType = (fieldCursor.type.kind == CXType_Record) && (clang_Cursor_isAnonymous(declCursor) == 1) + when { + isAnonymousRecordType -> { + // TODO: clang_Cursor_getOffsetOfField is OK for anonymous, but only for the 1st level of such nesting + AnonymousInnerRecord( + createStructDef(clang_getCursorDefinition(declCursor), structType)) } else -> { - // Nothing. + val name = getCursorSpelling(fieldCursor) + val fieldType = convertCursorType(fieldCursor) + val offset = clang_Type_getOffsetOf(structType, name) + if (offset < 0) { + IncompleteField(name) + } else if (clang_Cursor_isBitField(fieldCursor) == 0) { + val canonicalFieldType = clang_getCanonicalType(fieldCursor.type) + Field( + name, + fieldType, + offset, + clang_Type_getSizeOf(canonicalFieldType), + clang_Type_getAlignOf(canonicalFieldType) + ) + } else { + val size = clang_getFieldDeclBitWidth(fieldCursor) + BitField(name, fieldType, offset, size) + } } } } - } - } private fun getEnumDefAt(cursor: CValue): EnumDefImpl { if (clang_isCursorDefinition(cursor) == 0) { @@ -1019,17 +1032,15 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole return getParentName(parent, nextPkg) } - private fun getFunction(cursor: CValue, receiver: StructDecl? = null): FunctionDecl? { + private fun getFunction(cursor: CValue, receiver: RecordType? = null): FunctionDecl? { if (!isFuncDeclEligible(cursor)) { log("Skip function ${clang_getCursorSpelling(cursor).convertAndDispose()}") return null } - var name = clang_getCursorSpelling(cursor).convertAndDispose() + var name = cursor.spelling val cursorReturnType = clang_getCursorResultType(cursor) - val cursorReturnTypeSpelling = clang_getTypeSpelling(cursorReturnType).convertAndDispose() - - if (cursorReturnTypeSpelling.isUnknownTemplate()) return null + if (cursorReturnType.name.isUnknownTemplate()) return null var returnType = convertType(cursorReturnType, clang_getCursorResultTypeAttributes(cursor)) @@ -1048,20 +1059,20 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole // TODO Do the following if clang_getCursorLanguage(cursor) == CXLanguageKind.CXLanguage_CPlusPlus ... val parentName = getParentName(cursor) val cxxMethodInfo = receiver?.let { + val receiverPointerType = PointerType(receiver, clang_CXXMethod_isConst(cursor) != 0) CxxMethodInfo( - PointerType(RecordType(receiver), - clang_CXXMethod_isConst(cursor) != 0), // CXCursor_ConversionFunction has constness too + receiverPointerType, // CXCursor_ConversionFunction has constness too when (cursor.kind) { CXCursorKind.CXCursor_Constructor -> { - returnType = PointerType(RecordType(receiver)) + returnType = receiverPointerType name = "__init__" // It is intended to init preallocated memory with placement new, so it is not "create" factory method. TODO One may want "create" method also. // Parameter type for placement new is void*, but I want to emphasize that memory block ahall have proper size and alignment - parameters.add(0, Parameter("self", PointerType(RecordType(receiver)), false)) + parameters.add(0, Parameter("self", receiverPointerType, false)) CxxMethodKind.Constructor } CXCursorKind.CXCursor_Destructor -> { name = "__destroy__" - parameters.add(0, Parameter("self", PointerType(RecordType(receiver)), false)) + parameters.add(0, Parameter("self", receiverPointerType, false)) CxxMethodKind.Destructor } // CXCursorKind.CXCursor_ConversionFunction -> ... @@ -1069,9 +1080,7 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole if (clang_CXXMethod_isStatic(cursor) != 0) { CxxMethodKind.StaticMethod } else { - parameters.add(0, Parameter("self", - PointerType(RecordType(receiver), clang_CXXMethod_isConst(cursor) != 0), - false)) + parameters.add(0, Parameter("self", receiverPointerType, false)) CxxMethodKind.InstanceMethod } else -> CxxMethodKind.None // Not implemented. Not expected, OK to assert (?) @@ -1148,7 +1157,7 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole val argNum = clang_Cursor_getNumArguments(cursor) val args = (0..argNum - 1).map { val argCursor = clang_Cursor_getArgument(cursor, it) - if (clang_getTypeSpelling(clang_getCursorType(argCursor)).convertAndDispose().isUnknownTemplate()) { + if (argCursor.type.name.isUnknownTemplate()) { return null } val argName = getCursorSpelling(argCursor) diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt index 07ba3300573..5455786acd5 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt @@ -120,25 +120,30 @@ interface TypeDeclaration { val location: Location } -sealed class StructMember(val name: String, val type: Type) { +sealed class StructMember(val name: String) { abstract val offset: Long? } /** * C struct field. */ -class Field(name: String, type: Type, override val offset: Long, val typeSize: Long, val typeAlign: Long) - : StructMember(name, type) +class Field(name: String, val type: Type, override val offset: Long, val typeSize: Long, val typeAlign: Long) + : StructMember(name) val Field.isAligned: Boolean get() = offset % (typeAlign * 8) == 0L -class BitField(name: String, type: Type, override val offset: Long, val size: Int) : StructMember(name, type) +class BitField(name: String, val type: Type, override val offset: Long, val size: Int) : StructMember(name) -class IncompleteField(name: String, type: Type) : StructMember(name, type) { +class IncompleteField(name: String) : StructMember(name) { override val offset: Long? get() = null } +class AnonymousInnerRecord(val def: StructDef) : StructMember("") { + override val offset: Long? get() = null + val typeSize: Long = def.size +} + /** * C struct declaration. */ @@ -153,19 +158,36 @@ abstract class StructDecl(val spelling: String) : TypeDeclaration { * @param hasNaturalLayout must be `false` if the struct has unnatural layout, e.g. it is `packed`. * May be `false` even if the struct has natural layout. */ -abstract class StructDef(val size: Long, val align: Int, val decl: StructDecl) { +abstract class StructDef(val size: Long, val align: Int) { enum class Kind { STRUCT, UNION, CLASS } - abstract val methods: List - abstract val members: List - abstract val staticFields: List abstract val kind: Kind + abstract val members: List + abstract val methods: List + abstract val staticFields: List - val fields: List get() = members.filterIsInstance() - val bitFields: List get() = members.filterIsInstance() + val fields: List + get() = mutableListOf().apply { + members.forEach { + when (it) { + is Field -> add(it) + is AnonymousInnerRecord -> addAll(it.def.fields) + } + } + } + + val bitFields: List + get() = mutableListOf().apply { + members.forEach { + when (it) { + is BitField -> add(it) + is AnonymousInnerRecord -> addAll(it.def.bitFields) + } + } + } } /** diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/AbiSpecific.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/AbiSpecific.kt index cf2c50143f9..a1794e0a27e 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/AbiSpecific.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/AbiSpecific.kt @@ -95,22 +95,39 @@ class DarwinArm64AbiInfo : ObjCAbiInfo { } } +/* +Consider edge cases with anonymous inner: +hasIntegerLikeLayout +1 N struct X { struct {}; int v1; }; // despite the offset(v1) == 0 and sizeof(X) == 4 +2 N struct X { struct {}; char v1; }; // same with char +3 N struct X { struct {} v1; short v2; }; // same with named empty field; sizeof == 2, offset(v2) == 0 +4 N struct X { int v1; struct {}; }; // despite there is only one field but empty struct has offset == 4 +5 N struct X { char v1; struct {}; }; // same, sizeof is 1 +6 N struct X { char v1; struct {char v2:4;}; }; // despite v2 is bitfield +7 Y struct X { char v1; char v2:4; }; // but this is OK (bitfield) +8 Y struct X { struct {char v1;}; char v2:4; }; // same, bitfield is OK +9 Y struct X { struct {char v1;} v1; char v2:4; }; // same, OK v2 is bitfield +10 Y struct X { struct {} v1; char v2:4; }; // OK, v2 is bitfield +11 Y struct X { struct {char v1;}; short v2:16; }; // OK, v2 is bitfield even if it has full size + +#1..3: the field offset == 0 but still not eligible for `hasIntegerLikeLayout` +Looks like we have to use the field' sequential number instead of offset + */ +private fun StructDef.hasIntegerLikeLayout(): Boolean { + return size <= 4 && + members.mapIndexed { index, it -> + // Assuming the member order has not been changed + when (it) { + is BitField -> it.type.isIntegerLikeType() + is Field -> index == 0 && it.type.isIntegerLikeType() // assert(offset == 0) + is AnonymousInnerRecord -> index == 0 && it.def.hasIntegerLikeLayout() + is IncompleteField -> false + } + }.all {it} +} + private fun Type.isIntegerLikeType(): Boolean = when (this) { - is RecordType -> { - val def = this.decl.def - if (def == null) { - false - } else { - def.size <= 4 && - def.members.all { - when (it) { - is BitField -> it.type.isIntegerLikeType() - is Field -> it.offset == 0L && it.type.isIntegerLikeType() - is IncompleteField -> false - } - } - } - } + is RecordType -> decl.def?.hasIntegerLikeLayout() ?: false is ObjCPointer, is PointerType, CharType, is BoolType -> true is IntegerType -> this.size <= 4 is Typedef -> this.def.aliased.isIntegerLikeType() @@ -122,7 +139,7 @@ private fun Type.isIntegerLikeType(): Boolean = when (this) { private fun Type.hasUnalignedMembers(): Boolean = when (this) { is Typedef -> this.def.aliased.hasUnalignedMembers() is RecordType -> this.decl.def!!.let { def -> - def.fields.any { + def.fields.any { // TODO: what about bitfields? !it.isAligned || // Check members of fields too: it.type.hasUnalignedMembers() diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt index 5fae4e5fc0b..f8a792e4019 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt @@ -8,36 +8,75 @@ fun tryRenderStructOrUnion(def: StructDef): String? = when (def.kind) { StructDef.Kind.CLASS -> null } -private fun tryRenderStruct(def: StructDef): String? { - val isPackedStruct = def.fields.any { !it.isAligned } +/** + * Members of anonymous struct/union are the fields of enclosing named aggregate and has the corresponding offset. + * However for the purpose of alignment heuristic we use "immediate" offset, i.e. relative to the immediate parent. + * Consider for ex. a packed struct containing not packed anonymous `Inner`: inner fields are not aligned relative to the root. + * + * For the purpose of `isPacked` heuristic we should analyze immediate children only, i.e. ignore the members of nested + * anonymous struct / union (included by `fields` getter). For ex. inner anon struct may be packed and its members unaligned, + * however this does not imply `packed` attribute at outer struct. + * + * Empty inner records (ie offsetBytes == null) does not affect `packed` heuristic and shall be ignored here. + * Unsupported members (ie BitField and IncompleteField) to be ignored too but won't be compiled anyway. + */ +private val StructDef.isPacked: Boolean + get() { + val baseOffset = fields.firstOrNull()?.offsetBytes ?: return false + return members.any { member -> + when (member) { + is Field -> (member.offsetBytes - baseOffset) % member.typeAlign != 0L + is AnonymousInnerRecord -> + member.offsetBytes?.let { (it - baseOffset) % member.def.align != 0L } ?: false + else -> false + } + } + } +private fun tryRenderStruct(def: StructDef): String? { + // The only case when offset starts from non-zero is a inner anonymous struct or union + val baseOffset = def.fields.firstOrNull()?.offsetBytes ?: 0L var offset = 0L - return buildString { - append("struct") - if (isPackedStruct) append(" __attribute__((packed))") - append(" { ") + val isPackedStruct = def.isPacked - def.members.forEachIndexed { index, it -> - val name = "p$index" + // The following is to deal with the case when a field has big alignment but occasionally its offset is naturally aligned, + // so we can't guess it by heuristic. However the enclosing struct must be explicitly aligned. + val maxAlign = def.members.filterIsInstance().maxOfOrNull { it.typeAlign } + val forceAlign = maxAlign?.let { def.align > maxAlign } + ?: (def.align > 1) // Anonymous inner may be empty AND explicitly aligned + + return buildString { + append("struct { ") + + def.members.forEach { it -> val decl = when (it) { is Field -> { + val immediateOffset = it.offsetBytes - baseOffset val defaultAlignment = if (isPackedStruct) 1L else it.typeAlign - val alignment = guessAlignment(offset, it.offsetBytes, defaultAlignment) ?: return null + val alignment = guessAlignment(offset, immediateOffset, defaultAlignment) ?: return null + offset = immediateOffset + it.typeSize - offset = it.offsetBytes + it.typeSize - - tryRenderVar(it.type, name) - ?.plus(if (alignment == defaultAlignment) "" else "__attribute__((aligned($alignment)))") + tryRenderVar(it.type, it.name) + ?.plus(if (alignment == defaultAlignment) "" else " __attribute__((aligned($alignment)))") } is BitField, // TODO: tryRenderVar(it.type, name)?.plus(" : ${it.size}") is IncompleteField -> null // e.g. flexible array member. + is AnonymousInnerRecord -> { + // No need to advance offset if offsetBytes is null 'cause it means that record is empty. Assert that. + assert(it.offsetBytes != null || it.typeSize == 0L) + it.offsetBytes?.let { offsetBytes -> + offset = offsetBytes - baseOffset + it.typeSize + } + tryRenderStructOrUnion(it.def) + } } ?: return null append("$decl; ") } - append("}") + if (isPackedStruct) append(" __attribute__((packed))") + if (forceAlign) append(" __attribute__((aligned(${def.align})))") } } @@ -48,20 +87,26 @@ private fun guessAlignment(offset: Long, paddedOffset: Long, defaultAlignment: L private fun alignUp(x: Long, alignment: Long): Long = (x + alignment - 1) and ((alignment - 1).inv()) -private fun tryRenderUnion(def: StructDef): String? = - if (def.members.any { it.offset != 0L }) null else buildString { - append("union { ") - def.members.forEachIndexed { index, it -> - val decl = when (it) { - is Field -> tryRenderVar(it.type, "p$index") - is BitField, is IncompleteField -> null - } ?: return null - - append("$decl; ") - } - append("}") +private fun tryRenderUnion(def: StructDef): String? { + val maxAlign = def.members.filterIsInstance().maxOfOrNull { it.typeAlign } + val forceAlign = maxAlign?.let { def.align > maxAlign } + ?: (def.align > 1) // Anonymous inner may be empty AND explicitly aligned + return buildString { + append("union { ") + def.members.forEach { it -> + val name = it.name + val decl = when (it) { + is Field -> tryRenderVar(it.type, name) + is BitField, is IncompleteField -> null + is AnonymousInnerRecord -> tryRenderStructOrUnion(it.def) + } ?: return null + append("$decl; ") } + append("}") + if (forceAlign) append(" __attribute__((aligned(${def.align})))") + } +} private fun tryRenderVar(type: Type, name: String): String? = when (type) { CharType, is BoolType -> "char $name" @@ -78,7 +123,13 @@ private fun tryRenderVar(type: Type, name: String): String? = when (type) { else -> null } -private val Field.offsetBytes: Long get() { - require(this.offset % 8 == 0L) - return this.offset / 8 -} \ No newline at end of file +private val Field.offsetBytes: Long + get() { + require(this.offset % 8 == 0L) + return this.offset / 8 + } + +private val AnonymousInnerRecord.offsetBytes: Long? + get() { + return def.fields.firstOrNull()?.offsetBytes + } diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index bddc4e8c98b..1bfebe47acd 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -3700,6 +3700,10 @@ createInterop("ctypes") { it.defFile 'interop/basics/ctypes.def' } +createInterop("structAnonym") { + it.defFile 'interop/basics/structAnonym.def' +} + createInterop("cvalues") { it.defFile 'interop/basics/cvalues.def' } @@ -4039,6 +4043,12 @@ interopTest("interop_types") { interop = 'ctypes' } +interopTest("interop_structAnonym") { + disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. + source = "interop/basics/structAnonym.kt" + interop = 'structAnonym' +} + interopTest("interop_vectors") { disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. source = "interop/basics/vectors.kt" diff --git a/kotlin-native/backend.native/tests/interop/basics/bf.kt b/kotlin-native/backend.native/tests/interop/basics/bf.kt index 0739d4a6dc9..3729fd97349 100644 --- a/kotlin-native/backend.native/tests/interop/basics/bf.kt +++ b/kotlin-native/backend.native/tests/interop/basics/bf.kt @@ -11,7 +11,7 @@ fun assertEquals(value1: Any?, value2: Any?) { throw AssertionError("Expected $value1, got $value2") } -fun check(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean) { +fun check(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean, x9: Int) { assertEquals(x1, s.x1) assertEquals(x1, getX1(s.ptr)) @@ -35,9 +35,12 @@ fun check(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E assertEquals(x8, s.x8) assertEquals(x8, getX8(s.ptr)) + + assertEquals(x9, s.x9) + assertEquals(x9, getX9(s.ptr)) } -fun assign(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean) { +fun assign(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean, x9: Int) { s.x1 = x1 s.x2 = x2 s.x3 = x3 @@ -46,9 +49,11 @@ fun assign(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: s.x6 = x6 s.x7 = x7 s.x8 = x8 + s.x9 = x9 } -fun assignReversed(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean) { +fun assignReversed(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean, x9: Int) { + s.x9 = x9 s.x8 = x8 s.x7 = x7 s.x6 = x6 @@ -59,20 +64,20 @@ fun assignReversed(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Lo s.x1 = x1 } -fun test(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean) { - assign(s, x1, x2, x3, x4, x5, x6, x7, x8) - check(s, x1, x2, x3, x4, x5, x6, x7, x8) +fun test(s: S, x1: Long, x2: B2, x3: UShort, x4: UInt, x5: Int, x6: Long, x7: E, x8: Boolean, x9: Int) { + assign(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) + check(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) - assignReversed(s, x1, x2, x3, x4, x5, x6, x7, x8) - check(s, x1, x2, x3, x4, x5, x6, x7, x8) + assignReversed(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) + check(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) // Also check with some insignificant bits modified: - assign(s, x1 + 2, x2, (x3 + 8u).toUShort(), x4 - 16u, x5 + 32, x6 + Long.MIN_VALUE, x7, x8) - check(s, x1, x2, x3, x4, x5, x6, x7, x8) + assign(s, x1 + 2, x2, (x3 + 8u).toUShort(), x4 - 16u, x5 + 32, x6 + Long.MIN_VALUE, x7, x8, x9 + 16) + check(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) - assignReversed(s, x1 + 2, x2, (x3 + 8u).toUShort(), x4 - 16u, x5 + 32, x6 + Long.MIN_VALUE, x7, x8) - check(s, x1, x2, x3, x4, x5, x6, x7, x8) + assignReversed(s, x1 + 2, x2, (x3 + 8u).toUShort(), x4 - 16u, x5 + 32, x6 + Long.MIN_VALUE, x7, x8, x9 + 16) + check(s, x1, x2, x3, x4, x5, x6, x7, x8, x9) } fun main(args: Array) { @@ -86,6 +91,7 @@ fun main(args: Array) { for (x6 in longArrayOf(Long.MIN_VALUE/2, -1L shl 36, -325L, 0, 1L shl 48, Long.MAX_VALUE/2)) for (x7 in E.values()) for (x8 in arrayOf(false, true)) - test(s, x1, x2, x3.toUShort(), x4, x5, x6, x7, x8) + for (x9 in intArrayOf(-8, -2, -1, 0, 5, 7)) // 4 bits width + test(s, x1, x2, x3.toUShort(), x4, x5, x6, x7, x8, x9) } } diff --git a/kotlin-native/backend.native/tests/interop/basics/bitfields.def b/kotlin-native/backend.native/tests/interop/basics/bitfields.def index 729ab79c953..17ef7500c5a 100644 --- a/kotlin-native/backend.native/tests/interop/basics/bitfields.def +++ b/kotlin-native/backend.native/tests/interop/basics/bitfields.def @@ -16,6 +16,7 @@ struct __attribute__((packed)) S { long long x6 : 63; enum E x7: 2; _Bool x8 : 1; + struct { int x9:4; }; }; static long long getX1(struct S* s) { return s->x1; } @@ -26,3 +27,4 @@ static int getX5(struct S* s) { return s->x5; } static long long getX6(struct S* s) { return s->x6; } static enum E getX7(struct S* s) { return s->x7; } static _Bool getX8(struct S* s) { return s->x8; } +static int getX9(struct S* s) { return s->x9; } diff --git a/kotlin-native/backend.native/tests/interop/basics/ctypes.def b/kotlin-native/backend.native/tests/interop/basics/ctypes.def index 8fdf6942216..c6272ed75dc 100644 --- a/kotlin-native/backend.native/tests/interop/basics/ctypes.def +++ b/kotlin-native/backend.native/tests/interop/basics/ctypes.def @@ -1,4 +1,5 @@ --- + // KT-28065 struct StructWithConstFields { int x; diff --git a/kotlin-native/backend.native/tests/interop/basics/structAnonym.def b/kotlin-native/backend.native/tests/interop/basics/structAnonym.def new file mode 100644 index 00000000000..bdf8fc20522 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/basics/structAnonym.def @@ -0,0 +1,192 @@ +--- + +/* + * Test of return/send-by-value for aggregate type (struct or union) with anonymous inner struct or union member. + * Specific issues: alignment, packed, nested named and anon struct/union, other anon types (named field of anon struct type; anon bitfield) + */ + +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Winitializer-overrides" + +union _GLKVector3 +{ + struct { float x, y, z; }; + struct { float r, g, b; }; + struct { float s, t, p; }; + float v[3]; +}; + +static union _GLKVector3 get_GLKVector3() { + union _GLKVector3 ret = {{1, 2, 3}}; + return ret; +} + +static float hash_GLKVector3(union _GLKVector3 x) { + union _GLKVector3 ret = {{1, 2, 3}}; + return x.x + 2.0f * x.y + 4.0f * x.z; +} + +// trivial alignment: member is already aligned, but this implies implicit larger alignment of the root struct +struct StructAnonRecordMember_ImplicitAlignment { + int32_t a[4]; + struct { + int b __attribute__((aligned(16))); + }; +}; + +static struct StructAnonRecordMember_ImplicitAlignment retByValue_StructAnonRecordMember_ImplicitAlignment() { + struct StructAnonRecordMember_ImplicitAlignment t = { + .a = {1,2,3,4}, + .b = 42 + }; + return t; +} + +struct StructAnonRecordMember_ExplicitAlignment { + char a; + struct { + __attribute__((aligned(4))) + char x; + }; +}; + +static struct StructAnonRecordMember_ExplicitAlignment retByValue_StructAnonRecordMember_ExplicitAlignment() { + struct StructAnonRecordMember_ExplicitAlignment t = { + .a = 'a', + .x = 'x' + }; + return t; +} + +// Deep nesting +struct StructAnonRecordMember_Nested { + int x; + union { // implicitly aligned to 8 bytes due to int64, or 4 bytes at 32-bit arch + int a[2]; + struct { + int64_t b; + }; + }; + char z; + double y; +}; + +static struct StructAnonRecordMember_Nested retByValue_StructAnonRecordMember_Nested() { + struct StructAnonRecordMember_Nested c = { + .x = 37, + .b = 42, + .z = 'z', + .y = 3.14 + }; + return c; +} + +static int sendByValue_StructAnonRecordMember_Nested(struct StructAnonRecordMember_Nested c) { + return c.a[0] + 2 * c.a[1]; +} + +// Basic, 2 levels + +struct StructAnonRecordMember_Complicate { + char first; // __attribute__((aligned(16))); + union { + int a[2]; + union { char c1; int c2; }; + struct { char b1; int64_t b2; }; // implicit 64-bits alignment + }; + char second __attribute__((aligned(16))); + struct { + char x; + struct { int64_t b11, b12; } Y2; + float f __attribute__((aligned(16))); + }; // __attribute__((aligned(16))); + char last; +}; + +#define INIT(T, x) struct T x = \ +{ \ + .first = 'a', \ + .b1 = 'b', \ + .b2 = 42, \ + .second = 's', \ + .last = 'z', \ + .f = 3.14, \ + .Y2 = {11, 12} \ +} + +static struct StructAnonRecordMember_Complicate retByValue_StructAnonRecordMember_Complicate() { + INIT(StructAnonRecordMember_Complicate, c); + return c; +} + +struct StructAnonRecordMember_Packed { + char first; + union { + int a[2]; + union { char c1; int c2; }; + struct { char b1; int64_t b2; }; + }; + char second; + struct { + char x; + struct { int64_t b11, b12; } Y2; + float f; + } __attribute__((aligned(16))); + char last; +} __attribute__ ((packed)); + +static struct StructAnonRecordMember_Packed retByValue_StructAnonRecordMember_Packed() { + INIT(StructAnonRecordMember_Packed, c); + return c; +} + +// Nested struct may be packed too +#pragma pack(1) +struct StructAnonRecordMember_PragmaPacked { + char first; + union { + int a[2]; + union { char c1; int c2; }; + struct { char b1; int64_t b2; }; + }; + char second; + struct { + char x; + struct { int64_t b11, b12; } Y2; + float f __attribute__((aligned(16))); + }; // __attribute__((aligned(16))); + char last; +} __attribute__ ((packed)); +#pragma pack() + +static struct StructAnonRecordMember_PragmaPacked retByValue_StructAnonRecordMember_PragmaPacked() { + INIT(StructAnonRecordMember_PragmaPacked, c); + return c; +} + +#pragma pack(2) +struct StructAnonRecordMember_Packed2 { + char first; + union { + int a[2]; + union { char c1; int c2; }; + struct { char b1; int64_t b2; }; + }; + char second; + struct { + char x; + struct { int64_t b11, b12; } Y2; + float f __attribute__((aligned(16))); + }; // __attribute__((aligned(16))); + char last; +} __attribute__ ((packed)); +#pragma pack() + +static struct StructAnonRecordMember_Packed2 retByValue_StructAnonRecordMember_Packed2() { + INIT(StructAnonRecordMember_Packed2, c); + return c; +} + +#pragma clang diagnostic pop diff --git a/kotlin-native/backend.native/tests/interop/basics/structAnonym.kt b/kotlin-native/backend.native/tests/interop/basics/structAnonym.kt new file mode 100644 index 00000000000..65e2fafd345 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/basics/structAnonym.kt @@ -0,0 +1,116 @@ +import kotlinx.cinterop.* +import kotlin.test.* +import structAnonym.* + + +fun test_GLKVector3() { + get_GLKVector3().useContents { + assertEquals(1.0f, x) + assertEquals(2.0f, g) + assertEquals(3.0f, p) + r = 0.1f + g = 0.2f + b = 0.3f + assertEquals(v[0], r) + assertEquals(v[1], g) + assertEquals(v[2], b) + + val ret = hash_GLKVector3(this.readValue()) + assertEquals(s + 2f * t + 4f * p , ret) + } +} + +fun test_StructAnonRecordMember_ImplicitAlignment() { + retByValue_StructAnonRecordMember_ImplicitAlignment() + .useContents { + assertEquals(1, a[0]) + assertEquals(4, a[3]) + assertEquals(42, b) + } +} + +fun test_StructAnonRecordMember_ExplicitAlignment() { + retByValue_StructAnonRecordMember_ExplicitAlignment() + .useContents { + assertEquals('a', a.toInt().toChar()) + assertEquals('x', x.toInt().toChar()) + } +} + +fun test_StructAnonRecordMember_Nested() { + retByValue_StructAnonRecordMember_Nested() + .useContents { + assertEquals(37, x) + assertEquals(42, b) + assertEquals('z', z.toInt().toChar()) + assertEquals(3.14, y) + + a[0] = 3 + a[1] = 5 + assertEquals(3 + 2*5, sendByValue_StructAnonRecordMember_Nested(this.readValue())) + } +} + +fun test_StructAnonym_Complicate() { + retByValue_StructAnonRecordMember_Complicate() + .useContents{ + assertEquals('a', first.toInt().toChar()) + assertEquals('s', second.toInt().toChar()) + assertEquals('z', last.toInt().toChar()) + assertEquals('b', b1.toInt().toChar()) + assertEquals(42L, b2) + assertEquals(3.14F, f) + assertEquals(11L, Y2.b11) + } +} + +fun test_StructAnonym_Packed() { + retByValue_StructAnonRecordMember_Packed2() + .useContents{ + assertEquals('a', first.toInt().toChar()) + assertEquals('s', second.toInt().toChar()) + assertEquals('z', last.toInt().toChar()) + assertEquals('b', b1.toInt().toChar()) + assertEquals(42L, b2) + assertEquals(3.14F, f) + assertEquals(11L, Y2.b11) + } +} + +fun test_StructAnonym_PragmaPacked() { + retByValue_StructAnonRecordMember_PragmaPacked() + .useContents{ + assertEquals('a', first.toInt().toChar()) + assertEquals('s', second.toInt().toChar()) + assertEquals('z', last.toInt().toChar()) + assertEquals('b', b1.toInt().toChar()) + assertEquals(42L, b2) + assertEquals(3.14F, f) + assertEquals(11L, Y2.b11) + } +} + +fun test_StructAnonym_Packed2() { + retByValue_StructAnonRecordMember_Packed2() + .useContents{ + assertEquals('a', first.toInt().toChar()) + assertEquals('s', second.toInt().toChar()) + assertEquals('z', last.toInt().toChar()) + assertEquals('b', b1.toInt().toChar()) + assertEquals(42L, b2) + assertEquals(3.14F, f) + assertEquals(11L, Y2.b11) + } +} + +fun main() { + test_GLKVector3() + test_StructAnonRecordMember_ImplicitAlignment() + test_StructAnonRecordMember_ExplicitAlignment() + test_StructAnonRecordMember_Nested() + + test_StructAnonym_Complicate() + test_StructAnonym_Packed() + test_StructAnonym_PragmaPacked() + test_StructAnonym_Packed2() +}