【问题标题】:Call unmanaged Code from C# - returning a struct with arrays从 C# 调用非托管代码 - 返回带有数组的结构
【发布时间】:2010-12-11 16:01:39
【问题描述】:

[编辑] 我按照斯蒂芬马丁的建议更改了来源(以粗体突出显示)。并添加了 C++ 源代码。

我想在自写的 C++ dll 中调用非托管函数。该库读取机器的共享内存以获取第三方软件的状态信息。由于有几个值,我想返回结构中的值。但是,在结构中有char [](具有固定大小的字符数组)。我现在尝试从 dll 调用中接收该结构,如下所示:

[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_OUTPUT
{
    UInt16 ReadyForConnect;        

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
    String VersionStr;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]
    String NameOfFile;    
    // actually more of those
}

public partial class Form1 : Form
{
    public SYSTEM_OUTPUT output;

    [DllImport("testeshm.dll", EntryPoint="getStatus")]
    public extern static int getStatus(out SYSTEM_OUTPUT output);

    public Form1()
    {
        InitializeComponent();           

    }

    private void ReadSharedMem_Click(object sender, EventArgs e)
    {
        try
        {
            label1.Text = getStatus(out output).ToString();
        }
        catch (AccessViolationException ave)
        {
            label1.Text = ave.Message;
        }
    }
}

我也会发布来自 c++ dll 的代码,我相信还有更多内容需要寻找。原始结构STATUS_DATA 有一个由结构SYSTEM_CHARACTERISTICS 的四个实例组成的数组,并且在该结构中有char[]s,尚未填充(尚未),导致指针错误。这就是为什么我试图提取STATUS_DATA 中第一个SYSTEM_CHARACTERISTICS 项目的子集。

#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <iostream>
#if defined(_MSC_VER)
#include <windows.h>
#define DLL extern "C" __declspec(dllexport)
#else
#define DLL
#endif

using namespace std;

enum { SYSID_LEN = 1024, VERS_LEN = 128, SCENE_LEN = 1024 };
enum { MAX_ENGINES = 4 };

struct SYSTEM_CHARACTERISTICS
{
    unsigned short  ReadyForConnect;
    char            VizVersionStr[VERS_LEN];
    char            NameOfFile[SCENE_LEN];

    char            Unimplemented[SCENE_LEN]; // not implemented yet, resulting to bad pointer, which I want to exclude (reason to have SYSTEM_OUTPUT)
};

struct SYSTEM_OUTPUT
{
    unsigned short  ReadyForConnect;        
    char            VizVersionStr[VERS_LEN];
    char            NameOfFile[SCENE_LEN];
};

struct STATUS_DATA
{
    SYSTEM_CHARACTERISTICS engine[MAX_ENGINES];
};


TCHAR szName[]=TEXT("E_STATUS");


DLL int getStatus(SYSTEM_OUTPUT* output)
{
    HANDLE hMapFile;
    STATUS_DATA* pBuf;

    hMapFile = OpenFileMapping(
        FILE_MAP_READ,          // read access
        FALSE,                  // do not inherit the name
        szName);                // name of mapping object 

    if (hMapFile == NULL) 
    { 
        _tprintf(TEXT("Could not open file mapping object (%d).\n"), 
            GetLastError());
        return -2;

    } 

    pBuf = (STATUS_DATA*) MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0);                                          

    if (pBuf == NULL) 
    { 
        _tprintf(TEXT("Could not map view of file (%d).\n"), 
            GetLastError()); 

        CloseHandle(hMapFile);  
        return -1;

    }

    output->ReadyForConnect = pBuf->engine[0].ReadyForConnect;              
    memcpy(output->VizVersionStr, pBuf->engine[0].VizVersionStr, sizeof(pBuf->engine[0].VizVersionStr));
    memcpy(output->NameOfFile, pBuf->engine[0].NameOfFile, sizeof(pBuf->engine[0].NameOfFile));

    CloseHandle(hMapFile);
    UnmapViewOfFile(pBuf);  

    return 0;
}

现在我得到一个空的 output 结构,返回值不是预期的 0。它是一个七位数的变化数字,这让我感到困惑......我在dll中搞砸了吗?如果我使非托管代码可执行并对其进行调试,我可以看到 output 正在填充适当的值。

