Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,8 @@ jobs:
& $msbuild "${{ runner.temp }}\integration-tests\buildtask-sdk\buildtask-sdk.slnx" /r
shell: pwsh
- name: 🧪 dotnet publish buildtask-sdk-style
run: dotnet publish -r win-x64
run: dotnet publish -c Release -r win-x64
working-directory: ${{ runner.temp }}/integration-tests/buildtask-sdk
shell: pwsh

validate:
name: ✅ Validate
Expand Down
30 changes: 30 additions & 0 deletions Microsoft.Windows.CsWin32.sln
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerationSandbox.BuildTask
{DBA19B76-E786-297A-C44F-6121CA5FFCDC} = {DBA19B76-E786-297A-C44F-6121CA5FFCDC}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerationSandbox.AutoWinRTDisabled.Tests", "test\GenerationSandbox.AutoWinRTDisabled.Tests\GenerationSandbox.AutoWinRTDisabled.Tests.csproj", "{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}"
ProjectSection(ProjectDependencies) = postProject
{D3363193-DE88-5AFE-BA2D-CAF66280F243} = {D3363193-DE88-5AFE-BA2D-CAF66280F243}
{DBA19B76-E786-297A-C44F-6121CA5FFCDC} = {DBA19B76-E786-297A-C44F-6121CA5FFCDC}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerationSandbox.BuiltInCom.Tests", "test\GenerationSandbox.BuiltInCom.Tests\GenerationSandbox.BuiltInCom.Tests.csproj", "{1D316174-9568-4128-AE4C-B1A01EBA65FE}"
ProjectSection(ProjectDependencies) = postProject
{D3363193-DE88-5AFE-BA2D-CAF66280F243} = {D3363193-DE88-5AFE-BA2D-CAF66280F243}
{DBA19B76-E786-297A-C44F-6121CA5FFCDC} = {DBA19B76-E786-297A-C44F-6121CA5FFCDC}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -162,6 +174,22 @@ Global
{A0253377-BC40-63BD-F41A-AC7EBA7A3C54}.Release|Any CPU.Build.0 = Release|Any CPU
{A0253377-BC40-63BD-F41A-AC7EBA7A3C54}.Release|NonWindows.ActiveCfg = Release|Any CPU
{A0253377-BC40-63BD-F41A-AC7EBA7A3C54}.Release|NonWindows.Build.0 = Release|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Debug|NonWindows.Build.0 = Debug|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Release|Any CPU.Build.0 = Release|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Release|NonWindows.ActiveCfg = Release|Any CPU
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA}.Release|NonWindows.Build.0 = Release|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Debug|NonWindows.Build.0 = Debug|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Release|Any CPU.Build.0 = Release|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Release|NonWindows.ActiveCfg = Release|Any CPU
{1D316174-9568-4128-AE4C-B1A01EBA65FE}.Release|NonWindows.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -178,6 +206,8 @@ Global
{168AF67E-678E-FDC9-7C20-7407D58A37FE} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{DC26E9DC-DFB1-19BD-6BAA-38B23E99C3C6} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{A0253377-BC40-63BD-F41A-AC7EBA7A3C54} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{A3AACB6C-0398-4325-B1C7-D9CE72A909DA} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{1D316174-9568-4128-AE4C-B1A01EBA65FE} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E3944F6A-384B-4B0F-B93F-3BD513DC57BD}
Expand Down
2 changes: 1 addition & 1 deletion azure-pipelines/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
solution: $(Pipeline.Workspace)\integration-tests\buildtask-sdk\buildtask-sdk.slnx
msbuildArguments: /r
condition: eq(variables.currentSdk, 'true')
- pwsh: dotnet publish -r win-x64
- pwsh: dotnet publish -c Release -r win-x64
displayName: 🧪 dotnet publish
workingDirectory: $(Pipeline.Workspace)\integration-tests\buildtask-sdk
condition: eq(variables.currentSdk, 'true')
168 changes: 168 additions & 0 deletions docs/design/adaptive-com-winrt-object-marshalling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Automatic COM and Windows Runtime object out-parameter marshalling

## Status

Accepted.

CsWin32 will automatically detect Windows Runtime objects returned through recognized COM `IID`/`void**` out-parameter pairs. The automatic behavior is enabled by default and can be disabled globally in `NativeMethods.json`.

The caller-selected policy was considered but not selected. Unique COM wrapper ownership remains separate work.

## Motivation

CsWin32 projects COM object outputs as COM wrappers. That is correct for ordinary COM, but it prevents an object returned through an `IID`/`void**` pair from being used as a C#/WinRT projection:

```csharp
shellItem.BindToHandler<IStorageItem>(
null,
bhidStorageItem,
out IStorageItem storageItem);
```

The native object returned by `BindToHandler` implements `IInspectable`, but COM-only marshalling creates a `ComObject`. That wrapper cannot safely provide the C#/WinRT `IStorageItem` behavior. The problem also occurs when the immediate output type is `object` or a COM interface and the caller casts to a WinRT interface later.

Callers should not have to know which wrapper family to request. The returned native identity already provides the authoritative answer:

- An identity that implements `IInspectable` should be projected through C#/WinRT.
- An identity that returns `E_NOINTERFACE` for `IInspectable` should use normal COM projection.

The extra `QueryInterface(IInspectable)` is accepted in exchange for automatic behavior and substantially simpler generated APIs.

## Decision

For each eligible COM object output:

1. Request the native interface identified by the friendly method's `T`.
2. Query the returned identity for `IInspectable`.
3. On success, project the value with `WinRT.MarshalInspectable<object>.FromAbi`.
4. On `E_NOINTERFACE`, use the normal COM projection.
5. Propagate every other QI failure.

