Bytes are very foundational in C#, whether streams, image files, or cryptography. Sometimes, you want to re-use unmanaged code (it can compile almost anywhere), but don't want to write it all in C#, just pass the bytes. Fortunately, passing byte arrays can be done!
- An unmanaged C++ lib
- A managed C++ DLL
- A managed C# executable
But it could also be done in two files:
- A managed C++ DLL
- A managed C# executable
If possible, compile the native C++ into a lib file so it can be directly embedded in an application or managed C++ DLL.
// "NativeClass.h"
using namespace std;
namespace NativeCode
{
class NativeClass
{
public:
NativeClass(std::vector<unsigned int> items);
int length();
unsigned int operator[] (int index);
unsigned char* GetBytes();
private:
std::vector<unsigned int> items;
};
}
// "NativeClass.cpp"
#include "NativeClass.h"
namespace NativeCode
{
NativeClass::NativeClass(std::vector<unsigned int> items) {
this->items = items;
}
int NativeClass::length() {
return (int)(this->items.size());
}
unsigned int NativeClass::operator[] (int index) {
if (index < 0) {
return 0;
}
if (this->length() <= 0) {
return 0;
}
if (index >= this->length()) {
return 0;
}
return this->items[index];
}
unsigned char* NativeClass::GetBytes() {
unsigned char* ret = new unsigned char[this->length()];
// item-wise, to prove a point
for (int n = 0; n < this->length(); n++) {
ret[n] = (*this)[n];
}
return ret;
}
}
Once the lib file is generated, link it to a manged C++ DLL. Managed DLLs are made using the compiler flag /cli.
The thing to note is that you can't pass managed objects into unmanaged memory in most cases. So, to be safe, copies must be made. The managed C++ class holds a pointer to the unmanaged class - it is just a wrapper and has no real logic; only data copying.
// "ManagedClass.h"
#include "../NativeCode/NativeClass.h"
namespace ManagedCode
{
public ref class ManagedClass
{
public:
ManagedClass(cli::array<System::Byte>^ bytes);
property int Length {
int get();
}
unsigned int operator[] (int index);
cli::array<System::Byte>^ GetBytes();
// Dispose method will make IDisposable
~ManagedClass();
private:
NativeCode::NativeClass* _nativePtr;
};
}
// "ManagedClass.cpp"
#include "stdafx.h"
#include <string>
#include <msclr\marshal_cppstd.h>
#include "ManagedClass.h"
namespace ManagedCode
{
ManagedClass::ManagedClass(cli::array<ystem::Byte>^ bytes) {
pin_ptr<System::Byte> p = &bytes[0];
unsigned char* uchars = p;
char* chars = reinterpret_cast<char*>(uchars);
int sz = strlen(chars);
std::vector<unsigned int> items = std::vector<unsigned int>(sz);
for (int n = sz - 1; n >= 0; n--) {
items[n] = (unsigned int)chars[n];
}
_nativePtr = new NativeCode::NativeClass(items);
}
int ManagedClass::Length::get()
{
return _nativePtr->length();
}
unsigned int ManagedClass::operator[](unsigned int index)
{
return (*_nativePtr)[index];
}
cli::array<System::Byte>^ ManagedClass::GetBytes() {
unsigned char* buf = _nativePtr->GetBytes();
int len = _nativePtr->length();
cli::array<System::Byte>^ byteArray = gcnew cli::array<System::Byte>(len);
System::Runtime::InteropServices::Marshal::Copy((IntPtr)buf, byteArray, 0, len);
delete buf;
return byteArray;
}
ManagedClass::~ManagedClass() { delete _nativePtr; }
}
Since the managed C++ DLL is done, it can be referenced from a C# project easily (making certain of x64 vs x86 compatibility).
There is one little trick to do at the end if you want subsetting. The trick is to create a child class.
// "DotNetClass.cs"
namespace MyApplication
{
public class DotNetClass : ManagedCode.ManagedClass
{
public DotNetClass(byte[] bytes) : base(bytes){}
// Since subscript indexing does not translate, this must be done
public uint this[int key] => base.op_Subscript(key);
}
}
Now byte arrays can pass between managed and unmanaged code.