microsoft/CsWin32

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

watk opened this issue · 7 comments

watk commented

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
  1. NativeMethods.json: Not present

  2. 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

  • CsWin32 version: 0.3.106 (it worked in 0.3.49-beta)
  • Win32Metadata version: not set by project
  • Target Framework: net48
  • LangVersion 8.0

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.

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?

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.

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.

.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?

No.

Hmmm. That is surprising. Are you using DisableRuntimeMarshalling?

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