From be48bba2c4bc3687638ced6063342073b12948cf Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 01:37:26 -0700 Subject: [PATCH] Emit single-file helper types below the method class instead of mid-file In single-file mode the deferred method class (`Methods`) was flushed after the generate-helper-types helper types, so with both a struct and functions the helpers were emitted between the struct and `Methods` -- in the middle of the file rather than at the top or bottom. Flush the deferred method class output builders before the helper types so the helpers join the shared namespace at the bottom, after all user content. Fixes #429 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Close.cs | 37 +++-- .../HelperTypesTest.cs | 154 +++++++++++++++++- 2 files changed, 179 insertions(+), 12 deletions(-) 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"")]