Unity C++ Native Plugin Examples
October 30, 2017 Leave a comment
Unity scripting environment runs C# Mono container which supports native C/C++ plugins with Mono PInvoke. This allows easy integration of native library functions to both pass and receive data between Unity managed C# and native platform code.
Basic concept is simple. Native function descriptor that specify calling convention and dynamically loaded library (DDL) tell Mono how it’s parameters and returns values should be converted. Mono handles things mostly automatically from there on.
Some care is required to do this efficiently. C# PInvoke defines marshaling as a protocol to serialize and deserialize managed objects back and forth towards native code. This may generate lot of overhead. Mono garbage collector might accidentally destroy memory of an object while it’s used in the native code space.
Example C++ code is for a native Win32 plugin but is trivial to port to any platform.
Basic Data
Passing simple integral types, integers and arrays of them is straightforward.
Native c code.
extern "C" {
#define PLUGINEX(rtype) UNITY_INTERFACE_EXPORT rtype UNITY_INTERFACE_API
PLUGINEX(int) ReturnInt()
{
return 0xBABE;
}
PLUGINEX(void) AcceptArray1(char *arr, int length)
{
for (int i = 0; i < length; i++) {
arr[i] = 'A' + i;
}
}
}
C# declarations and example calling code.
[DllImport("ptest")]
private static extern int ReturnInt();
[DllImport("ptest")]
private static extern void AcceptArray1([In, Out] byte[] arr, int length);
void TestIntegral() {
// return int
print(ReturnInt());
// accept byte array, uses marshaling to pass array back and forth
byte[] arr1 = { 0, 0, 0 };
AcceptArray1(arr1, arr1.Length);
for (int i = 0; i < arr1.Length; i++)
{
print("arr" + i + "=" + arr1[i]);
}
}
Strings
Strings are passed and returned as character arrays. It’s possible to use automatic marshaling conversions and return dynamically allocated strings that will be automatically deallocated.
Native c code.
extern "C" {
#define PLUGINEX(rtype) UNITY_INTERFACE_EXPORT rtype UNITY_INTERFACE_API
PLUGINEX(bool) AcceptStr(LPCSTR pStr)
{
return !strcmp(pStr, "FOO");
}
PLUGINEX(LPSTR) ReturnDynamicStr()
{
LPSTR str = (LPSTR)CoTaskMemAlloc(512);
strcpy_s(str, 512, "Dynamic string");
return str;
}
PLUGINEX(LPCSTR) ReturnConstStr()
{
return "Constant string";
}
}
C# declarations and example calling code.
[DllImport("ptest")]
private static extern bool AcceptStr([MarshalAs(UnmanagedType.LPStr)] string ansiStr);
// automatically deallocates the return string with CoTaskMemFree
[DllImport("ptest")]
[return: MarshalAs(UnmanagedType.LPStr)]
private static extern string ReturnDynamicStr();
[DllImport("ptest")]
private static extern IntPtr ReturnConstStr();
void TestStrings() {
// accept string
bool r1 = AcceptStr("BAR");
bool r2 = AcceptStr("FOO");
print("r1=" + r1); // r1=false
print("r2=" + r2); // r1=true
// return dynamically allocated string
string s1 = ReturnDynamicStr();
print("s1=" + s1);
// return constant string
string s2 = Marshal.PtrToStringAnsi(ReturnConstStr());
print("s2=" + s2);
}
Arrays
Both dynamic and constant arrays can be supported but some helper functions are needed.
Native c code.
extern "C" {
#define PLUGINEX(rtype) UNITY_INTERFACE_EXPORT rtype UNITY_INTERFACE_API
PLUGINEX(void) AcceptArray1(char *arr, int length)
{
for (int i = 0; i < length; i++) {
arr[i] = 'A' + i;
}
}
PLUGINEX(void) AcceptArray2(char *arr, int length)
{
for (int i = 0; i < length; i++) {
arr[i] = 'A' + i;
}
}
PLUGINEX(int) AcceptStrArray(const char* const *strArray, int size)
{
int total = 0;
for (int i = 0; i < size; i++) {
auto str = strArray[i];
total += (int)strlen(str);
}
// return total length of the strings in the array to demonstrate that
// it was passed correctly
return total;
}
PLUGINEX(LPBYTE) ReturnDynamicByteArray(int &pSize)
{
pSize = 0xFF;
LPBYTE pData = (LPBYTE)CoTaskMemAlloc(pSize);
// fill with example data
for (int i = 0; i < pSize; i++) {
pData[i] = i + 1;
}
return pData;
}
PLUGINEX(LPSTR*) ReturnDynamicStrArray(int &pSize)
{
// Allocate an array with pointers to 3 dynamically allocated strings
pSize = 3;
LPSTR* pData = (LPSTR*)CoTaskMemAlloc((pSize)*sizeof(LPSTR));
pData[0] = (LPSTR)CoTaskMemAlloc(128);
pData[1] = (LPSTR)CoTaskMemAlloc(128);
pData[2] = (LPSTR)CoTaskMemAlloc(128);
strcpy_s(pData[0], 128, "String 1");
strcpy_s(pData[1], 128, "String 2");
strcpy_s(pData[2], 128, "String 3");
return pData;
}
}
C# declarations and example calling code.
[DllImport("ptest")]
private static extern void AcceptArray1(IntPtr arr, int length);
[DllImport("ptest")]
private static extern void AcceptArray2(IntPtr arr, int length);
[DllImport("ptest")]
private static extern int AcceptStrArray(IntPtr array, int size);
[DllImport("ptest")]
private static extern IntPtr ReturnDynamicByteArray(ref int size);
[DllImport("ptest")]
private static extern IntPtr ReturnDynamicStrArray(ref int size);
///// Helper functions for marshalling /////
// Convert and copy array of strings to raw memory
private static IntPtr MarshalStringArray(string[] strArr)
{
IntPtr[] dataArr = new IntPtr[strArr.Length];
for (int i = 0; i < strArr.Length; i++)
{
dataArr[i] = Marshal.StringToCoTaskMemAnsi(strArr[i]);
}
IntPtr dataNative = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(IntPtr)) * strArr.Length);
Marshal.Copy(dataArr, 0, dataNative, dataArr.Length);
return dataNative;
}
// Decodes string array from raw pointer
private static string[] MarshalStringArray(IntPtr dataPtr, int arraySize)
{
var dataPtrArray = new IntPtr[arraySize];
var strArray = new String[arraySize];
Marshal.Copy(dataPtr, dataPtrArray, 0, arraySize);
for (int i = 0; i < arraySize; i++)
{
strArray[i] = Marshal.PtrToStringAnsi(dataPtrArray[i]);
Marshal.FreeCoTaskMem(dataPtrArray[i]);
}
Marshal.FreeCoTaskMem(dataPtr);
return strArray;
}
// Dellocates encoded string array
private static void CleanUpNativeStrArray(IntPtr dataPtr, int arraySize)
{
var dataPtrArray = new IntPtr[arraySize];
Marshal.Copy(dataPtr, dataPtrArray, 0, arraySize);
for (int i = 0; i < arraySize; i++)
{
Marshal.FreeCoTaskMem(dataPtrArray[i]);
}
Marshal.FreeCoTaskMem(dataPtr);
}
void TestArrays() {
// accept byte array, uses marshalling to pass array back and forth
byte[] arr1 = { 0, 0, 0 };
AcceptArray1(arr1, arr1.Length);
for (int i = 0; i < arr1.Length; i++)
{
print("arr" + i + "=" + arr1[i]);
}
// accept byte array, passes no-copy raw memory pointer
byte[] arr2 = { 0, 0, 0 };
GCHandle h = GCHandle.Alloc(arr2, GCHandleType.Pinned);
AcceptArray2(h.AddrOfPinnedObject(), arr2.Length);
for (int i = 0; i < arr2.Length; i++)
{
print("arr" + i + "=" + arr2[i]);
}
h.Free();
// return dynamically allocated byte array
int arraySize = 0;
IntPtr dataPtr = ReturnDynamicByteArray(ref arraySize);
byte[] data = new byte[arraySize];
Marshal.Copy(dataPtr, data, 0, arraySize);
Marshal.FreeCoTaskMem(dataPtr); // deallocate unmanaged memory
print("data["+arraySize+"] = [" + data[0] + ", " + data[1] + ", " + data[2] + ",...]");
// return dynamically allocated string array
arraySize = 0;
dataPtr = ReturnDynamicStrArray(ref arraySize);
String[] strArray = MarshalStringArray(dataPtr, arraySize);
print("strArray["+arraySize+"] = [" + String.Join(",", strArray) + "]");
// string array as parameter
dataPtr = MarshalStringArray(new String[] { "foo1", "foo2", "foo3" });
int len = AcceptStrArray(dataPtr, arraySize);
print("len=" + len);
CleanUpNativeStrArray(dataPtr, arraySize);
}
Structures and Arrays of Structures
Array handling is similar to the string arrays. Most objects can be passed as is but if they have array properties those must be of fixed size.
Native c code.
extern "C" {
#define PLUGINEX(rtype) UNITY_INTERFACE_EXPORT rtype UNITY_INTERFACE_API
struct ExampleStruct {
INT16 val1;
INT32 array1[3];
INT16 array2len;
INT32 array2[10];
LPSTR str1;
};
PLUGINEX(int) AcceptStruct(ExampleStruct &s)
{
// Modify struct
s.val1 -= 1111;
for (int i= 0; i < 3; i++) {
s.array1[i] += 1;
}
for (int i = 0; i < s.array2len; i++) {
s.array2[i] += 10;
}
// return length of the string in the argument struct to demonstrate that
// it was passed correctly
return (int)strlen(s.str1);
}
struct ExamplePoint {
FLOAT x;
FLOAT y;
FLOAT z;
};
PLUGINEX(ExamplePoint *) ReturnArrayOfPoints(int &size)
{
size = 4;
ExamplePoint *pointArr = (ExamplePoint*)CoTaskMemAlloc(sizeof(ExamplePoint) * size);
// fill with some example data
for (int i = 0; i < size; i++) {
pointArr[i] = { i + 0.1f, i + 0.2f, i + 0.3f };
}
return pointArr;
}
// this return type is blittable
// https://stackoverflow.com/questions/10320502/c-sharp-calling-c-function-that-returns-struct-with-fixed-size-char-array
//
PLUGINEX(ExamplePoint) ReturnStruct()
{
return { 1, 2, 3 };
}
}
C# declarations and example calling code.
[StructLayout(LayoutKind.Sequential)]
public struct ExamplePoint
{
public float x;
public float y;
public float z;
// for debugging
public override String ToString()
{
return "{" + x + ","+ y + "," + z + "}";
}
}
[DllImport("ptest")]
private static extern IntPtr ReturnArrayOfPoints(ref int size);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ExampleStruct
{
public UInt16 val1;
[MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 3)]
public UInt32[] array1;
public UInt16 array2len;
[MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 10)]
public UInt32[] array2;
[MarshalAs(UnmanagedType.LPStr)]
public string str1;
}
[DllImport("ptest")]
private static extern int AcceptStruct(ref ExampleStruct s);
[DllImport("ptest")]
private static extern ExamplePoint ReturnStruct();
void TestStructures() {
// Structure as parameter
ExampleStruct s = new ExampleStruct
{
val1 = 9999,
array1 = new UInt32[3],
array2 = new UInt32[10]
};
s.array1[0] = 1;
s.array1[1] = 2;
s.array1[2] = 3;
s.array2len = 5;
s.array2[0] = 10;
s.array2[1] = 11;
s.array2[2] = 12;
s.array2[3] = 13;
s.array2[4] = 14;
s.str1 = "Cat is a feline";
len = AcceptStruct(ref s);
print("s.val1=" + s.val1 + " len=" + len);
// return struct
ExamplePoint p = ReturnStruct();
// Marshal array of point objects
arraySize = 0;
dataPtr = ReturnArrayOfPoints(ref arraySize);
ExamplePoint[] pointArr = new ExamplePoint[arraySize];
// memory layout
// |float|float|float|float|float|float|float|float|float|float..
// | ExamplePoint0 | ExamplePoint1 | ExamplePoint2 |
int offset = 0;
int pointSize = Marshal.SizeOf(typeof(ExamplePoint));
for(int i=0; i < arraySize; i++)
{
pointArr[i] = (ExamplePoint)Marshal.PtrToStructure(new IntPtr(dataPtr.ToInt32() + offset), typeof(ExamplePoint));
offset += pointSize;
}
print("pointArr["+arraySize+"]=["+pointArr[0]+", "+pointArr[1]+",...]");
Marshal.FreeCoTaskMem(dataPtr);
}
Many of these examples can be done in cleaner way using MarshalAsAttribute. Code above does show what is happening under the hood.
Get full implementation ftom https://github.com/tikonen/blog/tree/master/unityplugin