【问题讨论】:

    标签: c# c++ pinvoke ipc marshalling


    【解决方案1】:

    您实际上并没有将任何数据编组到托管端。当您在托管端声明output 时,它的默认值为null。然后,在非托管方面,您永远不会为output 分配任何内存。您应该分配一些非托管内存,将指向该内存的指针传递给您的 dll 函数,然后将该内存的指针编组到您的结构:

    [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Ansi)]
    public struct SYSTEM_OUTPUT
    {
        UInt16 ReadyForConnect;        
    
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        String VersionStr;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]
        String NameOfFile;    
        // actually more of those
    }
    
    public partial class Form1 : Form
    {
        public SYSTEM_OUTPUT output;
    
        [DllImport("testeshm.dll", EntryPoint="getStatus")]
        public extern static int getStatus(IntPtr output);
    
        public Form1()
        {
            InitializeComponent();           
    
        }
    
        private void ReadSharedMem_Click(object sender, EventArgs e)
        {
            IntPtr ptr;
            try
            {
                ptr = Marshall.AllocHGlobal(Marshall.SizeOf(typeof(SYSTEM_OUTPUT)));
                int ret = getStatus(ptr);
    
                if(ret == 0)
                {
                    output = (SYSTEM_OUTPUT)Marshal.PtrToStructure(ptr, typeof(SYSTEM_OUTPUT));
                }
    
            //do something with output
    
                label1.Text = ret;
            }
            catch (AccessViolationException ave)
            {
                label1.Text = ave.Message;
            }
            finally
            {
                Marshal.FreeHGlobal(ptr);  //make sure to free the memory
            }
        }
    }
    

    编辑:

    您的问题可能是packing 策略之间的差异问题。我已经更新了结构定义。

    【讨论】:

    • 感谢您的提示,但是我在 c++ 代码中仍然遇到问题。当从托管端调用时,getStatus 方法返回大量 8 位数字(如前所述),而不是 0 或任何其他定义的返回值(我在 if 语句中定义了另外两个,见上文)。这可能会混淆价值观。但是,当我将 c++ 代码作为可执行文件运行时,它应该返回 0。 c++ 代码中有什么奇怪的地方吗?
    • 你能发布完整的 c++ 可执行示例吗?
    • 我刚刚编译并运行了您的示例和我的示例,并在托管端收到了 -2(因为我没有映射文件)。
    • 那么打包结构可能有问题。我添加了一个链接并更新了代码
    【解决方案2】:

    你想做什么是可能的,但我认为你正在解决错误的问题。

    为什么不直接从 C# 读取内存映射文件? 看看Winterdom.IO.FileMap

    我用过,效果很好。

    MemoryMappedFile file = MemoryMappedFile.Open(FileMapRead, name);
    using (Stream stream = memoryMappedFile.MapView(MapAccess.FileMapAllAccess, 0, length))
    {
        // here read the information that you need   
    }
    

    你还没有完成 - 你仍然需要将字节缓冲区转换为结构,但你都在托管方面,这会更容易。

    【讨论】:

    • 谢谢,我一定会试一试的。如果可行,我会提供反馈。之前在c#中搜索过内存映射,但是只找到了一个.Net 4.0的类。
    【解决方案3】:

    编辑:我正在重写整个答案。

    我获取了您所有的 C++ 和 C# 代码,将其放入解决方案并运行它——一切都对我有用。我没有你特定的内存映射东西,所以我通过用一些假数据填充 pBuf 来模拟它,一切都让它恢复正常;返回值和输出结构都是正确的。

    您的项目设置可能有问题吗?这听起来很傻,但是您提到了运行和调试未管理的代码;您正在构建一个 dll 对吗?

    【讨论】:

    • 是的,我愿意。我只是做了一个构建作为测试目的的应用程序。然后将其改回dll并重新构建。
    • 非托管 dll 不会自动复制到 bin 目录中。 Release和Debug目录下的dll是一样的吗?
    【解决方案4】:
    1. 确保您的 ReadyForConnect 字段未填充到 4 个字节。在我的项目中,结果发现所有短 int(2 字节)字段都在非托管 DLL 中填充了 4 字节的虚拟字节。如果这是问题所在,您应该以这种方式编组结构:
    [StructLayout(LayoutKind.Sequential)] 公共结构 SYSTEM_OUTPUT { [MarshalAs(UnmanagedType.I2)] UInt16 ReadyForConnect; [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst=2)] 字节[]对齐; // 2 字节对齐,最多 4 字节边距 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] 字符串版本Str; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] 字符串名称文件; // ... }
    1. 如果字符串是 ANSI 空终止字符串,您可以将它们注释为:
    [MarshalAs(UnmanagedType.LPStr)] 公共字符串 VersionStr;

    【讨论】:

    • 在声明结构时使用 Pack 选项将为您解决此问题
    【解决方案5】:

    您是否考虑过将 C++/CLI 程序集添加到您的项目中?这是弥合托管代码和非托管代码之间差距的一种极其简单且强大的方法。我自己也经常用。

    【讨论】:

      【解决方案6】:

      谁为结构分配了内存?您不能从托管堆中删除本机内存。一般来说,如果本地 DLL 期望调用者释放内存,或者返回一个回调接口(如 IMalloc)来释放返回的内存,则本机 DLL 应该在 COM 堆上分配。这意味着您需要以 IntPtr 形式接收结果内存地址,并在释放内存之前使用 System.Runtime.InteropServices.Marshal 将数据从本机复制到托管堆(可能是结构)。

      编辑更新的函数签名: 使用公共静态外部 int getStatus(参考 SYSTEM_OUTPUT 输出);您没有在本机函数中的 COM 堆上进行分配,因此 out 是不必要的。

      【讨论】:

      • 这里不存在内存分配问题。他返回的是一个结构而不是一个指向结构的指针,因此运行时使用标准 C 方法为返回值大于 8 个字节的方法(在 x86 上)封送隐藏指针。
      • 原始问题显示的代码返回一个没有明确内存所有权的指针。该操作已使用新的函数签名修改了问题并添加了本机代码部分
      【解决方案7】:

      在结构中返回信息时,标准方法是将指向结构的指针作为方法的参数传递。该方法填充结构成员,然后返回某种状态代码(或布尔值)。因此,您可能想要更改您的 C++ 方法以采用 SYSTEM_OUTPUT* 并返回 0 表示成功或一些错误代码:

      public partial class Form1 : Form
      {
          public SYSTEM_OUTPUT output;
      
          [DllImport("testeshm.dll", EntryPoint="getStatus")]
          public extern static int getStatus(out SYSTEM_OUTPUT output);
      
          public Form1()
          {
              InitializeComponent();           
          }
      
          private void ReadSharedMem_Click(object sender, EventArgs e)
          {
              try
              {
                  if(getStatus(out output) != 0)
                  {
                      //Do something about error.
                  }
              }
              catch (AccessViolationException ave)
              {
                  label1.Text = ave.Message;
              }
          }
      }
      

      【讨论】:

      • 我编辑了帖子和来源以符合您的建议。还发布了非托管代码的 sn-p。
      • 我有没有提到我仍然不知道为什么它不起作用?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多