【问题标题】:C# calling C function that returns struct with fixed size char arrayC# 调用 C 函数,该函数返回具有固定大小的 char 数组的结构
【发布时间】:2012-04-25 17:10:01
【问题描述】:

所以,这个问题有很多变种,看了几个还是想不通。

这是 C 代码:

typedef struct
{
unsigned long Identifier;
char Name[128];
} Frame;

Frame GetFrame(int index);

这是 C# 代码:

struct Frame
{
    public ulong Identifier;
    [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = 128)]
    public char[] Name;
}

[DllImport("XNETDB.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern Frame GetFrame(int index);

这是我在 C# 中尝试的最后一次尝试,看起来很合乎逻辑,但我收到错误“方法的签名与 PInvoke 不兼容”。所以,我有点不知道下一步该尝试什么。任何帮助表示赞赏。

谢谢, 凯文

已更新 Kevin 将此作为编辑添加到我的回答中

我应该改为更改我的 C 代码:

void GetFrame(int index, Frame * f);

并用于 C#:

struct Frame
{
    public uint Identifier;
    [MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string Name;
}

[DllImport("XNETDB.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void GetFrame(int index, ref Frame f);

【问题讨论】:

标签: c# pinvoke


【解决方案1】:

您选择的 PInvoke 签名存在两个问题。

第一个很容易修复。您误译了unsigned long。在 C 中,unsigned long 通常只有 4 个字节。您选择了 C# 类型 long,它是 8 个字节。将 C# 代码更改为使用 uint 将解决此问题。

第二个有点难。正如 Tergiver 指出的那样,CLR Marshaller 仅支持在返回位置的结构,如果它是 blittable 的话。 Blittable 是一种奇特的说法,它在本机代码和托管代码中具有完全相同的内存表示。您选择的结构定义不是 blittable,因为它有一个嵌套数组。

如果您记得 PInvoke 是一个非常简单的过程,则可以解决此问题。 CLR Marshaller 真的只需要你用你的类型和 pinvoke 方法的签名来回答 2 个问题

  • 我要复制多少字节?
  • 他们需要朝哪个方向发展?

在这种情况下,字节数是sizeof(unsigned long) + 128 == 132。所以我们需要做的就是建立一个可blittable 且大小为132 字节的托管类型。最简单的方法是定义一个 blob 来处理数组部分

[StructLayout(LayoutKind.Sequential, Size = 128)]
struct Blob
{
   // Intentionally left empty. It's just a blob
}

这是一个没有成员的结构,在编组器看来,它的大小为 128 字节(作为奖励,它是 blittable!)。现在我们可以轻松地将Frame 结构定义为uint 和这种类型的组合

struct Frame
{
    public int Identifier;
    public Blob NameBlob;
    ...
}

现在我们有了一个 blittable 类型,编组器将看到其大小为 132 字节。这意味着它可以与您定义的 GetFrame 签名一起正常工作

剩下的唯一部分是让您访问实际的char[] 名称。这有点棘手,但可以通过一些元帅魔法来解决。

public string GetName()
{
    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(128);
        Marshal.StructureToPtr(NameBlob, ptr, false);
        return Marshal.PtrToStringAnsi(ptr, 128);
    }
    finally
    {
        if (ptr != IntPtr.Zero) 
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
}

注意:我无法对调用约定部分发表评论,因为我不熟悉 GetFrame API,但我肯定会检查这一点。

【讨论】:

  • 我看不出该类型会或多或少地与基于此更改的 P/Invoke 兼容,尽管这肯定是一个应该纠正的移植问题。
  • @BenVoigt 它可能会造成很大的不同。在原始签名中,编组器将假定 char[] 从结构开头的偏移量 8 开始,而不是实际值 4。因此,编组器看到 4 个字节实际上并不存在。它会在编组到本机时写入这 4 个字节,并在从本机编组返回时读取这 4 个字节。这 4 个字节本质上是垃圾,因此会导致未定义的行为
  • 当然会影响正确性。但它不会导致编译错误。 ulonguint 都是 blittable,p/invoke 以相同的方式处理它们(尽管程度不同)。
  • @BenVoigt 我认为用户在这两种情况下都不会遇到编译器错误。我相信这是一个运行时错误。 C# 编译器在它的任何消息中都不使用工作 PInvoke
  • 这是我得到的运行时错误。将 ulong 更改为 uint 仍会导致异常“方法的签名与 PInvoke 不兼容”。我正在使用来自同一个 DLL 的另一个函数,该函数需要一个 unsigned long,并且使用 ulong 或 uint 导入原型可以成功。
【解决方案2】:

问题是native函数返回一个不可blittable类型作为返回值。

http://msdn.microsoft.com/en-us/library/ef4c3t39.aspx

P/Invoke 不能有非 blittable 类型作为返回值。

您不能 p/Invoke 该方法。 [EDIT 其实是可以的,见JaredPar's answer]

按值返回 132 字节是个坏主意。如果这个本机代码是你的,我会修复它。您可以通过分配 132 个字节并返回一个指针来修复它。然后添加一个 FreeFrame 方法来释放该内存。现在它可以被 p/Invoked。

或者,您可以将其更改为接受指向将要填充的帧内存的指针。

【讨论】:

  • +1,完全忘记了返回类型必须是 blittable 规则。同意,如果您有控制权,最好提供一个更简单的层。如果您不这样做,尽管我添加了一个可行(但丑陋)的解决方案。
  • @kevin.key 我现在将 JaredPar 的答案标记为答案,因为如果您无法更改本机代码,它确实可以直接工作。
  • 我确实将其标记为答案。关键是在 C# 中通过 ref 传递结构,并更改 C 函数以通过指针参数传递结构。
【解决方案3】:

JaredPar 的另一个选项是利用 C# 固定大小缓冲区功能。但是,这确实需要您打开设置以允许不安全代码,但它避免了 2 个结构。

class Program
{
    private const int SIZE = 128;

    unsafe public struct Frame
    {
        public uint Identifier;
        public fixed byte Name[SIZE];
    }

    [DllImport("PinvokeTest2.DLL", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    private static extern Frame GetFrame(int index);

    static unsafe string GetNameFromFrame(Frame frame)
    {
        //Option 1: Use if the string in the buffer is always null terminated
        //return Marshal.PtrToStringAnsi(new IntPtr(frame.Name));

        //Option 2: Use if the string might not be null terminated for any reason,
        //like if were 128 non-null characters, or the buffer has corrupt data.

        return Marshal.PtrToStringAnsi(new IntPtr(frame.Name), SIZE).Split('\0')[0];
    }

    static void Main()
    {
        Frame a = GetFrame(0);
        Console.WriteLine(GetNameFromFrame(a));
    }
}

【讨论】:

  • 不错的一个!您可以使用大小为 128 的 PtrToStringAnsi 重载,这样如果没有终止 NULL 字符,它就不会溢出。
  • 如果有可能没有空字符,那么您应该使用我展示的第二个选项,它确实使用了该重载。不幸的是,重载的 AFAICT 将始终创建一个 128 个字符的字符串,而不是在第一个 NULL 处停止。这就是为什么我在空字符上添加拆分,并选择第一个结果。
  • 你是对的,生成的字符串长度将是你传递的大小。
猜你喜欢
  • 1970-01-01
  • 2016-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多