-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
COM interfaces returning structs use wrong calling convention #167
Comments
This sounds very similar to #51, except that was closed because we were discussing just the typedef structs. @AaronRobinsonMSFT can you comment? |
@AArnott I missed that being closed - it really shouldn't have been. @tannergooding's point was that the exact issue reported here needs to be handled manually - see #51 (comment) where
That is possible but only if special casing is performed.
That is the reason The DirectX and Direct2D interfaces are notoriously bad in this respect and are common offenders - see dotnet/runtime#10901. All source generation tools (e.g., SharpGenTools) need to manually handle returning structs (> 8 bytes in size) because of this case. We attempted to do the right thing here, but found that WPF and many others have been relying on the offending pattern of returning a wrapped /cc @jkoritzinsky |
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:
For the non-typedef structs, you can do one of the two following options:
We're introducing |
@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: virtual D3D12_RESOURCE_ALLOCATION_INFO STDMETHODCALLTYPE GetResourceAllocationInfo(
_In_ UINT visibleMask,
_In_ UINT numResourceDescs,
_In_reads_(numResourceDescs) const D3D12_RESOURCE_DESC *pResourceDescs) = 0; The fixup on C# should/can be: public D3D12_RESOURCE_ALLOCATION_INFO GetResourceAllocationInfo(uint visibleMask, uint numResourceDescs, D3D12_RESOURCE_DESC* pResourceDescs)
{
D3D12_RESOURCE_ALLOCATION_INFO result;
return *((delegate* unmanaged<ID3D12Device*, D3D12_RESOURCE_ALLOCATION_INFO*, uint, uint, D3D12_RESOURCE_DESC*, D3D12_RESOURCE_ALLOCATION_INFO*>)(lpVtbl[25]))((ID3D12Device*)Unsafe.AsPointer(ref this), &result, visibleMask, numResourceDescs, pResourceDescs);
} This is also slightly more convenient than having an additional return statement that is effectively |
Thanks for jumping in, folks. But my head explodes trying to grok all this at once. ABIs are not by strongpoint. So let's simplify it by setting up some rules:
That's not how I read it. #51 was explicitly opened about the typedef structs, all of which are 8 bytes or less.
That sounds like it is a pattern that worked in the past and that you won't break it the future. Doesn't it?
Are you saying that COM interfaces that define methods that return large structs are all broken? How could that be? Is it that most COM methods actually return HRESULT and use an out parameter for their more meaningful structs that I never noticed that larger returned structs fail? And how does non-PreserveSig method definitions work then?
I'm going to ignore your second "option" because it involves an attribute that does not exist. :) @tannergooding Your "fixup" method makes my jaw drop. I appreciate that it looks like something that fits my above criteria, but I'm imagining explaining to customers why the codegen looks like nothing they have ever written (and wasn't even legal before C# 9) while they have been calling into DirectX for years from C# without it. If they can't understand the p/invoke code we emit, they'll be less likely to trust it. So I guess my high level questions are:
Thanks for your patience as I struggle to get my head around this. |
The only reason it looks like nothing they've ever written is because it uses function pointers. If this were instead a method in a C# COM interface, it would look something like: [Guid("...")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface ID3D12Device : ID3D12Object
{
// More Methods
[PreserveSig]
D3D12_RESOURCE_ALLOCATION_INFO* GetResourceAllocationInfo(D3D12_RESOURCE_ALLOCATION_INFO* result, uint visibleMask, uint numResourceDescs, D3D12_RESOURCE_DESC* pResourceDescs);
// More Methods
} The function pointer merely mirrors this for the actual native call and the wrapper mirrors what the exposed C++ API looks like (which doesn't have a |
There aren't many COM APIs that return something other than For the cases that do exist, there are alternatives available both with regular delegates and with COM interface definitions.
Using function pointers in the wrapper methods for the struct scenario and a method like what I gave above for the COM interface scenario are the two best options. The only catch with function pointers is that they are not considered blittable pre .NET 5 and so you can't have them as fields in a struct or passed directly as method parameters. |
Yes, he probably was talking about the inverse case, i.e. you have nothing to worry for small C# structs when in C++ they are primitives (i.e. the HRESULT case). The behavior of C++ (i.e. the ABI) and C# is asymetric here.
Maybe for large sizes everything is on the stack, regardless of being a struct or primitive? Not sure about it. Regardless, the current projection is definitely broken, the native ABI definitely makes a distinction between small primitive types and small structs (not sure about what the ABI is for large primitives, never seen a COM method return a primitive larger than 8 bytes). I've seen the exact projection you are doing with a different Direct2D projection and it broke, which is why I'm aware of the problem, and I'd like to DirectX/Direct2D be usable with cswin32. If you want a test case its probably easy to construct one.
Its the newer Direct2D / DirectX APIs which started to return structs, older APIs never did this, or were so obscure that people weren't invoking them from .NET. When Direct2D started to become popular people were running into problems, which is why we are aware of them and support is being worked on. You can work around it with older syntax I believe, you can pass the return struct as ref/out argument and declare the return value as IntPtr? You just need to adjust the method signature instead of translating it literally from the C++ header. |
D3D12 has a few, one of which is the Edit: I misread the comment as struct, not primitive. As far as the ABI is concerned, things like |
Thank you. That's a very simple answer that I think I can work with. :)
I already do though. All our COM structs define vtbl's with function pointers as fields, and they work on net472. |
Do you have an example of what is being generated? I recall fairly simple examples showing this fails. |
Just hopping back in to clarify one response.
We do have to worry about structs of all sizes. Here's a modified example that we use in the CoreCLR test tree:
The C-style signature of Even though As we've chatted about, typedefs do not have this issue. A typedef of a primitive (for example |
Sure, @tannergooding. This struct ends up being a field member of another struct: private struct Vtbl
{
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, global::System.Guid*, void **, HRESULT>QueryInterface_1;
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, uint>AddRef_2;
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, uint>Release_3;
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, IEnumString**, HRESULT>get_SupportedLanguages_4;
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, PCWSTR, bool *, HRESULT>IsSupported_5;
internal delegate *unmanaged[Stdcall]<ISpellCheckerFactory*, PCWSTR, ISpellChecker**, HRESULT>CreateSpellChecker_6;
} And the wrapping struct then is used with a pointer: CoCreateInstance(
typeof(SpellCheckerFactory).GUID,
null,
(uint)CLSCTX.CLSCTX_INPROC_SERVER, // https://github.com/microsoft/win32metadata/issues/185
typeof(ISpellCheckerFactory).GUID,
out ISpellCheckerFactory* spellCheckerFactory).ThrowOnFailure(); |
@AArnott This is a bit dangerous. According to the doc, that |
Isn't this every implementation then? Writing (Of course besides potential memory corruption there's also the case of missing high bits, writing |
@weltkante Oh gosh. Yes you're right. Since |
@jkoritzinsky What's the difference between |
Ok, I'll update our native function pointers to not use |
@AArnott you're close, but not quite right. Let's take my example again, with some modifications. For the following C++: typedef int HRESULT;
struct IntWrapper { int i; };
struct S
{
virtual int __stdcall GetI()
{
return 0;
}
virtual HRESULT __stdcall GetHRESULT()
{
return 0;
}
virtual IntWrapper __stdcall GetIWrapper()
{
return {0};
}
};
I believe CsWin32 would generate the following vtable signatures and structs, or something similar to it: struct HRESULT
{
int value;
}
struct IntWrapper
{
int value;
}
struct S
{
struct Vtable
{
delegate* unmanaged[Stdcall]<S*, int> GetI;
delegate* unmanaged[Stdcall]<S*, HRESULT> GetHRESULT;
delegate* unmanaged[Stdcall]<S*, IntWrapper> GetIWrapper;
}
} However, here's the equivalent signatures at the C level of the C++ methods: typedef int HRESULT;
struct IntWrapper { int i; };
int S_GetI(S* _this);
HRESULT S_GetHRESULT(S* _this);
IntWrapper* S_GetIWrapper(S* _this, IntWrapper* retbuf); As you can see, the native C++ compiler does not generate a return buffer for a typedef of a primitive return from a member function (it's important to remember this is member-function only). It only generates a return buffer in the case of the return type being a struct of any size. Now, for the .NET side, .NET will generate code assuming that any struct of size 1, 2, 4, or 8 bytes will be returned in registers, the same rules as non-member functions. Let's look at each of the 3 methods on GetIOn the C++ side, On the .NET side, GetHRESULTOn the C++ side, On the .NET side, Since the managed GetIWrapperOn the C++ side, On the .NET side, This is the failure case. C++ assumes it will be passed a return buffer and returns a return buffer, but .NET assumes that it does not need to pass a return buffer and that it will receive the result from the return register(s). In this case, CsWin32 will need to manually adjust the signature to insert the return buffer since .NET will not do the correct thing without the new |
Ok, so I need to modify the return type to be a pointer, and I need to add that same pointer type as an additional parameter in the method as well (always in last position). |
@jkoritzinsky, do we know if the accidental success here is RyuJIT/Legacy JIT only or if it also applies to Mono and Mono/AOT? |
You'll need to add the additional parameter after the
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. |
For people (like me) that are searching for a performant workaround for the issue, you can disable COM mashaling and manually use new private unsafe D3D12_CPU_DESCRIPTOR_HANDLE GetDescriptorHandle(ID3D12DescriptorHeap* heap)
{
var vtable = *(nint**)heap;
var getDescriptor = (delegate* unmanaged[Stdcall, MemberFunction]<ID3D12DescriptorHeap*, D3D12_CPU_DESCRIPTOR_HANDLE>)(vtable[9]); // index in vtable needs to be found by inspecting code generated by cswin32
var getDescriptorResult = getDescriptor(heap);
return getDescriptorResult;
} |
Updating to .NET 8. Updating to latest CsWin32 package. Updating to latest Win32Metadata package. Updating overrides signatures to match the latest generated with CsWin32. Updating copywrite headers. Adding Code comment headers. preserveSignatureMethods is no longer needed. But the GetSize and GetPixelSize still aren't generated correctly quite yet. See [CsWin32 Issue #167](microsoft/CsWin32#167)
Updating to .NET 8. Updating to latest CsWin32 package. Updating to latest Win32Metadata package. Updating overrides signatures to match the latest generated with CsWin32. Updating copywrite headers. Adding Code comment headers. preserveSignatureMethods is no longer needed. But the GetSize and GetPixelSize still aren't generated correctly quite yet. See [CsWin32 Issue #167](microsoft/CsWin32#167)
I'm hitting this as well now. // ID2D1RenderTarget::GetSize()
// What is generated:
public winmdroot.Graphics.Direct2D.Common.D2D_SIZE_F GetSize()
{
return ((delegate *unmanaged [Stdcall]<ID2D1RenderTarget*,winmdroot.Graphics.Direct2D.Common.D2D_SIZE_F>)lpVtbl[53])((ID2D1RenderTarget*)Unsafe.AsPointer(ref this));
}
// What actually works:
D2D_SIZE_F result;
return *((delegate* unmanaged<ID2D1RenderTarget*, D2D_SIZE_F*, D2D_SIZE_F*>)lpVtbl[53])((ID2D1RenderTarget*)Unsafe.AsPointer(ref this), &result);
// OR
return ((delegate* unmanaged[Stdcall, MemberFunction]<ID2D1RenderTarget*, D2D_SIZE_F>)lpVtbl[53])((ID2D1RenderTarget*)Unsafe.AsPointer(ref this)); @jkoritzinsky I presume it would be safe to simply always emit @AArnott if this is true, can we emit this when targeting .NET 6+? |
@JeremyKuhne yes, using |
I suppose I can only add that syntax in the struct-based approach, where I actually handle the function calls natively, right? Is this a problem for folks where CsWin32 just declares the interfaces? |
yes, when the C++/Headers return a struct the interface (without stdcall/memberfunction) would need to return it as out parameter in last position to get the ABI right (returning the result via register vs. the stack) if you are supporting classic COM interop - this was done because (among others) WPF is returning a HRESULT struct commonly and "fixing" .NET to use stdcall/memberfunction by default breaks the common usecase of wrapping such returns like HRESULT in a struct. This was fixed in the past, regressed people, so rolled back to how Desktop Framework behaves. Net result is that C++ struct returns need to be special cased in COM APIs (and for Desktop Framework it always was this way anyways). The question is whether you can differentiate struct returns from typedef returns in your API source from which you generate, I haven't looked at it for a while since I went back to transcribing APIs manually again. HRESULT would be a typedef and returned via register as if it were just an integer, even if C# defines a wrapper struct for it, while D3D_SIZE_F above is a "real" C++ struct and need to be returned on the stack (out parameter or stdcall/memberfunction convention) |
So anything that has From the winmd: // Windows.Win32.Foundation.HRESULT
using Windows.Win32.Foundation.Metadata;
[NativeTypedef]
public struct HRESULT
{
public int Value;
}
// Windows.Win32.Graphics.Direct2D.Common.D2D_SIZE_F
using Windows.Win32.Foundation.Metadata;
public struct D2D_SIZE_F
{
public float width;
public float height;
} So if it is a class member that is StdCall (almost all cases), as long as it doesn't return a struct with I think this also means that we'd likely want to put |
@jkoritzinsky its worth noting this isn't actually the case, unfortunately 😢 COM APIs are instance members and so However, there do exist some COM APIs which are not explicitly Some other files that includes such COM methods are There's even one that uses |
@weltkante, it's worth noting this isn't correct. Struct returns are handled as a hidden "out parameter" yes, but it is not the last parameter positition, but rather the last implicit parameter. That is, it proceeds the implicit Something like: virtual D3D12_RESOURCE_ALLOCATION_INFO STDMETHODCALLTYPE GetResourceAllocationInfo(
_In_ UINT visibleMask,
_In_ UINT numResourceDescs,
_In_reads_(numResourceDescs) const D3D12_RESOURCE_DESC *pResourceDescs) = 0; Is therefore with a correct binding: delegate* unmanaged[StdCall, MemberFunction]<
ID3D12Device*, // implicit this
uint, // visibleMask
uint, // numResourceDescs
D3D12_RESOURCE_DESC*, // pResourceDescs
D3D12_RESOURCE_ALLOCATION_INFO // return value
>; or with an emulated binding: delegate* unmanaged[StdCall]<
ID3D12Device*, // implicit this
D3D12_RESOURCE_ALLOCATION_INFO*, // implicit return buffer
uint, // visibleMask
uint, // numResourceDescs
D3D12_RESOURCE_DESC*, // pResourceDescs
D3D12_RESOURCE_ALLOCATION_INFO* // return value, this is the same as the implicit return buffer passed in
> Noting that the emulated binding is technically only guaranteed to be correct for x86/x64 Windows. There are platforms, such as Arm64, where this can differ and where the emulated transform can break due to other underlying ABI requirements. |
@tannergooding it looks like the winmd might not contain this info (or I'm missing something). |
Which info in particular? |
The calling convention. |
Right, your requirement sounded backwards, because obviously the C++ ABI will always be The "correct" way (which is slightly more work) is to not lie about the ABI and never return a typedef as struct on the ABI layer, instead unwrap it to the underlying member field and e.g.
Yes, those calling conventions would have to explicitly be annotate per-method in the source dataset, there even exist interfaces which have some members in one and some in another calling convention. |
This comment was marked as outdated.
This comment was marked as outdated.
I made an issue to them to include these annotations I believe, when I stumbled upon one of these interfaces: microsoft/win32metadata#1053 They wanted to annotate any deviations from [StdCall, MemberFunction] if I read that right. If they are missing some interface member annotations it should be easy to get them added by making a similar request. |
I would not rely on this, as it can break on any future platforms. Doing this is effectively relying on an implementation detail of the x64 platform and historical JIT support. Platforms such as Arm64 or theoretical future platforms such as RISC-V or LoongArch can break from doing this (just as emulating the correct x64 signature does already break on Arm64 for some cases today). It's better to simply ensure your function pointers and P/Invokes are 1-to-1 to prevent any potential breaks (it's the only surefire correct thing). You can hide the direct P/Invoke and expose a wrapper which only lets users see the nice HRESULT wrapper struct if that is the surface area you want to expose. |
That being said, if the runtime opted to support Dev's would not be forced to define such wrapper methods for trivial (and super common) helper types like this, allowing them to do the more optimal thing and allowing the runtime to handle the blittable unwrapping correctly (rust has this via -- CC. @jkoritzinsky (I know I've bugged you on this a lot 😄) |
As I've mentioned before, as an interop-only feature it's too expensive for across the whole runtime ecosystem. We'd rather have a more generalized "transparent struct" feature instead that improves other areas of the runtime as well as interop scenarios. We've added support for "transparent struct" specifically for the HResult scenario in LibraryImport/ComInterfaceGenerator through |
Could you elaborate a bit on what's expensive about it? We already "have" the necessary JIT support (because it's required for |
Which is to say, I expect it's under 50 lines of code to enable for I could see some complexities for |
Based upon @tannergooding's suggestion, I've found the following workaround that still allows re-using as much of the CsWin32 generated code as possible, without having to find magic virtual table offsets in the c++ compiler kingdom and assembly fairyland. My limited testing did not seem to need the [Guid("8EFB471D-616C-4F49-90F7-127BB763FA51"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComImport()]
internal interface ID3D12DescriptorHeapExtended : ID3D12DescriptorHeap
{
void Unused1();
void Unused2();
void Unused3();
void Unused4();
void Unused5();
void Unused6();
unsafe D3D12_CPU_DESCRIPTOR_HANDLE* GetCPUDescriptorHandleForHeapStart(D3D12_CPU_DESCRIPTOR_HANDLE* result);
// Remaining unused functions unneeded
} EDIT: Don't forget to use PreserveSig |
@AffluentOwl You can play with the [Guid("8EFB471D-616C-4F49-90F7-127BB763FA51"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComImport()]
internal interface ID3D12DescriptorHeapExtended : ID3D12DescriptorHeap
{
void _VTblGap_6(); // Represents 6 vtable slots
unsafe D3D12_CPU_DESCRIPTOR_HANDLE* GetCPUDescriptorHandleForHeapStart(D3D12_CPU_DESCRIPTOR_HANDLE* result);
// Remaining unused functions unneeded
} |
After 1 month of trying to figure out why my program crashes 50% of the time, turns out |
That makes sense, since PreserveSig=false assumes an HRESULT return value, so if that assumption is false you must use PreserveSig=true. Sorry you had to find out the hard way though, @AffluentOwl. |
Right. The I highly recommend TerraFX tooling for this sort of thing. @tannergooding has done an amazing job of projecting these APIs properly. |
I greatly appreciate CsWin32 making P/Invoke substantially easier in C#, but this is an extremely frustrating gap to have to find out through crashes. Regardless of whether this issue is metadata, coreclr, cswin32, implementation, or documentation, it needs to be improved. If CsWin32 is not expected to generate valid clients for these interfaces, then please don't produce them (produce an error, a vtbl_gap, or hide them behind a flag). If CsWin32 is expected to generate valid clients, then please fix them (even if a better or more complete fix is coming long-term). The worst scenario is what we have today: subtle issues that cause memory corruption and crashes. TerraFX is great, I'm sure, but CsWin32 has become the default for all things Windows interop for our team. |
Some COM interfaces return structs instead of
HRESULT
s, 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 astdcall
+thiscall
combination. This is (probably?) mostly identical tostdcall
but (at least) differs in how structs are returned,stdcall
seems to return them in a register whilestdcall
+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 viaref
orout
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, returningHRESULT
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.
The text was updated successfully, but these errors were encountered: