-
Notifications
You must be signed in to change notification settings - Fork 120
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
d3d12: Problem with functions that return structures by value #636
Comments
I believe this is just something you have to deal with. https://devblogs.microsoft.com/oldnewthing/20220113-00/?p=106152 The windows crate for Rust already detects methods returning structs and transforms them in a way that is consistent with the Visual C++ compiler. For example: pub struct ID3D12Heap_Vtbl {
pub base: ID3D12Pageable_Vtbl,
pub GetDesc: unsafe extern "system" fn(this: *mut ::core::ffi::c_void, result__: *mut D3D12_HEAP_DESC),
} impl ID3D12Heap {
pub unsafe fn GetDesc(&self) -> D3D12_HEAP_DESC {
let mut result__: D3D12_HEAP_DESC = ::core::mem::zeroed();
(::windows::core::Interface::vtable(self).GetDesc)(::core::mem::transmute_copy(self), &mut result__);
result__
}
} |
Note that Microsoft DirectX-Headers project (https://github.com/microsoft/DirectX-Headers/blob/main/include/directx/d3d12.h) fixes this issue and declares function correctly. C prototype is: DECLSPEC_XFGVIRT(ID3D12Heap, GetDesc)
D3D12_HEAP_DESC *( STDMETHODCALLTYPE *GetDesc )(
ID3D12Heap * This,
D3D12_HEAP_DESC * RetVal); C++ prototype is: virtual D3D12_HEAP_DESC STDMETHODCALLTYPE GetDesc( void) = 0; |
Yes the C (not C++) declaration may be wrong in the Windows SDK. My point is just that the metadata is correct as it is based on the C++ declaration. |
I see, thanks for your input. |
So the goal of the win32metadata project is to represent what's in the C/C++ headers, not what the actual API is? So if there is a mistake in the headers, the win32metadata project is meant to keep the mistake in. If this is the case then we will need to walk back some of the other decisions that have been made such as fixing SAL annotations and mistyped parameters in certain functions. |
The actual API is correctly described by the C++ code in the header. The old C declarations (that returned the structs) were broken and would not work if you tried to use them to call those APIs. |
As Damyan said, the metadata correctly reflects the C++ declaration which is also correct. |
Yeah I think I'm confused. Re-reading the thread isn't clearing it up for me so ignore me for now. Not sure what the next step is here. |
Practically it means that if you are building a projection, you need to detect virtual functions that return a user-defined type (e.g. C-style |
If I understand, you're saying that unless you're creating a C++ projection that is only meant to be used by the Visual C++ compiler, then you need to modify the API. A few questions come to mind. Why is this? Does the Visual C++ compiler also make this correction internally when it encounters these APIs? Also, if every projection needs to do this transformation, how do we know when it needs to be done? Is it a particular set of namespaces in the metadata and only for COM functions, or maybe this needs to be applied to ALL COM functions across the board? Also, what exactly is a "user-defined" type? You mentioned a C-style struct as an example but what else would qualify? |
The APIs in question here are defined using COM IDL (https://github.com/microsoft/DirectX-Headers/blob/main/include/directx/d3d12.idl). This has a well-defined ABI that also maps well to virtual functions using stdcall. I'm pretty sure all other C++ compilers on Windows correctly implement stdcall. The d3d12 header files that are read by win32metadata are generated by midl. It generates two versions of each interface - the C++ one and the C one. The C++ version uses classes and virtual functions to define the interface, while the C version uses a struct with pointers to functions to define the vtable. The problem is that there was a bug in midl where it assumed that stdcall function pointers and virtual functions have the same ABI when, in fact, they don't. For a function defined in the IDL as, say:
Then in the C++ version midl would generate (correctly):
And for the C version it would generate:
However, this is a bug because the __stdcall ABI is different for function pointers vs virtual functions, and the end result is that the C function pointer is incorrect. This bug has now been fixed and so newer version of D3D12.h contain:
The relevant part of the docs:
This issue comes up more often in DirectX related APIs since these return large structs from methods that by definition can't fail. A more common pattern COM APIs is to always return an HRESULT and take an out-pointer. This, combined with very few people using the C-style interfaces, helps explain why this midl issue went unnoticed until relatively recently. |
That was a great explanation, thanks @damyanp. To make sure I understand, the problem comes from incorrectly interpreting the ABI of "virtual functions". Problems arise because virtual functions follow the stdcall ABI in most cases, but when you return a large enough struct it diverges. Also, you can represent a virtual function ABI that has a large return value with a stdcall function signature by making the return value an inout pointer argument. Assuming I've got it right, is this the only known case where the virtual function ABI differs from stdcall? A quick google reveals stdcall information is here: https://docs.microsoft.com/en-us/cpp/cpp/stdcall?view=msvc-170 They both say the "Callee" cleans up the stack and parameters are pushed onto the stack from "right to left". stdcall says return values are handled the same as "cdecl" but I can't seem to find where it says how "thiscall" handles return values. P.S. For Zig one option would be to add another calling convention for "thiscall" |
The metadata describes this as: D3D12_HEAP_DESC GetDesc(); Looks like it should be: DECLSPEC_XFGVIRT(ID3D12Heap, GetDesc)
D3D12_HEAP_DESC *( STDMETHODCALLTYPE *GetDesc )(
ID3D12Heap * This,
D3D12_HEAP_DESC * RetVal) We can fix that. Not sure why we got so off track on this one. |
Not sure there's value in "fixing" this in metadata unless you can catch all such cases. In Rust I just detect this automatically and transform the ABI accordingly. |
I think there is value is in baking in the necessary secret C++ compiler ABI knowledge needed to use these APIs correctly (on x86) so that other folks don't stumble on this. If no one else agrees though, then I'm happy to close this issue and move on. |
@kennykerr what criteria do you use in the rust bindings to know when to include the return type as a reference parameter? It looks like there are some BeefLang bindings. The author of those says they will only transform the return type if the type is:
Is this the same criteria that the rust bindings use? It seems to me that the correct final solution for this is that each language support the P.S. I wonder if LLVM has the ABI defined? |
I limit it to those methods that return a type that is both a |
Wow. A lot to take in there. It seems that the metadata could be changed for these problematic methods so that their signatures match what projections must declare so things work by default. Maybe that would even work for C++, I don't know. |
As I understand, the metadata already has all the information needed. Namely, all "virtual functions" (which I think is equivalent to "all COM methods") use the Take the following example signature in the metadata: class SomeComInterface {
SomeStruct foo(int a, int b);
} If your language has native support for the // C++, correct because virtual functions use "thiscall" ABI
class SomeComInterface {
virtual SomeStruct foo(int a, int b);
}; If your langauge doesn't understand the // Signature in C which doesn't have "thiscall" ABI defined
SomeStruct * (STDMETHODCALLTYPE *foo)(SomeComInterface *This, SomeStruct *RetVal, int a, int b); The metadata could try to make this "fixup" easier for languages that don't support "thiscall" by marking which methods would need this "fixup", but, that's a limitation of specific projections so we may not want to put it in the metadata that's shared by all projections. For Zig, I think we'll start by emulating the // an example of what it would look like in Zig if we added support for "thiscall"
var foo: *const fn(self: *SomeComInterface, a: c_int, b: c_int) callconv(.Thiscall) void; |
Thanks. I have a question though. If COM interfaces always use the |
COM has worked this way for time immemorial but it's a pretty obscure corner that few discover. It has become more prominent with the proliferation of compilers and languages on Windows in recent years. |
@kennykerr Are you saying this is a bug in the .NET (Framework/Core) runtime in that it improperly makes COM calls in these cases? |
Looks like there's no work here for metadata.
instance valuetype Windows.Win32.Graphics.Direct3D12.D3D12_HEAP_DESC GetDesc () cil managed which appears to conveys enough information to do the right thing. @marler8997 OK to close? |
How one figures out from above metadata that the real C function prototype is: DECLSPEC_XFGVIRT(ID3D12Heap, GetDesc)
D3D12_HEAP_DESC *( STDMETHODCALLTYPE *GetDesc )(
ID3D12Heap * This,
D3D12_HEAP_DESC * RetVal); ? |
It looks like DllImport accepts a calling convention and it supports "thiscall". https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.callingconvention?view=net-6.0 Are the .NET bindings specifying "thiscall" for COM methods? If they are, then it does sound like this is a big in how the marshaller implements "thiscall", which, wouldn't be the first time I've seen a bug in this area. |
I'd prefer that we not close the bug just yet.
I guess that method signature only conveys enough information if there is side information somewhere stating that It still feels odd that if every projection except C++ has to change the signature to work properly, for the metadata to side with the C++ signature, but I guess it sort of makes sense because the signature looks the nicest as the metadata represents it, so any projection that can represent it that way should do so. I'd just like to be clear on how the rest of the projections know that they can't represent it that way, and get that documented somewhere. |
Metadata has:
Its method definition signature (MethodDefSig) bytes are So with that information, we can create a compatible C signature (x86) as such (if needed):
Fast forwarding to your COM interface method in C# comment. You are probably looking at something like this: [ComImport]
[Guid("...")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ID3D12Heap
{
D3D12_HEAP_DESC GetDesc();
} In the debugger, hovering over that method (hanging off an object, not pictured) will show a Does that help any? Or did I misunderstand the question/bug? |
That helps, thank you. I didn't realize the calling convention was embedded in the method signature in a way that an IL decompiler wouldn't reveal it. Also, most of this discussion and the entire blog post seems to center around x86 rules. What does that mean for x64 and arm64 architectures? Should I not transform the method signature for those cases? Is it allowed but not required? If the transformation is required and allowed only for x86, I guess that effectively makes any COM interface that includes struct-returning methods to be arch-specific, making them unavailable to AnyCPU compilations. |
Heads up: The For the implementation side of that special return type rule, a decent heuristic might be to count the number of fields. If it's 2+, then do the dance. This will fail in cases where the struct contains tiny fields that all fit in a single register, or if one of the fields is a struct itself... but skimming the handful of affected COM methods that return structs, this doesn't seem to ever be the case. Alternatively, you could check if the return type is a struct and isn't annotated with Or maybe even a |
I think I'll go with that. But I'll have to modify it to also disregard primitive types since |
Did we conclude that this issue can be closed? |
I'd love to get an answer to my questions in the second paragraph of #636 (comment) first. |
For non-x86 the transformation is allowed because it effectively results in the same stack/register layout. For Rust for example, I just perform the transformation for all targets (x86, x64, ARM64) and it works everywhere. |
Thanks. I support closing this issue. |
Original Issue: marlersoft/zigwin32#6
I've coped the original issue here which was created by user @michal-z
The original d3d12.h that comes with Windows SDK has wrong C prototypes for functions that return structures. Fixed headers are released by Microsoft here: https://github.com/microsoft/DirectX-Headers/blob/main/include/directx/d3d12.h
Consider ID3D12Heap::GetDesc method, it should be defined like this:
The proper C prototype takes a pointer to RetVal as a second parameter and also returns the same pointer. This applies to all functions that return structure by value.
The text was updated successfully, but these errors were encountered: