Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add support for properties in COM source generator - #128869

Merged
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties
Jun 17, 2026
Merged

Add support for properties in COM source generator#128869
AaronRobinsonMSFT merged 33 commits into
dotnet:mainfrom
AaronRobinsonMSFT:com_properties

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Jun 1, 2026

Copy link
Copy Markdown
Member

Closes#96502
Closes#122708

This pull request introduces support for C# properties on [GeneratedComInterface]-attributed interfaces in the COM Interface Generator, aligning the generated ABI with built-in COM interop and enabling more natural C# interface design. The most important changes are:

Support for Properties on [GeneratedComInterface]:

  • Added support for declaring C# properties and indexers (with get/set) on [GeneratedComInterface] interfaces, mapping each accessor to a vtable slot in the same order and layout as [ComVisible(true)] managed interfaces. Properties can use the new modifier for explicit shadowing, and [MarshalUsing] attributes can now be applied directly to properties. The init keyword on accessors is not supported.

Marshalling Attribute Updates:

  • Extended [MarshalUsing] attribute usage to allow placement on properties, in addition to parameters and return values, enabling property/indexer-level marshalling configuration.

Shadowing and Inheritance Rules:

  • Updated the generator and documentation to allow explicit shadowing of inherited members (methods and properties) using the new modifier, with clear vtable slot allocation rules. Previous restrictions on the new modifier were removed.
Generated RCW / CCW samples for properties and indexers

Common notes for all five samples:

  • IID byte arrays in ThrowExceptionForHR are the C++/COM GUID byte order of each interface's Guid.
  • __vtable[N] is the 0-based slot index (0/1/2 are QueryInterface/AddRef/Release); the per-accessor slot numbers below come from the generated InterfaceImplementationVtable struct.
  • [MemberFunction] is the calling convention attribute that makes the unmanaged signature ABI-compatible with a C++ vtable method (i.e., a __thiscall-shaped this first-arg).

1. IProperties.IntPropertyint property, get + set

C# declaration: int IntProperty { get; set; } — getter is slot 3, setter is slot 4.

RCW (managed caller → native vtable)

intglobal::SharedTypes.ComInterfaces.IProperties.IntProperty
{get{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,&__retVal);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((global::System.Runtime.InteropServices.Marshalling.IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,value);}global::System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(global::System.IntPtr)__this);global::System.GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.IntProperty;__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_IntProperty(ComWrappers.ComInterfaceDispatch*__this_native,intvalue){global::SharedTypes.ComInterfaces.IProperties@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.IntProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note: getter writes the int return into the caller-supplied int* (HRESULT pattern); setter takes the int by value. Both sides are pure blittable — no pinning, no allocations.


2. IProperties.StringPropertystring property (UTF-16), get + set

C# declaration: string StringProperty { get; set; } — getter is slot 9, setter is slot 10.

RCW (managed caller → native vtable)

stringglobal::SharedTypes.ComInterfaces.IProperties.StringProperty
{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{{__invokeRetVal=__target(__this,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IProperties));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,int>)__vtable[10]);{int__invokeRetVal;
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value)){__invokeRetVal=__target(__this,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([131,10,232,248,247,83,144,79,128,9,198,79,212,5,244,69]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW (native caller → managed implementation)

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IProperties@this=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);__invokeRetVal=@this.StringProperty;__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer to hand back to the native caller; ownership transfers.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_StringProperty(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IProperties@this=default;stringvalue=default;int__retVal=default;try{// Native string -> managed copy (the caller still owns the buffer).value=Utf16StringMarshaller.ConvertToManaged(__value_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IProperties>(__this_native);@this.StringProperty=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the ownership protocol for the returned string: CCW get_StringProperty calls Utf16StringMarshaller.ConvertToUnmanaged (which CoTaskMemAllocs a new buffer), and the RCW frees it in finally via Utf16StringMarshaller.Free. The setter just pins the managed string in-place — no allocation, no free.


3. IIndexers.this[int i] — single-int indexer, get + set

C# declaration: int this[int i] { get; set; } — getter is slot 3, setter is slot 4.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int*,int>)__vtable[3]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int>)__vtable[4]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note the index parameter int i precedes the trailing out-pointer on the getter and precedes the value parameter on the setter — same parameter order as a [ComVisible(true)] managed indexer projected through the built-in CCW.


4. IIndexers.this[int i, int j] — two-int indexer, get + set

C# declaration: int this[int i, int j] { get; set; } — getter is slot 5, setter is slot 6.

RCW

intglobal::SharedTypes.ComInterfaces.IIndexers.this[inti,intj]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int*,int>)__vtable[5]);{int__retVal;int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,&__retVal);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,int,int,int,int>)__vtable[6]);{int__invokeRetVal;{__invokeRetVal=__target(__this,i,j,value);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,int*__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;refint__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;int__invokeRetVal=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[i,j];__retVal=0;// S_OK__invokeRetValUnmanaged=__invokeRetVal;}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,inti,intj,intvalue){global::SharedTypes.ComInterfaces.IIndexers@this=default;int__retVal=default;try{@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[i,j]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

Note both managed C# overloads emit identically-named IL methods (get_Item / set_Item) — they're disambiguated entirely by parameter list, exactly like ordinary method overloads. They get their own consecutive slot pair (5/6) appended after the one-arg indexer's pair (3/4).


5. IIndexers.this[string key] — string-keyed, string-valued indexer, get + set

C# declaration: string this[string key] { get; set; } — getter is slot 9, setter is slot 10.

RCW

stringglobal::SharedTypes.ComInterfaces.IIndexers.this[stringkey]{get{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort**,int>)__vtable[9]);{bool__invokeSucceeded=default;string__retVal=default;ushort*__retVal_native=default;int__invokeRetVal=default;try{
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,&__retVal_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);__invokeSucceeded=true;__retVal=Utf16StringMarshaller.ConvertToManaged(__retVal_native);}finally{if(__invokeSucceeded){// Free the callee-allocated buffer for the returned string.Utf16StringMarshaller.Free(__retVal_native);}}return__retVal;}}set{var(__this,__vtable)=((IUnmanagedVirtualMethodTableProvider)this).GetVirtualMethodTableInfoForKey(typeof(global::SharedTypes.ComInterfaces.IIndexers));var__target=((delegate* unmanaged[MemberFunction]<void*,ushort*,ushort*,int>)__vtable[10]);{int__invokeRetVal;// Pin the value first, then the key — pins must dominate the call.
fixed (void*__value_native=&Utf16StringMarshaller.GetPinnableReference(value))
fixed (void*__key_native=&Utf16StringMarshaller.GetPinnableReference(key)){__invokeRetVal=__target(__this,(ushort*)__key_native,(ushort*)__value_native);}Marshal.ThrowExceptionForHR(__invokeRetVal,new([157,92,14,124,62,95,224,73,140,154,26,138,42,44,155,26]),(IntPtr)__this);GC.KeepAlive(this);}}}

CCW

[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_get_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort**__invokeRetValUnmanaged__param){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;refushort*__invokeRetValUnmanaged=ref*__invokeRetValUnmanaged__param;string__invokeRetVal=default;int__retVal=default;try{// Caller-owned key buffer -> managed copy.key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);__invokeRetVal=@this[key];__retVal=0;// S_OK// Allocate a fresh UTF-16 buffer for the return value; ownership transfers to the caller.__invokeRetValUnmanaged=(ushort*)Utf16StringMarshaller.ConvertToUnmanaged(__invokeRetVal);}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}[UnmanagedCallersOnlyAttribute(CallConvs=new[]{typeof(CallConvMemberFunction)})]internalstaticintABI_set_Item(ComWrappers.ComInterfaceDispatch*__this_native,ushort*__key_native,ushort*__value_native){global::SharedTypes.ComInterfaces.IIndexers@this=default;stringkey=default;stringvalue=default;int__retVal=default;try{value=Utf16StringMarshaller.ConvertToManaged(__value_native);key=Utf16StringMarshaller.ConvertToManaged(__key_native);@this=ComWrappers.ComInterfaceDispatch.GetInstance<global::SharedTypes.ComInterfaces.IIndexers>(__this_native);@this[key]=value;__retVal=0;// S_OK}catch(System.Exception__exception){__retVal=ExceptionAsHResultMarshaller<int>.ConvertToUnmanaged(__exception);}return__retVal;}

AaronRobinsonMSFTand others added 9 commits May 29, 2026 11:12
Enables ordinary C# property syntax on [GeneratedComInterface]-attributed
interfaces. Each property accessor is treated as a vtable method: get
first, then set, in declaration order, both PreserveSig-style.
Phase 1 surface (intentionally strict; modifiers, init, indexers,
expression bodies, and auto-property initializers will be considered as
follow-ups):
- read/write `T Foo { get; set; }`
- read-only `T Foo { get; }`
- write-only `T Foo { set; }`
- accessor-level accessibility (e.g. `private set`)
- static instance properties
Implementation (Option B):
- `ComMethodInfo.GetMethodsFromInterface` includes property accessors;
the property declaration carries the SignatureContext source for both
accessors so property-level marshalling attributes apply to both.
- Per-accessor stubs emit `PropertyDeclarationSyntax` with a single
accessor; the writer in `WriteInterfaceImplementation` merges get+set
halves of the same property into one explicit-interface property
before output.
- CCW trampolines invoke the managed implementation via property syntax
(`@this.Foo` / `@this.Foo = value`).
- `StubMemberKind { Method, PropertyGetter, PropertySetter }` lives on
the base `IncrementalMethodStubGenerationContext` (it is symbol-derived,
classified by the shared `ClassifyMemberKind(IMethodSymbol)` helper).
- `TemplateName` on the source-available context returns the user-visible
member name — the method identifier as-is for methods, and the property
name with the Roslyn `get_`/`set_` prefix stripped for accessors.
- New `UnmanagedToManagedStubGenerator.GenerateStubBodyForProperty` and
`GeneratedStatements.CreateForProperty` encapsulate property-shape
body codegen inside the shared library, symmetric with the method
invoke shape; the property-accessing expression is the only new input
the caller passes in.
Test additions:
- Renamed `InterfaceWithPropertiesAndEvents` to `InterfaceWithEvents`
(events-only, still expects the existing event diagnostic).
- Added `InterfaceWithProperties` with read/write, read-only, write-only,
and a static instance property; this snippet is exercised by
`Compiles.ComInterfaceSnippetsToCompile`.
Inherited properties from a base `[GeneratedComInterface]` are not yet
emitted (the two inherited-method writer loops skip property accessors
with TODO comments). Tracked as a Phase 2 follow-up.
Refs: dotnet#96502
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 2 (items 1 & 2 of 3) for dotnet#96502.
Adds the managed surface, native shim, and xUnit e2e tests that exercise
the property codegen introduced in the previous commit. Three new files:
- tests/Common/ComInterfaces/IProperties.cs
Managed interface + [GeneratedComClass] impl. Covers int (R/W, RO, WO),
Guid, string (UTF-16), and self-referential IProperties? Self.
- tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.cs
Hand-rolled ComWrappers subclass with a 13-slot vtable feeding
[UnmanagedCallersOnly] accessor thunks. Uses [DNNE.C99DeclCode] to
declare a placeholder struct so DNNE can emit valid C for Guid params
it does not natively know about; the thunks are vtable targets and are
never called from native C.
- tests/ComInterfaceGenerator.Tests/IPropertiesTests.cs
Two test families: in-process RcwAroundCcw_* (CCW + RCW through
StrategyBasedComWrappers) and NativeShim_* (LibraryImport into the
native shim). String tests are [Theory] with empty, ASCII, and
Unicode-surrogate-pair inputs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a [GeneratedComInterface] inherits from another [GeneratedComInterface] that exposes properties, the generator now synthesizes the user-facing shadow members as PropertyDeclarationSyntax rather than separate accessor MethodDeclarationSyntax nodes, eliminating CS0109 (does not hide) and CS0686 (accessor cannot implement interface member) errors on derived implementation classes.
Generator:
* gen/ComInterfaceGenerator/ComInterfaceGenerator.cs:
- Centralize accessor merging into EmitMemberHonoringPropertyMerge so the
declared-interface and inherited-shadow loops share one buffered-getter
flush path.
- Rewrite the inherited-shadow loop to handle PropertyDeclarationSyntax
(retargeting the explicit-interface specifier to the derived interface).
- Add SynthesizeUnreachableInheritedPropertyAccessor so inherited
unreachable forwarders collapse get+set pairs into a single property.
* gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.cs:
- Centralize accessor-naming helpers (IsPropertyAccessorMethodName plus
ReadOnlySpan<char> get_/set_ prefixes) so the convention is not
sprinkled across the generator.
Tests:
* tests/Common/ComInterfaces/IDerivedProperties.cs (new) -- IDerivedProperties
managed surface (inheriting IProperties) plus [GeneratedComClass]
DerivedProperties implementation.
* tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.cs
(new) -- hand-rolled ComWrappers native shim with 13- and 18-slot vtables
for end-to-end RCW coverage of the derived scenario.
* tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.cs (new) --
in-process (RCW-around-CCW) tests plus native-shim RCW tests covering
inherited, derived-only, and QI-down-to-base scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the in-accessor [return:]/[param:] form for marshalling attributes
on GeneratedComInterface properties via a new IPropertyMarshalling
fixture (read-write, read-only, and write-only properties using
TrackedIntMarshaller with Interlocked counters) and three xUnit cases
that verify the marshaller is invoked on the appropriate accessors via
an in-process StrategyBasedComWrappers round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AttributeTargets.Property to MarshalUsingAttribute so the attribute
can be authored once on a COM property and apply to both its getter and
setter accessor stubs. When an accessor also carries [MarshalUsing] on
its return value or value-parameter, the accessor-level attribute wins;
otherwise the property-level attribute is propagated.
Implementation:
- MarshalUsingAttribute / ref source: add Property to AttributeTargets.
- SignatureContext.GenerateTypeInformation: for property-accessor
methods, dedup-merge the property's attributes into each element's
attribute list (accessor-wins per attribute type).
- DefaultMarshallingInfoParser.Create: route property accessors to a
new EmptyElementInfoProvider so that peer-element lookups
(e.g. [MarshalUsing(CountElementName=...)]) cleanly return no result
for signatures where peers are meaningless (getter has no params;
setter has only the implicit value parameter).
- MethodSignatureElementInfoProvider ctor asserts that property
accessors are not valid arguments, locking in the invariant.
Tests cover both accessor-level and property-level [MarshalUsing] and
verify the precedence rule for getter, setter, and round-trip.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssociatedAttributes to ComMethodInfo to carry attributes from a
related symbol (currently the source IPropertySymbol for property
accessors) alongside the accessor's own attributes. The derived-interface
shadow forwarder emits these via a denylist filter that strips
[MarshalUsing] and [MarshalAs] (the shadow is a pure C# forwarder so
marshalling attributes have no effect there). For non-property branches
a Debug.Assert enforces that AssociatedAttributes is empty.
Also factors the AttributeData -> SequenceEqualImmutableArray<AttributeInfo>
projection into a shared CreateAttributeInfoArray helper using the
zero-extra-copy MoveToImmutable pattern, and aligns the two accessor
helpers (AddExternallyDefinedAccessor / AddPropertyAccessor) on the same
inline construction style.
Adds e2e tests in IPropertiesTests.cs over a new IShadowAttributePropagation
fixture verifying that user attributes flow to derived shadows and that
property-level [MarshalUsing] is stripped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lifetime fixes
- Diagnose unsupported modifiers on COM properties (static, extern, init,
required) with a single targeted SYSLIB diagnostic, plus a separate
diagnostic for indexers. Updates ComMethodInfo / GeneratorDiagnostics
and refreshes Strings.resx + 14 xlf files. Adds Compiles/CompileFails
cases over the new snippets in CodeSnippets.
- Runtime tests for property shadowing via the 'new' keyword on a
derived [GeneratedComInterface]: new IPropertyShadowing.cs in
tests/Common/ComInterfaces plus end-to-end coverage in
IDerivedPropertiesTests.cs (round-trip get/set on both shadowed and
shadowing accessors).
- Wrap RCW-creating helpers (CreateRcwAroundCcw,
CreateRcwOverNativeShim, CreatePropertyMarshallingRcwAroundCcw,
CreatePropertyShadowingRcwAroundCcw) in try/finally so the input
IUnknown* is always released after GetOrCreateObjectForComInstance
takes ownership. Validated under DOTNET_GCStress=C.
- Suppress ApiCompat CP0015 for the purely additive Property target on
MarshalUsingAttribute (already shipped in commit 6288174).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Interface]
Treat a user-supplied body on an interface method or property accessor as
a signal to skip COM vtable emission. The member behaves as a managed-only
default implementation, letting callers wrap COM ABI methods with a
managed property or method abstraction without consuming a vtable slot.
Generator changes:
- MemberShapeOutcome tri-state classifies each member as Abi,
DefaultImplemented, or Invalid.
- AnalyzeMethodShape / AnalyzePropertyShape inspect declaring syntax for
expression bodies and accessor bodies.
- DIM members are excluded from the per-method ABI pipeline (no vtable
slot, no explicit-interface stub). Inherited DIM accessors are guarded
out of AddExternallyDefinedAccessor via an IsAbstract check.
- GetDiagnosticIfInvalidMethodForGeneration no longer rejects bodied
methods -- that path is now the DIM trigger.
Diagnostics:
- SYSLIB1091 (error) PropertyAccessorsMustBeAllOrNothing -- a property
cannot mix abstract and bodied accessors.
- SYSLIB1091 (warning) MarshalAttributeOnDefaultImplementedComInterfaceMember
-- [MarshalUsing] / [MarshalAs] on a DIM member, accessor, or parameter
has no ABI effect.
Tests:
- 5 new ComInterfaceGenerator.Unit.Tests snippets cover the positive DIM
cases and each new diagnostic.
- IDimMembers fixture (Common/ComInterfaces) exercises a DIM property
wrapper over paired ABI accessors, a read-only / write-only DIM
property, and a DIM helper method.
- 6 new functional tests in ComInterfaceGenerator.Tests round-trip the
DIM members through CCW + RCW and assert via raw vtable inspection
that DIM members occupy zero vtable slots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new design doc Properties.md covering vtable layout, allowed and
disallowed declaration shapes, marshalling-attribute handling, the DIM
opt-out, and the dispnameclash interaction with get_ / set_ method
names. Extends DerivedComInterfaces.md with property-aware shadow
emission and the user-facing 'new' modifier, and appends a .NET 11
section to Compatibility.md covering the new properties-on-COM-interface
support and the rejection of 'init' on the COM declaration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the COM Interface Generator to treat C# properties on [GeneratedComInterface] interfaces as COM ABI members (mapping each accessor to vtable slots), adds property-level marshalling attribute handling, and updates tests/docs to cover property inheritance, explicit shadowing (new), and default-implemented members.

Changes:

  • Add COM source-generator support for abstract interface properties by generating/getting accessor stubs and emitting property-shaped managed implementations while preserving COM vtable ordering.
  • Extend [MarshalUsing] to be applicable to properties and flow property-level marshalling attributes into accessor stub generation.
  • Add/expand unit + integration tests (native shim + CCW/RCW roundtrips) and update diagnostics/resources + design docs.
Show a summary per file
FileDescription
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/Properties.csAdds a native-shim CCW/vtable implementation used to validate generated RCW property accessor layout/marshalling.
src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/DerivedProperties.csAdds derived-interface native shim with base+derived vtables to validate inherited accessor layout and dispatch.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IShadowPropertyAttributePropagation.csIntroduces interfaces/attributes to validate which property attributes propagate to generated shadow members.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyShadowing.csAdds explicit new property shadowing test interface + implementation to validate independent slots.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IPropertyMarshalling.csAdds property/accessor marshalling scenarios for [MarshalUsing] placement and override precedence.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IProperties.csDefines a property-heavy [GeneratedComInterface] used across tests and native shim.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDimMembers.csAdds DIM method/property examples to validate “no vtable slot” behavior for default implementations.
src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/IDerivedProperties.csDefines derived properties interface/class for inheritance/vtable-prefix validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.csAdds compilation-only snippets covering properties, new shadowing, and DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CompileFails.csAdds generator diagnostics coverage for unsupported property shapes (indexer/init/etc.) and marshal attributes on DIM members.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/CodeSnippets.csProvides new source snippets for supported/unsupported property and DIM patterns.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IPropertiesTests.csAdds end-to-end tests for property ABI, marshalling attribute propagation, and shadow-property attribute behavior.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDimMembersTests.csAdds DIM behavior tests including raw vtable slot-count validation.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Tests/IDerivedPropertiesTests.csAdds inheritance tests (QI, vtable prefix equality, native shim dispatch, new shadowed property slots).
src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.csExpands public MarshalUsingAttribute AttributeUsage to include Property.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/UnmanagedToManagedStubGenerator.csAdds property-accessor-specific unmanaged-to-managed stub body generation.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SignatureContext.csMerges property-level attributes into accessor signature marshalling parsing with accessor-level override semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/MethodSignatureElementInfoProvider.csAsserts property accessors aren’t routed through the standard element-info provider.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/GeneratedStatements.csAdds invoke-statement generation for property get/set semantics.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/EmptyElementInfoProvider.csAdds an element-info provider that always fails peer-element lookup (used for property accessors).
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/DefaultMarshallingInfoParser.csRoutes property accessors to the empty element-info provider to disable peer-element count lookups.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds new diagnostic strings for property/indexer/DIM marshalling diagnostics.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds new localized resource entries (Czech).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds new localized resource entries (German).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds new localized resource entries (Spanish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds new localized resource entries (French).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds new localized resource entries (Italian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds new localized resource entries (Japanese).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds new localized resource entries (Korean).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds new localized resource entries (Polish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds new localized resource entries (Portuguese - Brazil).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds new localized resource entries (Russian).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds new localized resource entries (Turkish).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds new localized resource entries (Chinese - Simplified).
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds new localized resource entries (Chinese - Traditional).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.csExtends stub context creation to track whether a stub targets a method vs. property accessor.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.csEmits property-shaped managed-to-native stubs and property-aware unmanaged-to-managed invocation logic.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/IncrementalMethodStubGenerationContext.csIntroduces StubMemberKind + property accessor name handling and exposes a property-aware TemplateName.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratorDiagnostics.csAdds diagnostic descriptors for unsupported property declarations, indexers, DIM marshalling attributes, and mixed accessor shapes.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/GeneratedStubCodeContext.csGeneralizes stored stub node type to MemberDeclarationSyntax to support properties.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.csTreats properties as accessor method pairs, adds DIM handling, and wires property diagnostics/attribute association.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.csMerges generated get/set halves into single property declarations and emits property shadows for derived interfaces.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshalling/MarshalUsingAttribute.csUpdates runtime implementation AttributeUsage for MarshalUsingAttribute to include Property.
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xmlSuppresses baseline API-compat noise for the MarshalUsingAttribute AttributeUsage change.
docs/design/libraries/ComInterfaceGenerator/Properties.mdNew design doc describing supported property surface, vtable layout, DIM rules, and attribute behavior.
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.mdUpdates derived-interface doc to reflect property shadowing and user-defined new member shadowing rules.
docs/design/libraries/ComInterfaceGenerator/Compatibility.mdAdds .NET 11 compatibility notes for properties, DIM members, and new shadowing on members.

Copilot's findings

  • Files reviewed: 47/47 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 3, 2026 15:52
Extends the property support shipped in the prior commits to cover C#
indexers. Each accessor still gets its own vtable slot in source order;
the existing property emission paths were generalised rather than
duplicated, so all the same RCW / CCW marshalling, derived-shadow,
HRESULT-propagation, and modifier-validation behaviour applies to
indexers automatically.
Generator changes:
- Extend StubMemberKind with IndexerGetter / IndexerSetter and a small
StubMemberKindExtensions surface (IsPropertyOrIndexerAccessor,
IsAccessorSetter, IsIndexerAccessor) so dispatch in
VirtualMethodPointerStubGenerator and ComInterfaceGenerator can treat
property and indexer accessors uniformly while still distinguishing
them where the shape differs (setter value parameter position, shadow
emission syntax).
- ClassifyMemberKind now switches on
(MethodKind, AssociatedSymbol is IPropertySymbol { IsIndexer }) so an
accessor whose AssociatedSymbol is an indexer is routed to the new
IndexerGetter / IndexerSetter kinds instead of being misclassified as
a property accessor.
- Widen AddPropertyAccessorInfos / AnalyzePropertyShape / merge identity
/ shadow emission from PropertyDeclarationSyntax to
BasePropertyDeclarationSyntax so the same code handles both
PropertyDeclarationSyntax and IndexerDeclarationSyntax. Initializer
and ExpressionBody (which only live on subclasses) are reached via
subtype-typed switches. Merge identity for overloaded indexers also
compares the parameter list to keep distinct overloads apart.
- WriteIUnknownDerivedOriginalInterfacePart emits user-facing shadows
for indexers including the index parameter list / argument list and
preserves explicit-interface specifiers and [IndexerName].
- GeneratedStatements: setter value parameter is now resolved via
.Last() instead of .Single() so indexer setters
(idx0, ..., idxN, value) work alongside property setters (value).
- UnmanagedToManagedStubGenerator gains GenerateStubBodyForIndexer
(emits `@this[idx...] = value` or `__retVal = @this[idx...]`) and the
existing GenerateStubBody is renamed to GenerateStubBodyForMethod for
consistency with the new method.
- [IndexerName] flows through to derived-interface shadows
automatically: EmitPropertyAttributes already strips only
marshalling-related attributes, and Roslyn includes [IndexerName] in
IPropertySymbol.GetAttributes().
- Delete the now-untrue IndexerNotSupportedOnGeneratedComInterface
diagnostic from GeneratorDiagnostics + Strings.resx + 13 xlf files.
Tests:
- tests/Common/ComInterfaces/IIndexers.cs: IIndexers with five slots
(this[int], this[int,int], read-only this[long], write-only
this[short], and a string-keyed string-valued this[string]) plus an
Indexers impl using primitive arithmetic and a Dictionary<string,
string> backing store. Also IRenamedIndexer with
[IndexerName("Element")] and its impl.
- tests/Common/ComInterfaces/IDerivedIndexers.cs: derived-interface
shadow coverage split into four interfaces by [IndexerName] (C#
forbids mixed IndexerNames in one type, CS0668): default-named base
+ derived, and [IndexerName("Foo")] base + derived.
- tests/ComInterfaceGenerator.Tests/IIndexersTests.cs: RCW-around-CCW
round-trip theory tests for every shape (single param, two params,
read-only, write-only, string-keyed including empty key, empty value,
and non-ASCII / CJK / Hiragana keys to exercise UTF-16 marshalling on
both ends), IL accessor-name reflection assertions for [IndexerName],
and PropertyInfo assertions for derived shadows.
- Unit tests: five positive snippets (InterfaceWithIndexers,
InterfaceWithRenamedIndexer, DerivedInterfaceWithNewIndexer,
DerivedInterfaceWithNewRenamedIndexer,
InterfaceWithDefaultImplementedIndexer) added to CodeSnippets and
registered in Compiles. Four negative snippets covering extern, init,
mixed accessor bodies, and marshalling attributes on default-
implemented indexers added to CodeSnippets and CompileFails. Note
that the mixed-accessor-bodies case for indexers raises CS0501 (not
the CS0525 that the equivalent property case raises).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end RCW path validation that drives the generated
IIndexers RCW against an externally-defined native vtable, complementing
the in-process RCW-around-CCW tests committed in the prior change.
New NativeExports/ComInterfaceGenerator/Indexers.cs hand-rolls an
11-slot vtable matching the generator's source-declaration order
(QueryInterface, AddRef, Release, then get/set pairs for this[int],
this[int,int], read-only this[long], write-only this[short],
this[string]). The 8 ABI thunks dispatch to a backing IIndexers
implementation via ComInterfaceDispatch.GetInstance<T> and handle UTF-16
ownership transfer for the string-keyed indexer accessors
(GetItemString returns a Utf16StringMarshaller-allocated buffer that the
generated RCW frees in its finally block).
IIndexersTests.cs gains the matching NativeShim_* theory tests with the
same input data as the existing CCW round-trip tests so any divergence
reflects a marshalling mismatch rather than data-shape skew. Total
indexer functional coverage moves from 21 to 37 tests; full
ComInterfaceGenerator.Tests suite is 185/185.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Properties.md
- Widen H1 and intro to cover indexers alongside properties.
- Drop indexers from Non-goals and Disallowed lists.
- Add an Indexers section covering per-accessor slot mapping, default
get_Item / set_Item signatures, read-only and write-only forms,
overloading by index-parameter type, [IndexerName] propagation through
derived shadows (with the CS0668 same-name-per-type constraint), and
default-implemented indexer behavior including CS0501 / CS0525.
Compatibility.md
- Replace the stale 'Indexers are not supported' line with an Indexers
subsection under .NET 11 that mirrors the existing Properties
subsection and links to the full design in Properties.md.
DerivedComInterfaces.md
- Mention indexers explicitly in the shadowing summary alongside methods
and properties.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT marked this pull request as ready for review June 3, 2026 23:04
CopilotAI review requested due to automatic review settings June 3, 2026 23:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 51/51 changed files
  • Comments generated: 5

AaronRobinsonMSFTand others added 2 commits June 3, 2026 16:36
In SignatureContext.GenerateTypeInformation, attributes captured from the
associated IPropertySymbol were being merged into every parameter's attribute
set and into the accessor's return-type attribute set unconditionally. For an
indexer like
[MarshalUsing(typeof(X))] T this[I i] { get; set; }
the merge would propagate [MarshalUsing] onto the index parameter on both
get_Item and set_Item, and onto the setter's void return.
Restrict the merge to the accessor's value surface: the getter's return type,
or the setter's last (value) parameter. Index parameters on indexer accessors
and the setter's void return are not value surfaces and retain only their
own attribute set.
Add a regression test that uses [MarshalUsing(typeof(TrackedIntMarshaller))]
on int this[int] { get; set; } and asserts exact marshaller invocation counts.
The same int type for index and value means a buggy generator that propagates
the attribute to every parameter would invoke the marshaller four times per
set+get round-trip instead of the expected two.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erator API
UnmanagedToManagedStubGenerator.GenerateStubBody(ExpressionSyntax) was
renamed to GenerateStubBodyForMethod in this PR to make room for the
property/indexer overloads, but JSExportGenerator still called the old
name, breaking the JS source generator build on every leg that compiles
System.Runtime.InteropServices.JavaScript.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 18:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 58/58 changed files
  • Comments generated: 1

AaronRobinsonMSFTand others added 3 commits June 15, 2026 15:04
The ComInterfaceGenerator builds a Dictionary<ComMethodInfo, IMethodSymbol>
to ship symbols (which aren't cache-safe for incremental generation) alongside
the value-record keys that flow through the pipeline. For externally-defined
methods Syntax is null, so previously the key collapsed to MethodName plus a
few attribute bits, which collided whenever two unrelated external interfaces
in the same compilation declared a same-named method.
The pre-existing per-overload parameter-types fingerprint only disambiguated
overloads within the same containing type and degenerated to the empty string
for parameterless methods, so the disjoint-base case in dotnet#122708 still threw
'An item with the same key has already been added' during dictionary insertion.
Replace the fingerprint with the symbol's documentation comment id (with a
fully-qualified display-string fallback for the rare symbols that lack one).
The doc-comment id encodes containing type chain, member kind, IL name,
parameter types, generic arity, and EII slashes, giving a single key that
disambiguates every pair of distinct symbols. The field is entirely
generator-internal and is never emitted to generated source, so the change is
neither a binary nor a source break for any consumer of the generator.
Adds three regression tests covering the customer's scenario: both bases
implemented on one type, and each base implemented standalone.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TrackedIntMarshaller is a static class with mutable static counters
(Created, FreeCalled, etc.). The marshaller is required to be a static class
by the [CustomMarshaller] contract, so the counters cannot be instance state.
IPropertiesTests and IIndexersTests both exercise interfaces that use it, and
xUnit defaults to one collection per test class with collections running in
parallel — which lets the two classes race on the counters and flake the
'invokes marshaller on value surface only' assertions intermittently.
Put both classes in a shared collection named after the marshaller so xUnit
serializes them. Tests within each class continue to run sequentially as
before; only cross-class parallelism between these two classes is affected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 15, 2026 22:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 3

AaronRobinsonMSFTand others added 2 commits June 15, 2026 16:18
The EmitMemberHonoringPropertyMerge pipeline buffers a getter and pairs it
with the immediately-following setter to emit a single explicit-interface
indexer declaration. The 'same target' check for indexers previously compared
only parameter types, ignoring modifiers ('ref', 'in', 'out', 'ref readonly').
Two indexer overloads that differ only by parameter modifier occupy distinct
vtable slots, so a getter from one and a setter from the other arriving
consecutively in vtable order would mis-merge into one declaration with the
wrong ABI mapping.
Rename HaveSameParameterTypes to HaveSameParameterSignatures and compare the
modifier token kinds in addition to the type spelling. Adds a generator unit
test that asserts two distinct indexer declarations are emitted for
'this[int x]' / 'this[in int x]' overloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The diagnostic suggested moving 'count-only configuration' to the property
declaration as a workaround, which is misleading for the CountElementName
form: property and indexer accessors are routed through
EmptyElementInfoProvider, so CountElementName-based peer lookups do not
resolve even when the attribute is applied at the property level.
Reword the description and message to name 'ConstantElementCount' and
'ElementIndirectionDepth' explicitly as the property-level-movable forms
and call out that 'CountElementName' is not supported on property or
indexer accessors. The trigger list keeps mentioning CountElementName
because applying it on an accessor still produces this diagnostic.
xlf files re-synchronized to pick up the new English source strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AaronRobinsonMSFT
AaronRobinsonMSFT enabled auto-merge (squash) June 16, 2026 16:20
CopilotAI review requested due to automatic review settings June 16, 2026 23:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 60/60 changed files
  • Comments generated: 7

@AaronRobinsonMSFT

Copy link
Copy Markdown
MemberAuthor

/ba-g All unrelated failures.

@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 5a4c890 into dotnet:mainJun 17, 2026
159 of 168 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the com_properties branch June 17, 2026 18:13
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
Closes#96502Closes#122708
This pull request introduces support for C# properties on
`[GeneratedComInterface]`-attributed interfaces in the COM Interface
Generator, aligning the generated ABI with built-in COM interop and
enabling more natural C# interface design. The most important changes
are:
**Support for Properties on `[GeneratedComInterface]`:**
- Added support for declaring C# properties and indexers (with
`get`/`set`) on `[GeneratedComInterface]` interfaces, mapping each
accessor to a vtable slot in the same order and layout as
`[ComVisible(true)]` managed interfaces. Properties can use the `new`
modifier for explicit shadowing, and `[MarshalUsing]` attributes can now
be applied directly to properties. The `init` keyword on accessors is
not supported.
**Marshalling Attribute Updates:**
- Extended `[MarshalUsing]` attribute usage to allow placement on
properties, in addition to parameters and return values, enabling
property/indexer-level marshalling configuration.
**Shadowing and Inheritance Rules:**
- Updated the generator and documentation to allow explicit shadowing of
inherited members (methods and properties) using the `new` modifier,
with clear vtable slot allocation rules. Previous restrictions on the
`new` modifier were removed.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

4 participants

@AaronRobinsonMSFT@jtschuster@elinor-fung