3

Let me start another question because although I saw many similar ones, not one really speaks about this aspect... I have a C++ DLL (no source code but .lib and .h) and I wrote the necessary managed wrapper. There is no problem with that, the question is about the structs and enums defined in the original C++ code, and there are lots of them, all needed to be exposed to the C# code. Tutorials and samples usually use simple data types like floats and strings, not real world scenarios of complex data structures.

My managed C++/CLI wrapper consumes the unmanaged structs from the DLL's .h header files. The class member functions that I wrap use them all the time. Consequently, I need to use the same structs in my C# code, passing them and receiving from the C++ code. It seems clear that I can't avoid redefining all of them in C# but even then, using them is problematic. Let's have an example: a simple struct that is used by a function in the unmanaged code:

typedef struct INFO {
  ...
} INFO;

int GetInfo(INFO& out_Info);

I have the same structure declared in my C++/CLI wrapper code:

public ref struct INFO_WRAP {
  ...
};

int GetInfo(INFO_WRAP out_Info);

The implementation in the wrapper code tries to convert this new structure to the original one for the consumption of the old, unmanaged code:

int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info) {
  pin_ptr<INFO> pin_out_Info = out_Info;
  return self->GetInfo(*(::INFO*)pin_out_Info);
}

But this won't compile (cannot convert between the structs and no suitable conversion is found).

Is there a solution that doesn't involve creating new data structures and manually copying all struct members all the time, back and forth? Not only because of the extra work and time, but there are really many structs.

Gábor
  • 9,466
  • 3
  • 65
  • 79

2 Answers2

6
  public ref struct INFO_WRAP

You did not declare a struct, this is a class in C# terms. Quirky C++ implementation detail, a C++ struct is simply a class with all of its members public. You need to use value struct in C++/CLI to declare the equivalent of a C# struct.

  int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info)

That's wrong as well, since INFO_WRAP is actually a reference type you must always declare it with the ^ hat. Or with % to pass it by reference, surely the intention here.


Basic obstacles out of the way, what you are asking for is not directly supported. A managed compiler is not allowed to make any assumptions about the layout of a managed struct. And will bark when you try it anyway. For a very good reason, it is just not that predictable. Layout is a strong runtime implementation detail and can change if the code runs on a different runtime. Like 32-bit vs 64-bit, might work in one but not the other. As Jon found out.

Copying the fields one-by-one always works and is performant enough. Just not code that programmers ever like to maintain. Your can ask the framework to do it for you, call Marshal::PtrToStructure() or StructureToPtr().


Cheating is possible and certainly something you'd contemplate when you write C++/CLI code. After all, the point of the language is to make interop fast. You just void the warranty, you must thoroughly test the code on any of the platforms you intend to support. A simple example:

public value struct Managed {
    int member1;
    int member2;
};

struct Native {
    int member1;
    int member2;
};

void GetInfo(Managed% info) {
    Native n = { 1, 2 };
    pin_ptr<Managed> pinfo = &info;
    memcpy(pinfo, &n, sizeof(n));
}

With works just fine and executes predictably on any platform, the struct is simple. When the struct is not simple or you, say, modify Native and forget to modify Managed then there's hell to pay, stack and GC heap corruption are very unpleasant mishaps and very hard to debug.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • The first reason to try `ref structs` was that the actual structures have lots of `char[n]` members. As far as I could tell, I have to use `array<>` and initialize them to the correct size in the constructor--of which value structs have none. As to the marshaling function, good idea, I used it quite a few times in C# as well. – Gábor Mar 10 '16 at 12:03
  • `value struct`s don't allow you to define the *default* constructor, but they do allow you to define any other constructor. (The default constructor is always "Assign zeros & nullptr to all fields".) In my opinion, `ref struct` is easy to mistake at first glance. For maximum clarity, I always use `ref class` or `value struct` in my code, never the others. – David Yaw Mar 10 '16 at 14:48
  • Yep, but I'd prefer those structs to have char (or other) arrays by default, without me having to call some special constructor. And by perusing the web, I did find a solution, using a C++ template. A bit like a hack, but a clever one at that... :-) It's a pity a comment doesn't allow proper formatted code... – Gábor Mar 10 '16 at 17:02
  • Yes, it seems like that although I found it somewhere else. But I moved on because it gave other problems (namely, it's not easy to use it later in a generic way). `MarshalAs` with `ConstSize` is the one I chose in the end. – Gábor Mar 15 '16 at 09:58
0

Here is the whole solution, in a full description, for other people coming after me. :-) Let's suppose we have a DLL with enums, structs, classes, functions in the .h header file:

