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

C# 如何使用P/Invoke将字符串数组传递给C库?,c#,arrays,c,struct,pinvoke,C#,Arrays,C,Struct,Pinvoke,我试图将C#应用程序的p/Invoke转换为C库,通过包含字符串数组的结构发送。我确实可以控制C库,如果需要的话,我可以进行更改 这是一条单行道:从C#到C,我不需要观察在C端对结构所做的修改(我也通过值传递它,而不是通过引用传递它,尽管我稍后可能会改变这一点——首先尝试解决眼前的问题) 我的C结构目前看起来像这样: /C 结构MyArgs{ int32_t someArg; char**filesToProcess; int32_t filesToProcessLength; }; 在C#中

我试图将C#应用程序的p/Invoke转换为C库,通过包含字符串数组的结构发送。我确实可以控制C库,如果需要的话,我可以进行更改

这是一条单行道:从C#到C,我不需要观察在C端对结构所做的修改(我也通过值传递它,而不是通过引用传递它,尽管我稍后可能会改变这一点——首先尝试解决眼前的问题)

我的C结构目前看起来像这样:

/C
结构MyArgs{
int32_t someArg;
char**filesToProcess;
int32_t filesToProcessLength;
};
在C#中,我将结构复制为:

/C#
公共结构MyArgs
{
公共int-somerg;
[Marshallas(UnmanagedType.ByValArray,ArraySubType=UnmanagedType.LPStr)]
公共字符串[]filesToProcess;
公共int filesToProcessLength;
}
然后将其传递到库:

/C#
[DllImport(“myLib.so”,EntryPoint=“myFunction”,CallingConvention=CallingConvention.Cdecl)]
内部静态外部bool myFunction(MyArgs args);
var myArgs=新的myArgs{
someArg=10,
filesToProcess=新字符串[]{“一”、“二”、“三”}
};
myArgs.filesToProcessLength=myArgs.filesToProcess.Length;
Console.WriteLine(myFunction(myArgs));
在我试图消费它的地方:

/C
bool myFunction(struct MyArgs args){
printf(“要处理的文件:%i\n”,args.filesToProcessLength);
for(int i=0;i
这基本上会使应用程序崩溃。我得到一个输出,上面写着
要处理的文件:3
,但随后应用程序就停止了。如果我将for循环更改为不尝试访问字符串,那么它将在循环中计数-因此我似乎遇到了某种访问冲突

如果我将代码更改为接受数组作为函数参数的一部分,它将起作用:

/C
bool myFunction(struct MyArgs args,char**filesToProcess,int32_t filesToProcesLength){
printf(“要处理的文件:%i\n”,filesToProcessLength);
for(int i=0;i
我最初的想法是,因为在一个结构中,我使用ByValArray,它可能是一个指向字符串数组的指针(本质上是一个字符***?),但即使我在结构中将类型更改为
char***
,并执行
char**strArray=*args.filesToProcess
,我也会得到相同的结果/非工作崩溃

由于我主要是一名具有一些C知识的C#开发人员,所以我在这里有点不知所措。将字符串集合P/调用到结构中的C库中的最佳方法是什么?如前所述,我可以随意更改C库,只是更喜欢将其保留在结构中,而不是添加函数参数

<>如果这很重要,这是在Linux上,使用GCC 93.0,这只是普通C,而不是C++。 更新:

  • sizeof(args)为24
  • 以地址为准:
    • &args=…560
    • &args.someArg=…560
    • &args.filesToProcess=…568
    • &args.filesToProcessLength=…576
  • 因此,args.filesToProcess是指向某个对象的单个指针—将尝试挖掘以查看它指向的对象
更新2:查看使用的内存转储,似乎C#端没有以我想要的方式发送数组,我认为ByValArray是这里的问题所在

0000 6f 6e 65 00 00 00 00 01。。。。。。。。。。。。。
0010 50 44 fc 00 00 00 00 61 00 00 00 00 00 00 00 PD…a。。。。。。。
0020530079007300740065006D002E005300S.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 98 1f dc 7f 00 n.S.S.l。。。。。。。。。
所以我得到了第一个数组元素,但在那之后它只是随机垃圾(每次运行都会改变)——所以C端暂时可以,但C端不行

更新3:我做了更多的实验,并将C端从字符串数组更改为IntPtr和
封送。UnsafeAddrOfPinnedArrayElement(filesToProcess,0)
。在C端,我现在得到了C#数组,当然,其中包含了C#内容和错误的编码,但至少它表明C端确实存在封送问题

我发现了一个关键问题:如果我想按值传递数组,那么每次调用的结构大小都是动态的,这可能是一个问题。但传递ByValArray似乎也不正确。可能需要使用固定大小的数组、数组的IntPtr,或者放弃结构并将其作为函数参数传递


但一如既往,如果有人有更好的计划,我洗耳恭听:)

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

您可以在C代码中手动序列化数据,在C++中使用序列化,然后在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”\u declspec(dllexport) bool myFunction(char**filesToProcess,int filesToProcessLength) { printf(“要处理的文件:%i\n”,filesToProcessLength); for(int i=0;i// 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)); }
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;
    }
}
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);