From 3d10a88613313ea49d3c9a7c26c9c415a2a27c7a Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 18 Jul 2026 19:13:03 -0700 Subject: [PATCH 1/5] Place the restored namespace qualifier after leading cv-qualifiers The clang 22 namespace-qualifier restoration prepended `Namespace::` to the front of the whole declarator, so a `const` pointer parameter came out as the malformed `Ns::const Point *` instead of `const Ns::Point *`. Insert the qualifier after any leading `const`/`volatile` tokens so it lands on the type name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Naming.cs | 25 ++++++++++++++++++- ...iedNativeTypeNameIsPreservedTest.CSharp.cs | 6 +++++ ...ifiedNativeTypeNameIsPreservedTest.Xml.xml | 6 +++++ .../Baseline/NamespaceNativeTypeNameTest.cs | 5 +++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs index 82ad5b63..4834dc64 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs @@ -213,7 +213,30 @@ private static string GetNamespaceQualifiedNativeTypeName(Decl decl, string nati } var qualifier = qualifierBuilder.ToString(); - return nativeTypeName.StartsWith(qualifier, StringComparison.Ordinal) ? nativeTypeName : qualifier + nativeTypeName; + + // The qualifier belongs on the type name, after any leading cv-qualifiers, so a `const` + // pointer stays `const Ns::Point *` rather than the malformed `Ns::const Point *`. + var offset = 0; + + while (true) + { + var rest = nativeTypeName.AsSpan(offset); + + if (rest.StartsWith("const ", StringComparison.Ordinal)) + { + offset += 6; + } + else if (rest.StartsWith("volatile ", StringComparison.Ordinal)) + { + offset += 9; + } + else + { + break; + } + } + + return nativeTypeName.AsSpan(offset).StartsWith(qualifier, StringComparison.Ordinal) ? nativeTypeName : nativeTypeName.Insert(offset, qualifier); } private string GetCursorQualifiedName(NamedDecl namedDecl, bool truncateParameters = false) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs index 52febb1b..02744594 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs @@ -23,6 +23,12 @@ public unsafe partial struct Holder [NativeTypeName("Ns::Point *")] public Point* pointPtr; + [NativeTypeName("const Ns::Point *")] + public Point* constPointPtr; + + [NativeTypeName("const Ns::Real *")] + public float* constRealPtr; + [NativeTypeName("Ns::Real")] public float r; diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml index 74382d9a..37d2d0ff 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml @@ -31,6 +31,12 @@ Point* + + Point* + + + float* + float diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs index 61ac6576..96456f42 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs @@ -17,7 +17,8 @@ public NamespaceNativeTypeNameTest(BaselineVariant variant) : base(variant) // clang 22's type printer omits the enclosing C++ namespace from a reference spelled from within that // same namespace, where-as older releases always spelled it. The generator restores the dropped // `Namespace::` prefix from the decl so the emitted `NativeTypeName` keeps the fully qualified source - // spelling for typedef, record (including by-pointer), and enum references and stays version-stable. + // spelling for typedef, record (including by-pointer and const-qualified by-pointer), and enum + // references, placing the qualifier after any leading cv-qualifiers, and stays version-stable. [Test] public Task NamespaceQualifiedNativeTypeNameIsPreservedTest() { @@ -41,6 +42,8 @@ struct Holder { Point point; Point* pointPtr; + const Point* constPointPtr; + const Real* constRealPtr; Real r; Status status; }; From f6a2a4a4628d31c0c8f4c8bc4585c76afeccbe9f Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 18 Jul 2026 19:23:08 -0700 Subject: [PATCH 2/5] Don't qualify a nested type referenced from within its own container The clang 22 nested-type qualification unconditionally prefixed a nested type with its containing record(s), so a field referencing a sibling nested type came out as `Outer.Inner` even though `Inner` is directly in scope. Stop the qualification at the scope shared with the reference site so within-container references stay unqualified while cross-scope references keep their prefix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.TypeResolution.cs | 18 +++++++++++++++--- ...iedWithinContainer.CSharp.Latest.Windows.cs | 14 ++++++++++++++ .../NestedTypeReferenceTest.cs | 18 ++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NestedTypeReference/NestedTypeIsNotQualifiedWithinContainer.CSharp.Latest.Windows.cs diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs index 472d7291..2426e529 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.TypeResolution.cs @@ -385,12 +385,24 @@ private string GetTypeName(Cursor? cursor, Cursor? context, Type rootType, Type result.typeName = GetRemappedCursorName(tagType.Decl, out _, skipUsing: true); // A nested type needs to be qualified by its containing type(s) so it resolves - // when referenced from another scope (e.g. `A::Inner` -> `A.Inner`). Namespaces - // are flattened away, so only walk the enclosing record decls. + // when referenced from another scope (e.g. `A::Inner` -> `A.Inner`). A reference + // from within a containing type sees the nested type directly, so only qualify + // up to the scope shared with the reference site. Namespaces are flattened away, + // so only walk the enclosing record decls. + + var referenceScope = new HashSet(); + + for (var refContext = (cursor as Decl)?.DeclContext ?? (context as Decl)?.DeclContext; refContext is Decl refParent; refContext = refParent.DeclContext) + { + if (refParent is RecordDecl) + { + _ = referenceScope.Add(refParent); + } + } var qualificationBuilder = new StringBuilder(); - for (var declContext = tagType.Decl.DeclContext; declContext is RecordDecl parentRecordDecl; declContext = parentRecordDecl.DeclContext) + for (var declContext = tagType.Decl.DeclContext; declContext is RecordDecl parentRecordDecl && !referenceScope.Contains(parentRecordDecl); declContext = parentRecordDecl.DeclContext) { var parentRecordDeclName = GetRemappedCursorName(parentRecordDecl, out _, skipUsing: true); _ = qualificationBuilder.Insert(0, '.').Insert(0, EscapeName(parentRecordDeclName)); diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NestedTypeReference/NestedTypeIsNotQualifiedWithinContainer.CSharp.Latest.Windows.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NestedTypeReference/NestedTypeIsNotQualifiedWithinContainer.CSharp.Latest.Windows.cs new file mode 100644 index 00000000..fbae69c7 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NestedTypeReference/NestedTypeIsNotQualifiedWithinContainer.CSharp.Latest.Windows.cs @@ -0,0 +1,14 @@ +namespace ClangSharp.Test +{ + public unsafe partial struct Outer + { + public Inner field; + + public Inner* fieldPtr; + + public partial struct Inner + { + public int value; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs index 18178287..27176b0a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/NestedTypeReferenceTest.cs @@ -31,6 +31,24 @@ struct B { A::Inner inner; }; +"; + + return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents); + } + + [Test] + public Task NestedTypeIsNotQualifiedWithinContainer() + { + var inputContents = @"struct Outer +{ + struct Inner + { + int value; + }; + + Inner field; + Inner* fieldPtr; +}; "; return ValidateGeneratedCSharpLatestWindowsBaselineAsync(inputContents); From 12a03fc8bd1a2b5bce39021eade4cd1e82829603 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 18 Jul 2026 19:38:27 -0700 Subject: [PATCH 3/5] Keep ref readonly on computed-pointer GUID #define macros A `#define X (*(const GUID*)(n))` macro (as `MAKEDIPROP` expands to) emits a `ref`-returning body, but the copy-constructor Reference check only matched a `DeclRefExpr` alias, so the return type dropped `ref readonly` and left a by-value `Guid` paired with a `=> ref` body. Also treat a pointer dereference arg as ref-returnable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.VisitVarDecl.cs | 10 ++++++---- ...GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs | 5 ++++- .../GuidDefineAliasRefReadonlyTest.CSharp.cs | 5 ++++- .../GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml | 7 +++++++ .../GuidDefineAliasRefReadonlyTest.Xml.xml | 7 +++++++ .../Baseline/StructDeclarationTest.cs | 6 ++++-- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs index 903b9f74..88f7357d 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs @@ -204,10 +204,12 @@ private void VisitVarDecl(VarDecl varDecl) return; } - // clang wraps an alias to another constant (e.g. `#define IID_X IID_Y`) in a - // copy-constructor. The referenced constant has backing storage, so keep the - // `ref readonly` alias rather than emitting a by-value copy. - if (IsStmtAsWritten(cxxConstructExpr.Args[0], out _, removeParens: true)) + // clang wraps an alias to another constant (e.g. `#define IID_X IID_Y`) or a + // pointer dereference (e.g. `#define X MAKEDIPROP(n)` -> `*(const GUID*)(n)`) in a + // copy-constructor. Both reference backing storage, so keep the `ref readonly` alias + // rather than emitting a by-value copy. + if (IsStmtAsWritten(cxxConstructExpr.Args[0], out _, removeParens: true) + || (IsStmtAsWritten(cxxConstructExpr.Args[0], out var unaryOperator, removeParens: true) && (unaryOperator.Opcode == CXUnaryOperator_Deref))) { flags |= ValueFlags.Reference; } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs index 6761a303..02ab43b2 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs @@ -27,11 +27,14 @@ public partial struct IInternet public int x; } - public static partial class Methods + public static unsafe partial class Methods { [NativeTypeName("#define IID_IOInet IID_IInternet")] public static ref readonly Guid IID_IOInet => ref IID_IInternet; + [NativeTypeName("#define DIPROP_BUFFERSIZE (*(const GUID *)(1))")] + public static ref readonly Guid DIPROP_BUFFERSIZE => ref unchecked(*(Guid*)(1)); + public static ref readonly Guid IID_IInternet { get diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs index 036c8e7e..07f02001 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs @@ -33,11 +33,14 @@ public partial struct IInternet public int x; } - public static partial class Methods + public static unsafe partial class Methods { [NativeTypeName("#define IID_IOInet IID_IInternet")] public static ref readonly Guid IID_IOInet => ref IID_IInternet; + [NativeTypeName("#define DIPROP_BUFFERSIZE (*(const GUID *)(1))")] + public static ref readonly Guid DIPROP_BUFFERSIZE => ref unchecked(*(Guid*)(1)); + public static ref readonly Guid IID_IInternet { get diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml index 2f4b9c96..47a2a50a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml @@ -27,6 +27,13 @@ ref IID_IInternet + + Guid + + + ref (*(Guid*)(1)) + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml index 1ebab883..c6317947 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml @@ -33,6 +33,13 @@ ref IID_IInternet + + Guid + + + ref (*(Guid*)(1)) + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs index 97faf36b..25a7b610 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs @@ -604,8 +604,9 @@ public Task GuidDefineAliasRefReadonlyTest() return Task.CompletedTask; } - // A `#define IID_X IID_Y` alias over a `ref readonly Guid` IID must keep the `ref readonly` return so it - // stays a zero-copy alias; dropping it leaves a by-value `Guid` return paired with a `=> ref` body. + // A `#define IID_X IID_Y` alias and a `#define X (*(const GUID*)(n))` computed-pointer deref (as `MAKEDIPROP` + // expands to) both produce a `ref`-returning body, so both must keep the `ref readonly` return; dropping it + // leaves a by-value `Guid` return paired with a `=> ref` body. var inputContents = @"#define DECLSPEC_UUID(x) __declspec(uuid(x)) #define EXTERN_C extern ""C"" @@ -628,6 +629,7 @@ struct DECLSPEC_UUID(""79eac9e0-baf9-11ce-8c82-00aa004ba90b"") IInternet EXTERN_C const IID IID_IInternet; #define IID_IOInet IID_IInternet +#define DIPROP_BUFFERSIZE (*(const GUID *)(1)) "; var remappedNames = new Dictionary { ["_GUID"] = "Guid", ["GUID"] = "Guid" }; From 168c3d50fd23bcedccce9e1f88004d73e9787ad9 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 18 Jul 2026 19:48:57 -0700 Subject: [PATCH 4/5] Skip elaborated tag keywords and __unaligned before the namespace qualifier The restored `Namespace::` prefix belongs at the start of the nested-name-specifier, after the full run of leading type decl-specifiers. cv-qualifiers alone were skipped, so an elaborated `struct`/ `enum` specifier or `__unaligned` still produced a malformed `Ns::struct Point *`. Skip the whole closed set (cv-qualifiers, the elaborated class-key, and `__unaligned`). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Naming.cs | 27 ++++++++++++------- ...iedNativeTypeNameIsPreservedTest.CSharp.cs | 9 +++++++ ...ifiedNativeTypeNameIsPreservedTest.Xml.xml | 9 +++++++ .../Baseline/NamespaceNativeTypeNameTest.cs | 8 ++++-- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs index 4834dc64..05cd815b 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs @@ -190,6 +190,11 @@ private static string GetNamespaceQualifiedNativeTypeName(Type type, string nati return GetNamespaceQualifiedNativeTypeName(decl, nativeTypeName); } + // Tokens the clang type printer can spell before a type's nested-name-specifier: cv-qualifiers, + // elaborated tag keywords, and the MS `__unaligned` type qualifier. A restored `Namespace::` + // qualifier belongs on the type name, after any leading run of these. + private static readonly string[] s_leadingTypeQualifiers = ["const ", "volatile ", "struct ", "class ", "union ", "enum ", "__unaligned "]; + private static string GetNamespaceQualifiedNativeTypeName(Decl decl, string nativeTypeName) { // clang 22's type printer omits the enclosing C++ namespace from a reference when a @@ -214,23 +219,27 @@ private static string GetNamespaceQualifiedNativeTypeName(Decl decl, string nati var qualifier = qualifierBuilder.ToString(); - // The qualifier belongs on the type name, after any leading cv-qualifiers, so a `const` - // pointer stays `const Ns::Point *` rather than the malformed `Ns::const Point *`. + // 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 *`. var offset = 0; while (true) { var rest = nativeTypeName.AsSpan(offset); + var matched = false; - if (rest.StartsWith("const ", StringComparison.Ordinal)) - { - offset += 6; - } - else if (rest.StartsWith("volatile ", StringComparison.Ordinal)) + foreach (var prefix in s_leadingTypeQualifiers) { - offset += 9; + if (rest.StartsWith(prefix, StringComparison.Ordinal)) + { + offset += prefix.Length; + matched = true; + break; + } } - else + + if (!matched) { break; } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs index 02744594..42d1b31b 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.CSharp.cs @@ -29,6 +29,15 @@ public unsafe partial struct Holder [NativeTypeName("const Ns::Real *")] public float* constRealPtr; + [NativeTypeName("struct Ns::Point *")] + public Point* elabPointPtr; + + [NativeTypeName("const struct Ns::Point *")] + public Point* constElabPointPtr; + + [NativeTypeName("enum Ns::Status *")] + public Status* elabStatusPtr; + [NativeTypeName("Ns::Real")] public float r; diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml index 37d2d0ff..4655eae4 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/NamespaceNativeTypeName/NamespaceQualifiedNativeTypeNameIsPreservedTest.Xml.xml @@ -37,6 +37,15 @@ float* + + Point* + + + Point* + + + Status* + float diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs index 96456f42..dddb9052 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/NamespaceNativeTypeNameTest.cs @@ -17,8 +17,9 @@ public NamespaceNativeTypeNameTest(BaselineVariant variant) : base(variant) // clang 22's type printer omits the enclosing C++ namespace from a reference spelled from within that // same namespace, where-as older releases always spelled it. The generator restores the dropped // `Namespace::` prefix from the decl so the emitted `NativeTypeName` keeps the fully qualified source - // spelling for typedef, record (including by-pointer and const-qualified by-pointer), and enum - // references, placing the qualifier after any leading cv-qualifiers, and stays version-stable. + // spelling for typedef, record (including by-pointer, const-qualified, and elaborated `struct`/`enum` + // specifiers), and enum references, placing the qualifier after any leading cv-qualifiers or tag + // keywords, and stays version-stable. [Test] public Task NamespaceQualifiedNativeTypeNameIsPreservedTest() { @@ -44,6 +45,9 @@ struct Holder Point* pointPtr; const Point* constPointPtr; const Real* constRealPtr; + struct Point* elabPointPtr; + const struct Point* constElabPointPtr; + enum Status* elabStatusPtr; Real r; Status status; }; From b2f43fc3e8d5ee1cd9908f7426aee532eb9fb6d9 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 18 Jul 2026 20:05:14 -0700 Subject: [PATCH 5/5] Expose computed-pointer GUID macros as a pointer instead of a byref A `#define X (*(const GUID*)(n))` macro (as `MAKEDIPROP` expands to) targets an arbitrary address, so a managed byref to it is not valid. Emit it as a `Guid*` returning the pointer rather than a `ref readonly` alias. A `#define IID_X IID_Y` alias still references real storage and keeps its `ref readonly` form. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.VisitStmt.cs | 37 ++++++++++++------- .../PInvokeGenerator.VisitVarDecl.cs | 15 ++++++-- ...eAliasRefReadonlyTest.CSharp.Compatible.cs | 2 +- .../GuidDefineAliasRefReadonlyTest.CSharp.cs | 2 +- ...ineAliasRefReadonlyTest.Xml.Compatible.xml | 6 +-- .../GuidDefineAliasRefReadonlyTest.Xml.xml | 6 +-- .../Baseline/StructDeclarationTest.cs | 6 +-- 7 files changed, 46 insertions(+), 28 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs index 99d5eb64..c2b7f843 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs @@ -749,27 +749,38 @@ private void VisitCXXConstructExpr(CXXConstructExpr cxxConstructExpr) if (args.Count != 0) { - if (isUnmanagedConstant) + if (isUnmanagedConstant + && IsStmtAsWritten(args[0], out var derefOperator, removeParens: true) + && (derefOperator.Opcode == CXUnaryOperator_Deref)) { - outputBuilder.Write("ref "); + // A computed-pointer GUID macro (e.g. `MAKEDIPROP(n)` -> `*(const GUID*)(n)`) is exposed + // as the pointer itself, so emit its operand rather than a byref to arbitrary memory. + Visit(derefOperator.SubExpr); } - - var needsComma = false; - - for (var i = 0; i < args.Count; i++) + else { - var arg = args[i]; - - if (needsComma && (arg is not CXXDefaultArgExpr)) + if (isUnmanagedConstant) { - outputBuilder.Write(", "); + outputBuilder.Write("ref "); } - Visit(arg); + var needsComma = false; - if (arg is not CXXDefaultArgExpr) + for (var i = 0; i < args.Count; i++) { - needsComma = true; + var arg = args[i]; + + if (needsComma && (arg is not CXXDefaultArgExpr)) + { + outputBuilder.Write(", "); + } + + Visit(arg); + + if (arg is not CXXDefaultArgExpr) + { + needsComma = true; + } } } } diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs index 88f7357d..4c2f75a5 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs @@ -206,13 +206,20 @@ private void VisitVarDecl(VarDecl varDecl) // clang wraps an alias to another constant (e.g. `#define IID_X IID_Y`) or a // pointer dereference (e.g. `#define X MAKEDIPROP(n)` -> `*(const GUID*)(n)`) in a - // copy-constructor. Both reference backing storage, so keep the `ref readonly` alias - // rather than emitting a by-value copy. - if (IsStmtAsWritten(cxxConstructExpr.Args[0], out _, removeParens: true) - || (IsStmtAsWritten(cxxConstructExpr.Args[0], out var unaryOperator, removeParens: true) && (unaryOperator.Opcode == CXUnaryOperator_Deref))) + // copy-constructor. + if (IsStmtAsWritten(cxxConstructExpr.Args[0], out _, removeParens: true)) { + // An alias references the other constant's backing storage, so keep the + // `ref readonly` alias rather than emitting a by-value copy. flags |= ValueFlags.Reference; } + else if (IsStmtAsWritten(cxxConstructExpr.Args[0], out var unaryOperator, removeParens: true) && (unaryOperator.Opcode == CXUnaryOperator_Deref)) + { + // The dereference targets an arbitrary address (typically illegal to read), + // so expose the pointer itself; a managed byref to it would not be valid. + typeName += '*'; + _topLevelClassIsUnsafe[className] = true; + } flags |= ValueFlags.Copy; } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs index 02ab43b2..e42403e3 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.Compatible.cs @@ -33,7 +33,7 @@ public static unsafe partial class Methods public static ref readonly Guid IID_IOInet => ref IID_IInternet; [NativeTypeName("#define DIPROP_BUFFERSIZE (*(const GUID *)(1))")] - public static ref readonly Guid DIPROP_BUFFERSIZE => ref unchecked(*(Guid*)(1)); + public static Guid* DIPROP_BUFFERSIZE => unchecked((Guid*)(1)); public static ref readonly Guid IID_IInternet { diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs index 07f02001..14e840b6 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.CSharp.cs @@ -39,7 +39,7 @@ public static unsafe partial class Methods public static ref readonly Guid IID_IOInet => ref IID_IInternet; [NativeTypeName("#define DIPROP_BUFFERSIZE (*(const GUID *)(1))")] - public static ref readonly Guid DIPROP_BUFFERSIZE => ref unchecked(*(Guid*)(1)); + public static Guid* DIPROP_BUFFERSIZE => unchecked((Guid*)(1)); public static ref readonly Guid IID_IInternet { diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml index 47a2a50a..9fd3b683 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.Compatible.xml @@ -20,7 +20,7 @@ int - + Guid @@ -28,10 +28,10 @@ - Guid + Guid* - ref (*(Guid*)(1)) + ((Guid*)(1)) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml index c6317947..c8340391 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/StructDeclaration/GuidDefineAliasRefReadonlyTest.Xml.xml @@ -26,7 +26,7 @@ int - + Guid @@ -34,10 +34,10 @@ - Guid + Guid* - ref (*(Guid*)(1)) + ((Guid*)(1)) diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs index 25a7b610..4a201a01 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/StructDeclarationTest.cs @@ -604,9 +604,9 @@ public Task GuidDefineAliasRefReadonlyTest() return Task.CompletedTask; } - // A `#define IID_X IID_Y` alias and a `#define X (*(const GUID*)(n))` computed-pointer deref (as `MAKEDIPROP` - // expands to) both produce a `ref`-returning body, so both must keep the `ref readonly` return; dropping it - // leaves a by-value `Guid` return paired with a `=> ref` body. + // A `#define IID_X IID_Y` alias references another constant's storage, so it stays a `ref readonly Guid` + // alias. A `#define X (*(const GUID*)(n))` computed-pointer deref (as `MAKEDIPROP` expands to) targets an + // arbitrary address, so it is exposed as a `Guid*` pointer rather than an invalid managed byref. var inputContents = @"#define DECLSPEC_UUID(x) __declspec(uuid(x)) #define EXTERN_C extern ""C""