C# call C DLL – Difference between passing arguments to DLL using pointers manually and using automatic marshalling by P/Invoke

huangapple go评论80阅读模式
英文:

C# call C DLL - Difference between passing arguments to DLL using pointers manually and using automatic marshalling by P/Invoke

问题

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] - https://stackoverflow.com/questions/15544818/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] - https://stackoverflow.com/questions/5486938/c-sharp-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 C# call C DLL – Difference between passing arguments to DLL using pointers manually and using automatic marshalling by P/Invoke I can use the /unsafe flag

英文:

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] - https://stackoverflow.com/questions/15544818/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] - https://stackoverflow.com/questions/5486938/c-sharp-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 C# call C DLL – Difference between passing arguments to DLL using pointers manually and using automatic marshalling by P/Invoke I can use the /unsafe flag

答案1

得分: -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] = " + 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().

英文:

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().

huangapple
  • 本文由 发表于 2023年5月29日 17:46:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76356270.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定