C# 调用返回具有固定大小字符数组的结构的 C 函数

问题描述 投票:0回答:3

所以,这个问题有很多种变体,看了几个还是搞不明白。

这是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 不兼容”。所以,我有点迷失了下一步要尝试什么。如有任何帮助,我们将不胜感激。

谢谢, 凯文

已更新凯文将此添加为对我的答案的编辑

我应该更改我的 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
3个回答
25
投票

您选择的 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,但这是我肯定会检查的内容。

另请参阅:


10
投票

问题在于本机函数返回不可直接传送的类型作为返回值。

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

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

您无法 p/Invoke 该方法。 [编辑实际上是可能的,请参阅JaredPar的回答]

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

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


4
投票

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));
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.