From bf7d06fc00812be22c7b79bae5c3a6a9aafe71f7 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 19 Jul 2026 07:26:07 -0700 Subject: [PATCH 1/5] Don't duplicate a partially qualified namespace in NativeTypeName The restored `Namespace::` prefix was inserted whole whenever the spelling didn't already start with the full qualifier, so a reference that already carried a trailing run of the namespace (e.g. `Windows::Foundation::PropertyValue` spelled inside `Abi`) became `Abi::Windows::Foundation::Windows::Foundation::PropertyValue`. Insert only the leading segments the spelling is missing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Naming.cs | 24 +++++++--- ...fiedNamespaceIsNotDuplicatedTest.CSharp.cs | 24 ++++++++++ ...lifiedNamespaceIsNotDuplicatedTest.Xml.xml | 26 +++++++++++ .../Baseline/NamespaceNativeTypeNameTest.cs | 44 +++++++++++++++++++ 4 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.CSharp.cs create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.Xml.xml diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs index 05cd815b..361f6366 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs @@ -202,23 +202,21 @@ private static string GetNamespaceQualifiedNativeTypeName(Decl decl, string nati // Rebuild the dropped `Namespace::` prefix from the decl so the NativeTypeName keeps the // fully qualified source spelling (e.g. `Gdiplus::Status`) and stays stable across versions. - var qualifierBuilder = new StringBuilder(); + var namespaceNames = new List(); for (var declContext = decl.DeclContext; declContext is Decl parentDecl; declContext = parentDecl.DeclContext) { if (parentDecl is NamespaceDecl namespaceDecl && !string.IsNullOrEmpty(namespaceDecl.Name)) { - _ = qualifierBuilder.Insert(0, "::").Insert(0, namespaceDecl.Name); + namespaceNames.Insert(0, namespaceDecl.Name); } } - if (qualifierBuilder.Length == 0) + if (namespaceNames.Count == 0) { return nativeTypeName; } - var qualifier = qualifierBuilder.ToString(); - // The qualifier belongs on the type name, after any leading cv-qualifiers, elaborated tag // keywords, or `__unaligned`, so e.g. a `const struct` pointer stays `const struct Ns::Point *` // rather than the malformed `Ns::const struct Point *`. @@ -245,7 +243,21 @@ private static string GetNamespaceQualifiedNativeTypeName(Decl decl, string nati } } - return nativeTypeName.AsSpan(offset).StartsWith(qualifier, StringComparison.Ordinal) ? nativeTypeName : nativeTypeName.Insert(offset, qualifier); + // The source spelling may already carry a trailing run of the namespace (a minimally + // qualified reference, e.g. `Windows::Foundation::IPropertyValue` written inside + // `namespace ABI`). Insert only the leading segments it is missing so the qualifier isn't + // duplicated into `ABI::Windows::Foundation::Windows::Foundation::IPropertyValue`. + for (var index = 0; index <= namespaceNames.Count; index++) + { + var suffix = index < namespaceNames.Count ? string.Concat(string.Join("::", namespaceNames.Skip(index)), "::") : ""; + + if (nativeTypeName.AsSpan(offset).StartsWith(suffix, StringComparison.Ordinal)) + { + return index == 0 ? nativeTypeName : nativeTypeName.Insert(offset, string.Concat(string.Join("::", namespaceNames.Take(index)), "::")); + } + } + + return nativeTypeName; } private string GetCursorQualifiedName(NamedDecl namedDecl, bool truncateParameters = false) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.CSharp.cs new file mode 100644 index 00000000..06ff57b1 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.CSharp.cs @@ -0,0 +1,24 @@ +namespace ClangSharp.Test +{ + public partial struct PropertyValue + { + public int value; + } + + public partial struct EffectSource + { + public int source; + } + + public unsafe partial struct Interop + { + [NativeTypeName("Abi::Windows::Foundation::PropertyValue **")] + public PropertyValue** partiallyQualified; + + [NativeTypeName("Abi::Windows::Foundation::PropertyValue **")] + public PropertyValue** fullyQualified; + + [NativeTypeName("Abi::Windows::Graphics::Effects::EffectSource **")] + public EffectSource** sameNamespace; + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.Xml.xml new file mode 100644 index 00000000..6d5ddbdf --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/PartiallyQualifiedNamespaceIsNotDuplicatedTest.Xml.xml @@ -0,0 +1,26 @@ + + + + + + int + + + + + int + + + + + PropertyValue** + + + PropertyValue** + + + EffectSource** + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs index dddb9052..1fe1220f 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs @@ -56,4 +56,48 @@ enum Status* elabStatusPtr; return ValidateAsync(nameof(NamespaceQualifiedNativeTypeNameIsPreservedTest), inputContents); } + + // A reference spelled from within a nested namespace already carries part of the enclosing + // qualifier (e.g. `Windows::Foundation::PropertyValue` written inside `Abi`). The restored prefix + // must only add the leading segments the spelling is missing, so it stays + // `Abi::Windows::Foundation::PropertyValue` rather than doubling into + // `Abi::Windows::Foundation::Windows::Foundation::PropertyValue`. + [Test] + public Task PartiallyQualifiedNamespaceIsNotDuplicatedTest() + { + var inputContents = @"namespace Abi +{ + namespace Windows + { + namespace Foundation + { + struct PropertyValue + { + int value; + }; + } + + namespace Graphics + { + namespace Effects + { + struct EffectSource + { + int source; + }; + + struct Interop + { + Windows::Foundation::PropertyValue** partiallyQualified; + Abi::Windows::Foundation::PropertyValue** fullyQualified; + EffectSource** sameNamespace; + }; + } + } + } +} +"; + + return ValidateAsync(nameof(PartiallyQualifiedNamespaceIsNotDuplicatedTest), inputContents); + } } From cf43e484a1c5b658dd0f13051d5cacec7a2dba8f Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 19 Jul 2026 07:26:17 -0700 Subject: [PATCH 2/5] Emit cxx_delete for delete expressions in destructor bodies A user-declared destructor lowers to `Dispose`, but `CXXDeleteExpr` was unsupported, so `delete[] p` produced nothing and left a silently empty `if (p != null) { }` no-op that leaks. Emit a `cxx_delete(...)` placeholder, mirroring the existing `cxx_new(...)` lowering for `new`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.VisitStmt.cs | 19 ++++++++++++++- .../UserDeclaredDestructorTest.CSharp.cs | 15 ++++++++++++ .../UserDeclaredDestructorTest.Xml.xml | 17 ++++++++++++++ .../Baseline/StructDeclarationTest.cs | 23 +++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.CSharp.cs create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.Xml.xml diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs index c2b7f843..d2c71488 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs @@ -850,6 +850,18 @@ private void VisitCXXFunctionalCastExpr(CXXFunctionalCastExpr cxxFunctionalCastE } } + private void VisitCXXDeleteExpr(CXXDeleteExpr cxxDeleteExpr) + { + // Mirrors the `cxx_new(...)` placeholder emitted for `new`: `delete`/`delete[]` has no + // direct C# equivalent (the allocator is unknown), so emit a visible `cxx_delete(...)` call + // rather than nothing, which would leave a silently empty statement such as `if (p) { ; }`. + var outputBuilder = StartCSharpCode(); + outputBuilder.Write("cxx_delete("); + Visit(cxxDeleteExpr.Argument); + outputBuilder.Write(')'); + StopCSharpCode(); + } + private void VisitCXXNewExpr(CXXNewExpr cxxNewExpr) { var outputBuilder = StartCSharpCode(); @@ -2645,7 +2657,12 @@ private void VisitStmt(Stmt stmt) } // case CX_StmtClass_CXXDefaultInitExpr: - // case CX_StmtClass_CXXDeleteExpr: + + case CX_StmtClass_CXXDeleteExpr: + { + VisitCXXDeleteExpr((CXXDeleteExpr)stmt); + break; + } case CX_StmtClass_CXXDependentScopeMemberExpr: { diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.CSharp.cs new file mode 100644 index 00000000..8199027f --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.CSharp.cs @@ -0,0 +1,15 @@ +namespace ClangSharp.Test +{ + public unsafe partial struct WithDestructor + { + public int* data; + + public void Dispose() + { + if (data != null) + { + cxx_delete(data); + } + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.Xml.xml new file mode 100644 index 00000000..7042e59c --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/UserDeclaredDestructorTest.Xml.xml @@ -0,0 +1,17 @@ + + + + + + int* + + + void + if (data != null) + { + cxx_delete(data); + } + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs index 4a201a01..1761fe99 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs @@ -1220,4 +1220,27 @@ struct { int Value2; }; return ValidateAsync(nameof(DeeplyNestedAnonStructs), inputContents); } + + // A user-declared destructor lowers to `Dispose`. `delete`/`delete[]` has no direct C# form, so it + // emits the `cxx_delete(...)` placeholder (mirroring `cxx_new`) instead of nothing, which would + // leave a silently empty `if (data != null) { }` body that leaks. + [Test] + public Task UserDeclaredDestructorTest() + { + var inputContents = @"struct WithDestructor +{ + int* data; + + ~WithDestructor() + { + if (data != 0) + { + delete[] data; + } + } +}; +"; + + return ValidateAsync(nameof(UserDeclaredDestructorTest), inputContents); + } } From ad4f38d0d416274daa01cc76d17194ba636b528a Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 19 Jul 2026 07:42:02 -0700 Subject: [PATCH 3/5] Add --with-base option for injecting additional base types Lets a generated type derive from user-supplied base types, applied to the marker `Interface` for COM/vtbl types and to the struct itself for plain value types. Restores terrafx's hand-maintained `IUnknown.Interface : INativeGuid` without a manual post-regen patch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + .../Abstractions/IOutputBuilder.VisitDecl.cs | 3 +- .../Abstractions/StructDesc.cs | 2 ++ .../CSharp/CSharpOutputBuilder.VisitDecl.cs | 30 +++++++++++----- .../PInvokeGenerator.VisitRecordDecl.cs | 8 ++++- .../PInvokeGeneratorConfiguration.cs | 16 +++++++++ .../XML/XmlOutputBuilder.VisitDecl.cs | 3 +- .../Program.Options.cs | 3 ++ sources/ClangSharpPInvokeGenerator/Program.cs | 3 ++ .../Baseline/BaselineTest.cs | 8 ++--- .../WithBaseTest.CSharp.Compatible.cs | 31 ++++++++++++++++ .../StructDeclaration/WithBaseTest.CSharp.cs | 24 +++++++++++++ .../WithBaseTest.Xml.Compatible.xml | 35 +++++++++++++++++++ .../StructDeclaration/WithBaseTest.Xml.xml | 26 ++++++++++++++ .../Baseline/StructDeclarationTest.cs | 25 +++++++++++++ .../PInvokeGeneratorTest.cs | 7 ++-- 16 files changed, 206 insertions(+), 19 deletions(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.Compatible.cs create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.cs create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.Compatible.xml create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.xml diff --git a/README.md b/README.md index 698d064c..32f3abd3 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ Options: -v, --version Prints the current version information for the tool and its native dependencies. -was, --with-access-specifier An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence. [] -wa, --with-attribute An attribute to be added to the given remapped declaration name during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence. [] + -wb, --with-base An additional base type the generated type should derive from during binding generation. Applies to structs and COM interface types. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence. [] -wcc, --with-callconv A calling convention to be used for the given declaration during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence. [] -wc, --with-class A class to be used for the given remapped constant or function declaration name during binding generation. Supports a trailing `*` wildcard for prefix matching; an exact match takes precedence. [] -wcond, --with-conditional A preprocessor symbol used to wrap single-file C# output in a leading '#if ' and trailing '#endif'. Useful when files can't be conditionally excluded at the project level (e.g. Unity). [] diff --git a/sources/ClangSharp.PInvokeGenerator/Abstractions/IOutputBuilder.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/Abstractions/IOutputBuilder.VisitDecl.cs index 667d8231..a74ba201 100644 --- a/sources/ClangSharp.PInvokeGenerator/Abstractions/IOutputBuilder.VisitDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/Abstractions/IOutputBuilder.VisitDecl.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using ClangSharp.CSharp; namespace ClangSharp.Abstractions; @@ -49,7 +50,7 @@ internal partial interface IOutputBuilder void EndFunctionOrDelegate(in FunctionOrDelegateDesc info); void BeginStruct(in StructDesc info); - void BeginMarkerInterface(string[]? baseTypeNames); + void BeginMarkerInterface(string[]? baseTypeNames, IReadOnlyList? extraBaseTypeNames); void EndMarkerInterface(); void BeginExplicitVtbl(); void EndExplicitVtbl(); diff --git a/sources/ClangSharp.PInvokeGenerator/Abstractions/StructDesc.cs b/sources/ClangSharp.PInvokeGenerator/Abstractions/StructDesc.cs index 0606a2c3..b0e78661 100644 --- a/sources/ClangSharp.PInvokeGenerator/Abstractions/StructDesc.cs +++ b/sources/ClangSharp.PInvokeGenerator/Abstractions/StructDesc.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using ClangSharp.Interop; @@ -17,6 +18,7 @@ internal struct StructDesc public LayoutDesc Layout { get; set; } public Guid? Uuid { get; set; } public StructFlags Flags { get; set; } + public IReadOnlyList? ExtraBaseTypeNames { get; set; } public CXSourceLocation? Location { get; set; } public bool IsNested diff --git a/sources/ClangSharp.PInvokeGenerator/CSharp/CSharpOutputBuilder.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/CSharp/CSharpOutputBuilder.VisitDecl.cs index 81a9ce2e..d43136c5 100644 --- a/sources/ClangSharp.PInvokeGenerator/CSharp/CSharpOutputBuilder.VisitDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/CSharp/CSharpOutputBuilder.VisitDecl.cs @@ -854,6 +854,11 @@ public void BeginStruct(in StructDesc desc) baseTypeNames.Add($"IEquatable<{desc.EscapedName}>"); } + if (desc.ExtraBaseTypeNames is not null) + { + baseTypeNames.AddRange(desc.ExtraBaseTypeNames); + } + if (baseTypeNames.Count != 0) { Write(" : "); @@ -864,24 +869,31 @@ public void BeginStruct(in StructDesc desc) WriteBlockStart(); } - public void BeginMarkerInterface(string[]? baseTypeNames) + public void BeginMarkerInterface(string[]? baseTypeNames, IReadOnlyList? extraBaseTypeNames) { WriteIndented("public interface Interface"); + var bases = new List(); + if (baseTypeNames is not null) { - Write(" : "); - Write(baseTypeNames[0]); - Write(".Interface"); - - for (var i = 1; i < baseTypeNames.Length; i++) + foreach (var baseTypeName in baseTypeNames) { - Write(", "); - Write(baseTypeNames[i]); - Write(".Interface"); + bases.Add($"{baseTypeName}.Interface"); } } + if (extraBaseTypeNames is not null) + { + bases.AddRange(extraBaseTypeNames); + } + + if (bases.Count != 0) + { + Write(" : "); + Write(string.Join(", ", bases)); + } + WriteNewline(); WriteBlockStart(); _isInMarkerInterface = true; diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs index 0cd2e447..b04e45dc 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitRecordDecl.cs @@ -153,6 +153,11 @@ private void VisitRecordDecl(RecordDecl recordDecl) string[]? baseTypeNames = null; + if (!TryGetRemappedValue(recordDecl, _config._withBases, optOuts: null, out var extraBaseTypeNames, matchStar: true)) + { + extraBaseTypeNames = null; + } + string? nativeNameWithExtras = null, nativeInheritance = null; if ((cxxRecordDecl is not null) && cxxRecordDecl.Bases.Any()) { @@ -231,6 +236,7 @@ private void VisitRecordDecl(RecordDecl recordDecl) }, Uuid = nullableUuid, NativeType = nativeNameWithExtras, + ExtraBaseTypeNames = ((hasVtbl || hasBaseVtbl) && _config.GenerateMarkerInterfaces) ? null : extraBaseTypeNames, NativeInheritance = _config.GenerateNativeInheritanceAttribute ? nativeInheritance : null, NativeAlignment = nativeAlignment, Location = recordDecl.Location, @@ -625,7 +631,7 @@ private void VisitRecordDecl(RecordDecl recordDecl) csharpOutputBuilder.NeedsNewline = true; } - _outputBuilder.BeginMarkerInterface(baseTypeNames); + _outputBuilder.BeginMarkerInterface(baseTypeNames, extraBaseTypeNames); OutputMarkerInterfaces(cxxRecordDecl, cxxRecordDecl); _outputBuilder.EndMarkerInterface(); } diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs index d66341a9..ec0bf900 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGeneratorConfiguration.cs @@ -62,6 +62,7 @@ public sealed class PInvokeGeneratorConfiguration internal readonly Dictionary _remappedFieldNames; internal readonly Dictionary _withAccessSpecifiers; internal readonly Dictionary> _withAttributes; + internal readonly Dictionary> _withBases; internal readonly Dictionary _withCallConvs; internal readonly Dictionary _withClasses; internal readonly Dictionary _withEnumMemberStrip; @@ -137,6 +138,7 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s _remappedFieldNames = new Dictionary(QualifiedNameComparer.Default); _withAccessSpecifiers = new Dictionary(QualifiedNameComparer.Default); _withAttributes = new Dictionary>(QualifiedNameComparer.Default); + _withBases = new Dictionary>(QualifiedNameComparer.Default); _withCallConvs = new Dictionary(QualifiedNameComparer.Default); _withClasses = new Dictionary(StringComparer.Ordinal); _withEnumMemberStrip = new Dictionary(QualifiedNameComparer.Default); @@ -609,6 +611,20 @@ public IReadOnlyDictionary> WithAttributes } } + [AllowNull] + public IReadOnlyDictionary> WithBases + { + get + { + return _withBases; + } + + init + { + AddRange(_withBases, value); + } + } + [AllowNull] public IReadOnlyDictionary WithCallConvs { diff --git a/sources/ClangSharp.PInvokeGenerator/XML/XmlOutputBuilder.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/XML/XmlOutputBuilder.VisitDecl.cs index 093139aa..d36d6912 100644 --- a/sources/ClangSharp.PInvokeGenerator/XML/XmlOutputBuilder.VisitDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/XML/XmlOutputBuilder.VisitDecl.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text; @@ -308,7 +309,7 @@ public void BeginStruct(in StructDesc info) _ = _sb.Append('>'); info.WriteCustomAttrs?.Invoke(info.CustomAttrGeneratorData); } - public void BeginMarkerInterface(string[]? baseTypeNames) => _sb.Append(""); + public void BeginMarkerInterface(string[]? baseTypeNames, IReadOnlyList? extraBaseTypeNames) => _sb.Append(""); public void EndMarkerInterface() => _sb.Append(""); public void BeginExplicitVtbl() => _sb.Append(""); public void EndExplicitVtbl() => _sb.Append(""); diff --git a/sources/ClangSharpPInvokeGenerator/Program.Options.cs b/sources/ClangSharpPInvokeGenerator/Program.Options.cs index 404b87d1..91757d0e 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.Options.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.Options.cs @@ -35,6 +35,7 @@ internal static partial class Program private static readonly string[] s_versionOptionAliases = ["--version", "-v"]; private static readonly string[] s_withAccessSpecifierOptionAliases = ["--with-access-specifier", "-was"]; private static readonly string[] s_withAttributeOptionAliases = ["--with-attribute", "-wa"]; + private static readonly string[] s_withBaseOptionAliases = ["--with-base", "-wb"]; private static readonly string[] s_withCallConvOptionAliases = ["--with-callconv", "-wcc"]; private static readonly string[] s_withClassOptionAliases = ["--with-class", "-wc"]; private static readonly string[] s_withConditionalOptionAliases = ["--with-conditional", "-wcond"]; @@ -98,6 +99,7 @@ internal static partial class Program private static readonly CommandLineOption s_versionOption = Flag(s_versionOptionAliases, "Prints the current version information for the tool and its native dependencies."); private static readonly CommandLineOption s_withAccessSpecifierNameValuePairs = Multi(s_withAccessSpecifierOptionAliases, "An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence."); private static readonly CommandLineOption s_withAttributeNameValuePairs = Multi(s_withAttributeOptionAliases, "An attribute to be added to the given remapped declaration name during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence."); + private static readonly CommandLineOption s_withBaseNameValuePairs = Multi(s_withBaseOptionAliases, "An additional base type the generated type should derive from during binding generation. Applies to structs and COM interface types. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence."); private static readonly CommandLineOption s_withCallConvNameValuePairs = Multi(s_withCallConvOptionAliases, "A calling convention to be used for the given declaration during binding generation. Supports `*` (any run) and `?` (single character) wildcards; exact matches take precedence."); private static readonly CommandLineOption s_withClassNameValuePairs = Multi(s_withClassOptionAliases, "A class to be used for the given remapped constant or function declaration name during binding generation. Supports a trailing `*` wildcard for prefix matching; an exact match takes precedence."); private static readonly CommandLineOption s_withConditional = Single(s_withConditionalOptionAliases, "A preprocessor symbol used to wrap single-file C# output in a leading '#if ' and trailing '#endif'. Useful when files can't be conditionally excluded at the project level (e.g. Unity).", valueName: "symbol"); @@ -163,6 +165,7 @@ internal static partial class Program s_versionOption, s_withAccessSpecifierNameValuePairs, s_withAttributeNameValuePairs, + s_withBaseNameValuePairs, s_withCallConvNameValuePairs, s_withClassNameValuePairs, s_withConditional, diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index e68d7270..0eb09c6d 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -168,6 +168,7 @@ public static int Run() var withConditional = s_withConditional.SingleValue; var withAccessSpecifierNameValuePairs = s_withAccessSpecifierNameValuePairs.GetValues(); var withAttributeNameValuePairs = s_withAttributeNameValuePairs.GetValues(); + var withBaseNameValuePairs = s_withBaseNameValuePairs.GetValues(); var withCallConvNameValuePairs = s_withCallConvNameValuePairs.GetValues(); var withClassNameValuePairs = s_withClassNameValuePairs.GetValues(); var withConstantFoldedValues = s_withConstantFoldedValues.GetValues(); @@ -223,6 +224,7 @@ public static int Run() ParseKeyValuePairs(remappedFieldNameValuePairs, errorList, out Dictionary remappedFieldNames); ParseKeyValuePairs(withAccessSpecifierNameValuePairs, errorList, out Dictionary withAccessSpecifiers); ParseKeyValuePairs(withAttributeNameValuePairs, errorList, out Dictionary> withAttributes); + ParseKeyValuePairs(withBaseNameValuePairs, errorList, out Dictionary> withBases); ParseKeyValuePairs(withCallConvNameValuePairs, errorList, out Dictionary withCallConvs); ParseKeyValuePairs(withClassNameValuePairs, errorList, out Dictionary withClasses); ParseKeyValuePairs(withEnumMemberStripNameValuePairs, errorList, out Dictionary withEnumMemberStrip); @@ -395,6 +397,7 @@ public static int Run() TestOutputLocation = testOutputLocation, WithAccessSpecifiers = withAccessSpecifiers, WithAttributes = withAttributes, + WithBases = withBases, WithCallConvs = withCallConvs, WithClasses = withClasses, WithConditional = withConditional, diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/BaselineTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/BaselineTest.cs index b3067444..4d0cbbac 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/BaselineTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/BaselineTest.cs @@ -58,13 +58,13 @@ protected static IEnumerable Variants() // Mirrors the full ValidateGeneratedCSharpLatestWindowsBindingsAsync surface (minus expectedOutputContents, // which now lives in a checked-in baseline) so any migrated area can express its options exactly. The // Mode/Config come from the fixture variant; additionalConfigOptions OR-in the area's positional flags. - protected Task ValidateAsync(string method, string inputContents, string? discriminator = null, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? withConditional = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null) - => ValidateCoreAsync(BaselineHarness.CaseName(method, discriminator), inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, withConditional, withGuids, withoutSetLastErrors, withoutCallConvs, withConstantFoldedValues, withoutConstantFoldedValues); + protected Task ValidateAsync(string method, string inputContents, string? discriminator = null, PInvokeGeneratorConfigurationOptions additionalConfigOptions = PInvokeGeneratorConfigurationOptions.None, string[]? excludedNames = null, IReadOnlyDictionary? remappedNames = null, IReadOnlyDictionary? withAccessSpecifiers = null, IReadOnlyDictionary>? withAttributes = null, IReadOnlyDictionary? withCallConvs = null, IReadOnlyDictionary? withClasses = null, IReadOnlyDictionary? withLibraryPaths = null, IReadOnlyDictionary? withNamespaces = null, string[]? withSetLastErrors = null, IReadOnlyDictionary? withTransparentStructs = null, IReadOnlyDictionary? withTypes = null, IReadOnlyDictionary>? withUsings = null, IReadOnlyDictionary? withPackings = null, IEnumerable? expectedDiagnostics = null, string libraryPath = DefaultLibraryPath, string[]? commandLineArgs = null, string language = "c++", string languageStandard = DefaultCppStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? withConditional = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null, IReadOnlyDictionary>? withBases = null) + => ValidateCoreAsync(BaselineHarness.CaseName(method, discriminator), inputContents, additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, withConditional, withGuids, withoutSetLastErrors, withoutCallConvs, withConstantFoldedValues, withoutConstantFoldedValues, withBases); - private async Task ValidateCoreAsync(string caseName, string inputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames, IReadOnlyDictionary? remappedFieldNames, string? withConditional, IReadOnlyDictionary? withGuids, string[]? withoutSetLastErrors, IReadOnlyCollection? withoutCallConvs, string[]? withConstantFoldedValues, string[]? withoutConstantFoldedValues) + private async Task ValidateCoreAsync(string caseName, string inputContents, PInvokeGeneratorConfigurationOptions additionalConfigOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames, IReadOnlyDictionary? remappedFieldNames, string? withConditional, IReadOnlyDictionary? withGuids, string[]? withoutSetLastErrors, IReadOnlyCollection? withoutCallConvs, string[]? withConstantFoldedValues, string[]? withoutConstantFoldedValues, IReadOnlyDictionary>? withBases) { var effectiveCommandLineArgs = BaselineHarness.WithUnixTarget(commandLineArgs, DefaultCppClangCommandLineArgs, _variant.Os); - var actual = await GenerateBindingsAsync(inputContents, _variant.Mode, _variant.ConfigOptions | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, effectiveCommandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, withConditional: withConditional, withGuids: withGuids, withoutSetLastErrors: withoutSetLastErrors, withoutCallConvs: withoutCallConvs, withConstantFoldedValues: withConstantFoldedValues, withoutConstantFoldedValues: withoutConstantFoldedValues).ConfigureAwait(false); + var actual = await GenerateBindingsAsync(inputContents, _variant.Mode, _variant.ConfigOptions | additionalConfigOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, effectiveCommandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, withConditional: withConditional, withGuids: withGuids, withoutSetLastErrors: withoutSetLastErrors, withoutCallConvs: withoutCallConvs, withConstantFoldedValues: withConstantFoldedValues, withoutConstantFoldedValues: withoutConstantFoldedValues, withBases: withBases).ConfigureAwait(false); await BaselineAssertions.AssertOrUpdateAsync(Area, caseName, _variant, actual).ConfigureAwait(false); } } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.Compatible.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.Compatible.cs new file mode 100644 index 00000000..442f7fce --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.Compatible.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace ClangSharp.Test +{ + public unsafe partial struct IThing : IThing.Interface + { + public void** lpVtbl; + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate int _DoWork(IThing* pThis); + + public int DoWork() + { + fixed (IThing* pThis = &this) + { + return Marshal.GetDelegateForFunctionPointer<_DoWork>((IntPtr)(lpVtbl[0]))(pThis); + } + } + + public interface Interface : INativeGuid + { + int DoWork(); + } + } + + public partial struct PlainThing : IDisposable + { + public int value; + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.cs new file mode 100644 index 00000000..02fa3d94 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.CSharp.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace ClangSharp.Test +{ + public unsafe partial struct IThing : IThing.Interface + { + public void** lpVtbl; + + public int DoWork() + { + return ((delegate* unmanaged[Thiscall])(lpVtbl[0]))((IThing*)Unsafe.AsPointer(ref this)); + } + + public interface Interface : INativeGuid + { + int DoWork(); + } + } + + public partial struct PlainThing : IDisposable + { + public int value; + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.Compatible.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.Compatible.xml new file mode 100644 index 00000000..675fd739 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.Compatible.xml @@ -0,0 +1,35 @@ + + + + + + void** + + + int + + IThing* + + + + int + + fixed (IThing* pThis = &this) + { + return Marshal.GetDelegateForFunctionPointer<_DoWork>((IntPtr)(lpVtbl[0]))(pThis); + } + + + + + int + + + + + + int + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.xml new file mode 100644 index 00000000..6410237f --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/WithBaseTest.Xml.xml @@ -0,0 +1,26 @@ + + + + + + void** + + + int + + return ((delegate* unmanaged[Thiscall]<IThing*, int>)(lpVtbl[0]))((IThing*)Unsafe.AsPointer(ref this)); + + + + + int + + + + + + int + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs index 1761fe99..0dd7ee9d 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs @@ -1243,4 +1243,29 @@ public Task UserDeclaredDestructorTest() return ValidateAsync(nameof(UserDeclaredDestructorTest), inputContents); } + + // A user-supplied `--with-base` entry appends extra base types: onto the nested marker `Interface` + // for a COM/vtbl type (the `IUnknown.Interface : INativeGuid` case terrafx patches by hand) and onto + // the struct itself for a plain value type. + [Test] + public Task WithBaseTest() + { + var inputContents = @"struct IThing +{ + virtual int DoWork() = 0; +}; + +struct PlainThing +{ + int value; +}; +"; + + var withBases = new Dictionary> { + ["IThing"] = ["INativeGuid"], + ["PlainThing"] = ["IDisposable"], + }; + + return ValidateAsync(nameof(WithBaseTest), inputContents, additionalConfigOptions: PInvokeGeneratorConfigurationOptions.GenerateMarkerInterfaces, withBases: withBases); + } } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs index 4a304140..9ce94d4f 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/PInvokeGeneratorTest.cs @@ -96,9 +96,9 @@ private static async Task ValidateGeneratedBindingsAsync(string inputContents, s // Convenience wrapper over GenerateBindingsWithTestOutputAsync that returns only the primary bindings text, // used by both the inline-string harness and the checked-in baseline harness (neither asserts test output). - internal static async Task GenerateBindingsAsync(string inputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, string? withConditional = null, string[]? withEqualityMembers = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null) + internal static async Task GenerateBindingsAsync(string inputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, string? withConditional = null, string[]? withEqualityMembers = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null, IReadOnlyDictionary>? withBases = null) { - var (actualOutputContents, _) = await GenerateBindingsWithTestOutputAsync(inputContents, outputMode, configOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip, withEnumMemberStrip, withConditional, withEqualityMembers, withGuids, withoutSetLastErrors, withoutCallConvs, withConstantFoldedValues, withoutConstantFoldedValues).ConfigureAwait(false); + var (actualOutputContents, _) = await GenerateBindingsWithTestOutputAsync(inputContents, outputMode, configOptions, excludedNames, remappedNames, withAccessSpecifiers, withAttributes, withCallConvs, withClasses, withLibraryPaths, withNamespaces, withSetLastErrors, withTransparentStructs, withTypes, withUsings, withPackings, expectedDiagnostics, libraryPath, commandLineArgs, language, languageStandard, remappedTypeNames, remappedFieldNames, typePrefixToStrip, withEnumMemberStrip, withConditional, withEqualityMembers, withGuids, withoutSetLastErrors, withoutCallConvs, withConstantFoldedValues, withoutConstantFoldedValues, withBases).ConfigureAwait(false); return actualOutputContents; } @@ -107,7 +107,7 @@ internal static async Task GenerateBindingsAsync(string inputContents, P // tests are written to separate output streams (keyed off the config's OutputLocation vs TestOutputLocation), // so both can be validated independently. TestOutputContents is the empty string when no test output was // generated (either because tests weren't requested or nothing in the input produced any). - internal static async Task<(string OutputContents, string TestOutputContents)> GenerateBindingsWithTestOutputAsync(string inputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, string? withConditional = null, string[]? withEqualityMembers = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null) + internal static async Task<(string OutputContents, string TestOutputContents)> GenerateBindingsWithTestOutputAsync(string inputContents, PInvokeGeneratorOutputMode outputMode, PInvokeGeneratorConfigurationOptions configOptions, string[]? excludedNames, IReadOnlyDictionary? remappedNames, IReadOnlyDictionary? withAccessSpecifiers, IReadOnlyDictionary>? withAttributes, IReadOnlyDictionary? withCallConvs, IReadOnlyDictionary? withClasses, IReadOnlyDictionary? withLibraryPaths, IReadOnlyDictionary? withNamespaces, string[]? withSetLastErrors, IReadOnlyDictionary? withTransparentStructs, IReadOnlyDictionary? withTypes, IReadOnlyDictionary>? withUsings, IReadOnlyDictionary? withPackings, IEnumerable? expectedDiagnostics, string libraryPath, string[]? commandLineArgs, string language, string languageStandard, IReadOnlyDictionary? remappedTypeNames = null, IReadOnlyDictionary? remappedFieldNames = null, string? typePrefixToStrip = null, IReadOnlyDictionary? withEnumMemberStrip = null, string? withConditional = null, string[]? withEqualityMembers = null, IReadOnlyDictionary? withGuids = null, string[]? withoutSetLastErrors = null, IReadOnlyCollection? withoutCallConvs = null, string[]? withConstantFoldedValues = null, string[]? withoutConstantFoldedValues = null, IReadOnlyDictionary>? withBases = null) { Assert.That(DefaultInputFileName, Does.Exist); commandLineArgs ??= DefaultCppClangCommandLineArgs; @@ -135,6 +135,7 @@ internal static async Task GenerateBindingsAsync(string inputContents, P TestOutputLocation = generatesTests ? Path.GetRandomFileName() : null, WithAccessSpecifiers = withAccessSpecifiers, WithAttributes = withAttributes, + WithBases = withBases, WithCallConvs = withCallConvs, WithoutCallConvs = withoutCallConvs, WithClasses = withClasses, From c646e1132396ee20ef72bdac5ee0f4b443d32bdf Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 19 Jul 2026 07:54:29 -0700 Subject: [PATCH 4/5] Fix Microsoft vftable index for virtual destructors clang 22's MicrosoftVTableContext reports Index == 0 for a virtual destructor's vftable location regardless of its declaration position, so the destructor collides with whatever occupies slot 0 whenever it is not declared first (e.g. CHttpModule's trailing ~CHttpModule after 30 On* notification methods, which regressed from VtblIndex 30 to 0). The vftable layout itself is ordered correctly, so recover the true slot by counting the function-pointer components that precede the deleting-destructor component; non-slot components are skipped so the count stays in the same space as MethodVFTableLocation::Index. Validated by building libClangSharp against LLVM 22.1.8 locally and exercising it through both the interop layer and the generator: trailing, middle, leading, overloaded, and pure-virtual destructor placements now all resolve to the correct slot. The checked-in generator golden tests still load the pinned prebuilt libClangSharp package (which has the bug), so a baseline regression test cannot be added until a new native package is published. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sources/libClangSharp/ClangSharp.cpp | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sources/libClangSharp/ClangSharp.cpp b/sources/libClangSharp/ClangSharp.cpp index b0429ee2..5c1f66fd 100644 --- a/sources/libClangSharp/ClangSharp.cpp +++ b/sources/libClangSharp/ClangSharp.cpp @@ -109,6 +109,36 @@ int64_t getVtblIdx(const GlobalDecl& d) if (MicrosoftVTableContext* MSVTC = dyn_cast(VTC)) { MethodVFTableLocation ML = MSVTC->getMethodVFTableLocation(d); + + // clang 22's Microsoft vftable layout reports Index == 0 for a virtual destructor + // regardless of its declaration position, so it collides with whatever occupies slot 0 + // whenever the destructor is not declared first. The vftable layout itself is ordered + // correctly, so recover the true slot by counting the function-pointer components that + // precede the deleting-destructor component (non-slot components such as offsets are + // skipped so the count stays in the same space as MethodVFTableLocation::Index). + if (const CXXDestructorDecl* DD = dyn_cast(CMD)) { + const VTableLayout& layout = MSVTC->getVFTableLayout(RD, ML.VFPtrOffset); + int64_t slot = 0; + + for (const VTableComponent& component : layout.vtable_components()) { + switch (component.getKind()) { + case VTableComponent::CK_DeletingDtorPointer: + if (component.getDestructorDecl()->getCanonicalDecl() == DD->getCanonicalDecl()) { + return slot; + } + slot++; + break; + case VTableComponent::CK_FunctionPointer: + case VTableComponent::CK_UnusedFunctionPointer: + case VTableComponent::CK_CompleteDtorPointer: + slot++; + break; + default: + break; + } + } + } + return ML.Index; } From 5b9cedc6c5fa0aa8f39557ce04c72f91e43d6d7d Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 19 Jul 2026 08:07:06 -0700 Subject: [PATCH 5/5] Bump libClangSharp native packages to 22.1.8.3 The vftable destructor-index fix changes sources/libClangSharp, so the regenerate-native workflow will produce new libClangSharp.runtime.* packages. Bump the revision so they don't collide with the published 22.1.8.2. The managed libClangSharp pin in Directory.Packages.props stays at 22.1.8.2 (the published package) until 22.1.8.3 is published; bumping the pin, regenerating any affected baselines, and adding the destructor-slot regression test are a follow-up once the native package is available. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../libClangSharp.runtime.linux-arm64.nuspec | 2 +- .../libClangSharp.runtime.linux-x64.nuspec | 2 +- .../libClangSharp.runtime.osx-arm64.nuspec | 2 +- .../libClangSharp.runtime.win-arm64.nuspec | 2 +- .../libClangSharp.runtime.win-x64.nuspec | 2 +- .../libClangSharp/libClangSharp/libClangSharp.nuspec | 2 +- packages/libClangSharp/libClangSharp/runtime.json | 10 +++++----- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/libClangSharp/libClangSharp.runtime.linux-arm64/libClangSharp.runtime.linux-arm64.nuspec b/packages/libClangSharp/libClangSharp.runtime.linux-arm64/libClangSharp.runtime.linux-arm64.nuspec index cb33cb03..60edc18c 100644 --- a/packages/libClangSharp/libClangSharp.runtime.linux-arm64/libClangSharp.runtime.linux-arm64.nuspec +++ b/packages/libClangSharp/libClangSharp.runtime.linux-arm64/libClangSharp.runtime.linux-arm64.nuspec @@ -2,7 +2,7 @@ libClangSharp.runtime.linux-arm64 - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp.runtime.linux-x64/libClangSharp.runtime.linux-x64.nuspec b/packages/libClangSharp/libClangSharp.runtime.linux-x64/libClangSharp.runtime.linux-x64.nuspec index 4ef7d465..a053ccc6 100644 --- a/packages/libClangSharp/libClangSharp.runtime.linux-x64/libClangSharp.runtime.linux-x64.nuspec +++ b/packages/libClangSharp/libClangSharp.runtime.linux-x64/libClangSharp.runtime.linux-x64.nuspec @@ -2,7 +2,7 @@ libClangSharp.runtime.linux-x64 - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp.runtime.osx-arm64/libClangSharp.runtime.osx-arm64.nuspec b/packages/libClangSharp/libClangSharp.runtime.osx-arm64/libClangSharp.runtime.osx-arm64.nuspec index b1e813f4..f30fcf59 100644 --- a/packages/libClangSharp/libClangSharp.runtime.osx-arm64/libClangSharp.runtime.osx-arm64.nuspec +++ b/packages/libClangSharp/libClangSharp.runtime.osx-arm64/libClangSharp.runtime.osx-arm64.nuspec @@ -2,7 +2,7 @@ libClangSharp.runtime.osx-arm64 - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp.runtime.win-arm64/libClangSharp.runtime.win-arm64.nuspec b/packages/libClangSharp/libClangSharp.runtime.win-arm64/libClangSharp.runtime.win-arm64.nuspec index fabe3499..959c33a9 100644 --- a/packages/libClangSharp/libClangSharp.runtime.win-arm64/libClangSharp.runtime.win-arm64.nuspec +++ b/packages/libClangSharp/libClangSharp.runtime.win-arm64/libClangSharp.runtime.win-arm64.nuspec @@ -2,7 +2,7 @@ libClangSharp.runtime.win-arm64 - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp.runtime.win-x64/libClangSharp.runtime.win-x64.nuspec b/packages/libClangSharp/libClangSharp.runtime.win-x64/libClangSharp.runtime.win-x64.nuspec index ba12773b..14aeeeea 100644 --- a/packages/libClangSharp/libClangSharp.runtime.win-x64/libClangSharp.runtime.win-x64.nuspec +++ b/packages/libClangSharp/libClangSharp.runtime.win-x64/libClangSharp.runtime.win-x64.nuspec @@ -2,7 +2,7 @@ libClangSharp.runtime.win-x64 - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp/libClangSharp.nuspec b/packages/libClangSharp/libClangSharp/libClangSharp.nuspec index df8de877..e65a11f5 100644 --- a/packages/libClangSharp/libClangSharp/libClangSharp.nuspec +++ b/packages/libClangSharp/libClangSharp/libClangSharp.nuspec @@ -2,7 +2,7 @@ libClangSharp - 22.1.8.2 + 22.1.8.3 .NET Foundation and Contributors .NET Foundation and Contributors true diff --git a/packages/libClangSharp/libClangSharp/runtime.json b/packages/libClangSharp/libClangSharp/runtime.json index c74df919..7cc5d4c5 100644 --- a/packages/libClangSharp/libClangSharp/runtime.json +++ b/packages/libClangSharp/libClangSharp/runtime.json @@ -2,27 +2,27 @@ "runtimes": { "linux-arm64": { "libClangSharp": { - "libClangSharp.runtime.linux-arm64": "22.1.8.2" + "libClangSharp.runtime.linux-arm64": "22.1.8.3" } }, "linux-x64": { "libClangSharp": { - "libClangSharp.runtime.linux-x64": "22.1.8.2" + "libClangSharp.runtime.linux-x64": "22.1.8.3" } }, "osx-arm64": { "libClangSharp": { - "libClangSharp.runtime.osx-arm64": "22.1.8.2" + "libClangSharp.runtime.osx-arm64": "22.1.8.3" } }, "win-arm64": { "libClangSharp": { - "libClangSharp.runtime.win-arm64": "22.1.8.2" + "libClangSharp.runtime.win-arm64": "22.1.8.3" } }, "win-x64": { "libClangSharp": { - "libClangSharp.runtime.win-x64": "22.1.8.2" + "libClangSharp.runtime.win-x64": "22.1.8.3" } } }