【问题标题】:Marshalling an array of structures from C++ to C#?将一组结构从 C++ 编组到 C#?
【发布时间】:2010-12-17 10:01:10
【问题描述】:

在我的 C# 代码中,我试图从旧版 C++ DLL(我无法更改的代码)中获取结构数组。

在那个 C++ 代码中,结构是这样定义的:

struct MyStruct
{
    char* id;
    char* description;
};

我正在调用的方法 (get_my_structures) 返回一个指向 MyStruct 结构数组的指针:

MyStruct* get_my_structures()
{
    ...
}

还有另一种方法可以返回结构的数量,所以我知道返回了多少结构。

在我的 C# 代码中,我这样定义 MyStruct:

[StructLayout(LayoutKind.Sequential)]  
public class MyStruct
{
  [MarshalAsAttribute(UnmanagedType.LPStr)]    // <-- also tried without this
  private string _id;
  [MarshalAsAttribute(UnmanagedType.LPStr)]
  private string _description;
}

互操作签名如下所示:

[DllImport("legacy.dll", EntryPoint="get_my_structures")]
public static extern IntPtr GetMyStructures();

最后,获取 MyStruct 结构数组的代码如下所示:

int structuresCount = ...;
IntPtr myStructs = GetMyStructures();
int structSize = Marshal.SizeOf(typeof(MyStruct));    // <- returns 8 in my case
for (int i = 0; i < structuresCount; i++)
{
    IntPtr data = new IntPtr(myStructs.ToInt64() + structSize * i);
    MyStruct ms = (MyStruct) Marshal.PtrToStructure(data, typeof(MyStruct));
    ...
}

问题在于,只有第一个结构(偏移量为零的结构)才能正确编组。后续的 _id 和 _description 成员中有虚假值。这些值并没有被完全丢弃,或者看起来是这样的:它们是来自其他一些内存位置的字符串。代码本身不会崩溃。

我已验证 get_my_structures() 中的 C++ 代码确实返回了正确的数据。在通话期间或通话后不会意外删除或修改数据。

在调试器中查看,返回数据的 C++ 内存布局如下:

0: id (char*)           <---- [MyStruct 1]
4: description (char*)
8: id (char*)           <---- [MyStruct 2]
12: description (char*)
16: id (char*)          <---- [MyStruct 3]
...

[2009 年 18 月 11 日更新]

以下是 C++ 代码准备这些结构的方式(实际代码要丑得多,但这是一个足够接近的近似值):

static char buffer[12345] = {0};
MyStruct* myStructs = (MyStruct*) &buffer;
for (int i = 0; i < structuresCount; i++)
{
    MyStruct* ms = <some other permanent address where the struct is>;
    myStructs[i].id = (char*) ms->id;
    myStructs[i].description = (char*) ms->description;
}
return myStructs;

诚然,上面的代码做了一些丑陋的转换并复制了原始指针,但它似乎仍然正确地做到了这一点。至少这是我在调试器中看到的:上面的(静态)缓冲区确实包含所有这些裸 char* 指针一个接一个地存储,它们指向内存中的有效(非本地)位置。

Pavel 的示例表明,这确实是唯一可能出错的地方。我将尝试分析字符串真正所在的“结束”位置会发生什么,而不是存储指针的位置。

