从C#调用非托管代码 - 返回带数组的结构

时间:2009-10-30 16:10:22

标签: c# c++ pinvoke ipc marshalling

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

我想在自编的C ++ dll中调用非托管函数。该库读取机器的共享内存,以获取第三方软件的状态信息。由于有几个值,我想在结构中返回值。但是,在结构中有char [](具有固定大小的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,它们尚未被填充(尚未),从而导致错误的指针。这就是我试图在SYSTEM_CHARACTERISTICS中提取第一个STATUS_DATA项的子集的原因。

#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正在填充适当的值。

7 个答案:

答案 0 :(得分:3)

在结构中返回信息时,标准方法是将指向结构的指针作为方法的参数传递。该方法填充结构成员,然后返回某种状态代码(或布尔值)。因此,您可能希望更改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;
        }
    }
}

答案 1 :(得分:3)

  1. 确保您的ReadyForConnect字段最多不填充4个字节。在我的项目中,结果发现所有短的int(2字节)字段都填充了非托管DLL中的4字节的虚拟字节。如果这是问题,你应该用这种方式编组结构:
  2.     [StructLayout(LayoutKind.Sequential)] 
        public struct SYSTEM_OUTPUT 
        {     
           [MarshalAs(UnmanagedType.I2)] 
           UInt16 ReadyForConnect;
           [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst=2)]
           byte[] aligment;          // 2 byte aligment up to 4 bytes margin
           [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]    
           String VersionStr;    
           [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]    
           String NameOfFile;        // ...
        }
    
    1. 如果字符串是ANSI空终止字符串,则可以将它们标注为:
    2.   [MarshalAs(UnmanagedType.LPStr)]                   public String VersionStr;
      

答案 2 :(得分:2)

您实际上并没有将任何数据整理到管理方。当您在管理方声明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策略之间差异的问题。我已经更新了struct定义。

答案 3 :(得分:1)

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

我把你所有的C ++和C#代码都删除了,把它放到一个解决方案中并运行它 - 一切都适合我。我没有你的特定内存映射的东西,所以我通过填充pBuf与一些假数据模拟它,一切都使它恢复正常;返回值和输出结构都是正确的。

您的项目设置可能有问题吗?这听起来很愚蠢,但是你提到了运行和调试未经修改的代码;你正在构建一个DLL吗?

答案 4 :(得分:1)

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

为什么不直接从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   
}

由于你没有完成 - 你仍然需要将一个字节缓冲区转换为一个结构,但你们都在管理方面,这将更容易。

答案 5 :(得分:0)

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

编辑更新的功能签名: 使用public static extern int getStatus(ref SYSTEM_OUTPUT output);您没有在本机函数中分配COM堆,因此不需要输出。

答案 6 :(得分:0)

您是否考虑过在项目中添加C ++ / CLI程序集?这是弥合托管代码和非托管代码之间差距的极其简单而强大的方法。我自己用了很多。