typedef int (*DelegateProc)(long inLong, char* inString, STRUCT2* inStruct, long* outLong, char* outString, STRUCT2* outString);

typedef struct STRUCT1 {
  long aLong;
  short aShort;
  BOOL aBoolean;
  char aString[64];
  STRUCT2 aStruct;
  DelegateProc aDelegateProc;
  char Reserved[32];
} STRUCT1;

Convert the struct the usual way, and add two static conversion functions that handle the marshaling. As Hans has noted, as tedious as it seems, piecewise copying is the only really reliable solution across platforms and architectures.

#include <msclr\marshal.h>
using namespace msclr::interop;

public delegate int DelegateProc(long inLong, String^ inString, STRUCT2 inStruct, [Out] long% outLong, [Out] String^ outString, [Out] STRUCT2 outStruct);

[StructLayout(LayoutKind::Sequential, Pack = 1)]
public value struct WRAP_STRUCT1 {
  long aLong;
  short aShort;
  bool aBoolean;
  [MarshalAs(UnmanagedType::ByValArray, SizeConst = 64)]
  array<char>^ aString;
  WRAP_STRUCT2 aStruct;
  DelegateProc^ aDelegateProc;
  [MarshalAs(UnmanagedType::ByValArray, SizeConst = 32)]
  array<char>^ Reserved;

  static STRUCT1 convert(WRAP_STRUCT1^ other) {
    STRUCT1 clone;
    clone.aLong = other->aLong;
    clone.aShort = other->aShort;
    clone.aBoolean = other->aBoolean;
    sprintf(clone.aString, "%s", other->aString);
    clone.aStruct = WRAP_STRUCT2::convert(other->aStruct);
    clone.aDelegateProc = (Delegate1Proc)Marshal::GetFunctionPointerForDelegate(other->aDelegateProc).ToPointer();
    return clone;
  }

  static WRAP_STRUCT1 convert(STRUCT1 other) {
    WRAP_STRUCT1 clone;
    clone.aLong = other.aLong;
    clone.aShort = other.aShort;
    clone.aBoolean = (other.aBoolean > 0);
    clone.aString = marshal_as<String^>(other.aString)->ToCharArray();
    clone.aStruct = WRAP_STRUCT2::convert(other.aStruct);
    clone.aDelegateProc = (DelegateProc)Marshal::GetDelegateForFunctionPointer((IntPtr)other.aDelegateProc, DelegateProc::typeid);
    return clone;
  }
};

Next, we have usual a class in the .h header file:

class __declspec(dllexport) CLASS1 {

public:
  CLASS1();
  virtual ~CLASS1();

  virtual int Function1(long inLong, char* inString, STRUCT2* inStruct);
  virtual int Function2(long* outLong, char* outString, STRUCT2* outStruct);
};

We need to create a wrapper class. Its header:

public ref class Class1Wrapper {

public:
  Class1Wrapper();
  ~Class1Wrapper();

  int Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct);
  int Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct);

private:
  CLASS1* self;
};

And its implementation:

Namespace::Class1Wrapper::Class1Wrapper() {
  self = new CLASS1();
}

Namespace::Class1Wrapper::~Class1Wrapper() {
  self->~CLASS1();
}

int Namespace::Class1Wrapper::Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct) {
  char pinString[64];
  sprintf(pinString, "%s", inString);
  STRUCT2 pinStruct = WRAP_STRUCT2::convert(inStruct);

  return self->Function1(inLong, pinString, pinStruct);
}

int Namespace::Class1Wrapper::Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct) {
  long poutLong;
  char poutString[64];
  STRUCT2 poutStruct;
  ::ZeroMemory(&poutStruct, sizeof(poutStruct));

  int result = self->Function2(poutLong, poutString, poutStruct);

  outLong = poutLong;
  outString = marshal_as<String^>(poutString);
  outStruct = WRAP_STRUCT2::convert(poutStruct);
  return result;
}

Basically, you need to use the usual and your own struct marshalling functions to convert both incoming and outgoing data manually.

Gábor
  • 9,466
  • 3
  • 65
  • 79