我应该如何使用 P/Invoke 将字符串数组传递给 C 库?
How should I pass an array of strings to a C library using P/Invoke?
我正在尝试 P/Invoke 从 C# 应用程序到 C 库,发送一个包含字符串数组的结构。我确实可以控制 C 库,并且可以根据需要进行更改。
这是一条单行道:从 C# 到 C,我不需要观察 C 端对结构所做的修改(我也是按值而不是按引用传递它,尽管我稍后可能会更改 - 首先尝试解决眼前的问题)。
我的 C 结构目前看起来像这样:
// C
struct MyArgs {
int32_t someArg;
char** filesToProcess;
int32_t filesToProcessLength;
};
在 C# 中,我复制了这样的结构:
// C#
public struct MyArgs
{
public int someArg;
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.LPStr)]
public string[] filesToProcess;
public int filesToProcessLength;
}
然后传给图书馆:
// C#
[DllImport("myLib.so", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(MyArgs args);
var myArgs = new MyArgs {
someArg = 10,
filesToProcess = new string[] { "one", "two", "three" }
};
myArgs.filesToProcessLength = myArgs.filesToProcess.Length;
Console.WriteLine(myFunction(myArgs));
我想在哪里消费它:
// C
bool myFunction(struct MyArgs args) {
printf("Files to Process: %i\n", args.filesToProcessLength);
for (int i = 0; i < args.filesToProcessLength; i++) {
char* str = args.filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
这基本上会使应用程序崩溃。我得到一个显示 Files to Process: 3
的输出,但随后应用程序就停止了。如果我将 for 循环更改为不尝试访问字符串,它会在循环中计数 - 所以我似乎遇到了某种访问冲突。
如果我更改我的代码以接受数组作为函数参数的一部分,它会起作用:
// C
bool myFunction(struct MyArgs args, char** filesToProcess, int32_t filesToProcesLength) {
printf("Files to Process: %i\n", filesToProcessLength);
for (int i = 0; i < filesToProcessLength; i++) {
char* str = filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
// C#
[DllImport("myLib.so", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(MyArgs args, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] filesToProcess, int filesToProcessLength);
我最初的想法是,因为在一个结构中,我使用了 ByValArray,它可能是一个指向字符串数组的指针(所以本质上是一个 char***?),但即使我将类型更改为 char***
并执行 char** strArray = *args.filesToProcess
,我得到相同的 result/non-working 崩溃。
由于我主要是具有一些 C 知识的 C# 开发人员,所以我在这里有点不知所措。将字符串集合 P/Invoke 放入结构中的 C 库的最佳方法是什么?如前所述,我可以随心所欲地更改 C 库,只是更喜欢将其保留在结构中而不是添加函数参数。
如果重要,这是在 Linux 上,使用 gcc 9.3.0,这只是普通的 C,而不是 C++。
更新:
- sizeof(args) 为 24
- 获取地址:
- &args = ...560
- &args.someArg = ...560
- &args.filesToProcess = ...568
- &args.filesToProcess长度= ...576
- 所以 args.filesToProcess 是指向某物的单个指针 - 将尝试挖掘以查看它指向什么
更新 2: 查看使用 this code 获取的内存转储,似乎 C# 端没有以我想要的方式发送数组,我假设 ByValArray 是这里的问题。
0000 6f 6e 65 00 00 00 00 00 00 00 00 00 00 00 00 00 one.............
0010 50 44 fc 00 00 00 00 00 61 00 00 00 00 00 00 00 PD......a.......
0020 53 00 79 00 73 00 74 00 65 00 6d 00 2e 00 53 00 S.y.s.t.e.m...S.
0030 65 00 63 00 75 00 72 00 69 00 74 00 79 00 2e 00 e.c.u.r.i.t.y...
0040 43 00 72 00 79 00 70 00 74 00 6f 00 67 00 72 00 C.r.y.p.t.o.g.r.
0050 61 00 70 00 68 00 79 00 2e 00 4f 00 70 00 65 00 a.p.h.y...O.p.e.
0060 6e 00 53 00 73 00 6c 00 00 00 98 1f dc 7f 00 00 n.S.s.l.........
所以我得到了第一个数组元素,但之后它只是随机垃圾(它随着每个 运行 而改变) - 所以 C 端暂时没问题,但 C# 端不是。
更新 3:我进行了更多试验,将 C# 端从字符串数组更改为 IntPtr 和 Marshal.UnsafeAddrOfPinnedArrayElement(filesToProcess, 0)
。在 C 端,我现在得到了 C# 数组,当然,它有 C# 的东西和错误的编码,但至少它表明它确实是 C# 端的编组问题。
0000 90 0f 53 f7 27 7f 00 00 03 00 00 00 6f 00 6e 00 ..S.'.......o.n.
0010 65 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e...............
0020 90 0f 53 f7 27 7f 00 00 03 00 00 00 74 00 77 00 ..S.'.......t.w.
0030 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 o...............
0040 90 0f 53 f7 27 7f 00 00 05 00 00 00 74 00 68 00 ..S.'.......t.h.
0050 72 00 65 00 65 00 00 00 00 00 00 00 00 00 00 00 r.e.e...........
0060 90 0f 53 f7 27 7f 00 00 27 00 00 00 20 00 4d 00 ..S.'...'... .M.
我遇到了关键问题:如果我想按值传递数组,结构大小在每次调用时都是动态的,这可能是个问题。但是传递 ByValArray 似乎也不正确。可能需要使用固定大小的数组、数组的 IntPtr,或者放弃结构并将其作为函数参数传递。
但一如既往,如果有人有更好的计划,我会洗耳恭听:)
您不能使用那样的结构,因为结构必须具有编译时大小,而根据定义,您的结构没有那个。
您可以在 C# 的 Marshal.AllocHGlobal()
缓冲区中手动序列化您的数据,然后在 C++ 中反序列化它,或者像这样使用函数调用编组器:
// C++
// extern "C" __declspec(dllexport)
bool myFunction(char **filesToProcess, int filesToProcessLength)
{
printf("Files to Process: %i\n", filesToProcessLength);
for (int i = 0; i < filesToProcessLength; i++)
{
char *str = filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
// C#
[DllImport("dlltest.dll", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
string[] files,
int count);
static void Main(string[] args)
{
Console.WriteLine(myFunction(new string[] { "one", "two", "three" }, 3));
}
您不能在结构中使用可变大小数组,您必须手动编组整个数组,或者使用更容易的参数,尤其是在(C# 到 C)-only 方式中。
如果你出于某种原因想使用一个结构,那么你可以这样做:
C端(我用的是Windows,你可能要适配):
struct MyArgs {
int32_t someArg;
char** filesToProcess;
int32_t filesToProcessLength;
};
// I pass struct as reference, not value, but this is not relevant
// I also use __stdcall which is quite standard on Windows
extern "C" {
__declspec(dllexport) bool __stdcall myFunction(struct MyArgs* pargs) {
printf("Files to Process: %i\n", pargs->filesToProcessLength);
for (int i = 0; i < pargs->filesToProcessLength; i++) {
char* str = pargs->filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
}
C# 端:
static void Main(string[] args)
{
var files = new List<string>();
files.Add("hello");
files.Add("world!");
var elementSize = IntPtr.Size;
var my = new MyArgs();
my.filesToProcessLength = files.Count;
// allocate the array
my.filesToProcess = Marshal.AllocCoTaskMem(files.Count * elementSize);
try
{
for (var i = 0; i < files.Count; i++)
{
// allocate each file
// I use Ansi as you do although Unicode would be better (at least on Windows)
var filePtr = Marshal.StringToCoTaskMemAnsi(files[i]);
// write the file pointer to the array
Marshal.WriteIntPtr(my.filesToProcess + elementSize * i, filePtr);
}
myFunction(ref my);
}
finally
{
// free each file pointer
for (var i = 0; i < files.Count; i++)
{
var filePtr = Marshal.ReadIntPtr(my.filesToProcess + elementSize * i);
Marshal.FreeCoTaskMem(filePtr);
}
// free the array
Marshal.FreeCoTaskMem(my.filesToProcess);
}
}
[StructLayout(LayoutKind.Sequential)]
struct MyArgs
{
public int someArg;
public IntPtr filesToProcess;
public int filesToProcessLength;
};
// stdcall is the default calling convention
[DllImport("MyProject.dll")]
static extern bool myFunction(ref MyArgs args);
我正在尝试 P/Invoke 从 C# 应用程序到 C 库,发送一个包含字符串数组的结构。我确实可以控制 C 库,并且可以根据需要进行更改。
这是一条单行道:从 C# 到 C,我不需要观察 C 端对结构所做的修改(我也是按值而不是按引用传递它,尽管我稍后可能会更改 - 首先尝试解决眼前的问题)。
我的 C 结构目前看起来像这样:
// C
struct MyArgs {
int32_t someArg;
char** filesToProcess;
int32_t filesToProcessLength;
};
在 C# 中,我复制了这样的结构:
// C#
public struct MyArgs
{
public int someArg;
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.LPStr)]
public string[] filesToProcess;
public int filesToProcessLength;
}
然后传给图书馆:
// C#
[DllImport("myLib.so", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(MyArgs args);
var myArgs = new MyArgs {
someArg = 10,
filesToProcess = new string[] { "one", "two", "three" }
};
myArgs.filesToProcessLength = myArgs.filesToProcess.Length;
Console.WriteLine(myFunction(myArgs));
我想在哪里消费它:
// C
bool myFunction(struct MyArgs args) {
printf("Files to Process: %i\n", args.filesToProcessLength);
for (int i = 0; i < args.filesToProcessLength; i++) {
char* str = args.filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
这基本上会使应用程序崩溃。我得到一个显示 Files to Process: 3
的输出,但随后应用程序就停止了。如果我将 for 循环更改为不尝试访问字符串,它会在循环中计数 - 所以我似乎遇到了某种访问冲突。
如果我更改我的代码以接受数组作为函数参数的一部分,它会起作用:
// C
bool myFunction(struct MyArgs args, char** filesToProcess, int32_t filesToProcesLength) {
printf("Files to Process: %i\n", filesToProcessLength);
for (int i = 0; i < filesToProcessLength; i++) {
char* str = filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
// C#
[DllImport("myLib.so", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(MyArgs args, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] filesToProcess, int filesToProcessLength);
我最初的想法是,因为在一个结构中,我使用了 ByValArray,它可能是一个指向字符串数组的指针(所以本质上是一个 char***?),但即使我将类型更改为 char***
并执行 char** strArray = *args.filesToProcess
,我得到相同的 result/non-working 崩溃。
由于我主要是具有一些 C 知识的 C# 开发人员,所以我在这里有点不知所措。将字符串集合 P/Invoke 放入结构中的 C 库的最佳方法是什么?如前所述,我可以随心所欲地更改 C 库,只是更喜欢将其保留在结构中而不是添加函数参数。
如果重要,这是在 Linux 上,使用 gcc 9.3.0,这只是普通的 C,而不是 C++。
更新:
- sizeof(args) 为 24
- 获取地址:
- &args = ...560
- &args.someArg = ...560
- &args.filesToProcess = ...568
- &args.filesToProcess长度= ...576
- 所以 args.filesToProcess 是指向某物的单个指针 - 将尝试挖掘以查看它指向什么
更新 2: 查看使用 this code 获取的内存转储,似乎 C# 端没有以我想要的方式发送数组,我假设 ByValArray 是这里的问题。
0000 6f 6e 65 00 00 00 00 00 00 00 00 00 00 00 00 00 one.............
0010 50 44 fc 00 00 00 00 00 61 00 00 00 00 00 00 00 PD......a.......
0020 53 00 79 00 73 00 74 00 65 00 6d 00 2e 00 53 00 S.y.s.t.e.m...S.
0030 65 00 63 00 75 00 72 00 69 00 74 00 79 00 2e 00 e.c.u.r.i.t.y...
0040 43 00 72 00 79 00 70 00 74 00 6f 00 67 00 72 00 C.r.y.p.t.o.g.r.
0050 61 00 70 00 68 00 79 00 2e 00 4f 00 70 00 65 00 a.p.h.y...O.p.e.
0060 6e 00 53 00 73 00 6c 00 00 00 98 1f dc 7f 00 00 n.S.s.l.........
所以我得到了第一个数组元素,但之后它只是随机垃圾(它随着每个 运行 而改变) - 所以 C 端暂时没问题,但 C# 端不是。
更新 3:我进行了更多试验,将 C# 端从字符串数组更改为 IntPtr 和 Marshal.UnsafeAddrOfPinnedArrayElement(filesToProcess, 0)
。在 C 端,我现在得到了 C# 数组,当然,它有 C# 的东西和错误的编码,但至少它表明它确实是 C# 端的编组问题。
0000 90 0f 53 f7 27 7f 00 00 03 00 00 00 6f 00 6e 00 ..S.'.......o.n.
0010 65 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e...............
0020 90 0f 53 f7 27 7f 00 00 03 00 00 00 74 00 77 00 ..S.'.......t.w.
0030 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 o...............
0040 90 0f 53 f7 27 7f 00 00 05 00 00 00 74 00 68 00 ..S.'.......t.h.
0050 72 00 65 00 65 00 00 00 00 00 00 00 00 00 00 00 r.e.e...........
0060 90 0f 53 f7 27 7f 00 00 27 00 00 00 20 00 4d 00 ..S.'...'... .M.
我遇到了关键问题:如果我想按值传递数组,结构大小在每次调用时都是动态的,这可能是个问题。但是传递 ByValArray 似乎也不正确。可能需要使用固定大小的数组、数组的 IntPtr,或者放弃结构并将其作为函数参数传递。
但一如既往,如果有人有更好的计划,我会洗耳恭听:)
您不能使用那样的结构,因为结构必须具有编译时大小,而根据定义,您的结构没有那个。
您可以在 C# 的 Marshal.AllocHGlobal()
缓冲区中手动序列化您的数据,然后在 C++ 中反序列化它,或者像这样使用函数调用编组器:
// C++
// extern "C" __declspec(dllexport)
bool myFunction(char **filesToProcess, int filesToProcessLength)
{
printf("Files to Process: %i\n", filesToProcessLength);
for (int i = 0; i < filesToProcessLength; i++)
{
char *str = filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
// C#
[DllImport("dlltest.dll", EntryPoint = "myFunction", CallingConvention = CallingConvention.Cdecl)]
internal static extern bool myFunction(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
string[] files,
int count);
static void Main(string[] args)
{
Console.WriteLine(myFunction(new string[] { "one", "two", "three" }, 3));
}
您不能在结构中使用可变大小数组,您必须手动编组整个数组,或者使用更容易的参数,尤其是在(C# 到 C)-only 方式中。
如果你出于某种原因想使用一个结构,那么你可以这样做:
C端(我用的是Windows,你可能要适配):
struct MyArgs {
int32_t someArg;
char** filesToProcess;
int32_t filesToProcessLength;
};
// I pass struct as reference, not value, but this is not relevant
// I also use __stdcall which is quite standard on Windows
extern "C" {
__declspec(dllexport) bool __stdcall myFunction(struct MyArgs* pargs) {
printf("Files to Process: %i\n", pargs->filesToProcessLength);
for (int i = 0; i < pargs->filesToProcessLength; i++) {
char* str = pargs->filesToProcess[i];
printf("\t%i. %s\n", i, str);
}
return true;
}
}
C# 端:
static void Main(string[] args)
{
var files = new List<string>();
files.Add("hello");
files.Add("world!");
var elementSize = IntPtr.Size;
var my = new MyArgs();
my.filesToProcessLength = files.Count;
// allocate the array
my.filesToProcess = Marshal.AllocCoTaskMem(files.Count * elementSize);
try
{
for (var i = 0; i < files.Count; i++)
{
// allocate each file
// I use Ansi as you do although Unicode would be better (at least on Windows)
var filePtr = Marshal.StringToCoTaskMemAnsi(files[i]);
// write the file pointer to the array
Marshal.WriteIntPtr(my.filesToProcess + elementSize * i, filePtr);
}
myFunction(ref my);
}
finally
{
// free each file pointer
for (var i = 0; i < files.Count; i++)
{
var filePtr = Marshal.ReadIntPtr(my.filesToProcess + elementSize * i);
Marshal.FreeCoTaskMem(filePtr);
}
// free the array
Marshal.FreeCoTaskMem(my.filesToProcess);
}
}
[StructLayout(LayoutKind.Sequential)]
struct MyArgs
{
public int someArg;
public IntPtr filesToProcess;
public int filesToProcessLength;
};
// stdcall is the default calling convention
[DllImport("MyProject.dll")]
static extern bool myFunction(ref MyArgs args);