diff --git a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt index 914ac1f3ca5..9a57285720f 100644 --- a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt +++ b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt @@ -34,6 +34,7 @@ private class StructDefImpl( ) { override val fields = mutableListOf() + override val bitFields = mutableListOf() } private class EnumDefImpl(spelling: String, type: Type) : EnumDef(spelling, type) { @@ -158,7 +159,8 @@ internal class NativeIndexImpl(val library: NativeLibrary) : NativeIndex() { val typeAlign = clang_Type_getAlignOf(clang_getCursorType(fieldCursor)) structDef.fields.add(Field(name, fieldType, offset, typeAlign)) } else { - // Ignore bit fields for now. + val size = clang_getFieldDeclBitWidth(fieldCursor) + structDef.bitFields.add(BitField(name, fieldType, offset, size)) } } else { // Unnamed field. diff --git a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt index 204416ce163..50960d32443 100644 --- a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt +++ b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt @@ -55,6 +55,8 @@ class Field(val name: String, val type: Type, val offset: Long, val typeAlign: L val Field.isAligned: Boolean get() = offset % (typeAlign * 8) == 0L +class BitField(val name: String, val type: Type, val offset: Long, val size: Int) + /** * C struct declaration. */ @@ -74,6 +76,8 @@ abstract class StructDef(val size: Long, val align: Int, val hasNaturalLayout: Boolean) { abstract val fields: List + // TODO: merge two lists to preserve declaration order. + abstract val bitFields: List } /** diff --git a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/NativeMem.kt b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/NativeMem.kt index 43b9210099f..6f5773b3a2b 100644 --- a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/NativeMem.kt +++ b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/NativeMem.kt @@ -99,4 +99,7 @@ object nativeMemUtils { private external fun malloc(size: Long, align: Int): NativePtr @SymbolName("Kotlin_interop_free") -private external fun cfree(ptr: NativePtr) \ No newline at end of file +private external fun cfree(ptr: NativePtr) + +@Intrinsic external fun readBits(ptr: NativePtr, offset: Long, size: Int, signed: Boolean): Long +@Intrinsic external fun writeBits(ptr: NativePtr, offset: Long, size: Int, value: Long) \ No newline at end of file diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/StubGenerator.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/StubGenerator.kt index 02797099ba6..5a0f717f079 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/StubGenerator.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/StubGenerator.kt @@ -303,9 +303,39 @@ class StubGenerator( log("Warning: cannot generate definition for field ${decl.kotlinName}.${field.name}") } } + + if (platform == KotlinPlatform.NATIVE) { + for (field in def.bitFields) { + val typeMirror = mirror(field.type) + val typeInfo = typeMirror.info + val kotlinType = typeMirror.argType + val rawType = typeInfo.bridgedType + + out("var ${field.name.asSimpleName()}: $kotlinType") + + val signed = field.type.getUnderlyingIntegerType().isSigned + + val readBitsExpr = + "readBits(this.rawPtr, ${field.offset}, ${field.size}, $signed).to${rawType.kotlinType}()" + + out(" get() = ${typeInfo.argFromBridged(readBitsExpr)}") + + val rawValue = typeInfo.argToBridged("value") + val setExpr = "writeBits(this.rawPtr, ${field.offset}, ${field.size}, $rawValue.toLong())" + out(" set(value) = $setExpr") + out("") + } + } } } + private tailrec fun Type.getUnderlyingIntegerType(): IntegerType = when (this) { + is IntegerType -> this + is EnumType -> this.def.baseType.getUnderlyingIntegerType() + is Typedef -> this.def.aliased.getUnderlyingIntegerType() + else -> error(this) + } + /** * Produces to [out] the definition of Kotlin class representing the reference to given forward (incomplete) struct. */ diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/InteropUtils.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/InteropUtils.kt index 96d8bf04070..17d3364f8c3 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/InteropUtils.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/InteropUtils.kt @@ -102,6 +102,9 @@ internal class InteropBuiltIns(builtIns: KonanBuiltIns) { val narrow = packageScope.getContributedFunctions("narrow").single() + val readBits = packageScope.getContributedFunctions("readBits").single() + val writeBits = packageScope.getContributedFunctions("writeBits").single() + val objCObject = packageScope.getContributedClassifier("ObjCObject") as ClassDescriptor val objCPointerHolder = packageScope.getContributedClassifier("ObjCPointerHolder") as ClassDescriptor diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt index 1aa9a07dfb4..38f3dc1e70e 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt @@ -345,8 +345,38 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, return LLVMBlockAddress(function, bbLabel)!! } + fun and(arg0: LLVMValueRef, arg1: LLVMValueRef, name: String = ""): LLVMValueRef = LLVMBuildAnd(builder, arg0, arg1, name)!! fun or(arg0: LLVMValueRef, arg1: LLVMValueRef, name: String = ""): LLVMValueRef = LLVMBuildOr(builder, arg0, arg1, name)!! + fun zext(arg: LLVMValueRef, type: LLVMTypeRef): LLVMValueRef = + LLVMBuildZExt(builder, arg, type, "")!! + + fun sext(arg: LLVMValueRef, type: LLVMTypeRef): LLVMValueRef = + LLVMBuildSExt(builder, arg, type, "")!! + + fun ext(arg: LLVMValueRef, type: LLVMTypeRef, signed: Boolean): LLVMValueRef = + if (signed) { + sext(arg, type) + } else { + zext(arg, type) + } + + fun trunc(arg: LLVMValueRef, type: LLVMTypeRef): LLVMValueRef = + LLVMBuildTrunc(builder, arg, type, "")!! + + private fun shift(op: LLVMOpcode, arg: LLVMValueRef, amount: Int) = + if (amount == 0) { + arg + } else { + LLVMBuildBinOp(builder, op, arg, LLVMConstInt(arg.type, amount.toLong(), 0), "")!! + } + + fun shl(arg: LLVMValueRef, amount: Int) = shift(LLVMOpcode.LLVMShl, arg, amount) + + fun shr(arg: LLVMValueRef, amount: Int, signed: Boolean) = + shift(if (signed) LLVMOpcode.LLVMAShr else LLVMOpcode.LLVMLShr, + arg, amount) + /* integers comparisons */ fun icmpEq(arg0: LLVMValueRef, arg1: LLVMValueRef, name: String = ""): LLVMValueRef = LLVMBuildICmp(builder, LLVMIntPredicate.LLVMIntEQ, arg0, arg1, name)!! diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index f7a338b9848..2a6bee18223 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -1991,10 +1991,103 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map genReadBits(args) + interop.writeBits -> genWriteBits(args) + else -> TODO(callee.descriptor.original.toString()) } } + private fun genReadBits(args: List): LLVMValueRef { + val ptr = args[0] + assert(ptr.type == int8TypePtr) + + val offset = extractConstUnsignedInt(args[1]) + val size = extractConstUnsignedInt(args[2]).toInt() + val signed = extractConstUnsignedInt(args[3]) != 0L + + val prefixBitsNum = (offset % 8).toInt() + val suffixBitsNum = (8 - ((size + offset) % 8).toInt()) % 8 + + // Note: LLVM allows to read without padding tail up to byte boundary, but the result seems to be incorrect. + + val bitsWithPaddingNum = prefixBitsNum + size + suffixBitsNum + val bitsWithPaddingType = LLVMIntType(bitsWithPaddingNum)!! + + with (functionGenerationContext) { + val bitsWithPaddingPtr = bitcast(pointerType(bitsWithPaddingType), gep(ptr, Int64(offset / 8).llvm)) + val bitsWithPadding = load(bitsWithPaddingPtr).setUnaligned() + + val bits = shr( + shl(bitsWithPadding, suffixBitsNum), + prefixBitsNum + suffixBitsNum, signed + ) + + return if (bitsWithPaddingNum == 64) { + bits + } else if (bitsWithPaddingNum > 64) { + trunc(bits, kInt64) + } else { + ext(bits, kInt64, signed) + } + } + } + + private fun genWriteBits(args: List): LLVMValueRef { + val ptr = args[0] + assert(ptr.type == int8TypePtr) + + val offset = extractConstUnsignedInt(args[1]) + val size = extractConstUnsignedInt(args[2]).toInt() + + val value = args[3] + assert(value.type == kInt64) + + val bitsType = LLVMIntType(size)!! + + val prefixBitsNum = (offset % 8).toInt() + val suffixBitsNum = (8 - ((size + offset) % 8).toInt()) % 8 + + val bitsWithPaddingNum = prefixBitsNum + size + suffixBitsNum + val bitsWithPaddingType = LLVMIntType(bitsWithPaddingNum)!! + + // 0011111000: + val discardBitsMask = LLVMConstShl( + LLVMConstZExt( + LLVMConstAllOnes(bitsType), // 11111 + bitsWithPaddingType + ), // 1111100000 + LLVMConstInt(bitsWithPaddingType, prefixBitsNum.toLong(), 0) + ) + + val preservedBitsMask = LLVMConstNot(discardBitsMask)!! + + with (functionGenerationContext) { + val bitsWithPaddingPtr = bitcast(pointerType(bitsWithPaddingType), gep(ptr, Int64(offset / 8).llvm)) + + val bits = trunc(value, bitsType) + + val bitsToStore = if (prefixBitsNum == 0 && suffixBitsNum == 0) { + bits + } else { + val previousValue = load(bitsWithPaddingPtr).setUnaligned() + val preservedBits = and(previousValue, preservedBitsMask) + val bitsWithPadding = shl(zext(bits, bitsWithPaddingType), prefixBitsNum) + + or(bitsWithPadding, preservedBits) + } + + LLVMBuildStore(builder, bitsToStore, bitsWithPaddingPtr)!!.setUnaligned() + } + + return codegen.theUnitInstanceRef.llvm + } + + private fun extractConstUnsignedInt(value: LLVMValueRef): Long { + assert(LLVMIsConstant(value) != 0) + return LLVMConstIntGetZExtValue(value) + } + private fun genGetObjCClass(classDescriptor: ClassDescriptor): LLVMValueRef { assert(!classDescriptor.isInterface) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt index a602931b557..f0d58d77f4f 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/LlvmUtils.kt @@ -126,6 +126,7 @@ internal val RuntimeAware.kTypeInfoPtr: LLVMTypeRef get() = pointerType(kTypeInfo) internal val kInt1 = LLVMInt1Type()!! internal val kBoolean = kInt1 +internal val kInt64 = LLVMInt64Type()!! internal val kInt8Ptr = pointerType(int8Type) internal val kInt8PtrPtr = pointerType(kInt8Ptr) internal val kNullInt8Ptr = LLVMConstNull(kInt8Ptr)!! @@ -258,3 +259,5 @@ fun parseBitcodeFile(path: String): LLVMModuleRef = memScoped { internal fun String.mdString() = LLVMMDString(this, this.length)!! internal fun node(vararg it:LLVMValueRef) = LLVMMDNode(it.toList().toCValues(), it.size) + +internal fun LLVMValueRef.setUnaligned() = apply { LLVMSetAlignment(this, 1) } diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index a83be98a576..ee9a1d634ce 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -1989,6 +1989,11 @@ kotlinNativeInterop { flavor 'native' } + bitfields { + defFile 'interop/basics/bitfields.def' + flavor 'native' + } + if (isMac()) { opengl { defFile '../../samples/opengl/src/main/c_interop/opengl.def' @@ -2046,6 +2051,12 @@ task interop4(type: RunInteropKonanTest) { interop = 'cstdio' } +task interop_bitfields(type: RunInteropKonanTest) { + disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. + source = "interop/basics/bf.kt" + interop = 'bitfields' +} + task interop_echo_server(type: RunInteropKonanTest) { disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. if (!isMac()) { @@ -2066,6 +2077,7 @@ if (isMac()) { } task interop_objc_smoke(type: RunInteropKonanTest) { + disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. goldValue = "Hello, World!\nKotlin says: Hello, everybody!\nHello from Kotlin\n2, 1\nDeallocated\nDeallocated\n" source = "interop/objc/smoke.kt" diff --git a/backend.native/tests/interop/basics/bf.kt b/backend.native/tests/interop/basics/bf.kt new file mode 100644 index 00000000000..c8456097819 --- /dev/null +++ b/backend.native/tests/interop/basics/bf.kt @@ -0,0 +1,74 @@ +import bitfields.* +import kotlinx.cinterop.* + +fun assertEquals(value1: Any?, value2: Any?) { + if (value1 != value2) + throw AssertionError("Expected $value1, got $value2") +} + +fun check(s: S, x1: Long, x2: B2, x3: Short, x4: Int, x5: Int, x6: Long) { + assertEquals(x1, s.x1) + assertEquals(x1, getX1(s.ptr)) + + assertEquals(x2, s.x2) + assertEquals(x2, getX2(s.ptr)) + + assertEquals(x3, s.x3) + assertEquals(x3, getX3(s.ptr)) + + assertEquals(x4, s.x4) + assertEquals(x4, getX4(s.ptr)) + + assertEquals(x5, s.x5) + assertEquals(x5, getX5(s.ptr)) + + assertEquals(x6, s.x6) + assertEquals(x6, getX6(s.ptr)) +} + +fun assign(s: S, x1: Long, x2: B2, x3: Short, x4: Int, x5: Int, x6: Long) { + s.x1 = x1 + s.x2 = x2 + s.x3 = x3 + s.x4 = x4 + s.x5 = x5 + s.x6 = x6 +} + +fun assignReversed(s: S, x1: Long, x2: B2, x3: Short, x4: Int, x5: Int, x6: Long) { + s.x6 = x6 + s.x5 = x5 + s.x4 = x4 + s.x3 = x3 + s.x2 = x2 + s.x1 = x1 +} + +fun test(s: S, x1: Long, x2: B2, x3: Short, x4: Int, x5: Int, x6: Long) { + assign(s, x1, x2, x3, x4, x5, x6) + check(s, x1, x2, x3, x4, x5, x6) + + assignReversed(s, x1, x2, x3, x4, x5, x6) + check(s, x1, x2, x3, x4, x5, x6) + + // Also check with some insignificant bits modified: + + assign(s, x1 + 2, x2, (x3 + 8).toShort(), x4 - 16, x5 + 32, x6 + Long.MIN_VALUE) + check(s, x1, x2, x3, x4, x5, x6) + + assignReversed(s, x1 + 2, x2, (x3 + 8).toShort(), x4 - 16, x5 + 32, x6 + Long.MIN_VALUE) + check(s, x1, x2, x3, x4, x5, x6) +} + +fun main(args: Array) { + memScoped { + val s = alloc() + for (x1 in -1L..0L) + for (x2 in B2.values()) + for (x3 in 0..7) + for (x4 in intArrayOf(0, 6, 15)) + for (x5 in intArrayOf(-16, -2, -1, 0, 5, 15)) + for (x6 in longArrayOf(Long.MIN_VALUE/2, -1L shl 36, -325L, 0, 1L shl 48, Long.MAX_VALUE/2)) + test(s, x1, x2, x3.toShort(), x4, x5, x6) + } +} diff --git a/backend.native/tests/interop/basics/bitfields.def b/backend.native/tests/interop/basics/bitfields.def new file mode 100644 index 00000000000..11bf1ca4b5c --- /dev/null +++ b/backend.native/tests/interop/basics/bitfields.def @@ -0,0 +1,20 @@ +--- +enum B2 { + ZERO, ONE, TWO, THREE +}; + +struct __attribute__((packed)) S { + long long x1 : 1; + enum B2 x2 : 2; + unsigned short x3 : 3; + unsigned int x4 : 4; + int x5 : 5; + long long x6 : 63; +}; + +static long long getX1(struct S* s) { return s->x1; } +static enum B2 getX2(struct S* s) { return s->x2; } +static unsigned short getX3(struct S* s) { return s->x3; } +static unsigned int getX4(struct S* s) { return s->x4; } +static int getX5(struct S* s) { return s->x5; } +static long long getX6(struct S* s) { return s->x6; }