microsoft / CsWin32

A source generator to add a user-defined set of Win32 P/Invoke methods and supporting types to a C# project.
MIT License
1.99k stars 84 forks source link

SetupDiGetDeviceInterfaceDetail() throws MashalDirectiveException "Pointers cannot reference marshaled structures" #1184

Open watk opened 1 month ago

watk commented 1 month ago

Hi, I just ran into this issue. Let me know if any other info would be helpful!

Actual behavior

Calling SetupDiGetDeviceInterfaceDetail() in a net48 project results in:

Unhandled Exception: System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal 'parameter #3': Pointers cannot reference marshaled structures.  Use ByRef instead.
   at Windows.Win32.PInvoke.SetupDiGetDeviceInterfaceDetail(HDEVINFO DeviceInfoSet, SP_DEVICE_INTERFACE_DATA* DeviceInterfaceData, SP_DEVICE_INTERFACE_DETAIL_DATA_W* DeviceInterfaceDetailData, UInt32 DeviceInterfaceDetailDataSize, UInt32* RequiredSize, SP_DEVINFO_DATA* DeviceInfoData)

Expected behavior

It should be callable. It worked in version 0.3.49-beta.

Repro steps

  1. NativeMethods.txt content:

    SetupDiGetDeviceInterfaceDetail
  2. NativeMethods.json: Not present

  3. Any of your own code that should be shared?

Program.cs:

using System;
using Microsoft.Win32.SafeHandles;
using Windows.Win32.Devices.DeviceAndDriverInstallation;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            unsafe
            {
                uint size = 0;
                // The argument values are not relevant here.
                Windows.Win32.PInvoke.SetupDiGetDeviceInterfaceDetail(new SafeFileHandle(IntPtr.Zero, false), new SP_DEVICE_INTERFACE_DATA(), null, 0, &size, null);
            }
        }
    }
}

Context

AArnott commented 1 month ago

Thanks for reporting. The char type referenced in the DevicePath field is causing .NET Framework to consider SP_DEVICE_INTERFACE_DETAIL_DATA_W to be a managed type. Changing it to ushort gets it to work. I'll investigate what the best fix is for CsWin32 here.

AArnott commented 1 month ago

I tested this and it appeared to work. I think it would be safe.

-internal global::Windows.Win32.VariableLengthInlineArray<char> DevicePath;
+internal global::Windows.Win32.VariableLengthInlineArray<char, ushort> DevicePath;

and

-    internal struct VariableLengthInlineArray<T>
+    internal struct VariableLengthInlineArray<T, T2>
 where T : unmanaged
+where T2 : unmanaged
     {
-        internal T e0;
+        internal T2 e0;

         internal unsafe ref T this[int index]
         {
             [UnscopedRef]
             [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get => ref Unsafe.Add(ref this.e0, index);
+            get => ref Unsafe.Add(ref Unsafe.AsRef<T>(Unsafe.AsPointer(ref this.e0)), index);
         }

This works by changing the field from char to ushort, while still presenting almost the same char based API. In fact we could probably change the field e0 itself to private and add a ref T property getter to even present char there.

@tannergooding @AaronRobinsonMSFT do you have any ideas here?

AaronRobinsonMSFT commented 1 month ago

The char type referenced in the DevicePath field is causing .NET Framework to consider SP_DEVICE_INTERFACE_DETAIL_DATA_W to be a managed type

A nit on this statement. The concept of "managed" or "unmanaged" is historically a C# ism and not defined by the runtime. The runtime deals with the terms blittable and non-blittable. In .NET the bool and char primitives are non-blittable but are considered "unmanaged" by C#. This is friction we are slowly trying to fix in .NET 7+, it will be a long process.

@tannergooding @AaronRobinsonMSFT do you have any ideas here?

Using short, as you discovered, is what I would recommend.

AArnott commented 1 month ago

In .NET the bool and char primitives are non-blittable

@aaronrobinsonmsft Do you mean in .NET Framework? .NET 8 didn't throw an exception in the OP's repro -- only .NET Framework did. That seems to suggest that .NET 8 considers char to be blittable.

AaronRobinsonMSFT commented 1 month ago

.NET 8 didn't throw an exception in the OP's repro -- only .NET Framework did. That seems to suggest that .NET 8 considers char to be blittable.

Hmmm. That is surprising. Are you using DisableRuntimeMarshalling?

AArnott commented 1 month ago

No.

Lightczx commented 1 month ago

Hmmm. That is surprising. Are you using DisableRuntimeMarshalling?

I can confirm that it will not throw when the [assembly: DisableRuntimeMarshalling] present in .NET 8