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"")]