diff --git a/src/Generator.Tests/GeneratorTest.cs b/src/Generator.Tests/GeneratorTest.cs index 54777fae..20478f58 100644 --- a/src/Generator.Tests/GeneratorTest.cs +++ b/src/Generator.Tests/GeneratorTest.cs @@ -29,6 +29,7 @@ namespace CppSharp.Utils options.OutputDir = Path.Combine(GetOutputDirectory(), "gen", name); options.Quiet = true; options.GenerateDebugOutput = true; + options.GenerateSequentialLayout = true; var testModule = options.AddModule(name); testModule.SharedLibraryName = $"{name}.Native"; diff --git a/src/Generator/Generators/CSharp/CSharpSources.cs b/src/Generator/Generators/CSharp/CSharpSources.cs index f1376ddb..3731cf3d 100644 --- a/src/Generator/Generators/CSharp/CSharpSources.cs +++ b/src/Generator/Generators/CSharp/CSharpSources.cs @@ -531,19 +531,24 @@ namespace CppSharp.Generators.CSharp public void GenerateClassInternals(Class @class) { + var sequentialLayout = Options.GenerateSequentialLayout && CanUseSequentialLayout(@class); + PushBlock(BlockKind.InternalsClass); - if (!Options.GenerateSequentialLayout || @class.IsUnion) - WriteLine($"[StructLayout(LayoutKind.Explicit, Size = {@class.Layout.GetSize()})]"); - else if (@class.MaxFieldAlignment > 0) - WriteLine($"[StructLayout(LayoutKind.Sequential, Pack = {@class.MaxFieldAlignment})]"); + + if (@class.Layout.GetSize() > 0) + { + var layout = sequentialLayout ? "Sequential" : "Explicit"; + var pack = @class.MaxFieldAlignment > 0 ? $", Pack = {@class.MaxFieldAlignment}" : string.Empty; + WriteLine($"[StructLayout(LayoutKind.{layout}, Size = {@class.Layout.GetSize()}{pack})]"); + } GenerateClassInternalHead(@class); WriteOpenBraceAndIndent(); TypePrinter.PushContext(TypePrinterContextKind.Native); - foreach (var field in @class.Layout.Fields) - GenerateClassInternalsField(field, @class); + GenerateClassInternalsFields(@class, sequentialLayout); + if (@class.IsGenerated) { var functions = GatherClassInternalFunctions(@class); @@ -764,24 +769,71 @@ namespace CppSharp.Generators.CSharp Write(" : {0}", string.Join(", ", bases)); } - private void GenerateClassInternalsField(LayoutField field, Class @class) + private bool CanUseSequentialLayout(Class @class) { - TypePrinterResult retType = TypePrinter.VisitFieldDecl( - new Field { Name = field.Name, QualifiedType = field.QualifiedType }); + if (@class.IsUnion) + return false; - PushBlock(BlockKind.Field); + var fields = @class.Layout.Fields; - if (!Options.GenerateSequentialLayout || @class.IsUnion) - WriteLine($"[FieldOffset({field.Offset})]"); - Write($"internal {retType}"); - if (field.Expression != null) - { - var fieldValuePrinted = field.Expression.CSharpValue(ExpressionPrinter); - Write($" = {fieldValuePrinted}"); + if (fields.Count > 1) + { + for (var i = 1; i < fields.Count; ++i) + { + if (fields[i].Offset == fields[i - 1].Offset) + return false; + } } - WriteLine(";"); - PopBlock(NewLineKind.BeforeNextBlock); + return true; + } + + private void GenerateClassInternalsFields(Class @class, bool sequentalLayout) + { + var fields = @class.Layout.Fields; + + for (var i = 0; i < fields.Count; ++i) + { + var field = fields[i]; + + TypePrinterResult retType = TypePrinter.VisitFieldDecl( + new Field { Name = field.Name, QualifiedType = field.QualifiedType }); + + PushBlock(BlockKind.Field); + + if (!sequentalLayout) + WriteLine($"[FieldOffset({field.Offset})]"); + + Write($"internal {retType}"); + if (field.Expression != null) + { + var fieldValuePrinted = field.Expression.CSharpValue(ExpressionPrinter); + Write($" = {fieldValuePrinted}"); + } + WriteLine(";"); + + // HACK: work around the lack of alignment in StructLayout + // it's been requested multiple times to no avail: + // https://github.com/dotnet/runtime/issues/5931, https://github.com/dotnet/runtime/issues/9089, https://github.com/dotnet/runtime/issues/22990 + // Sometimes padding is needed due to aligment. + // The linux 32 bit target pads at the end the structure + // which is already handled by using [StructLayout(Size = n)]. + // However the windows 32 bit target pads at the front, + // right after the vtable pointer, which is what we are handling here + if (sequentalLayout && fields[i].IsVTablePtr) + { + var nativePointerSize = Context.TargetInfo.PointerWidth / 8; + + if (i + 1 < fields.Count) + { + var padding = fields[i + 1].Offset - field.Offset - nativePointerSize; + if (padding > 0 && padding <= @class.Layout.Size) + WriteLine($"internal fixed byte {field.Name}Padding[{padding}];"); + } + } + + PopBlock(sequentalLayout && i + 1 != fields.Count ? NewLineKind.Never : NewLineKind.BeforeNextBlock); + } } private void GenerateClassField(Field field, bool @public = false) diff --git a/tests/CSharp/CSharp.Tests.cs b/tests/CSharp/CSharp.Tests.cs index 424fe592..6bde60c9 100644 --- a/tests/CSharp/CSharp.Tests.cs +++ b/tests/CSharp/CSharp.Tests.cs @@ -721,8 +721,11 @@ public unsafe class CSharpTests : GeneratorTestFixture { var independentFields = internalType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic); var fieldOffset = (FieldOffsetAttribute) independentFields[0].GetCustomAttribute(typeof(FieldOffsetAttribute)); - Assert.That(fieldOffset.Value, Is.EqualTo(0)); + if (fieldOffset != null) + Assert.That(fieldOffset.Value, Is.EqualTo(0)); + Assert.That((int)Marshal.OffsetOf(internalType, independentFields[0].Name), Is.EqualTo(0)); } + foreach (var internalType in new Type[] { typeof(CSharp.TwoTemplateArgs.__Internal_Ptr), @@ -733,12 +736,21 @@ public unsafe class CSharpTests : GeneratorTestFixture var independentFields = internalType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(independentFields.Length, Is.EqualTo(2)); var fieldOffsetKey = (FieldOffsetAttribute) independentFields[0].GetCustomAttribute(typeof(FieldOffsetAttribute)); - Assert.That(fieldOffsetKey.Value, Is.EqualTo(0)); + if (fieldOffsetKey != null) + Assert.That(fieldOffsetKey.Value, Is.EqualTo(0)); + Assert.That((int)Marshal.OffsetOf(internalType, independentFields[0].Name), Is.EqualTo(0)); var fieldOffsetValue = (FieldOffsetAttribute) independentFields[1].GetCustomAttribute(typeof(FieldOffsetAttribute)); - Assert.That(fieldOffsetValue.Value, Is.EqualTo(Marshal.SizeOf(IntPtr.Zero))); + if (fieldOffsetValue != null) + Assert.That(fieldOffsetValue.Value, Is.EqualTo(Marshal.SizeOf(IntPtr.Zero))); + Assert.That((int)Marshal.OffsetOf(internalType, independentFields[1].Name), Is.EqualTo(Marshal.SizeOf(IntPtr.Zero))); } } + public void TestClassSize() + { + Assert.That(Marshal.SizeOf, Is.EqualTo(Marshal.SizeOf() * 2)); + } + [Test] public void TestConstantArray() {