diff --git a/docs/design/adaptive-com-winrt-object-marshalling.md b/docs/design/adaptive-com-winrt-object-marshalling.md new file mode 100644 index 00000000..7aef6c0b --- /dev/null +++ b/docs/design/adaptive-com-winrt-object-marshalling.md @@ -0,0 +1,288 @@ +# 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 described in [Caller-selected COM and WinRT object out-parameter marshalling](caller-selected-com-winrt-object-marshalling.md) 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( + 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.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. +- `[GeneratedComInterface]` RCW calls. +- `[GeneratedComInterface]` CCW calls to managed implementations. +- Built-in P/Invoke and `[ComImport]` friendly overloads. + +The generated friendly signature remains: + +```csharp +public static void BindToHandler( + 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 + } +} +``` + +It can be disabled for a generated projection: + +```json +{ + "comInterop": { + "autoWinRTMarshalling": false + } +} +``` + +Disabling the option preserves the existing COM-only behavior and avoids the additional `QI(IInspectable)`. + +The option has no effect when: + +- `allowMarshaling` is `false`. +- C#/WinRT is not referenced. +- The target framework does not provide the required custom-marshalling support for source-generated interop. + +In those cases CsWin32 emits the existing projection without C#/WinRT dependencies. + +## Eligible methods + +The initial implementation recognizes the canonical final parameter pair: + +```text +Guid* riid, [ComOutPtr] void** ppv +``` + +The pair must be the final two metadata parameters. The existing generic-friendly-overload option remains independent: disabling `friendlyOverloads.comOutPtrGenericOverloads` suppresses the generic overload but does not disable source-generated ABI marshalling. + +A metadata scan found: + +- 420 generator-relevant methods with one canonical pair. +- 15 of those methods with a non-final pair, which remain future work. +- One method with two canonical pairs, which remains future work. + +## IID selection + +The requested native IID remains type-directed: + +- `object` uses `IID_IUnknown`. +- A C#/WinRT type uses `WinRT.GuidGenerator.CreateIID(typeof(T))`. +- A generated COM type uses `typeof(T).GUID`. + +The generic type parameter carries: + +```csharp +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] +``` + +This preserves the fields used by C#/WinRT IID generation under trimming and Native AOT. + +IID selection determines which native interface is requested. It does not select the managed wrapper family; the returned identity does that through the `IInspectable` probe. + +## Adaptive output marshaller + +Source-generated interop uses one generated object marshaller: + +```csharp +[CustomMarshaller( + typeof(object), + MarshalMode.ManagedToUnmanagedOut, + typeof(ComOrWinRTObjectMarshaller))] +[CustomMarshaller( + typeof(object), + MarshalMode.UnmanagedToManagedOut, + typeof(ComOrWinRTObjectMarshaller))] +internal static unsafe class ComOrWinRTObjectMarshaller +{ + public static object ConvertToManaged(nint value); + public static nint ConvertToUnmanaged(object value); + public static void Free(nint value); +} +``` + +`ConvertToManaged`: + +- Returns `null` for a null native pointer. +- Queries `IInspectable`. +- Projects through C#/WinRT on success. +- Falls back to `ComInterfaceMarshaller` only for `E_NOINTERFACE`. + +The original output reference and the temporary `IInspectable` QI reference are released independently. + +## Flat P/Invoke + +Eligible `[LibraryImport]` outputs replace `[MarshalAs(UnmanagedType.Interface)]` with: + +```csharp +[MarshalUsing(typeof(ComOrWinRTObjectMarshaller))] +out object ppv +``` + +The friendly overload computes the IID, invokes the existing declaration, and casts the adaptively projected object to `T`. + +No duplicate raw P/Invoke is generated. + +## Source-generated COM interfaces + +Generated COM interfaces apply the adaptive object marshaller in both directions: + +- RCW: native code returns an interface pointer to managed code. +- CCW: a managed implementation returns an object to a native caller. + +For CCWs, `ComInterfaceMarshaller.ConvertToUnmanaged` returns the object's identity pointer. +A generated managed consumer passes that pointer through the adaptive input projection, then the +friendly overload casts the projected object to `T`. The cast performs the required interface QI. + +### Generated managed signature + +An eligible generated COM method has this managed shape: + +```csharp +void BindToHandler( + IBindCtx? pbc, + Guid* bhid, + Guid* riid, + [MarshalUsing(typeof(ComOrWinRTObjectMarshaller))] + out object ppv); +``` + +### Managed-to-native output + +For a managed implementation: + +```csharp +public unsafe void BindToHandler(..., Guid* riid, out object ppv) +{ + ppv = value; +} +``` + +the output marshaller converts `value` to its COM identity. The consuming adaptive projection and +generic cast select the requested interface. + +This permits the same managed method to return: + +- A managed or projected WinRT object. +- An inspectable COM object. +- A non-inspectable COM object. +- `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 `[MarshalUsing]` marshallers. CsWin32 therefore adapts the object in the friendly overload after built-in COM marshalling: + +1. Receive the built-in COM wrapper as `object`. +2. Call `Marshal.GetIUnknownForObject`. +3. Query the identity for `IInspectable`. +4. Project through C#/WinRT on success. +5. Return the original built-in COM wrapper on `E_NOINTERFACE`. +6. Release the temporary identity and QI references. + +This creates a transient built-in RCW before an inspectable value is reprojected. CsWin32 must not call `FinalReleaseComObject` on it because the RCW may be identity-cached and shared. + +Runtime validation must invoke a WinRT member after adaptation. A cast alone is insufficient because a classic COM wrapper can appear castable to a WinRT interface while dispatching through the wrong vtable. + +## Inspectable objects used through COM interfaces + +An inspectable object is represented by its C#/WinRT wrapper even when the immediate `T` is a generated COM interface. + +On .NET 8 and later, C#/WinRT dynamic interface casting can query source-generated COM IIDs and use their generated vtables. An inspectable shell stream can therefore be projected as `WinRT.IInspectable`, cast to CsWin32's generated `IStream`, and invoked successfully. + +Consumers that disable C#/WinRT dynamic interface casting cannot rely on this behavior. + +## Native AOT + +The implementation uses source-generated COM metadata, custom marshallers, and C#/WinRT's generated projection support. It does not require runtime-generated interop stubs. + +Native AOT callers must rely on the requested interface contract rather than a concrete runtime-class wrapper. JIT may return `Windows.Storage.StorageFile` where Native AOT returns a generic `WinRT.IInspectable` wrapper that still implements `IStorageItem`. + +The integration suite publishes a Native AOT package-consumption application. + +## Behavior and compatibility + +This is an observable wrapper-family change: + +- Inspectable values that previously appeared as COM wrappers now appear as C#/WinRT wrappers. +- Non-inspectable values remain COM wrappers. +- `object` receives the natural adaptive result. + +CsWin32 projections are primarily generated as internal implementation details. Preserving the previous friendly or ABI signature across generated assemblies is not a design constraint. + +The generated native ABI remains unchanged. + +## Cost and failure behavior + +Every eligible output performs one `QI(IInspectable)`. + +Only `E_NOINTERFACE` selects COM fallback. Other HRESULT failures propagate because they can represent disconnection, proxy failure, or a broken COM implementation. + +The probe is limited to recognized object outputs; it is not added to every COM parameter or return value. + +## Non-goals + +- Unique or independently releasable COM wrapper ownership. +- Input parameter marshalling changes. +- Applying adaptive projection to every fixed-type COM output. +- Non-final or multiple IID/output pairs in the initial implementation. +- Preserving concrete WinRT runtime-class wrapper identity. +- Exact-`riid` output pointers from managed implementations consumed directly by arbitrary native + callers. + +## Validation + +The implementation includes: + +- Generator-shape tests for flat P/Invoke, generated COM, built-in COM, C#/WinRT absence, and both opt-outs. +- Runtime tests for native WinRT, inspectable COM, non-inspectable COM, `object`, and null outputs. +- Managed `[GeneratedComClass]` tests returning WinRT, inspectable COM, and non-inspectable COM values. +- Built-in COM runtime tests that invoke `IStorageItem.Name`. +- Enabled and disabled runtime tests demonstrating the behavior change. +- Native AOT package-consumption publish. diff --git a/docs/design/caller-selected-com-winrt-object-marshalling.md b/docs/design/caller-selected-com-winrt-object-marshalling.md new file mode 100644 index 00000000..34d038d2 --- /dev/null +++ b/docs/design/caller-selected-com-winrt-object-marshalling.md @@ -0,0 +1,468 @@ +# Caller-selected COM and WinRT object out-parameter marshalling + +## Status + +Not selected. + +Related documents: + +- [Adaptive COM and WinRT object out-parameter marshalling](adaptive-com-winrt-object-marshalling.md) +- [COM and WinRT object out-parameter marshalling options](com-winrt-object-marshalling-options.md) + +This note records the caller-selected alternative that was evaluated. CsWin32 instead selected automatic runtime detection as described in the adaptive proposal. + +## Summary + +CsWin32 currently projects objects returned through COM `IID`/`void**` pairs with `ComInterfaceMarshaller`. That produces the expected source-generated COM wrapper, but it fails when the caller needs a C#/WinRT projection such as `Windows.Storage.IStorageItem`. + +This proposal adds a caller-visible policy: + +```csharp +public enum ComOutPtrMarshalling +{ + Default, + ComObject, + WindowsRuntime, +} +``` + +```csharp +public static void BindToHandler( + this IShellItem @this, + IBindCtx? pbc, + in Guid bhid, + out T ppv, + ComOutPtrMarshalling marshalling = ComOutPtrMarshalling.Default) + where T : class; +``` + +`Default` uses the closed generic `T`: + +- A projected C#/WinRT interface selects `WindowsRuntime`. +- `object` and a source-generated COM interface select `ComObject`. + +The caller selects `WindowsRuntime` explicitly when `T` does not reveal the intent, including `out object` or a generated COM interface that must later expose WinRT interfaces. + +`Default` and `ComObject` never probe the returned object merely because it might implement `IInspectable`. Ordinary COM calls keep their current wrapper behavior and do not pay a detection QI. Explicit `WindowsRuntime` with a generated COM `T` performs an intentional `QI(IInspectable)` because the caller requested a WinRT wrapper after requesting the object through a COM IID. + +Applying the policy requires the raw output pointer. Flat P/Invokes can expose `out nint` directly. Source-generated COM methods require a same-IID raw companion because their public `out object` declaration cannot observe the friendly method's policy. + +Unique COM ownership is not required by this proposal and is described as separate future work. + +## Problem + +A method following the `IID_PPV_ARGS` pattern returns an ABI interface pointer: + +```csharp +shellItem.BindToHandler( + null, + bhidStorageItem, + out IStorageItem storageItem); +``` + +The current source-generated COM projection creates a `ComObject`. That wrapper cannot be cast to the C#/WinRT `IStorageItem` projection. + +The generic type often communicates the desired wrapper: + +- `IStorageItem` implies C#/WinRT. +- `IStream` implies source-generated COM. + +It does not always do so: + +- `object` may later be cast to either family. +- An object requested through a COM interface may later be cast to a WinRT interface. +- An inspectable object may still need to be represented primarily as COM. + +Automatically querying every output for `IInspectable` solves the ambiguity, but changes the cost and wrapper selection of ordinary COM calls. This proposal keeps that choice at the call site. + +## Goals + +- Correctly project C#/WinRT interfaces returned through IID/output pairs. +- Preserve existing source-generated COM wrapper behavior and cost by default. +- Let callers explicitly choose COM or Windows Runtime projection. +- Infer the common choice from the closed generic `T`. +- Support `object` and generated COM `T` when the caller explicitly wants a WinRT wrapper. +- Keep managed `[GeneratedComInterface]` implementation methods object-shaped. +- Support Native AOT. +- Release every native reference on success and failure. + +## Non-goals + +- Probe every returned object for `IInspectable`. +- Infer a future WinRT cast after a value escapes as `object`. +- Select unique versus identity-cached COM ownership. +- Change input parameter marshalling. +- Apply the policy to every fixed-type COM output in the first implementation. +- Preserve exact source or binary signatures of generated projections. +- Solve sibling-`riid` correlation for arbitrary native callers of managed COM servers. + +## Proposed API + +### Generated enum + +CsWin32 generates the policy enum when it emits a policy-bearing friendly method: + +```csharp +namespace Windows.Win32; + +public enum ComOutPtrMarshalling +{ + Default = 0, + ComObject = 1, + WindowsRuntime = 2, +} +``` + +The enum follows the configured visibility of generated APIs. + +### Friendly method + +The generated friendly method gains an optional trailing policy: + +```csharp +public static void BindToHandler( + this IShellItem @this, + IBindCtx? pbc, + in Guid bhid, + out T ppv, + ComOutPtrMarshalling marshalling = ComOutPtrMarshalling.Default) + where T : class; +``` + +CsWin32 projections are primarily internal implementation details, so the exact existing signature does not need a forwarding compatibility overload. + +The optional `Default` keeps the common call concise: + +```csharp +shellItem.BindToHandler( + null, + bhidStorageItem, + out IStorageItem storageItem); +``` + +Ambiguous cases state the wrapper intent: + +```csharp +shellItem.BindToHandler( + null, + bhidStorageItem, + out object storageItem, + ComOutPtrMarshalling.WindowsRuntime); +``` + +```csharp +shellItem.BindToHandler( + null, + bhidStream, + out IStream stream, + ComOutPtrMarshalling.ComObject); +``` + +## Policy semantics + +| Policy | Supported `T` | Requested IID | Managed projection | +| --- | --- | --- | --- | +| `Default` | `object` or an interface | Selected from the type-directed policy | Selected from the type-directed policy | +| `ComObject` | `object` or a generated COM interface | `IID_IUnknown` for `object`; otherwise `typeof(T).GUID` | `ComInterfaceMarshaller` | +| `WindowsRuntime` | `object`, a projected WinRT interface, or a generated COM interface | `IID_IInspectable` for `object`; WinRT IID for a projected WinRT interface; otherwise `typeof(T).GUID` | C#/WinRT wrapper, then cast to `T`; a generated COM `T` first queries the returned pointer for `IInspectable` | + +Invalid or unsupported combinations fail before invoking native code: + +- `ComObject` with a projected WinRT interface. +- A runtime class or other non-interface `T`, except for `object`. +- `WindowsRuntime` when C#/WinRT is not referenced. +- `WindowsRuntime` with a generated COM `T` when C#/WinRT dynamic interface casting is disabled. + +Explicit `WindowsRuntime` with a generated COM `T` still has a runtime requirement: the object returned for the requested COM IID must implement `IInspectable`. `E_NOINTERFACE` from that projection QI is an error rather than a fallback to `ComObject`, because fallback would violate the selected policy. + +Some APIs interpret the requested IID as part of the operation rather than a final QI. `out object` requests `IID_IUnknown` or `IID_IInspectable` and may not be accepted. Callers should use the semantic interface `T` when the API requires one. + +### `Default` + +The generated method classifies `typeof(T)` once per closed generic instantiation: + +```csharp +WinRT.Projections.IsTypeWindowsRuntimeType(typeof(T)) +``` + +The result is cached: + +- Projected WinRT `T` resolves to `WindowsRuntime`. +- `object` resolves to `ComObject`. +- Generated COM `T` resolves to `ComObject`. + +This is type-directed runtime classification, not source-generation-time specialization. It does not inspect the returned native object. + +If the classifier cannot reliably cover every supported C#/WinRT type shape, an analyzer should detect calls with a statically known WinRT `T`, require explicit `WindowsRuntime`, and offer a code fix. The analyzer still cannot infer a later WinRT cast from `object`. + +### Explicit `WindowsRuntime` with generated COM `T` + +An inspectable object may also implement a source-generated COM interface. The caller may want the WinRT wrapper family while retaining immediate COM access: + +```csharp +shellItem.BindToHandler( + null, + bhidStream, + out IStream stream, + ComOutPtrMarshalling.WindowsRuntime); +``` + +The raw call requests `IID_IStream`, then queries the returned `IStream` pointer for `IID_IInspectable`. It projects that second pointer through C#/WinRT and casts the resulting wrapper to `IStream`. + +On .NET 8 and later, C#/WinRT's `IWinRTObject` dynamic interface path recognizes source-generated COM interface metadata, queries the generated IID, and supplies its vtable. The adaptive prototype validated this behavior by invoking `IStream.Read` through a `WinRT.IInspectable` wrapper. + +The original `IStream` output reference and the temporary `IInspectable` QI reference are released independently. This QI occurs only because the caller explicitly selected `WindowsRuntime`; it is not added to `Default` or `ComObject`. + +This mode requires C#/WinRT dynamic interface casting to remain enabled. A generated guard should throw a targeted `NotSupportedException` when it is disabled. + +## Raw ABI requirement + +The policy must be applied before any managed wrapper is created. + +Applying `ComInterfaceMarshaller` first is insufficient: + +- It creates the COM wrapper before `WindowsRuntime` can be selected. +- Converting that wrapper back to ABI adds work. +- It can leave an unnecessary wrapper in the COM identity cache. + +The friendly method must receive the raw pointer, choose the projection, and release that pointer exactly once. + +## Flat P/Invoke methods + +Eligible flat methods expose the IID/output parameter as `out nint` in the generated interop declaration: + +```csharp +[LibraryImport("shell32.dll", EntryPoint = "SHCreateItemFromParsingName")] +public static partial HRESULT SHCreateItemFromParsingName( + string pszPath, + IBindCtx? pbc, + in Guid riid, + out nint ppv); +``` + +The friendly method computes the IID, calls the raw declaration, applies the selected projection, and releases the returned reference in `finally`. + +This changes the generated managed signature, not the native ABI. No duplicate P/Invoke entry point is required. + +## COM interface impact + +### Why the public interface cannot carry the policy + +A generated COM interface method is not generic and its parameters must match the native vtable: + +```csharp +[GeneratedComInterface] +public partial interface IShellItem +{ + void BindToHandler( + IBindCtx? pbc, + Guid* bhid, + Guid* riid, + [MarshalAs(UnmanagedType.Interface)] out object ppv); +} +``` + +The method cannot receive the friendly method's managed-only policy. A custom marshaller on `out object` also cannot observe the caller's `T` or enum value. + +The object-shaped method remains useful for managed implementers, so this proposal does not replace it with an unsafe output pointer. + +### Same-IID raw companion + +CsWin32 generates an internal interface with the same IID and vtable layout, but exposes policy-relevant outputs as `nint`: + +```csharp +[GeneratedComInterface] +[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] +internal partial interface IShellItem__ComOutPtrRaw +{ + void BindToHandler( + IBindCtx? pbc, + Guid* bhid, + Guid* riid, + out nint ppv); +} +``` + +For an existing RCW, the friendly method dynamically casts the receiver to the raw companion, invokes the same COM slot, and projects the pointer according to the policy. + +The companion must mirror the complete inherited and declared vtable layout through the target method. Generated managed classes must not implement both same-IID interfaces because that would place duplicate IID entries on one CCW. + +### Direct managed implementations + +A direct managed implementation of the public interface does not implement the private raw companion. + +To keep the friendly method callable directly on that object, the generated extension: + +1. Obtains the public interface's CCW pointer. +2. Projects a temporary unique RCW for the raw same-IID companion. +3. Invokes the raw slot. +4. Releases the temporary RCW and CCW references. + +Existing RCWs stay on the direct raw-companion path. + +This adapter is implementation complexity specific to the caller-selected design. The adaptive custom-marshaller design does not need it. + +### Managed implementers + +The managed method remains natural: + +```csharp +public void BindToHandler(..., out object value) +{ + value = this.returnWinRT + ? this.storageFile + : this.comObject; +} +``` + +`ComInterfaceMarshaller` already accepts C#/WinRT wrappers, source-generated COM wrappers, and managed generated-COM objects on the CCW side. + +The raw caller then chooses `ComObject` or `WindowsRuntime` independently of what managed type the implementation assigned. + +### Adjacent `riid` limitation + +The object marshaller used by the managed CCW cannot see the sibling `riid`. It may return the object's identity pointer instead of a pointer already adjusted to the requested interface. + +The generated friendly/raw-companion caller tolerates this because its projection performs the necessary QI. Arbitrary native clients that immediately dereference `ppv` as `riid` are not covered by this proposal unless the implementation independently guarantees the correct pointer. + +This limitation is shared with the adaptive proposal and with the current object-shaped generated COM method. + +## Projection and cleanup + +The raw output carries one owned reference. + +### COM + +```csharp +T value = ComInterfaceMarshaller.ConvertToManaged((void*)native); +ComInterfaceMarshaller.Free((void*)native); +``` + +### Windows Runtime interface + +```csharp +T value = WinRT.MarshalInterface.FromAbi(native); +WinRT.MarshalInterface.DisposeAbi(native); +``` + +### Windows Runtime wrapper with `object` + +```csharp +object value = WinRT.MarshalInspectable.FromAbi(native); +WinRT.MarshalInspectable.DisposeAbi(native); +``` + +### Windows Runtime wrapper with generated COM `T` + +The native call returns a pointer for the requested COM IID. That pointer is not itself an `IInspectable` pointer, even when the object also implements `IInspectable`: + +```csharp +nint inspectable = 0; +T result; +try +{ + int hr = Marshal.QueryInterface(native, in IID_IInspectable, out inspectable); + Marshal.ThrowExceptionForHR(hr); + + object value = WinRT.MarshalInspectable.FromAbi(inspectable); + result = (T)value; +} +finally +{ + if (inspectable != 0) + { + Marshal.Release(inspectable); + } + + Marshal.Release(native); +} + +ppv = result; +``` + +The generated implementation may use equivalent marshaller cleanup helpers. It must release both owned references, including when the QI, projection, or final cast fails. + +Native AOT may use a generic `WinRT.IInspectable` wrapper instead of a concrete runtime-class wrapper. The requested interface remains the supported contract. + +## Multiple IID/output pairs + +An ECMA-335 metadata scan of `Microsoft.Windows.SDK.Win32Metadata` 71.0.14-preview and `Microsoft.Windows.WDK.Win32Metadata` 0.13.25-experimental, restricted to interface and P/Invoke methods that CsWin32 sends through friendly-overload generation, found: + +- 420 generator-relevant methods with exactly one canonical adjacent IID/output pair. +- One method with two pairs: `ID3D12SwapChainAssistant.GetCurrentResourceAndCommandQueue`. +- 15 single-pair methods where the pair is not the final two parameters. + +Each pair needs its own type parameter and policy: + +```csharp +GetCurrentResourceAndCommandQueue( + out TResource resource, + out TQueue queue, + ComOutPtrMarshalling resourceMarshalling = ComOutPtrMarshalling.Default, + ComOutPtrMarshalling queueMarshalling = ComOutPtrMarshalling.Default); +``` + +Each output is projected and released independently. + +The first implementation should remain scoped to IID/output generic methods rather than the broader set of fixed-type COM outputs. + +## Diagnostics + +The generated method validates policy/type combinations before native invocation. + +An analyzer can provide earlier diagnostics for: + +- `ComObject` with a projected WinRT `T`. +- `WindowsRuntime` without C#/WinRT. +- Unsupported non-interface `T`. +- `WindowsRuntime` with a generated COM `T` while dynamic interface casting is disabled. + +If runtime type classification is not reliable enough for `Default`, the analyzer becomes required for a statically known WinRT `T`. + +## Unique COM ownership + +Unique wrapper ownership is separate from COM-versus-WinRT selection. + +The existing prototype included `ComObjectUniqueInstance` and validated deterministic release through `UniqueComInterfaceMarshaller`. The primary policy proposal does not require that enum value. It can be added in a later proposal or omitted without weakening the WinRT fix. + +## Prototype evidence + +The policy implementation prototype on draft PR #1771 validates: + +- Type-directed `Default` for generated COM and projected WinRT interfaces. +- Explicit `WindowsRuntime` for `object`. +- Explicit `ComObject` for `object` and generated COM interfaces. +- Raw flat P/Invoke declarations. +- Same-IID raw COM companions. +- Native RCWs, managed COM proxies, and direct managed implementations. +- .NET 9, .NET 10, and Native AOT. +- Balanced cleanup on success and projection failure. + +The prototype currently includes the optional unique-ownership value. The core COM-versus-WinRT design does not depend on it. + +The separate adaptive prototype validates the projection building block required for explicit `WindowsRuntime` with a generated COM `T`: after querying the returned COM pointer for `IInspectable`, a C#/WinRT wrapper can dynamically cast to and invoke a source-generated COM interface. The policy prototype does not yet wire this combination end to end. + +## Required validation + +- `Default` with native COM, projected WinRT, generated COM, and `object`. +- Explicit `WindowsRuntime` with projected WinRT, generated COM, and `object`. +- Explicit `ComObject` with generated COM and `object`. +- Invalid combinations fail before native invocation. +- Flat P/Invoke and COM interface methods. +- Native server and managed `[GeneratedComClass]` server. +- Direct managed implementation and COM-proxied managed implementation. +- Managed implementation returning WinRT, inspectable COM, and non-inspectable COM objects. +- Dynamic interface casting enabled and disabled. +- Null outputs, failed HRESULTs, and projection failures. +- Multiple IID/output pairs. +- C#/WinRT absent. +- Native AOT publish and execution. +- Full generator and runtime suites. + +## Open questions + +1. Is preserving zero detection QIs on ordinary COM outputs worth the policy API and raw-companion complexity? +2. Is cached classification reliable enough for `Default`, or should known WinRT `T` require explicit `WindowsRuntime` through an analyzer? +3. Should explicit `WindowsRuntime` with a generated COM `T` be supported as proposed? +4. Is the same-IID raw companion acceptable as a generated implementation detail? +5. Should unique COM ownership remain a separate follow-up or be added as a fourth policy value? diff --git a/docs/design/com-winrt-object-marshalling-options.md b/docs/design/com-winrt-object-marshalling-options.md new file mode 100644 index 00000000..58ce335e --- /dev/null +++ b/docs/design/com-winrt-object-marshalling-options.md @@ -0,0 +1,153 @@ +# COM and WinRT object out-parameter marshalling decision + +## Status + +Decided. + +CsWin32 will use [automatic COM and Windows Runtime object out-parameter marshalling](adaptive-com-winrt-object-marshalling.md). + +The [caller-selected policy](caller-selected-com-winrt-object-marshalling.md) remains documented as the principal alternative that was evaluated. Unique COM ownership remains separate work. + +## Problem + +Objects returned through COM `IID`/`void**` pairs may be ordinary COM objects or Windows Runtime objects. COM-only projection creates a `ComObject`, which cannot safely provide C#/WinRT behavior such as `IStorageItem.Name`. + +The immediate generic type is not a complete signal: + +- A caller may request `object` and cast to a WinRT interface later. +- A WinRT object may be requested through a COM interface and used through both families. +- A managed COM server may return either a COM or WinRT object from the same method. + +The native identity is the authoritative source: successful `QI(IInspectable)` identifies an inspectable object, while `E_NOINTERFACE` identifies ordinary COM. + +## Options evaluated + +### Caller-selected policy + +The caller-selected proposal adds: + +```csharp +public enum ComOutPtrMarshalling +{ + Default, + ComObject, + WindowsRuntime, +} +``` + +The policy is passed to each generated friendly overload. `Default` classifies the closed generic `T`; callers must select `WindowsRuntime` explicitly when the immediate type does not reveal future WinRT use. + +Flat P/Invokes need raw pointer declarations. Generated COM interfaces need same-IID raw companions and an adapter for direct managed implementations because the friendly call policy is not part of the COM method. + +### Automatic runtime detection + +The automatic proposal keeps the existing friendly API. Each eligible output: + +1. Queries `IInspectable`. +2. Projects through C#/WinRT on success. +3. Falls back to COM only for `E_NOINTERFACE`. +4. Propagates other failures. + +Source-generated declarations use a custom marshaller. Built-in COM friendly overloads post-process the built-in wrapper because `[ComImport]` does not support source-generated custom marshalling. + +The behavior is enabled by default and can be disabled with: + +```json +{ + "comInterop": { + "autoWinRTMarshalling": false + } +} +``` + +## Comparison + +| Concern | Caller-selected policy | Automatic runtime detection | +| --- | --- | --- | +| Selection rule | Caller and closed `T` | Returned native identity | +| Ordinary COM output | No detection QI | One failed `QI(IInspectable)` | +| WinRT `T` | Usually inferred | Works automatically | +| `object` followed by WinRT cast | Requires explicit policy | Works automatically | +| COM `T` followed by WinRT cast | Requires explicit policy | Works when the object is inspectable | +| Non-inspectable COM | COM projection | Failed QI then COM projection | +| Public generated API | Adds policy enum and parameter | No new caller-facing API | +| Flat P/Invoke | Raw pointer path | Existing declaration with custom marshaller | +| Generated COM RCW | Same-IID raw companion | Custom output marshaller | +| Generated COM CCW | Raw companion and managed adapter | Coordinated IID and output marshallers | +| Managed implementation | Adapter path | Returns COM, WinRT, or null naturally | +| Multiple outputs | One policy per output | Same detection rule per output | +| Wrapper identity | Explicit policy | Determined by native identity | +| Runtime cost | Avoids detection QI | One QI per eligible output | +| Native AOT | Supported | Supported | +| Unique COM ownership | Separate | Separate | + +## Decision + +Automatic runtime detection is selected. + +### Reasons + +- Correctness should not depend on the immediate generic type revealing every later cast. +- `object` and COM-interface requests should remain usable as WinRT when the returned identity is inspectable. +- Callers expect COM APIs returning WinRT objects to work without a marshalling policy. +- The generated API remains small and does not multiply policy parameters across output pairs. +- One marshalling model covers flat P/Invoke, generated COM callers, managed COM implementations, and Native AOT. +- The extra `QI(IInspectable)` is acceptable for the recognized set of object outputs. + +### Accepted consequences + +- Inspectable values that previously appeared as COM wrappers now appear as C#/WinRT wrappers. +- Ordinary COM outputs pay one failed `QI(IInspectable)`. +- Unexpected QI failures other than `E_NOINTERFACE` now propagate. +- Inspectable values consumed through generated COM interfaces depend on C#/WinRT dynamic interface casting. +- JIT and Native AOT may expose different concrete wrappers while preserving the requested interface. + +The opt-out exists for consumers that require prior COM-only wrapper behavior or cannot accept the probe. + +## Managed COM server result + +The selected implementation applies the adaptive object marshaller without coordinating with the +sibling `riid`. A managed implementation returns a WinRT object, COM object, or `null`; the output +marshaller returns its COM identity. A generated managed consumer applies the adaptive projection, +then casts to `T`, which performs the requested-interface QI. + +The native signature remains `Guid*` plus `void**`. Producing an exact `riid` interface pointer for +arbitrary native callers of managed implementations is a separate generated COM marshalling concern +and is not part of this proposal. + +## Built-in COM result + +Built-in `[ComImport]` does not honor `[MarshalUsing]`. The friendly overload therefore: + +1. Obtains the built-in wrapper's identity. +2. Queries `IInspectable`. +3. Returns a C#/WinRT projection on success. +4. Returns the original wrapper on `E_NOINTERFACE`. + +The transient built-in RCW is not final-released because it may be identity-cached and shared. + +## Scope + +The initial implementation recognizes a canonical final `Guid*`/`void**` pair. + +Not included: + +- Non-final pairs. +- The SDK method with two pairs. +- Fixed-type COM outputs that do not use the recognized pair. +- Input marshalling changes. +- Exact-`riid` output pointers from managed implementations consumed directly by arbitrary native + callers. +- Unique COM wrapper ownership. + +## Validation + +The selected design is covered by: + +- Generator-shape tests. +- Source-generated runtime tests on .NET 9 and .NET 10. +- Built-in COM runtime tests that invoke WinRT members. +- An opt-out regression that preserves the former cast failure. +- Managed COM server tests for WinRT, inspectable COM, non-inspectable COM, and null. +- Native AOT package-consumption publish. +- Full Windows build, ordinary test, and hardware-dependent test suites.