CsWin32: COM interfaces returning structs use wrong calling convention
Some COM interfaces return structs instead of HRESULTs, for example the Direct2D and Direct3D12 APIs have this, some examples:
DXGI_RGBA ID2D1SolidColorBrush::GetColor()D2D_SIZE_U ID2D1RenderTarget::GetPixelSize()D3D12_RESOURCE_DESC ID3D12Resource::GetDesc()
Historically .NET only implements the stdcall calling convention which is what is used for P/Invoke methods but is not the calling convention used by COM interfaces, which instead uses a stdcall+thiscall combination. This is (probably?) mostly identical to stdcall but (at least) differs in how structs are returned, stdcall seems to return them in a register while stdcall+thiscall returns them on the stack. See dotnet/coreclr#23974 for a technical discussion.
When you are doing the projection from the metadata you can only return structs using [NativeTypedef] over primitive types (like integers) from a COM interface. If you need to return a “real” struct then you need to actually return it via ref or out to simulate what the calling convention does.
There’s been desire to add the proper calling convention to the dotnet runtime (dotnet/runtime#46775) but its not finished and not available on older versions, so unless you want to reject access to these APIs you’ll have to work around the shortcomings of the runtime. (Also when this calling convention gets in, I don’t know how it will handle HRESULT structs, which aren’t actually structs. If you have no way to signify its to be treated differently you have the reverse problem, returning HRESULT on the stack instead of via register.)
PS: I’m no expert on calling conventions and only aware of this because I got broken by it, so you may want to talk to some of the runtime guys who (hopefully) know the details of this calling convention better.
About this issue
- Original URL
- State: open
- Created 3 years ago
- Comments: 23 (20 by maintainers)
Commits related to this issue
- Always use BOOL instead of `bool` in native methods This in response to https://github.com/microsoft/CsWin32/issues/167#issuecomment-793124853 — committed to microsoft/CsWin32 by AArnott 3 years ago
- Always use BOOL instead of `bool` in native function pointers This in response to https://github.com/microsoft/CsWin32/issues/167#issuecomment-793124853 — committed to microsoft/CsWin32 by AArnott 3 years ago
- Always use BOOL instead of `bool` in native function pointers This in response to https://github.com/microsoft/CsWin32/issues/167#issuecomment-793124853 — committed to microsoft/CsWin32 by AArnott 3 years ago
- some COM methods need to return in an out parameter https://github.com/microsoft/CsWin32/issues/167 — committed to jayrulez/Win32-Beef by jayrulez 2 years ago
- Latest CsWin32 Update Updating to .NET 8. Updating to latest CsWin32 package. Updating to latest Win32Metadata package. Updating overrides signatures to match the latest generated with CsWin32. Updat... — committed to Shkyrockett/AutoGenDirectWritePlayground by Shkyrockett a year ago
- Updating to Latest CsWin32 Updating to .NET 8. Updating to latest CsWin32 package. Updating to latest Win32Metadata package. Updating overrides signatures to match the latest generated with CsWin32. ... — committed to Shkyrockett/Direct2DCanvasPlayground by Shkyrockett a year ago
For people (like me) that are searching for a performant workaround for the issue, you can disable COM mashaling and manually use new
MemberFunctioncalling convention which resolves the issue:@jkoritzinsky, it’s worth noting that this is compatible but also not quite right either:
The underlying ABI also returns a pointer to the return buffer parameter and so for a method such as:
The fixup on C# should/can be:
This is also slightly more convenient than having an additional return statement that is effectively
return result. You can see an example of this behavior here: https://godbolt.org/z/4EofM4 (and can also go browse the MSVC source if needed).You’ll need to add the additional parameter after the
thisparameter and before all other parameters.Yes.
I do not know if Mono has the same problem with their COM Interop support, but I wouldn’t be shocked if it did. I haven’t had a chance to test it yet. They’ll definitely have the same problem with function pointers though.
Do you have an example of what is being generated? I recall fairly simple examples showing this fails.
D3D12 has a few, one of which is the
GetResourceAllocationInfoexample I keep giving.D3D12_RESOURCE_ALLOCATION_INFOis a 16-byte struct containing 2xUINT64fields. You can find many examples (currently 107) of these types of fixups by grepping forresult;in my TerraFX.Interop.Windows project: https://github.com/terrafx/terrafx.interop.windows/tree/main/sources/Interop/Windows, https://source.terrafx.dev/#TerraFX.Interop.Windows/um/d3d12/ID3D12Device.cs,8b9b043be241c1ea. I’ve provided my owngrepresults here for convenience: result.txtEdit: I misread the comment as struct, not primitive. As far as the ABI is concerned, things like
__int128aren’t true primitives but rather their own type.__m128and friends are primitives and respected by the ABI. These correspond toVector128<T>and friends in .NET and are only available in .NET Core 3.0+. They are currently blocked in P/Invokes because we don’t correctly handle the return scenario on Windows.The correct solution here would be to do a two-pronged approach:
For the typedef structs, you can either do one of the two following options:
CallConvMemberFunction.CallConvMemberFunction.For the non-typedef structs, you can do one of the two following options:
outreturn buffer parameter after the nativethisparameter and change the method to returnvoid. This will be necessary in the “pointer-free” mode you mentioned. The built-in COM system will never support using the natural signature for this case because it breaks the other case pretty severely.CallConvMemberFunction.We’re introducing
CallConvMemberFunctionto enable using the natural function signature with member functions that return structs since we can’t change the built-in behavior without breaking people that rely on the “feature” that theirHRESULTstruct type can be used in place of anHRESULT.