1

I realise there are many questions related to this one. I am able to follow them and get functional code, but I don't understand how it works or which way is better. I'm afraid this question might be multiple questions in itself, but I believe they make more sense together.

For context, I've never programmed in C# or for the Windows platform. However, I understand C decently well.

From https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-data-with-platform-invoke:

To create a prototype that enables platform invoke to marshal data correctly, you must do the following: (...) Substitute managed data types for unmanaged data types.

This seems to be much trickier to understand than I thought. The problem is that there seems to be many different ways to do this, and I can't tell if they're equivalent. I will use a specific example, but more general explanations are very welcome.

I have a C function in my DLL with the following signature: unsigned long GetList(unsigned long *List, unsigned long *listCount)

List is a pointer to an array of unsigned longs, and listCount is a pointer to an actual unsigned long that holds the size of the array. The way the function works is:

1- if List == NULL, then GetList puts in listCount the minimum size that a non-null array should have to be passed to the function

2- if List != NULL, then GetList reads from listCount the size of List and writes into its entries, provided the array is big enough according to listCount

The application will call using the first mode of functioning to get the minimum size, allocate an array of that size and then call the function again with the second mode

As per https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-data-with-platform-invoke#platform-invoke-data-types I substitute unsigned long with System.UInt32 (or uint [2], they are aliases).

Here are the 2 ways in which I have implemented this. They both seem to function:

[DllImport("mydll.dll")]
unsafe public static extern System.UInt32 GetList(IntPtr List, System.UInt32* listCount);

static void Main(string[] args)
{
        System.UInt32 slotCount = 10;
        unsafe
        {
            result = GetList(IntPtr.Zero, &slotCount);
        }
        
        System.UInt32[] slotList = new System.UInt32[slotCount];
        slotList[0] = 10; // a value just to show that the array is being changed
        GCHandle handle = GCHandle.Alloc(slotList, GCHandleType.Pinned);
        IntPtr slotListPointer = handle.AddrOfPinnedObject();
        
        unsafe {
            result = GetList(slotListPointer, &slotCount);
        }
        handle.Free();
}

I am confused as to whether it makes sense to pin [3] the array. It seems like P/Invoke does this automatically when passing arguments to the DLL, and the DLL doesn't keep pointers to the memory after the end of the GetList() function. I believe I can do it like this because the array is blittable [4], even though the page I'm linking to says

However, a type that contains a variable array of blittable types is not itself blittable.

Which I don't understand. What is a variable array? Googling led to [5] which does not contain an answer.

Another way, perhaps better for C# programmers is:


    [DllImport("mydll.dll")]
    public static extern uint GetList(uint[] List, ref uint listCount);

    static void Main(string[] args)
    {
        uint slotCount = 10;
        result = GetList(null, ref slotCount);
        uint[] slotList = new uint[slotCount];
        slotList[0] = 10; // a value just to show that the array is being changed
        GetList(slotList, ref slotCount);
        Console.WriteLine("slotList[0] = {0}", slotList[0]);        
    } 

This one confuses me: passing the array just like so seems like it might cause trouble later on. I guess I don't understand how the Platform Invoke Marshalling will map to regular C code. From https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-different-types-of-arrays

In contrast, the interop marshaller passes an array as In parameters by default.

That information is confirmed in https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-different-types-of-arrays

Reading https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-parameter-modifier

The in keyword causes arguments to be passed by reference but ensures the argument is not modified.

Now, that is the exact opposite of what I want. I want to change the entries of the argument. However, I seem to be misinterpreting something, because the entries are being changed and so it's as if it is In/Out (since the meaning of the array when it is passed as an argument matters as well)?

Yet a third way seems to be to allocate memory for the array myself [6], deal only with pointers and marshal the array with the methods of the Marshal class [7], like in [8].

So which one is better, and more hassle-free for someone with my background? How do each of them work under the hood - are they different at all? I'm assuming that in my first version everything is just like in C - the slot list, after being pinned, is like an array that was malloc-ed and can only be touched by the Garbage Collector after the free(), from which point onward it might be moved (or freed if the GC thinks it can).

[2] - https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types?redirectedfrom=MSDNCompare

[3] - https://learn.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning

