diff --git a/InteropGenerator.Runtime/Attributes/BitFieldAttribute.cs b/InteropGenerator.Runtime/Attributes/BitFieldAttribute.cs new file mode 100644 index 0000000000..2b9eece3c4 --- /dev/null +++ b/InteropGenerator.Runtime/Attributes/BitFieldAttribute.cs @@ -0,0 +1,8 @@ +namespace InteropGenerator.Runtime.Attributes; + +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +public sealed class BitFieldAttribute(string name, int index, int length = 1) : Attribute { + public string Name { get; } = name; + public int Index { get; } = index; + public int Length { get; } = length; +} diff --git a/InteropGenerator.Runtime/BitOps.cs b/InteropGenerator.Runtime/BitOps.cs new file mode 100644 index 0000000000..ed03280d35 --- /dev/null +++ b/InteropGenerator.Runtime/BitOps.cs @@ -0,0 +1,43 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace InteropGenerator.Runtime; + +public static class BitOps { + /// + /// Creates a mask with number of set bits starting from the LSB.
+ /// Example: CreateLowBitMask<byte>(3) results in 0b0000_0111 + ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T CreateLowBitMask(int length) + where T : unmanaged, IBinaryInteger { + if (length <= 0) return T.Zero; + if (length >= Unsafe.SizeOf() * 8) return T.AllBitsSet; + return (T.One << length) - T.One; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetBit(T value, int index) + where T : unmanaged, IBinaryInteger { + return ((value >> index) & T.One) != T.Zero; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T SetBit(T value, int index, bool enable) + where T : unmanaged, IBinaryInteger { + T mask = T.One << index; + return enable ? value | mask : value & ~mask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetBits(T value, int shift, T mask) + where T : unmanaged, IBinaryInteger { + return (value >> shift) & mask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T SetBits(T value, int shift, T mask, T newValue) + where T : unmanaged, IBinaryInteger { + return (value & ~(mask << shift)) | ((newValue & mask) << shift); + } +} diff --git a/InteropGenerator.Tests/Analyzer/BitFieldAttributeIsValidAnalyzerTests.cs b/InteropGenerator.Tests/Analyzer/BitFieldAttributeIsValidAnalyzerTests.cs new file mode 100644 index 0000000000..a0c3974353 --- /dev/null +++ b/InteropGenerator.Tests/Analyzer/BitFieldAttributeIsValidAnalyzerTests.cs @@ -0,0 +1,164 @@ +using InteropGenerator.Diagnostics.Analyzers; +using InteropGenerator.Tests.Helpers; +using Xunit; + +namespace InteropGenerator.Tests.Analyzer; + +public class BitFieldAttributeIsValidAnalyzerTests { + [Fact] + public async Task BitFieldAttributeIsValid_NoWarn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=30)] + [GenerateInterop] + public partial struct TestStruct + { + [BitField("TestBool1", 0)] + [BitField("TestByte1", 1)] + [BitField("TestByte2", 2, 3)] + [BitField("TestShort1", 5, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + + [BitField("TestBool2", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal ushort _testField2; + + [BitField("TestBool3", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal uint _testField3; + + [BitField("TestBool4", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal ulong _testField4; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeStartBitNegative_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [{|CSIG0401:BitField("TestShort", -1, 1)|}] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeLengthNegative_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [{|CSIG0402:BitField("TestShort", 0, -1)|}] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeLengthZero_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [{|CSIG0402:BitField("TestShort", 0, 0)|}] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeExceedsFieldSize_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [{|CSIG0403:BitField("TestShort", 0, 9)|}] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeEnumDoesNotExceedFieldSize_NoWarn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [BitField("TestEnum1", 2, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeInvalidBackingFieldType_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [BitField("TestBool1", 0)] + [BitField("TestByte1", 1)] + [BitField("TestByte2", 2, 3)] + [BitField("TestShort1", 5, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal sbyte {|CSIG0405:_testField1|}; + + [BitField("TestBool2", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal short {|CSIG0405:_testField2|}; + + [BitField("TestBool3", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal int {|CSIG0405:_testField3|}; + + [BitField("TestBool4", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal long {|CSIG0405:_testField4|}; + + [BitField("TestBool5", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal TestEnum {|CSIG0405:_testField5|}; + + [BitField("TestEnum1", 0, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal TestEnum {|CSIG0405:_testField6|}; + + [BitField("TestBool6", 0)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal DateTime {|CSIG0405:_testField7|}; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } + + [Fact] + public async Task BitFieldAttributeInvalidBooleanLength_Warn() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=1)] + [GenerateInterop] + public partial struct TestStruct + { + [{|CSIG0406:BitField("TestBool1", 0, 2)|}] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + } + """; + await AnalyzerVerifier.VerifyAnalyzerAsync(code); + } +} diff --git a/InteropGenerator.Tests/Generator/BitFieldAttributeTests.cs b/InteropGenerator.Tests/Generator/BitFieldAttributeTests.cs new file mode 100644 index 0000000000..c881beaea0 --- /dev/null +++ b/InteropGenerator.Tests/Generator/BitFieldAttributeTests.cs @@ -0,0 +1,161 @@ +using Xunit; +using VerifyIG = InteropGenerator.Tests.Helpers.IncrementalGeneratorVerifier; + +namespace InteropGenerator.Tests.Generator; + +public partial class BitFieldAttributeTests { + [Fact] + public async Task GenerateBitField() { + const string code = """ + [GenerateInterop] + public partial struct TestStruct + { + [BitField(nameof(TestBool1), 0)] + [BitField(nameof(TestBool2), 1)] + [BitField(nameof(TestEnum1), 2, 2)] + internal byte _testField1; + + [BitField(nameof(TestBool3), 0)] + [BitField(nameof(TestShort1), 2, 16)] + [BitField(nameof(TestShort2), 18, 4)] + [BitField(nameof(TestEnum2), 24, 2)] + internal uint _testField2; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + } + """; + + const string result = """ + // + unsafe partial struct TestStruct + { + public bool TestBool1 + { + get => BitOps.GetBit(_testField1, 0); + set => _testField1 = BitOps.SetBit(_testField1, 0, value); + } + public bool TestBool2 + { + get => BitOps.GetBit(_testField1, 1); + set => _testField1 = BitOps.SetBit(_testField1, 1, value); + } + public global::TestStruct.TestEnum TestEnum1 + { + get => (global::TestStruct.TestEnum)BitOps.GetBits(_testField1, 2, BitOps.CreateLowBitMask(2)); + set => _testField1 = BitOps.SetBits(_testField1, 2, BitOps.CreateLowBitMask(2), (byte)value); + } + public bool TestBool3 + { + get => BitOps.GetBit(_testField2, 0); + set => _testField2 = BitOps.SetBit(_testField2, 0, value); + } + public ushort TestShort1 + { + get => (ushort)BitOps.GetBits(_testField2, 2, BitOps.CreateLowBitMask(16)); + set => _testField2 = BitOps.SetBits(_testField2, 2, BitOps.CreateLowBitMask(16), (uint)value); + } + public ushort TestShort2 + { + get => (ushort)BitOps.GetBits(_testField2, 18, BitOps.CreateLowBitMask(4)); + set => _testField2 = BitOps.SetBits(_testField2, 18, BitOps.CreateLowBitMask(4), (uint)value); + } + public global::TestStruct.TestEnum TestEnum2 + { + get => (global::TestStruct.TestEnum)BitOps.GetBits(_testField2, 24, BitOps.CreateLowBitMask(2)); + set => _testField2 = BitOps.SetBits(_testField2, 24, BitOps.CreateLowBitMask(2), (uint)value); + } + } + """; + + await VerifyIG.VerifyGeneratorAsync( + code, + ("TestStruct.InteropGenerator.g.cs", result)); + } + + [Fact] + public async Task GenerateBitFieldWithDefinition() { + const string code = """ + [GenerateInterop] + public partial struct TestStruct + { + [BitField(nameof(TestBool1), 0)] + [BitField(nameof(TestBool2), 1)] + [BitField(nameof(TestEnum1), 2, 2)] + internal byte _testField1; + + [BitField(nameof(TestBool3), 0)] + [BitField(nameof(TestShort1), 2, 16)] + [BitField(nameof(TestShort2), 18, 4)] + [BitField(nameof(TestEnum2), 24, 2)] + internal uint _testField2; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + + public partial bool TestBool1 { get; set; } + + public partial bool TestBool2 { get; } + + public partial TestEnum TestEnum1 { get; set; } + + public partial bool TestBool3 { get; set; } + + public partial ushort TestShort1 { get; set; } + + public partial ushort TestShort2 { get; } + + public partial TestEnum TestEnum2 { get; } + } + """; + + const string result = """ + // + unsafe partial struct TestStruct + { + public partial bool TestBool1 + { + get => BitOps.GetBit(_testField1, 0); + set => _testField1 = BitOps.SetBit(_testField1, 0, value); + } + public partial bool TestBool2 + { + get => BitOps.GetBit(_testField1, 1); + } + public partial global::TestStruct.TestEnum TestEnum1 + { + get => (global::TestStruct.TestEnum)BitOps.GetBits(_testField1, 2, BitOps.CreateLowBitMask(2)); + set => _testField1 = BitOps.SetBits(_testField1, 2, BitOps.CreateLowBitMask(2), (byte)value); + } + public partial bool TestBool3 + { + get => BitOps.GetBit(_testField2, 0); + set => _testField2 = BitOps.SetBit(_testField2, 0, value); + } + public partial ushort TestShort1 + { + get => (ushort)BitOps.GetBits(_testField2, 2, BitOps.CreateLowBitMask(16)); + set => _testField2 = BitOps.SetBits(_testField2, 2, BitOps.CreateLowBitMask(16), (uint)value); + } + public partial ushort TestShort2 + { + get => (ushort)BitOps.GetBits(_testField2, 18, BitOps.CreateLowBitMask(4)); + } + public partial global::TestStruct.TestEnum TestEnum2 + { + get => (global::TestStruct.TestEnum)BitOps.GetBits(_testField2, 24, BitOps.CreateLowBitMask(2)); + } + } + """; + + await VerifyIG.VerifyGeneratorAsync( + code, + ("TestStruct.InteropGenerator.g.cs", result)); + } +} diff --git a/InteropGenerator.Tests/Generator/InheritsAttributeTests.cs b/InteropGenerator.Tests/Generator/InheritsAttributeTests.cs index 3792646621..a08531e569 100644 --- a/InteropGenerator.Tests/Generator/InheritsAttributeTests.cs +++ b/InteropGenerator.Tests/Generator/InheritsAttributeTests.cs @@ -1654,6 +1654,294 @@ await VerifyIG.VerifyGeneratorAsync( ("ChildStruct.Inheritance.InteropGenerator.g.cs", childStructInheritanceCode)); } + [Fact] + public async Task BitFieldInheritance() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayoutAttribute(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=5)] + [GenerateInterop(true)] + public partial struct BaseStruct + { + [BitField(nameof(TestBool1), 0)] + [BitField(nameof(TestBool2), 1)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + + [BitField(nameof(TestBool3), 0)] + [BitField(nameof(TestShort1), 2, 16)] + [BitField(nameof(TestShort2), 18, 4)] + [BitField(nameof(TestEnum1), 22, 2)] + [BitField(nameof(TestEnum2), 24, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(1)] internal uint _testField2; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + } + + [global::System.Runtime.InteropServices.StructLayoutAttribute(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=8)] + [GenerateInterop] + [Inherits] + public partial struct ChildStruct + { + } + """; + + const string baseStructCode = """ + // + unsafe partial struct BaseStruct + { + public const int StructSize = 5; + public bool TestBool1 + { + get => BitOps.GetBit(_testField1, 0); + set => _testField1 = BitOps.SetBit(_testField1, 0, value); + } + public bool TestBool2 + { + get => BitOps.GetBit(_testField1, 1); + set => _testField1 = BitOps.SetBit(_testField1, 1, value); + } + public bool TestBool3 + { + get => BitOps.GetBit(_testField2, 0); + set => _testField2 = BitOps.SetBit(_testField2, 0, value); + } + public ushort TestShort1 + { + get => (ushort)BitOps.GetBits(_testField2, 2, BitOps.CreateLowBitMask(16)); + set => _testField2 = BitOps.SetBits(_testField2, 2, BitOps.CreateLowBitMask(16), (uint)value); + } + public ushort TestShort2 + { + get => (ushort)BitOps.GetBits(_testField2, 18, BitOps.CreateLowBitMask(4)); + set => _testField2 = BitOps.SetBits(_testField2, 18, BitOps.CreateLowBitMask(4), (uint)value); + } + public global::BaseStruct.TestEnum TestEnum1 + { + get => (global::BaseStruct.TestEnum)BitOps.GetBits(_testField2, 22, BitOps.CreateLowBitMask(2)); + set => _testField2 = BitOps.SetBits(_testField2, 22, BitOps.CreateLowBitMask(2), (uint)value); + } + public global::BaseStruct.TestEnum TestEnum2 + { + get => (global::BaseStruct.TestEnum)BitOps.GetBits(_testField2, 24, BitOps.CreateLowBitMask(2)); + set => _testField2 = BitOps.SetBits(_testField2, 24, BitOps.CreateLowBitMask(2), (uint)value); + } + } + """; + + const string childStructInheritanceCode = """ + // + unsafe partial struct ChildStruct + { + /// Inherited parent class accessor for BaseStruct + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] public BaseStruct BaseStruct; + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool1 + { + get => BaseStruct.TestBool1; + set => BaseStruct.TestBool1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool2 + { + get => BaseStruct.TestBool2; + set => BaseStruct.TestBool2 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool3 + { + get => BaseStruct.TestBool3; + set => BaseStruct.TestBool3 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public ushort TestShort1 + { + get => BaseStruct.TestShort1; + set => BaseStruct.TestShort1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public ushort TestShort2 + { + get => BaseStruct.TestShort2; + set => BaseStruct.TestShort2 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public global::BaseStruct.TestEnum TestEnum1 + { + get => BaseStruct.TestEnum1; + set => BaseStruct.TestEnum1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public global::BaseStruct.TestEnum TestEnum2 + { + get => BaseStruct.TestEnum2; + set => BaseStruct.TestEnum2 = value; + } + } + """; + + await VerifyIG.VerifyGeneratorAsync( + code, + ("BaseStruct.InteropGenerator.g.cs", baseStructCode), + ("ChildStruct.Inheritance.InteropGenerator.g.cs", childStructInheritanceCode)); + } + + [Fact] + public async Task BitFieldWithDefinitionInheritance() { + const string code = """ + [global::System.Runtime.InteropServices.StructLayoutAttribute(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=5)] + [GenerateInterop(true)] + public partial struct BaseStruct + { + [BitField(nameof(TestBool1), 0)] + [BitField(nameof(TestBool2), 1)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] internal byte _testField1; + + [BitField(nameof(TestBool3), 0)] + [BitField(nameof(TestShort1), 2, 16)] + [BitField(nameof(TestShort2), 18, 4)] + [BitField(nameof(TestEnum1), 22, 2)] + [BitField(nameof(TestEnum2), 24, 2)] + [global::System.Runtime.InteropServices.FieldOffsetAttribute(1)] internal uint _testField2; + + [Flags] + public enum TestEnum { + TestValue1 = 1 << 0, + TestValue2 = 1 << 1, + } + + public partial bool TestBool1 { get; set; } + + public partial bool TestBool2 { get; } + + public partial bool TestBool3 { get; set; } + + public partial ushort TestShort1 { get; set; } + + public partial ushort TestShort2 { get; } + + public partial TestEnum TestEnum1 { get; set; } + + public partial TestEnum TestEnum2 { get; } + } + + [global::System.Runtime.InteropServices.StructLayoutAttribute(global::System.Runtime.InteropServices.LayoutKind.Explicit, Size=8)] + [GenerateInterop] + [Inherits] + public partial struct ChildStruct + { + } + """; + + const string baseStructCode = """ + // + unsafe partial struct BaseStruct + { + public const int StructSize = 5; + public partial bool TestBool1 + { + get => BitOps.GetBit(_testField1, 0); + set => _testField1 = BitOps.SetBit(_testField1, 0, value); + } + public partial bool TestBool2 + { + get => BitOps.GetBit(_testField1, 1); + } + public partial bool TestBool3 + { + get => BitOps.GetBit(_testField2, 0); + set => _testField2 = BitOps.SetBit(_testField2, 0, value); + } + public partial ushort TestShort1 + { + get => (ushort)BitOps.GetBits(_testField2, 2, BitOps.CreateLowBitMask(16)); + set => _testField2 = BitOps.SetBits(_testField2, 2, BitOps.CreateLowBitMask(16), (uint)value); + } + public partial ushort TestShort2 + { + get => (ushort)BitOps.GetBits(_testField2, 18, BitOps.CreateLowBitMask(4)); + } + public partial global::BaseStruct.TestEnum TestEnum1 + { + get => (global::BaseStruct.TestEnum)BitOps.GetBits(_testField2, 22, BitOps.CreateLowBitMask(2)); + set => _testField2 = BitOps.SetBits(_testField2, 22, BitOps.CreateLowBitMask(2), (uint)value); + } + public partial global::BaseStruct.TestEnum TestEnum2 + { + get => (global::BaseStruct.TestEnum)BitOps.GetBits(_testField2, 24, BitOps.CreateLowBitMask(2)); + } + } + """; + + const string childStructInheritanceCode = """ + // + unsafe partial struct ChildStruct + { + /// Inherited parent class accessor for BaseStruct + [global::System.Runtime.InteropServices.FieldOffsetAttribute(0)] public BaseStruct BaseStruct; + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool1 + { + get => BaseStruct.TestBool1; + set => BaseStruct.TestBool1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool2 + { + get => BaseStruct.TestBool2; + } + /// + /// Property inherited from parent class BaseStruct. + public bool TestBool3 + { + get => BaseStruct.TestBool3; + set => BaseStruct.TestBool3 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public ushort TestShort1 + { + get => BaseStruct.TestShort1; + set => BaseStruct.TestShort1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public ushort TestShort2 + { + get => BaseStruct.TestShort2; + } + /// + /// Property inherited from parent class BaseStruct. + public global::BaseStruct.TestEnum TestEnum1 + { + get => BaseStruct.TestEnum1; + set => BaseStruct.TestEnum1 = value; + } + /// + /// Property inherited from parent class BaseStruct. + public global::BaseStruct.TestEnum TestEnum2 + { + get => BaseStruct.TestEnum2; + } + } + """; + + await VerifyIG.VerifyGeneratorAsync( + code, + ("BaseStruct.InteropGenerator.g.cs", baseStructCode), + ("ChildStruct.Inheritance.InteropGenerator.g.cs", childStructInheritanceCode)); + } + [Fact] public async Task StringOverloadsInheritance() { const string code = """ diff --git a/InteropGenerator/AnalyzerReleases.Shipped.md b/InteropGenerator/AnalyzerReleases.Shipped.md index 3ad45e0480..a89547894d 100644 --- a/InteropGenerator/AnalyzerReleases.Shipped.md +++ b/InteropGenerator/AnalyzerReleases.Shipped.md @@ -1,4 +1,4 @@ -; Shipped analyzer releases +; Shipped analyzer releases ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md | Rule ID | Category | Severity | Notes | @@ -25,4 +25,13 @@ | CSIG0301 | InteropGenerator.Field | Error | Field marked for fixed size array generation must be internal | | CSIG0302 | InteropGenerator.Field | Error | Field marked for fixed size array generation must have a proper type name (FixedSizeArray#\) | | CSIG0303 | InteropGenerator.Field | Error | Field marked for fixed size array generation must have a proper filed name | -| CSIG0304 | InteropGenerotor.Field | Error | Field marked for fixed size array generation with isString set to true must use byte or char as the underlying type | \ No newline at end of file +| CSIG0304 | InteropGenerotor.Field | Error | Field marked for fixed size array generation with isString set to true must use byte or char as the underlying type | +| CSIG0305 | InteropGenerator.Field | Error | Fixed size bit array backing field must be byte type | +| CSIG0306 | InteropGenerator.Field | Error | Fixed size bit array backing field must have a size that fits the bit count | +| CSIG0307 | InteropGenerator.Field | Error | Fixed size array cannot be both string and bit array | +| CSIG0401 | InteropGenerator.Field | Error | Bit Field index must be non-negative | +| CSIG0402 | InteropGenerator.Field | Error | Bit Field length must be greater than zero | +| CSIG0403 | InteropGenerator.Field | Error | Bit Field exceeds underlying type size | +| CSIG0404 | InteropGenerator.Property | Error | Bit Field property definition is missing a getter | +| CSIG0405 | InteropGenerator.Field | Error | Bit Field backing field type not supported | +| CSIG0406 | InteropGenerator.Field | Error | Bit Field length is invalid for bool | \ No newline at end of file diff --git a/InteropGenerator/Diagnostics/Analyzers/BitFieldAttributeIsValidAnalyzer.cs b/InteropGenerator/Diagnostics/Analyzers/BitFieldAttributeIsValidAnalyzer.cs new file mode 100644 index 0000000000..2c649b1853 --- /dev/null +++ b/InteropGenerator/Diagnostics/Analyzers/BitFieldAttributeIsValidAnalyzer.cs @@ -0,0 +1,127 @@ +using System.Collections.Immutable; +using InteropGenerator.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using static InteropGenerator.Diagnostics.DiagnosticDescriptors; + +namespace InteropGenerator.Diagnostics.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class BitFieldAttributeIsValidAnalyzer : DiagnosticAnalyzer { + + public override ImmutableArray SupportedDiagnostics { get; } = [ + BitFieldIndexIsNegative, + BitFieldLengthIsNegativeOrZero, + BitFieldExceedsUnderlyingFieldSize, + BitFieldPropertyDefinitionMissingGetter, + BitFieldBackingFieldTypeNotSupported, + BitFieldBooleanLengthInvalid, + ]; + + public override void Initialize(AnalysisContext context) { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => { + // get the attribute symbol + if (context.Compilation.GetTypeByMetadataName(InteropTypeNames.BitFieldAttribute) is not { } bitFieldAttribute) + return; + + context.RegisterSymbolAction(context => { + if (context.Symbol is not IFieldSymbol fieldSymbol) + return; + + if (fieldSymbol.ContainingSymbol is not INamedTypeSymbol structSymbol) + return; + + int fieldSize = fieldSymbol.Type.SpecialType switch { + SpecialType.System_Byte => 1, + SpecialType.System_UInt16 => 2, + SpecialType.System_UInt32 => 4, + SpecialType.System_UInt64 => 8, + _ => 0 + }; + + int fieldBits = fieldSize * 8; + + foreach (var attributeData in fieldSymbol.GetAttributes()) { + if (attributeData.AttributeClass is not { } attributeSymbol) + continue; + + if (!SymbolEqualityComparer.Default.Equals(attributeSymbol.OriginalDefinition, bitFieldAttribute)) + continue; + + if (fieldSize == 0) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldBackingFieldTypeNotSupported, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol.Name)); + return; + } + + if (attributeSymbol.TypeArguments.Length != 1) + continue; + + var bitfieldType = attributeSymbol.TypeArguments[0]; + + if (!attributeData.TryGetConstructorArgument(0, out string? name) || + !attributeData.TryGetConstructorArgument(1, out int index) || + !attributeData.TryGetConstructorArgument(2, out int length)) + continue; + + var location = attributeData.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() + ?? fieldSymbol.Locations.FirstOrDefault(); + + if (index < 0) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldIndexIsNegative, + location, + name, + fieldSymbol.Name)); + } + + if (length <= 0) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldLengthIsNegativeOrZero, + location, + name, + fieldSymbol.Name)); + } + + if (bitfieldType.SpecialType == SpecialType.System_Boolean && length != 1) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldBooleanLengthInvalid, + location, + name, + fieldSymbol.Name)); + } + + if ((long)index + length > fieldBits) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldExceedsUnderlyingFieldSize, + location, + name, + fieldSymbol.Name)); + } + + foreach (IPropertySymbol propertySymbol in structSymbol.GetMembers().OfType()) { + if (!propertySymbol.IsPartialDefinition || propertySymbol.Name != name) + continue; + + if (propertySymbol.GetMethod == null) { + context.ReportDiagnostic(Diagnostic.Create( + BitFieldPropertyDefinitionMissingGetter, + propertySymbol.Locations.FirstOrDefault(), + name, + fieldSymbol.Name)); + } + + break; + } + } + }, + SymbolKind.Field); + }); + } +} diff --git a/InteropGenerator/Diagnostics/DiagnosticDescriptors.cs b/InteropGenerator/Diagnostics/DiagnosticDescriptors.cs index 36cf01975b..aba4d53355 100644 --- a/InteropGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/InteropGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -251,4 +251,58 @@ internal static class DiagnosticDescriptors { DiagnosticSeverity.Error, true, "A field marked with the FixedSizeArray attribute cannot set both isString and isBitArray to true."); + + public static readonly DiagnosticDescriptor BitFieldIndexIsNegative = new( + "CSIG0401", + "Bit Field index must be non-negative", + "The bit field '{0}' has an index that is negative", + "InteropGenerator.Field", + DiagnosticSeverity.Error, + true, + "Bit field indices are zero-based and must be greater than or equal to zero."); + + public static readonly DiagnosticDescriptor BitFieldLengthIsNegativeOrZero = new( + "CSIG0402", + "Bit Field length must be greater than zero", + "The bit field '{0}' on field '{1}' has a length that is less than or equal to zero", + "InteropGenerator.Field", + DiagnosticSeverity.Error, + true, + "Bit field lengths must specify at least one bit."); + + public static readonly DiagnosticDescriptor BitFieldExceedsUnderlyingFieldSize = new( + "CSIG0403", + "Bit Field exceeds underlying type size", + "The bit field '{0}' on field '{1}' exceeds the size of the type", + "InteropGenerator.Field", + DiagnosticSeverity.Error, + true, + "The sum of the index and bit length exceeds the size of the underlying field."); + + public static readonly DiagnosticDescriptor BitFieldPropertyDefinitionMissingGetter = new( + "CSIG0404", + "Bit Field property definition is missing a getter", + "The property definition for bit field '{0}' on field '{1}' is missing a getter", + "InteropGenerator.Property", + DiagnosticSeverity.Error, + true, + "The property definition for a bit field is required to have a getter."); + + public static readonly DiagnosticDescriptor BitFieldBackingFieldTypeNotSupported = new( + "CSIG0405", + "Bit Field backing field type not supported", + "The backing field '{0}' needs to be of type byte, ushort, uint or ulong", + "InteropGenerator.Field", + DiagnosticSeverity.Error, + true, + "The backing field for a bit field is required to be either byte, ushort, uint or ulong."); + + public static readonly DiagnosticDescriptor BitFieldBooleanLengthInvalid = new( + "CSIG0406", + "Bit Field length is invalid for bool", + "The bit field '{0}' on field '{1}' is declared as bool but its length is not 1", + "InteropGenerator.Field", + DiagnosticSeverity.Error, + true, + "A bit field of type bool is required to have a length of 1 bit."); } diff --git a/InteropGenerator/Generator/InteropGenerator.Parsing.cs b/InteropGenerator/Generator/InteropGenerator.Parsing.cs index a6aa522903..1fa6e6b5ac 100644 --- a/InteropGenerator/Generator/InteropGenerator.Parsing.cs +++ b/InteropGenerator/Generator/InteropGenerator.Parsing.cs @@ -27,7 +27,7 @@ private static StructInfo ParseStructInfo(INamedTypeSymbol structSymbol, Attribu token.ThrowIfCancellationRequested(); // collect info on struct fields - ParseFields(structSymbol, token, isInherited, out EquatableArray fixedSizeArrays, out EquatableArray publicFields); + ParseFields(structSymbol, token, isInherited, out EquatableArray fixedSizeArrays, out EquatableArray bitFields, out EquatableArray publicFields); token.ThrowIfCancellationRequested(); // other struct attributes @@ -101,6 +101,7 @@ private static StructInfo ParseStructInfo(INamedTypeSymbol structSymbol, Attribu virtualTableSignatureInfo, virtualTableFunctionCount, fixedSizeArrays, + bitFields, inheritanceInfoBuilder.ToImmutable(), structSize, extraInheritedStructInfo); @@ -267,9 +268,10 @@ private static bool TryParseMethod(IMethodSymbol methodSymbol, bool isInherited, isInherited ? ParseInheritedAttributes(parameterSymbol, token) : null); private static void ParseFields(INamedTypeSymbol structSymbol, CancellationToken token, bool isInherited, - out EquatableArray fixedSizeArrays, out EquatableArray publicFields) { + out EquatableArray fixedSizeArrays, out EquatableArray bitFields, out EquatableArray publicFields) { using ImmutableArrayBuilder fixedSizeArrayBuilder = new(); + using ImmutableArrayBuilder bitFieldBuilder = new(); using ImmutableArrayBuilder publicFieldBuilder = new(); foreach (IFieldSymbol fieldSymbol in structSymbol.GetMembers().OfType()) { @@ -307,6 +309,59 @@ private static void ParseFields(INamedTypeSymbol structSymbol, CancellationToken fixedSizeArrayBuilder.Add(fixedSizeArrayInfo); } + + foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { + if (attributeData.AttributeClass is not { } attributeSymbol) + continue; + + if (!attributeSymbol.HasFullyQualifiedMetadataName(InteropTypeNames.BitFieldAttribute)) + continue; + + if (!attributeData.TryGetConstructorArgument(0, out string? name) || + !attributeData.TryGetConstructorArgument(1, out int index) || + !attributeData.TryGetConstructorArgument(2, out int length)) + continue; + + if (attributeSymbol.TypeArguments.Length != 1) + continue; + + // defaults + bool isPartial = false; + bool hasGetter = true; + bool hasSetter = true; + + // check partial property definition + foreach (IPropertySymbol propertySymbol in structSymbol.GetMembers().OfType()) { + if (propertySymbol.Name != name) + continue; + + isPartial = propertySymbol.IsPartialDefinition; + hasGetter = propertySymbol.GetMethod != null; + hasSetter = !propertySymbol.IsReadOnly && propertySymbol.SetMethod != null; + + break; + } + + string typeName = attributeSymbol.TypeArguments[0].GetFullyQualifiedName(); + + EquatableArray inheritableAttributes = ParseInheritedAttributes(fieldSymbol, token); + + BitFieldInfo bitFieldInfo = new( + fieldSymbol.Name, + name!, + typeName, + fieldSymbol.Type.GetFullyQualifiedName(), + index, + length, + isPartial, + hasGetter, + hasSetter, + inheritableAttributes + ); + + bitFieldBuilder.Add(bitFieldInfo); + } + if (isInherited && fieldSymbol.DeclaredAccessibility == Accessibility.Public) { if (!fieldSymbol.TryGetAttributeWithFullyQualifiedMetadataName("System.Runtime.InteropServices.FieldOffsetAttribute", out AttributeData? fieldOffsetAttributeData)) continue; @@ -329,6 +384,7 @@ private static void ParseFields(INamedTypeSymbol structSymbol, CancellationToken } fixedSizeArrays = fixedSizeArrayBuilder.ToImmutable(); + bitFields = bitFieldBuilder.ToImmutable(); publicFields = publicFieldBuilder.ToImmutable(); } diff --git a/InteropGenerator/Generator/InteropGenerator.Rendering.Inheritance.cs b/InteropGenerator/Generator/InteropGenerator.Rendering.Inheritance.cs index 70038cab6d..d7c59b2557 100644 --- a/InteropGenerator/Generator/InteropGenerator.Rendering.Inheritance.cs +++ b/InteropGenerator/Generator/InteropGenerator.Rendering.Inheritance.cs @@ -119,6 +119,14 @@ private static void RenderInheritance(StructInfo structInfo, ImmutableArray"""); + writer.WriteLine($"""/// Property inherited from parent class {inheritedStruct.Name}."""); + foreach (string inheritedAttribute in bitFieldInfo.InheritableAttributes) { + writer.WriteLine(inheritedAttribute); + } + writer.WriteLine($"public {bitFieldInfo.Type} {bitFieldInfo.Name}"); + using (writer.WriteBlock()) { + if (bitFieldInfo.HasGetter) { + writer.WriteLine($"get => {path}.{bitFieldInfo.Name};"); + } + if (bitFieldInfo.HasSetter) { + writer.WriteLine($"set => {path}.{bitFieldInfo.Name} = value;"); + } + } + } + } + private static void RenderInheritedFixedSizeArrayAccessors(StructInfo inheritedStruct, string path, IndentedTextWriter writer) { foreach (FixedSizeArrayInfo fixedSizeArrayInfo in inheritedStruct.FixedSizeArrays) { writer.WriteLine($"""/// """); diff --git a/InteropGenerator/Generator/InteropGenerator.Rendering.cs b/InteropGenerator/Generator/InteropGenerator.Rendering.cs index 14a67c975f..41c663d72b 100644 --- a/InteropGenerator/Generator/InteropGenerator.Rendering.cs +++ b/InteropGenerator/Generator/InteropGenerator.Rendering.cs @@ -75,6 +75,12 @@ private static string RenderStructInfo(StructInfo structInfo, CancellationToken token.ThrowIfCancellationRequested(); } + // write bitfield accessors + if (!structInfo.BitFields.IsEmpty) { + RenderBitFields(structInfo, writer); + token.ThrowIfCancellationRequested(); + } + // write closing struct hierarchy for (var i = 0; i < structInfo.Hierarchy.Length; i++) { writer.DecreaseIndent(); @@ -439,4 +445,24 @@ private static string RenderFixedArrayTypes(ImmutableArray structInf return writer.ToString(); } + + private static void RenderBitFields(StructInfo structInfo, IndentedTextWriter writer) { + foreach (BitFieldInfo bitField in structInfo.BitFields) { + foreach (string inheritedAttribute in bitField.InheritableAttributes) + writer.WriteLine(inheritedAttribute); + + writer.WriteLine($"public {(bitField.IsPartial ? "partial " : "")}{bitField.Type} {bitField.Name}"); + using (writer.WriteBlock()) { + if (bitField.Type == "bool") { + if (bitField.HasGetter) writer.WriteLine($"get => BitOps.GetBit<{bitField.BackingType}>({bitField.FieldName}, {bitField.Index});"); + if (bitField.HasSetter) writer.WriteLine($"set => {bitField.FieldName} = BitOps.SetBit<{bitField.BackingType}>({bitField.FieldName}, {bitField.Index}, value);"); + } + else { + string mask = $"BitOps.CreateLowBitMask<{bitField.BackingType}>({bitField.Length})"; + if (bitField.HasGetter) writer.WriteLine($"get => ({bitField.Type})BitOps.GetBits<{bitField.BackingType}>({bitField.FieldName}, {bitField.Index}, {mask});"); + if (bitField.HasSetter) writer.WriteLine($"set => {bitField.FieldName} = BitOps.SetBits<{bitField.BackingType}>({bitField.FieldName}, {bitField.Index}, {mask}, ({bitField.BackingType})value);"); + } + } + } + } } diff --git a/InteropGenerator/InteropTypeNames.cs b/InteropGenerator/InteropTypeNames.cs index b9a2d7cad6..acf2adf0de 100644 --- a/InteropGenerator/InteropTypeNames.cs +++ b/InteropGenerator/InteropTypeNames.cs @@ -11,9 +11,10 @@ public static class InteropTypeNames { public const string GenerateStringOverloadsAttribute = AttributeNamespace + ".GenerateStringOverloadsAttribute"; public const string StringIgnoreAttribute = AttributeNamespace + ".StringIgnoreAttribute"; public const string FixedSizeArrayAttribute = AttributeNamespace + ".FixedSizeArrayAttribute"; + public const string BitFieldAttribute = AttributeNamespace + ".BitFieldAttribute`1"; public const string InheritsAttribute = AttributeNamespace + ".InheritsAttribute`1"; public const string CStringPointer = "InteropGenerator.Runtime.CStringPointer"; - public static HashSet UninheritableAttributes = [MemberFunctionAttribute, VirtualFunctionAttribute, StaticAddressAttribute, GenerateStringOverloadsAttribute, StringIgnoreAttribute, FixedSizeArrayAttribute, "System.Runtime.InteropServices.FieldOffsetAttribute"]; + public static HashSet UninheritableAttributes = [MemberFunctionAttribute, VirtualFunctionAttribute, StaticAddressAttribute, GenerateStringOverloadsAttribute, StringIgnoreAttribute, FixedSizeArrayAttribute, BitFieldAttribute, "System.Runtime.InteropServices.FieldOffsetAttribute"]; } diff --git a/InteropGenerator/Models/BitFieldInfo.cs b/InteropGenerator/Models/BitFieldInfo.cs new file mode 100644 index 0000000000..b58ab7b79a --- /dev/null +++ b/InteropGenerator/Models/BitFieldInfo.cs @@ -0,0 +1,16 @@ +using InteropGenerator.Helpers; + +namespace InteropGenerator.Models; + +internal record BitFieldInfo( + string FieldName, + string Name, + string Type, + string BackingType, + int Index, + int Length, + bool IsPartial, + bool HasGetter, + bool HasSetter, + EquatableArray InheritableAttributes +); diff --git a/InteropGenerator/Models/StructInfo.cs b/InteropGenerator/Models/StructInfo.cs index a69a51d54a..964be4a53b 100644 --- a/InteropGenerator/Models/StructInfo.cs +++ b/InteropGenerator/Models/StructInfo.cs @@ -13,6 +13,7 @@ internal sealed record StructInfo( SignatureInfo? StaticVirtualTableSignature, uint? VirtualTableFunctionCount, EquatableArray FixedSizeArrays, + EquatableArray BitFields, EquatableArray InheritedStructs, int? Size, ExtraInheritedStructInfo? ExtraInheritedStructInfo) { @@ -20,5 +21,5 @@ internal sealed record StructInfo( public bool HasSignatures() => !MemberFunctions.IsEmpty || !StaticAddresses.IsEmpty || StaticVirtualTableSignature is not null; public bool HasVirtualTable() => !VirtualFunctions.IsEmpty || StaticVirtualTableSignature is not null; - public bool NeedsRender() => MemberFunctions.Any() || VirtualFunctions.Any() || StaticAddresses.Any() || StringOverloads.Any() || StaticVirtualTableSignature is not null || FixedSizeArrays.Any(); + public bool NeedsRender() => MemberFunctions.Any() || VirtualFunctions.Any() || StaticAddresses.Any() || StringOverloads.Any() || StaticVirtualTableSignature is not null || FixedSizeArrays.Any() || BitFields.Any(); }