Skip to content

Add CoRegisterClassObject regression tests for #1670 (no projection change)#1692

Closed
jevansaks wants to merge 1 commit into
mainfrom
user/jevansa/issue-1670-iunknown-pinvoke
Closed

Add CoRegisterClassObject regression tests for #1670 (no projection change)#1692
jevansaks wants to merge 1 commit into
mainfrom
user/jevansa/issue-1670-iunknown-pinvoke

Conversation

@jevansaks
Copy link
Copy Markdown
Member

@jevansaks jevansaks commented May 6, 2026

Investigates #1670. Conclusion: not a CsWin32 bug — the issue is a usage error. This PR adds two regression tests that document the correct vs incorrect call pattern; no source-generator changes.

What the filer did

var thumbnailProviderFactory = new ThumbnailProviderFactory(client);
var thumbnailProviderFactoryInterface = ComWrappers.GetOrCreateComInterfaceForObject(thumbnailProviderFactory, CreateComInterfaceFlags.None);
CoRegisterClassObject(
    typeof(ThumbnailProvider).GUID,
    thumbnailProviderFactoryInterface, // an nint
    (uint)CLSCTX.CLSCTX_LOCAL_SERVER,
    (uint)REGCLS.REGCLS_MULTIPLEUSE,
    out var cookie);

The cswin32-generated parameter is [MarshalAs(UnmanagedType.Interface)] object pUnk. Passing the nint boxes it as System.IntPtr; the LibraryImport-generated ComInterfaceMarshaller<object> then calls ComWrappers again to wrap that boxed IntPtr as a fresh COM object. What gets registered with COM is a wrapper around an IntPtr, not the original IClassFactory. SCM's QueryInterface for IClassFactory then fails, so CoCreateInstance (or out-of-proc activation) never invokes the factory.

What they should do

Pass the managed factory directly — let the source-generated marshaller do the ComWrappers call for you:

PInvoke.CoRegisterClassObject(
    typeof(ThumbnailProvider).GUID,
    thumbnailProviderFactory,    // managed object, NOT a pre-marshalled nint
    CLSCTX.CLSCTX_LOCAL_SERVER,
    REGCLS.REGCLS_MULTIPLEUSE,
    out var cookie);

(ThumbnailProviderFactory must be a [GeneratedComClass] implementing a [GeneratedComInterface] for IClassFactory, which they already had since their hand-written workaround proved their ComWrappers setup was sound.)

If they want explicit control over raw COM pointers (e.g. to use a specific ComWrappers strategy), they can set "allowMarshaling": false in NativeMethods.json, which projects IUnknown* parameters as void* and lets them pass the nint directly via (void*)factoryPunk.

Tests added

Both run on a dedicated STA thread under the existing useComSourceGenerators: true + DisableRuntimeMarshalling=true setup (matches the filer's environment).

  • RegisteringManagedFactory_InvokesCreateInstance — happy path. Passes the managed [GeneratedComClass] TestClassFactory directly to PInvoke.CoRegisterClassObject and verifies CoCreateInstance invokes CreateInstance.
  • PassingRawPointerAsObject_DoesNotRegisterFactory — documents the broken pattern from the issue. Pre-marshals via ComWrappers, passes the resulting nint, and asserts the factory is never invoked. This locks in the documented behavior so we notice if it ever changes.

Validation

  • New tests pass on net9.0 and net10.0
  • All other tests in GenerationSandbox.BuildTask.Tests still pass

Issue #1670 reported that `PInvoke.CoRegisterClassObject` did not register
their class factory under NativeAOT + DisableRuntimeMarshalling. Investigation
shows this is a usage error, not a CsWin32 bug:

* The reporter pre-marshalled their factory via
  `ComWrappers.GetOrCreateComInterfaceForObject` and then passed the resulting
  `nint` to the generated PInvoke. Because the parameter is
  `[MarshalAs(UnmanagedType.Interface)] object`, the boxed `IntPtr` is then
  wrapped *again* by `ComInterfaceMarshaller<object>`, so what gets registered
  with COM is a wrapper around an `IntPtr` rather than the real
  `IClassFactory`. `CoCreateInstance` then fails to invoke the factory.

* Passing the managed factory object directly to the same parameter works
  correctly: the source-generated marshaller calls ComWrappers under the hood
  and registers the right IUnknown.

These tests document both behaviors so future regressions are caught:

* `RegisteringManagedFactory_InvokesCreateInstance` -- happy path; verifies
  that the cswin32-generated PInvoke registers a managed `[GeneratedComClass]`
  factory and `CoCreateInstance` invokes its `CreateInstance`.
* `PassingRawPointerAsObject_DoesNotRegisterFactory` -- documents the
  `[MarshalAs(UnmanagedType.Interface)]` semantics: passing a pre-marshalled
  `nint` boxed as `object` does not register the original factory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jevansaks jevansaks force-pushed the user/jevansa/issue-1670-iunknown-pinvoke branch from 6e7b7b8 to 7b94e3f Compare May 7, 2026 00:23
@jevansaks jevansaks changed the title Fix #1670: project IUnknown* P/Invoke parameters as void* under source generators Add CoRegisterClassObject regression tests for #1670 (no projection change) May 7, 2026
@jevansaks jevansaks closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant