Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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')
184 changes: 184 additions & 0 deletions docs/design/adaptive-com-winrt-object-marshalling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# 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.

Managed-to-native output first preserves existing RCWs, then selects the CCW marshaller from the
runtime type:

- An RCW discoverable through `ComWrappers.TryGetComInstance` returns its current native `IUnknown`
identity.
- A classic built-in COM RCW returns its current identity through `Marshal.GetIUnknownForObject`.
- A type marked with `[GeneratedComClass]` uses `ComInterfaceMarshaller<object>`, preserving its
generated COM interface table.
- Every other type uses `WinRT.MarshalInspectable<object>.FromManaged`, enabling the full WinRT CCW
interface set for ordinary managed objects.

C#/WinRT diagnoses types that combine `[GeneratedComClass]` with projected WinRT interfaces, keeping
the two CCW paths mutually exclusive.

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 either the generated COM identity or the
C#/WinRT identity according to the runtime type. 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, CsWinRT and COM RCW identity, WinRT CCWs, generated COM
CCWs, 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