diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs index dcd3a3ce..db9dd758 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs @@ -1128,6 +1128,12 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record var currentSize = fieldDecl.Type.Handle.SizeOf; var bitfieldName = "_bitfield"; + // A `bool` bitfield cannot be shifted or masked directly and needs an integer + // backing store. It is emitted using the same unsigned-integer path as the + // equivalently-sized integer type, with the public accessor kept as `bool` and + // the conversion done at the get/set boundary. + var isBooleanBitfield = fieldDecl.Type.CanonicalType.Kind == CXType_Bool; + Type typeBacking; string typeNameBacking; @@ -1149,6 +1155,11 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record typeBacking = bitfieldDesc.TypeBacking; typeNameBacking = GetRemappedTypeName(fieldDecl, context: null, typeBacking, out _); + if (isBooleanBitfield) + { + typeNameBacking = "byte"; + } + if (parent == recordDecl) { var fieldDesc = new FieldDesc { @@ -1201,6 +1212,11 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record var bitfieldDesc = (index > 0) ? bitfieldDescs[index - 1] : bitfieldDescs[0]; typeBacking = bitfieldDesc.TypeBacking; typeNameBacking = GetRemappedTypeName(fieldDecl, context: null, typeBacking, out _); + + if (isBooleanBitfield) + { + typeNameBacking = "byte"; + } } var bitfieldOffset = (currentSize * 8) - remainingBits; @@ -1216,6 +1232,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record switch (builtinTypeBacking.Kind) { + case CXType_Bool: case CXType_Char_U: case CXType_UChar: case CXType_UShort: @@ -1315,6 +1332,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record switch (builtinType.Kind) { + case CXType_Bool: case CXType_Char_U: case CXType_UChar: case CXType_UShort: @@ -1389,6 +1407,16 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record var name = GetRemappedCursorName(fieldDecl); var escapedName = EscapeName(name); + // The public accessor keeps the original type (e.g. `bool`) while the shift/mask + // arithmetic is done against the integer backing type; the two only differ for a + // `bool` bitfield, which is converted at the get/set boundary below. + var fieldTypeName = typeName; + + if (isBooleanBitfield) + { + typeName = typeNameBacking; + } + var desc = new FieldDesc { AccessSpecifier = accessSpecifier, NativeTypeName = nativeTypeName, @@ -1409,7 +1437,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record _outputBuilder.WriteDivider(); _outputBuilder.BeginField(in desc); - _outputBuilder.WriteRegularField(typeName, escapedName); + _outputBuilder.WriteRegularField(fieldTypeName, escapedName); _outputBuilder.BeginBody(); var recordDeclName = GetCursorName(recordDecl); @@ -1461,7 +1489,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record code.Write(")("); } - if (needsCastToFinal) + if (needsCastToFinal && !isBooleanBitfield) { code.Write('('); code.BeginMarker("typeName"); @@ -1565,6 +1593,11 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record code.Write(')'); } + if (isBooleanBitfield) + { + code.Write(" != 0"); + } + code.WriteSemicolon(); code.WriteNewline(); _outputBuilder.EndCSharpCode(code); @@ -1658,6 +1691,10 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record code.Write(typeNameBacking); code.Write(")(value)"); } + else if (isBooleanBitfield) + { + code.Write("(value ? 1 : 0)"); + } else { code.Write("value"); diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/BooleanBitfieldTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/BooleanBitfieldTest.cs new file mode 100644 index 00000000..9f9561df --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/BooleanBitfieldTest.cs @@ -0,0 +1,140 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System.Threading.Tasks; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +public sealed class BooleanBitfieldTest : PInvokeGeneratorTest +{ + // Regression test for https://github.com/dotnet/clangsharp/issues/508 + // A `bool` bitfield previously generated invalid C# (a `bool` backing field plus + // `bool & int` arithmetic and `(bool)` casts). It should use an integer backing store + // with the public accessor kept as `bool`, converting at the get/set boundary. An + // adjacent non-bool bitfield must be unaffected. + private const string InputContents = @"struct MyStruct +{ + bool a : 1; + bool b : 1; + int c : 2; +}; +"; + + [Test] + public Task LatestTest() + { + var expectedOutputContents = @"namespace ClangSharp.Test +{ + public partial struct MyStruct + { + public byte _bitfield1; + + [NativeTypeName(""bool : 1"")] + public bool a + { + readonly get + { + return (_bitfield1 & 0x1u) != 0; + } + + set + { + _bitfield1 = (byte)((_bitfield1 & ~0x1u) | ((value ? 1 : 0) & 0x1u)); + } + } + + [NativeTypeName(""bool : 1"")] + public bool b + { + readonly get + { + return ((_bitfield1 >> 1) & 0x1u) != 0; + } + + set + { + _bitfield1 = (byte)((_bitfield1 & ~(0x1u << 1)) | (((value ? 1 : 0) & 0x1u) << 1)); + } + } + + public int _bitfield2; + + [NativeTypeName(""int : 2"")] + public int c + { + readonly get + { + return (_bitfield2 << 30) >> 30; + } + + set + { + _bitfield2 = (_bitfield2 & ~0x3) | (value & 0x3); + } + } + } +} +"; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(InputContents, expectedOutputContents); + } + + [Test] + public Task CompatibleTest() + { + var expectedOutputContents = @"namespace ClangSharp.Test +{ + public partial struct MyStruct + { + public byte _bitfield1; + + [NativeTypeName(""bool : 1"")] + public bool a + { + get + { + return (_bitfield1 & 0x1u) != 0; + } + + set + { + _bitfield1 = (byte)((_bitfield1 & ~0x1u) | ((value ? 1 : 0) & 0x1u)); + } + } + + [NativeTypeName(""bool : 1"")] + public bool b + { + get + { + return ((_bitfield1 >> 1) & 0x1u) != 0; + } + + set + { + _bitfield1 = (byte)((_bitfield1 & ~(0x1u << 1)) | (((value ? 1 : 0) & 0x1u) << 1)); + } + } + + public int _bitfield2; + + [NativeTypeName(""int : 2"")] + public int c + { + get + { + return (_bitfield2 << 30) >> 30; + } + + set + { + _bitfield2 = (_bitfield2 & ~0x3) | (value & 0x3); + } + } + } +} +"; + + return ValidateGeneratedCSharpCompatibleWindowsBindingsAsync(InputContents, expectedOutputContents); + } +}