如何用最简单的方式从本地C调用C#委托以传递字符串数组?

4
我知道这可以通过在C语言中进行动态分配内存,在委托中传递已分配的指针,将其编组为string[],然后使用单独的导出C函数从托管代码中释放已分配的内存来实现。
我的问题是:有没有更简单的方法?例如:
  • C#委托参数的类型为string []?
  • 无需从托管代码调用单独的free函数
编辑:我尝试了委托签名:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
MyManagedDelegate(string[] values, int valueCount)

C中的and函数:

void NativeCallDelegate(char *pStringValues[], int nValues)
{
    if (gSetStringValuesCB)
        gSetStringValuesCB(pStringValues, nValues);
}

C语言中的函数调用:

char *Values[]= {"One", "Two", "Three"};
NativeCallDelegate(Values, 3);

这导致我只能使用数组中的第一个字符串。

也许你可以使用一个 BSTR 数组,也许编组程序能够为你完成这项工作。 - David Heffernan
2
没有任何可以想象的理由以这种方式进行操作。您的 C 代码只是在调用委托之前分配字符串,在之后销毁它们。或者保留存储以供下一次使用,这也很好。这是纯 C 代码实现的细节。P/Invoke marshaller 所做的就是从本机字符串创建托管字符串和/或数组。 - Hans Passant
我真的很希望得到一个完整的答案。谢谢。 - char m
你能把你的需求说得更清楚一些吗?是谁在调用什么? 如果你想从C语言中调用C#,那么使用DllExport进行装饰就足够了。 - weismat
2个回答

9

以下是正确的操作方法,我会提供一个完整的示例以保证可复制性。

C端

typedef void(*setStringValuesCB_t)(char *pStringValues[], int nValues);

static setStringValuesCB_t gSetStringValuesCB;

void NativeCallDelegate(char *pStringValues[], int nValues)
{
    if (gSetStringValuesCB)
        gSetStringValuesCB(pStringValues, nValues);
}

__declspec(dllexport) void NativeLibCall(setStringValuesCB_t callback)
{
    gSetStringValuesCB = callback;
    char *Values[] = { "One", "Two", "Three" };
    NativeCallDelegate(Values, 3);
}

这里没有什么花哨的东西,我只是添加了必要的粘合代码并保留了其余部分。

C#方面

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void MyManagedDelegate(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
    string[] values,
    int valueCount);

[DllImport("NativeTemp", CallingConvention = CallingConvention.Cdecl)]
public static extern void NativeLibCall(MyManagedDelegate callback);

public static void Main()
{
    NativeLibCall(PrintReceivedData);
}

public static void PrintReceivedData(string[] values, int valueCount)
{
    foreach (var item in values)
        Console.WriteLine(item);
}

技巧在于编组部分:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void MyManagedDelegate(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
    string[] values,
    int valueCount);
MarshalAs属性告诉.NET编组器以下内容:
  • UnmanagedType.LPArray 你要获取一个数组...
  • ArraySubType = UnmanagedType.LPStr ...这个数组由标准C字符串组成...
  • SizeParamIndex = 1 ...并且数组的大小由第二个参数指定。

.NET编组器在调用C#方法之前,将C字符串复制并转换为System.String实例。因此,如果您需要将动态生成的字符串传递给C#,则可以在C代码中malloc它们,然后调用gSetStringValuesCB,最后可以立即free它们,因为.NET有自己的数据副本。


您可以参考文档

UnmanagedType.LPArray

指向C风格数组的第一个元素的指针。在从托管代码到非托管代码进行编组时,数组的长度由托管数组的长度确定。当从非托管代码到托管代码进行编组时,数组的长度是从MarshalAsAttribute.SizeConstMarshalAsAttribute.SizeParamIndex字段中确定的,在必要时跟随数组中元素的非托管类型以区分字符串类型。

UnmanagedType.LPStr

单字节、以 null 结尾的 ANSI 字符串。可以在System.StringSystem.Text.StringBuilder数据类型上使用此成员。

MarshalAs.SizeParamIndex

指示包含数组元素计数的基于零的参数,类似于COM中的size_is


1
谢谢!SizeParamIndex 正是我所需要的。 - char m

-1

我想出了远非最优解决方案:

public delegate void MyManagedDelegate([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeConst=10)]string[] values, int valueCount);

如果像这样调用,它就无法工作:

char *Values[]= {"One", "Two", "Three"};
NativeCallDelegate(Values, 3);

我可以使用固定大小为10的数组,将其复制并始终传递给委托。但这不是我想要的。我想知道是否有更好的解决方案...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接