This rule applies to source-generated flat P/Invokes, generated COM calls, and built-in COM friendly overloads.

The generated friendly signature remains:

```csharp
public static void BindToHandler<T>(
this IShellItem @this,
IBindCtx? pbc,
in Guid bhid,
out T ppv)
where T : class;
```

No caller-visible marshalling enum, raw companion method, same-IID companion interface, or analyzer is required.

## Configuration

Automatic projection is enabled by default:

```json
{
"comInterop": {
"autoWinRTMarshalling": true
}
}
```

Set the option to `false` to preserve COM-only projection and avoid the additional `QI(IInspectable)`.

The option has no effect when marshaling is disabled, C#/WinRT is absent, or the target framework lacks required custom-marshalling support.

## Eligible methods

The initial implementation recognizes a final metadata pair:

```text
Guid* riid, [ComOutPtr] void** ppv
```

Non-final pairs and the one SDK method with two pairs remain future work. Disabling generic COM out-pointer friendly overloads does not disable source-generated ABI marshalling.

## IID selection

- `object` uses `IID_IUnknown`.
- C#/WinRT types use `WinRT.GuidGenerator.CreateIID(typeof(T))`.
- Generated COM types use `typeof(T).GUID`.

The generic `T` is annotated with `DynamicallyAccessedMembers(PublicFields)` for trimming and Native AOT.

IID selection chooses the native interface. The returned identity chooses the managed wrapper family.

## Adaptive output marshaller

Source-generated interop uses one object marshaller for `ManagedToUnmanagedOut` and `UnmanagedToManagedOut`.

Native-to-managed conversion queries `IInspectable`, uses C#/WinRT on success, falls back to `ComInterfaceMarshaller<object>` only for `E_NOINTERFACE`, and releases the original and temporary QI references independently.

Eligible `[LibraryImport]` declarations apply `[MarshalUsing]` directly to `out object`; no duplicate raw P/Invoke is generated.

## Generated COM interfaces

CsWin32 applies the adaptive marshaller to the object output without changing the IID parameter:

```csharp
void BindToHandler(
IBindCtx? pbc,
Guid* bhid,
Guid* riid,
[MarshalUsing(typeof(ComOrWinRTObjectMarshaller))]
out object ppv);
```

For managed implementations, the output marshaller returns the object's COM identity. A generated
managed consumer then applies the adaptive input projection, and the friendly overload casts the
projected object to `T`. That cast performs the required interface QI, so no sibling-parameter state
is needed.

Managed implementations may return WinRT objects, inspectable COM objects, non-inspectable COM
objects, or `null`. Producing the exact interface pointer named by `riid` for arbitrary native callers
of managed implementations is a separate generated COM marshalling concern and is not added by this
proposal.

## Built-in COM interop

Classic `[ComImport]` and `DllImport` do not honor source-generated custom marshallers. Their friendly overloads post-process the built-in wrapper:

1. Obtain its identity with `Marshal.GetIUnknownForObject`.
2. Query `IInspectable`.
3. Project through C#/WinRT on success.
4. Return the original built-in wrapper on `E_NOINTERFACE`.
5. Release temporary references.

This creates a transient built-in RCW. CsWin32 does not final-release it because it may be identity-cached and shared.

Runtime coverage invokes a WinRT member after adaptation; a cast alone is not sufficient to prove correct vtable dispatch.

## Inspectable objects used through COM interfaces

C#/WinRT wrappers on .NET 8 and later can dynamically expose source-generated COM interfaces. An inspectable shell stream can therefore be projected as `WinRT.IInspectable`, cast to CsWin32's `IStream`, and invoked.

Consumers that disable C#/WinRT dynamic interface casting cannot rely on this behavior.

## Native AOT

The design uses generated COM metadata and custom marshallers. Native AOT callers rely on interface contracts rather than concrete runtime-class wrapper types.

The integration suite publishes a Native AOT package-consumption app.

## Behavior and cost

Inspectable values that previously appeared as COM wrappers now appear as C#/WinRT wrappers. Non-inspectable values remain COM wrappers.

Each eligible output adds one `QI(IInspectable)`. Only `E_NOINTERFACE` selects COM fallback; other failures propagate.

CsWin32 projections are primarily internal, so preserving previous generated source or managed ABI signatures is not a requirement. The native ABI remains unchanged.

## Non-goals

- Unique COM wrapper ownership.
- Input marshalling changes.
- Every fixed-type COM output.
- Non-final or multiple IID/output pairs in the initial implementation.
- Concrete WinRT runtime-class wrapper identity.
- Exact-`riid` output pointers from managed implementations consumed directly by arbitrary native callers.

## Validation

Coverage includes generator-shape tests, source-generated and built-in runtime tests, enabled and disabled behavior, WinRT and COM outputs, managed round trips, null output, and Native AOT package publication.
2 changes: 1 addition & 1 deletion integration-tests/buildtask-sdk/app/buildtask-sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<LangVersion>9</LangVersion>
<LangVersion>12</LangVersion>
<PublishAot>true</PublishAot>
<CsWin32RunAsBuildTask>true</CsWin32RunAsBuildTask>
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.Windows.CsWin32/Generator.Com.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,10 @@ static ExpressionSyntax ThisPointer(PointerTypeSyntax? typedPointer = null)
methodDeclaration = methodDeclaration.AddModifiers(TokenWithSpace(SyntaxKind.UnsafeKeyword));
}

methodDeclaration = methodDefinition.Generator.ApplyAutoWinRTMarshalling(
methodDefinition.Method,
signature,
methodDeclaration);
propertyOrMethod = methodDeclaration;
}

Expand Down
Loading
Loading