【问题讨论】:

    标签: c# .net c++ interop marshalling


    【解决方案1】:

    我无法重现您的问题,这让我怀疑它确实是 C++ 方面的问题。这是我尝试的完整源代码。

    dll.cpp - 使用 cl.exe /LD 编译:

    extern "C" {
    
    struct MyStruct
    {
        char* id;
        char* description;
    };
    
    __declspec(dllexport)
    MyStruct* __stdcall get_my_structures()
    {
        static MyStruct a[] =
        {
            { "id1", "desc1" },
            { "id2", "desc2" },
            { "id3", "desc3" }
        };
        return a;
    
    }
    
    }
    

    test.cs - 使用 csc.exe /platform:x86 编译:

    using System;
    using System.Runtime.InteropServices;
    
    
    [StructLayout(LayoutKind.Sequential)]  
    public class MyStruct
    {
      [MarshalAsAttribute(UnmanagedType.LPStr)]
      public string _id;
      [MarshalAsAttribute(UnmanagedType.LPStr)]
      public string _description;
    }
    
    
    class Program
    {
        [DllImport("dll")]
        static extern IntPtr get_my_structures();
    
        static void Main()
        {
            int structSize = Marshal.SizeOf(typeof(MyStruct));
            Console.WriteLine(structSize);
    
            IntPtr myStructs = get_my_structures();
            for (int i = 0; i < 3; ++i)
            {
                IntPtr data = new IntPtr(myStructs.ToInt64() + structSize * i);
                MyStruct ms = (MyStruct) Marshal.PtrToStructure(data, typeof(MyStruct));
    
                Console.WriteLine();
                Console.WriteLine(ms._id);
                Console.WriteLine(ms._description);
            }
        }
    }
    

    这会正确打印出所有 3 个结构。

    你能展示你的 C++ 代码来填充结构吗?您可以直接从 C++ 调用它并获得正确结果的事实并不一定意味着它是正确的。例如,您可以返回一个指向堆栈分配结构的指针。那么,在进行直接调用时,您会得到一个技术上无效的指针,但数据可能会保留下来。在进行 P/Invoke 编组时,堆栈可能会被 P/Invoke 数据结构覆盖,直到它尝试从那里读取值。

    【讨论】:

    • 我更新了我的问题,添加了 C++ 代码。由于您已经证明互操作部分本身没有问题(在 C# 方面也没有问题),因此得出一定是 C++ 代码的结论确实是合乎逻辑的。
    【解决方案2】:

    我会改变结构。而不是字符串等,使用 IntPtr:

    [StructLayout(LayoutKind.Sequential)] 公共类 MyStruct { 私人 IntPtr _id; 私有 IntPtr _description; }

    然后可以使用 Marshal.PtrToString 手动将 C# 数组的每个值编组为字符串,同时考虑字符集等。

    【讨论】:

    • 听起来是一种可行的方法,但不幸的是它似乎并没有解决我遇到的问题。即使使用 IntPtr 成员,这些成员在除第一个结构之外的所有结构中仍然具有相同(无效)的值。
    【解决方案3】:

    我通常会通过反复试验来解决这些问题。确保在 StructLayout 上设置了 CharSet 属性,我会尝试 UnmanagedType.LPTStr,它似乎更适合 char *,尽管我不确定为什么。

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]  
    public class MyStruct
    {
        [MarshalAsAttribute(UnmanagedType.LPTStr)]
        private string _id;
        [MarshalAsAttribute(UnmanagedType.LPTStr)]
        private string _description;
    }
    

    【讨论】:

      【解决方案4】:

      我认为,除了给出的答案之外,您还需要提供长度,即 [MarshalAsAttribute(UnmanagedType.LPTStr), SizeConst = , ArraySubType = System.Runtime.InteropServices.UnmanagedType.AnsiBStr)]

      这是一个试验和错误,也是要考虑的另一件事,在一些需要字符串参数(通常是 ref 参数)的 WinAPI 调用中,尝试 StringBuilder 类可能也是值得的。 .除了我在这里提到的几点之外,没有其他想到的......希望这会有所帮助,汤姆

      【讨论】:

      • char* 是空终止的,所以我不会提供长度。这就是我们有 UnmanagedType.LPWStr 和 UnmanagedType.LPTStr 的原因。如果您要提供长度,您还必须使其非常大,以免截断任何数据。不是一个优雅的解决方案。
      【解决方案5】:

      对于 char*,您必须使用 UnmanagedType.LPTStr还建议将StringBuilder 用于非 const char*: 还有一个 CharSet 规范:

      [StructLayout(LayoutKind.Sequential, Charset = CharSet.Auto)]  
      public class MyStruct
      {
        [MarshalAsAttribute(UnmanagedType.LPTStr)]
        private StringBuilder _id;
        [MarshalAsAttribute(UnmanagedType.LPTStr)]
        private StringBuilder _description;
      }
      

      至于DllImport声明,你试过了吗

      [DllImport("legacy.dll", EntryPoint="get_my_structures")]
      public static extern MarshalAs(UnmanagedType.LPArray) MyStruct[] GetMyStructures();
      

      ?

      另外,如果前一个不起作用,请将其保留在 IntPtr 并尝试像这样对返回的结构进行 Mashal:

      for (int i = 0; i < structuresCount; i++)
      {
          MyStruct ms = (MyStruct) Marshal.PtrToStructure(myStructs, typeof(MyStruct));
          ...
          myStructs += Marshal.SizeOf(ms);
      }
      

      【讨论】:

      • 如果我尝试使用 StringBuilder,我会在尝试执行此操作时收到 ArgumentException: int itemSize = Marshal.SizeOf(typeof(MyStruct));错误消息是“类型 'MyStruct' 不能作为非托管结构进行封送;无法计算有意义的大小或偏移量”。
      • @vladimir:你在使用UnmanagedType.LPTStr吗?另外:尝试像其他人建议的那样指定CharSet
      • "字符串是结构的有效成员;但是,StringBuilder 缓冲区在结构中是无效的。" -> msdn.microsoft.com/en-us/library/…
      • @hjb417:我的立场是正确的......似乎 StringBuilder 只能用于以 char* 作为参数的函数的 DllImport 语句中。
      • @Vladimir:我想问题不在于您的结构声明,而在于您将返回的 IntPtr 转换为您的个人结构的方式。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-01
      • 2014-06-27
      相关资源
      最近更新 更多