diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Close.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Close.cs index 02852348..fe837916 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Close.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Close.cs @@ -290,6 +290,33 @@ public void Close() } } + // The method class output builders are emitted here, before the helper types, so that in + // single-file mode the helper types are appended at the bottom of the shared namespace + // rather than in the middle of the generated output. In multi-file mode the method classes + // were already written directly above, so both dictionaries are empty and these loops are a + // no-op. + + if (leaveStreamOpen) + { + Debug.Assert(stream is not null); + + foreach (var entry in methodClassOutputBuilders) + { + CloseOutputBuilder(stream, entry.Value, isMethodClass: true, leaveStreamOpen, emitNamespaceDeclaration); + emitNamespaceDeclaration = false; + } + + foreach (var entry in methodClassTestOutputBuilders) + { + CloseOutputBuilder(testStream ?? stream, entry.Value, isMethodClass: true, leaveStreamOpen, emitNamespaceDeclaration); + } + } + else + { + Debug.Assert(methodClassOutputBuilders.Count == 0); + Debug.Assert(methodClassTestOutputBuilders.Count == 0); + } + if (generateHelperTypes) { // In single-file mode the helper types are emitted inside the shared namespace and are @@ -349,16 +376,6 @@ public void Close() { Debug.Assert(stream is not null); - foreach (var entry in methodClassOutputBuilders) - { - CloseOutputBuilder(stream, entry.Value, isMethodClass: true, leaveStreamOpen, emitNamespaceDeclaration); - } - - foreach (var entry in methodClassTestOutputBuilders) - { - CloseOutputBuilder(testStream ?? stream, entry.Value, isMethodClass: true, leaveStreamOpen, emitNamespaceDeclaration); - } - using var sw = new StreamWriter(stream, s_defaultStreamWriterEncoding, DefaultStreamWriterBufferSize, leaveStreamOpen); sw.NewLine = "\n"; diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/HelperTypesTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/HelperTypesTest.cs index 3d2d70e9..04a21397 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/HelperTypesTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/HelperTypesTest.cs @@ -6,11 +6,14 @@ namespace ClangSharp.UnitTests; /// -/// Regression test for https://github.com/dotnet/ClangSharp/issues/657. +/// Regression tests for https://github.com/dotnet/ClangSharp/issues/657 and +/// https://github.com/dotnet/ClangSharp/issues/429. /// When generate-helper-types is used with a single output file, the helper types must be /// emitted inside the shared namespace with their usings hoisted to the top of the file, rather than /// each helper emitting its own using directives and namespace wrapper (which produced -/// invalid C# with nested namespaces, usings after a namespace, and a stray trailing brace). +/// invalid C# with nested namespaces, usings after a namespace, and a stray trailing brace). The +/// helper types must also be emitted at the bottom of the shared namespace, after the deferred method +/// class, rather than in the middle of the generated output. /// [Platform("win")] public sealed class HelperTypesTest : PInvokeGeneratorTest @@ -122,6 +125,153 @@ public NativeTypeNameAttribute(string name) public string Name => _name; } +/// Defines the annotation found in a native declaration. +[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +[Conditional(""DEBUG"")] +internal sealed partial class NativeAnnotationAttribute : Attribute +{ + private readonly string _annotation; + + /// Initializes a new instance of the class. + /// The annotation that was used in the native declaration. + public NativeAnnotationAttribute(string annotation) + { + _annotation = annotation; + } + + /// Gets the annotation that was used in the native declaration. + public string Annotation => _annotation; +} +"; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(inputContents, expectedOutputContents, additionalConfigOptions: PInvokeGeneratorConfigurationOptions.GenerateHelperTypes | PInvokeGeneratorConfigurationOptions.GenerateFileScopedNamespaces); + } + + [Test] + public Task HelperTypesEmittedAfterMethodClass() + { + var inputContents = @"struct SRC_DATA +{ + const float *data_in; + long input_frames; +}; + +extern ""C"" void MyFunction(); +"; + + var expectedOutputContents = @"using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + public unsafe partial struct SRC_DATA + { + [NativeTypeName(""const float *"")] + public float* data_in; + + [NativeTypeName(""long"")] + public int input_frames; + } + + public static partial class Methods + { + [DllImport(""ClangSharpPInvokeGenerator"", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void MyFunction(); + } + + /// Defines the type of a member as it was used in the native signature. + [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)] + [Conditional(""DEBUG"")] + internal sealed partial class NativeTypeNameAttribute : Attribute + { + private readonly string _name; + + /// Initializes a new instance of the class. + /// The name of the type that was used in the native signature. + public NativeTypeNameAttribute(string name) + { + _name = name; + } + + /// Gets the name of the type that was used in the native signature. + public string Name => _name; + } + + /// Defines the annotation found in a native declaration. + [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + [Conditional(""DEBUG"")] + internal sealed partial class NativeAnnotationAttribute : Attribute + { + private readonly string _annotation; + + /// Initializes a new instance of the class. + /// The annotation that was used in the native declaration. + public NativeAnnotationAttribute(string annotation) + { + _annotation = annotation; + } + + /// Gets the annotation that was used in the native declaration. + public string Annotation => _annotation; + } +} +"; + + return ValidateGeneratedCSharpLatestWindowsBindingsAsync(inputContents, expectedOutputContents, additionalConfigOptions: PInvokeGeneratorConfigurationOptions.GenerateHelperTypes); + } + + [Test] + public Task HelperTypesEmittedAfterMethodClassFileScoped() + { + var inputContents = @"struct SRC_DATA +{ + const float *data_in; + long input_frames; +}; + +extern ""C"" void MyFunction(); +"; + + var expectedOutputContents = @"using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ClangSharp.Test; + +public unsafe partial struct SRC_DATA +{ + [NativeTypeName(""const float *"")] + public float* data_in; + + [NativeTypeName(""long"")] + public int input_frames; +} + +public static partial class Methods +{ + [DllImport(""ClangSharpPInvokeGenerator"", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void MyFunction(); +} + +/// Defines the type of a member as it was used in the native signature. +[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)] +[Conditional(""DEBUG"")] +internal sealed partial class NativeTypeNameAttribute : Attribute +{ + private readonly string _name; + + /// Initializes a new instance of the class. + /// The name of the type that was used in the native signature. + public NativeTypeNameAttribute(string name) + { + _name = name; + } + + /// Gets the name of the type that was used in the native signature. + public string Name => _name; +} + /// Defines the annotation found in a native declaration. [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] [Conditional(""DEBUG"")]