Managed C# Byte Arrays to Un-Managed C++ Byte Arrays

Posted in software by Christopher R. Wirz on Sat Jul 08 2017



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!

Note: This article assumes three separate files:
  • 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.