dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.95k stars 4.65k forks source link

Non-POD arguments passed incorrectly on ARM64 #106471

Open azeno opened 1 month ago

azeno commented 1 month ago

Description

While trying to run ImGui.NET on win-arm64 I ran into some sort of argument passing issue from managed to native code when calling https://github.com/cimgui/cimgui/blob/35a4e8f8932c6395156ffacee288b9c30e50cb63/cimgui.cpp#L207 via the managed wrapper https://github.com/ImGuiNET/ImGui.NET/blob/70a87022f775025b90dbe2194e44983c79de0911/src/ImGui.NET/Generated/ImGui.gen.cs#L21374

I managed to reproduce it with following stripped down version. Note that this code runs fine in win-x64. It also runs fine in win-arm64 when removing the default constructor from the Vector2 struct. This makes me wonder whether it's even a dotnet issue, therefor feel free to point me to the appropriate channels if it isn't.

Reproduction Steps

C#

using System.Runtime.InteropServices;

var result = NativeMethods.MyFunction(default, default);
Console.WriteLine($"{nameof(NativeMethods.MyFunction)} returned {result}");

struct Vector2 { float x, y; }

static class NativeMethods
{
    [DllImport("PInvoke.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int MyFunction(Vector2 vector, int value);
}

C++

struct Vector2
{
    float x, y;
    // Uncomment the following line and MyFunction returns the correct result
    Vector2() : x(0.0f), y(0.0f) { }
};

extern "C" __declspec(dllexport) int MyFunction(Vector2 vector, int value)
{
    return value;
}

Attached is a little VS solution containing the above code PInvoke.zip

Expected behavior

MyFunction should receive 0 as value.

Actual behavior

MyFunction receives some random number as value.

Regression?

No response

Known Workarounds

No response

Configuration

.net8, Windows 11, ARM64

Other information

I've also tried building the project with clang but got the same result.

huoyaoyuan commented 1 month ago

It's caused by ABI difference on ARM64. You can see more details at https://learn.microsoft.com/cpp/build/arm64-windows-abi-conventions

In short, the default constructor in C++ code make it no longer considered HFA on ARM64:

A type is considered to be an HFA or HVA if all of the following hold:

  • It's non-empty,
  • It doesn't have any non-trivial default or copy constructors, destructors, or assignment operators,
  • All of its members have the same HFA or HVA type, or are float, double, or neon types that match the other members' HFA or HVA types.

The argument passing schema for HFA and regular structs are different on ARM64. On x64 there's no such concept and all structs are passed in the same way.

I'm not sure whether .NET supports passing HFAs as a single argument at all.

azeno commented 4 weeks ago

Thank you for the explanation. As a workaround it was possible in my case to change the default constructor to Vector2() = default;.

jkotas commented 4 weeks ago

.NET interop targets C ABI. The structs used for interop must be C structs on the unmanaged side in a portable code. They cannot have C++ constructors, destructors or virtual methods.

The calling convention details for non-POD (Plain Old Data) C++ types are often different from the calling convention details of plain C structs as you have discovered.

This came up a few times before (e.g. in https://github.com/dotnet/runtime/issues/12312). We should mention this in the docs.