Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 14, 2025

Fix NativeMarshalling attribute override for COM interfaces

Plan

  • Understand the current code flow and identify where NativeMarshallingAttribute is not being respected for COM interfaces
  • Modify ComInterfaceMarshallingInfoProvider to check for NativeMarshallingAttribute on the type
  • Add test to verify NativeMarshallingAttribute is respected for COM interfaces in LibraryImport
  • Add test to verify NativeMarshallingAttribute is respected for COM interface parameters
  • Build and test the changes
  • Validate all tests pass
  • Add behavioral tests to ComInterfaceGenerator.Tests
  • Simplify native export implementation
  • Cache COM pointer and verify unique marshalling behavior
  • Address code review feedback
  • Move test to LibraryImportGenerator.Tests
  • Run code review
  • Run security scan

Changes Made

  1. MarshallingInfoParser.cs & ComInterfaceMarshallingInfoProvider.cs: Improved implementation by the maintainer to iterate through parsers in order (NativeMarshallingAttributeParser before ComInterfaceMarshallingInfoProvider) rather than iterating through attributes, ensuring NativeMarshallingAttribute is always checked first.

  2. Unit Tests: Added compilation tests to verify the generators handle NativeMarshalling correctly

  3. Behavioral Tests:

    • IUniqueMarshalling.cs: Created test interface with [NativeMarshalling(typeof(UniqueComInterfaceMarshaller<IUniqueMarshalling>))]
    • UniqueMarshalling.cs: Simplified native export using StrategyBasedComWrappers and GeneratedComClass, with cached pointer to return same COM object
    • NativeMarshallingAttributeTests.cs (LibraryImportGenerator.Tests): Tests LibraryImport marshalling - calling method twice returns different managed instances for the same COM object
    • NativeMarshallingAttributeTests.cs (ComInterfaceGenerator.Tests): Tests COM interface method behavior - methods returning interface type return new managed instances

Test Results

  • All ComInterfaceGenerator.Unit.Tests passed ✓
  • All LibraryImportGenerator.Unit.Tests passed ✓
  • LibraryImportGenerator.Tests NativeMarshallingAttributeTests passed ✓
  • ComInterfaceGenerator.Tests NativeMarshallingAttributeTests passed ✓

Issue Summary

When a COM interface has both [GeneratedComInterface] and [NativeMarshalling(typeof(UniqueComInterfaceMarshaller<IFoo>))], the generator should use the custom marshaller specified in NativeMarshallingAttribute. Previously, it always used the default ComInterfaceMarshaller<T>.

The fix ensures that when NativeMarshallingAttribute is present on a COM interface, it takes precedence over the default marshaller by ensuring the parser order checks NativeMarshallingAttributeParser before ComInterfaceMarshallingInfoProvider.

Original prompt

This section details on the original issue you should resolve

<issue_title>Interop source generators do not respect [NativeMarshalling(typeof(TMarshaller))] for COM interfaces</issue_title>
<issue_description>In the COM interface generator and LibraryImport generator, we should be able to specify [NativeMarshalling(typeof(UniqueComInterfaceMarshaller<IFoo>))] to instruct the generators to use that marshaller. However, it doesn't override the default marshaller for COM interfaces.

In the example below, the marshalling of foo parameters should use UniqueComInterfaceMarshaller, but use ComInterfaceMarshaller.

[GeneratedComInterface]
[GUID(...)]
[NativeMarshalling(typeof(UniqueComInterfaceMarshaller<IFoo>))]
internal partial interface IFoo
{
    string GetMessage();
    void DoWorkTogether(IFoo foo);
}

public static class PInvokes
{
    [LibraryImport("lib")]
    bool TryGetFoo(out IFoo foo);
}

This shouldn't require changing code outside of src/libraries/System.Runtime.InteropServices.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Modified ComInterfaceMarshallingInfoProvider to check for NativeMarshallingAttribute on COM interfaces and defer to that parser when present. Added tests to verify the fix works for both ComInterfaceGenerator and LibraryImportGenerator.

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
@jtschuster
Copy link
Member

@copilot you need to add behavioral tests to ComInterfaceGenerator.Tests.csproj. Add a test that gets the same ComInterface instance from a LibraryImportMethod twice and validate that they are not the same instance. Also add a method on the interface that returns a COM interface. The implementation should return 'this'. In the test, validate that the returned instance is a new object, not the same cached instance.

Added tests to ComInterfaceGenerator.Tests that verify:
1. Getting the same COM interface twice returns unique instances when using NativeMarshallingAttribute with UniqueComInterfaceMarshaller
2. COM interface methods returning the same interface type return new managed instances, not cached ones

Created IUniqueMarshalling interface with NativeMarshallingAttribute, native exports, and comprehensive tests.

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Nov 17, 2025

@copilot you need to add behavioral tests to ComInterfaceGenerator.Tests.csproj. Add a test that gets the same ComInterface instance from a LibraryImportMethod twice and validate that they are not the...

Added behavioral tests in commit 3d67609. The tests verify:

  1. GetSameComInterfaceTwiceReturnsUniqueInstances: Calls LibraryImport method twice and validates returned instances are different objects
  2. MethodReturningComInterfaceReturnsUniqueInstance: Tests a COM interface method that returns this, validates it returns a new managed object rather than the cached instance

Created IUniqueMarshalling interface with [NativeMarshalling(typeof(UniqueComInterfaceMarshaller<T>))], native exports, and test implementation.

Copilot finished work on behalf of jtschuster November 17, 2025 18:53
@jtschuster
Copy link
Member

@copilot You should be able to use StrategyBasedComWrappers in UniqueMarshalling.CreateComObject without having to create a new ComWrappers implementation. Delete MyComWrapper and use StrategyBasedComWrappers instead. Create a SharedTypes.ComInterfaces.UniqueMarshalling instance instead of ImplementingObject.

Removed custom MyComWrapper implementation and ImplementingObject class. Now using StrategyBasedComWrappers directly with SharedTypes.ComInterfaces.UniqueMarshalling instance, which leverages the GeneratedComClass attribute to handle the COM vtable automatically.

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Nov 17, 2025

@copilot You should be able to use StrategyBasedComWrappers in UniqueMarshalling.CreateComObject without having to create a new ComWrappers implementation. Delete MyComWrapper and use StrategyBasedCom...

Simplified in commit 1a37329. Removed MyComWrapper and ImplementingObject classes. Now using StrategyBasedComWrappers directly with SharedTypes.ComInterfaces.UniqueMarshalling instance, which uses the GeneratedComClass attribute to automatically handle the COM vtable.

…parser order, not arbitrary attribute order.
@jtschuster jtschuster marked this pull request as ready for review November 17, 2025 21:17
Copilot AI review requested due to automatic review settings November 17, 2025 21:17
Copy link
Contributor

Copilot AI left a comment

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 fixes a bug where the [NativeMarshalling(typeof(CustomMarshaller<T>))] attribute on COM interfaces was being ignored in favor of the default ComInterfaceMarshaller<T>. The fix enables consumers to specify custom marshallers (e.g., UniqueComInterfaceMarshaller<T> for unique RCW instances) on COM interfaces.

Key Changes

  • Parser Priority Fix: Modified MarshallingInfoParser.cs to iterate parsers before attributes in GetMarshallingInfoForAttributes, ensuring NativeMarshallingAttributeParser (which appears before ComInterfaceMarshallingInfoProvider in the parser list) handles [NativeMarshalling] attributes before the default COM interface marshaller.

  • Comprehensive Test Coverage: Added unit tests for compilation validation in both ComInterfaceGenerator and LibraryImportGenerator test projects, plus end-to-end behavioral tests verifying unique RCW instance creation.

  • Test Infrastructure: Simplified native export implementation using StrategyBasedComWrappers and GeneratedComClass instead of manual COM vtable management.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
MarshallingInfoParser.cs Refactored attribute parsing to prioritize parsers over attributes, enabling NativeMarshallingAttributeParser to handle custom marshallers before ComInterfaceMarshallingInfoProvider
CodeSnippets.cs (ComInterfaceGenerator.Unit.Tests) Added unit test snippet for COM interface with NativeMarshalling attribute
Compiles.cs (ComInterfaceGenerator.Unit.Tests) Added test case for compilation validation
CodeSnippets.cs (LibraryImportGenerator.UnitTests) Added unit test snippet for COM interface with NativeMarshalling in LibraryImport
Compiles.cs (LibraryImportGenerator.UnitTests) Added test case for compilation validation
IUniqueMarshalling.cs Defined test COM interface with NativeMarshalling attribute and implementation class
NativeMarshallingAttributeTests.cs Added behavioral tests verifying unique RCW instance creation
UniqueMarshalling.cs (NativeExports) Native export function to create COM objects for testing

Modified UniqueMarshalling.CreateComObject to cache and return the same pointer on every call, ensuring that the same underlying COM object is used. Updated test to verify that while UniqueComInterfaceMarshaller creates different managed instances for each call, they both refer to the same underlying COM object (same cached pointer).

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Copilot AI requested a review from jtschuster November 17, 2025 21:43
Copilot finished work on behalf of jtschuster November 17, 2025 21:43
Updated comment to clarify that calling NewUniqueMarshalling() twice returns different managed instances for the same COM object, addressing code review feedback.

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Copilot finished work on behalf of jtschuster November 17, 2025 22:14
Copilot AI requested a review from jtschuster November 17, 2025 22:14
Added IUniqueMarshalling.cs as a Compile item in LibraryImportGenerator.Tests.csproj to enable the generator to process the COM interface definition. Moved the GetSameComInterfaceTwiceReturnsUniqueInstances test to LibraryImportGenerator.Tests since it tests LibraryImport marshalling behavior. Kept MethodReturningComInterfaceReturnsUniqueInstance in ComInterfaceGenerator.Tests as it tests COM interface method behavior.

Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Copilot finished work on behalf of jtschuster November 18, 2025 00:38
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

@jtschuster
Copy link
Member

@AaronRobinsonMSFT PTAL when you have a chance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Runtime.InteropServices source-generator Indicates an issue with a source generator feature

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Interop source generators do not respect [NativeMarshalling(typeof(TMarshaller))] for COM interfaces

2 participants