英文:
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).
[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
[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 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).
[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
[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 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()
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论