[4] - https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types

[5] - Non-blittable error on a blittable type

[6] - https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.allochglobal?view=net-7.0

[7] - https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal?view=net-7.0

[8] - C# how to get Byte[] from IntPtr

Edit- The value returned by the function is used for error handling purposes. I purposely omitted that part of the code because I didn't think it was relevant.

PS - I do believe everything I'm trying to do would be much smoother if I'd do it from a C application calling the DLL, but I have to do it from C#. As such, if there is a way to make C# behave more like C, I'd be pleased :) I can use the /unsafe flag

chilliefiber
  • 571
  • 2
  • 7
  • 18
  • 2
    `public static extern uint GetList(uint[] List, ref uint listCount);` is the way to go (no need for unsafe, no need for pinning, allocation, free, etc.). – Simon Mourier May 29 '23 at 10:42
  • Thanks for the reply @SimonMourier: I realised that that would be the preferred way for a C# programmer. But is it completely equivalent to the other one? – chilliefiber May 29 '23 at 10:47
  • 1
    It's not equivalent, it's much better/simpler – Simon Mourier May 29 '23 at 10:52
  • I agree that it looks simpler: but is this gonna bite me in the ass later? I'm doing something I don't really understand, and went down the rabbit hole of reading too much documentation but not really understanding much of it - now it's just information overload – chilliefiber May 29 '23 at 11:27
  • If you don't believe people with reputation about your question in this forum, why do you ask in the first place? – Simon Mourier May 29 '23 at 14:07
  • @SimonMourier it's not a matter of trust: I had already gotten to that code, and I had already understood that a C# programmer would prefer it. So to answer your question directly, I ask in the first place to get a deeper explanation than just "do it like so because it is better". Perhaps SO is not the best place for this kind of question though: if you know some place better, let me know. – chilliefiber May 30 '23 at 20:35

1 Answers1

-2

Custom marshalling is only necessary in specialized cases. Using unsafe and/or pinning your own arrays is messy and very easy to make mistakes: eg your option 1 misses out a finally for the Free so if there is an error then the handle will leak.

The correct way to do it is the following declaration, which uses a normal array, so that the marshaller can handle everything for you.

[DllImport("mydll.dll", CallingConvention = CallingConvention.CDecl)]
public static extern uint GetList(
  [Out, MarshalAs(Unmanagedtype.LPArray, SizeParamIndex = 1)] uint[] List,
  [In, Out] ref uint listCount
);

Note the use of SizeParamIndex, so that the marshaller knows that the size of the C array to copy is stored in the second parameter. Note also that the calling convention is set to CDecl.

You then call it like this

static void Main(string[] args)
{
    var slotCount = 0;
    result = GetList(null, ref slotCount);
    if (result != 0)
        throw new Exception("Some Error {result}");
    var slotList = new uint[slotCount];
    slotList[0] = 10; // a value just to show that the array is being changed
    result = GetList(slotList, ref slotCount);
    if (result != 0)
        throw new Exception("Some Error {result}");
    Console.WriteLine("slotList[0] = {0}", slotList[0]);        
} 

It's unclear how you want to handle errors. I've assumed you've used the return value, but you could also set a Win32 error code and retrieve it using Marshal.GetLastWin32Error().

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Thank you Wrt error handling: the return value of every function in my DLL is how I handle errors. I edited that into the question right now. However, I'm not sure about "Note the use of SizeParamIndex, so that the marshaller knows that the size of the C array to copy is stored in the second parameter. " This is not necessarily true in all cases: if the array is NULL (C style NULL), the second parameter can be anything. And I didn't gain much understanding: what is the difference between this and the second version I presented? Why use IN/OUT and ref? I thought ref was like In and Out – chilliefiber May 29 '23 at 11:41
  • 1
    You're right you don't need to specify `In, Out` explicitly on `ref` but it's best to be clear, as it's possible to have `[In] ref` for example. The marshaller doesn't know you did `ref` as opposed to `in` or `out` it only sees a pointer. There isn't much difference from your option except for `SizeParamIndex` which (if the array is **not** null) then the marshaller will use to work out how much to copy (if it needs to). If the array is null then no it won't use it obviously. – Charlieface May 29 '23 at 11:56