views:

1046

answers:

7

[EDIT] I changed the source as suggested by Stephen Martin (highlighted in bold). And added the C++ source code as well.

Hi,

I'd like to call an unmanaged function in a self-written C++ dll. This library reads the machine's shared memory for status information of a third party software. Since there are a couple of values, I'd like to return the values in a struct. However, within the struct there are char [] (Arrays of char with a fixed size). I now try to receive that struct from the dll call like this:

[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;
        }
    }
}

I will post code from the c++ dll as well, I'm sure there's more to hunt down. The original struct STATUS_DATA has an array of four instances of the struct SYSTEM_CHARACTERISTICS and within that struct there are char[]s, that are not being filled (yet), resulting in a bad pointer. That's why I'm trying to extract a subset of the first SYSTEM_CHARACTERISTICS item in 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;
}

Now I'm getting an empty output struct and the return value ist not 0 as intended. It is rather a changing number with seven digits, which leaves me puzzled... Have I messed up in the dll? If I make the unmanaged code executable and debug it, I can see, that output is being filled with the appropriate values.

Any ideas?

+3  A: 

When returning information in a struct the standard method is to pass a pointer to a struct as a parameter of the method. The method fills in the struct members and then returns a status code (or boolean) of some kind. So you probably want to change your C++ method to take a SYSTEM_OUTPUT* and return 0 for success or some error code:

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;
        }
    }
}
Stephen Martin
I edited the post and the source to match your suggestions. Also posted a snippet of the unmanaged code.
rdoubleui
and did i mention that I still have no clue why it won't work?
rdoubleui
A: 

who allocated the memory for the structure? You cannot delete native memory from the managed heap. Generally speaking the native DLL should allocate on the COM heap if it expect the caller to free the memory, or return a callback interface like IMalloc to free the returning memory. That means you need to receive the result memory address as IntPtr and use System.Runtime.InteropServices.Marshal to copy data from native to managed heap (may be to a structure) before freeing the memory.

Edit for the updated function signature: use public static extern int getStatus(ref SYSTEM_OUTPUT output); You are not allocating on the COM heap in the native function, so out is unnecessary.

Sheng Jiang 蒋晟
There is no memory allocation problem here. He is returning a struct not a pointer to a struct so the runtime uses the standard C method of marshaling a hidden pointer for methods that return values larger than 8 bytes (on x86).
Stephen Martin
The original question showed code that returns a pointer with no clear memory ownership. The op has modified the question with a new function signature and added the native code part
Sheng Jiang 蒋晟
+2  A: 

You aren't actually marshaling any data over to the managed side. When you declare output on the managed side, it's default value is null. Then, on the unmanaged side, you never allocate any memory for output. You should allocate some unmanaged memory, pass the pointer to that memory to your dll function, then marshal the pointer for that memory to your struct:

[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
     }
    }
}

Edit:

Your problem could be an issue with the difference between packing strategies. I've updated the struct definition.

scottm
Thank you for your hint, however I'm still having trouble in the c++ code. When being called from the managed side, the getStatus-Method returns a high number of 8 digits (as before), not 0 or any other defined return value (I defined two more in the if-statements, see above). That probably messes up the values. When I run the c++ code as an executable, however, it returns 0 as it should. Anything odd in the c++ code?
rdoubleui
can you post the full c++ executable example?
scottm
I just compiled and ran both your example and mine, and received -2 on the managed side (because I don't have the file mapped).
scottm
that was basically the whole c++ code.
rdoubleui
Then it may be an issue with packing the struct. I've added a link and updated the code
scottm
A: 

Have you considered adding a C++/CLI assembly to your project? That's an extremely easy and powerful way to bridge the gap between managed and unmanaged code. I use it quite a lot myself.

danbystrom
+3  A: 
  1. Make sure your ReadyForConnect field is not filled up to 4 bytes. In my project it turned out all short int (2 bytes) fields were filled with dummy bytes to 4 bytes in unmanaged DLL. If that's the issue, you should marshall the struct this way:
    [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. If the strings are ANSI null terminated strings you can annotate them as:
  [MarshalAs(UnmanagedType.LPStr)]                   public String VersionStr;
PanJanek
Using the Pack option when declaring the struct will fix this for you
scottm
+1  A: 

EDIT: I am rewriting this whole answer.

I took all of both your C++ and C# code, dropped it into a solution and ran it -- and everything works for me. I didn't have your specific memory mapping stuff so I simulated it by filling pBuf with some fake data, and everything makes it back fine; both the return value and the output struct are correct.

Could something be amiss with your project settings? This sounds silly, but you mentioned running and debugging the unamnaged code; you are building a dll right?

David Hay
Yes, I do. I just did a build as an application for testing purposes. Then changed it back to dll and built it again.
rdoubleui
Unmanaged dlls are not copied automatically into the bin dirs. Are the dlls in the Release and Debug directory the same?
weismat
+1  A: 

What are you trying to do is possible, but I think you are solving the wrong problem.

Why not read the memory mapped file direct from C#? Take a look at Winterdom.IO.FileMap

I have used it and it works fine.

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

With that you are not finished - you still have to convert a byte buffer to a struct, but you are all on the managed side and it will be easier.

devdimi
thank you, I will give that a try definitely. I'll give feedback if that will work. I searched for memory mapping in c# earlier, but only found a .Net 4.0 class.
rdoubleui