我应该如何使用 P/Invoke 将字符串数组传递给 C 库?

Posted

技术标签:

【中文标题】我应该如何使用 P/Invoke 将字符串数组传递给 C 库?【英文标题】:How should I pass an array of strings to a C library using P/Invoke? 【发布时间】:2020-11-14 12:33:15 【问题描述】:

我正在尝试从 C# 应用程序 P/Invoke 到 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,我得到相同的结果/非工作崩溃。

由于我主要是一名具有一定 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.filesToProcessLength = ...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 程序中 printf sizeof(struct Myargs) 看看是否在 char ** 成员之前添加了 32 位填充?在 64 位平台上可能会出现这种情况。 @AdrianMole 也这么认为,但是filesToProcessLength 应该是不同的值。 @KonradRudolph 说得好。 您必须将 filesToProcess 声明为 IntPtr 并自己进行编组工作。或者不要使用结构并将所有作为参数传递,如下所示:[DllImport("myLib.so", CharSet = CharSet.Ansi)] static extern bool myFunction(string[] files, int count); 是的,您无法使用具有可变大小数组的结构。 IntPtr 是唯一的方法。你需要一个例子吗? 【参考方案1】:

您不能在结构中使用可变大小的数组,您必须手动编组整个事物,或者使用更容易的参数,尤其是在仅 (C# 到 C) 的方式中。

如果你出于某种原因想使用结构,那么你可以这样做:

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);

【讨论】:

好奇:是否有特定原因使用 COM 内存分配而不是 HGlobal 分配? @MichaelStum - 不是真的。在 Windows 上,很长一段时间以来都是一样的:***.com/questions/36420692/… 我不确定它是否符合合同规定,也不确定其他平台。【参考方案2】:

您不能使用这样的结构,因为结构必须具有编译时大小,而根据定义,您的结构没有。

您可以在 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));
    

【讨论】:

如果你仔细想想,只有函数 marshaller 才能做到这一点是有道理的,因为它需要一个明确定义的范围,之后它可以删除指向为这些字符串分配的内存的指针,以及包含它们的数组,并且该范围是函数调用。在 C++ 中,这意味着您需要从收到的参数中复制所需的任何内容,因为它们将在函数退出后被释放。 这里不需要任何 MarshalAs arg,因为 .NET 在这种情况下可以推断出所有内容。如果 C 使用 stdcall,则不需要 EntryPoint,也不需要 CallingConvention。仅在我的评论中添加 CharSet = CharSet.Ansi。 如果你写 LPStr 也不需要字符集。做同一件事的不同方式。 “你的结构没有[编译时大小]”——这是错误的。结构具有完全定义好的编译时大小。请注意,C 结构使用的是常规指针指向指针,而不是(编译器扩展)带有尾随数组的可变大小结构。 但是C#结构没有,字节大小根据字符串长度不同。那是不能表示为 POD 结构的那个。如果您在可视化方面需要帮助,请与包含固定长度字符串的 const 大小数组进行比较和对比。

以上是关于我应该如何使用 P/Invoke 将字符串数组传递给 C 库?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 p/invoke 终止从 C# 调用的 C++ 函数

使用 C# P/Invoke 方法在 Struct 内编组数组

通过 P/Invoke 将 MFC CArray 编组为 C#

P/Invoke 改变传递参数的值

如何在本机代码中传递数组数组

通过 P/Invoke 调用 GetGUIThreadInfo