This repo contains a project with examples for using a embedded .NET 6 runtime in a C++ application using dotnet_runtime.
- Embedded .NET 6 Runtime
- Build and Run
- Examples
- LICENSE
- Visual Studio 2019
- .NET 6 SDK
- Clone this repository recursively using
git clone --recursive git@github.com:KevinGliewe/embedded_dotnet_runtime_examples.git
- Run
build_run.bat
- Imports
VsDevCmd.bat
- Installs dotnet tool
runtimedl
- Downloads the propper .NET 6 runtime for your system using
runtimedl
- Builds
Lib
andHost
using MSBuild - Runs
Host
- CMake 3.16 or newer
- .NET 6 SDK
- C++ compiler
- Clone this repository recursively using
git clone --recursive git@github.com:KevinGliewe/embedded_dotnet_runtime_examples.git
- Run
sh build_run.sh
- Installs dotnet tool
runtimedl
- Downloads the propper .NET 6 runtime for your system using
runtimedl
- Builds
Lib
using .NET 6 SDK - Creates makefile using CMake
- Builds
Host
using make - Runs
Host
// Init runtime and lib
auto runtime = dotnet_runtime::Runtime(hostfxr_path, libRuntimeconfig_path);
auto lib = dotnet_runtime::Library(&runtime, libDll_path, STR("Lib"));
Entrypoints can only use blittable types
Native
struct args
{
int number1;
int number2;
};
auto fpTest_ComponentEntryPoint = a_lib.GetComponentEntrypoint(
STR("LibNamespace.Test_ManagedEntryPoint"),
STR("Test_ComponentEntryPoint")
);
bool success = fpTest_ComponentEntryPoint(&_args, sizeof(_args)) == 3;
LogTest(success, L"Test_ComponentEntryPoint");
Managed
[StructLayout(LayoutKind.Sequential)]
public unsafe struct Args
{
public int Number1;
public int Number2;
}
public static int Test_ComponentEntryPoint(IntPtr arg, int argLength)
{
if (argLength < Marshal.SizeOf(typeof(Args)))
{
return 1;
}
Args args = Marshal.PtrToStructure<Args>(arg);
return args.Number1 + args.Number2;
}
Entrypoints can only use blittable types
Native
struct args
{
int number1;
int number2;
};
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)(args);
auto fpTest_CustomEntryPoint = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedEntryPoint"),
STR("Test_CustomEntryPoint")
);
bool success = fpTest_CustomEntryPoint(_args) == 3;
LogTest(success, L"Test_CustomEntryPoint");
Managed
[StructLayout(LayoutKind.Sequential)]
public unsafe struct Args
{
public int Number1;
public int Number2;
}
[UnmanagedCallersOnly]
public static int Test_CustomEntryPoint(Args args)
{
return args.Number1 + args.Number2;
}
Native
auto fpTest_ManagedString_Ansi = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedString"),
STR("Test_ManagedString_Ansi")
);
char* str = (char*)fpTest_ManagedString_Ansi();
// Compare the ASCII strings
bool success = cmp(str, (char*)"Hello Ansi");
LogTest(success, L"Test_ManagedString_Ansi");
Managed
public static readonly CString HelloAnsi = new CString("Hello Ansi");
[UnmanagedCallersOnly]
public static IntPtr Test_ManagedString_Ansi()
{
return HelloAnsi.Ptr;
}
What does CString do?
- Apends a
\0
character on the end of the string. - Converts the string into ANSI encoding.
- Allocate memory for native access using
Marshal.AllocHGlobal
. - Copy the encoded string into the allocated memory.
The encoding depends on the platform. For windows systems it is UTF16 and for posix systems it is UTF32.
Native
auto fpTest_ManagedString_Wide = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedString"),
STR("Test_ManagedString_Wide")
);
wchar_t* str = (wchar_t*)fpTest_ManagedString_Wide();
// Compare the wide strings
bool success = cmp(str, (wchar_t*)L"Hello ❤");
LogTest(success, L"Test_ManagedString_Wide");
Managed
What does CString do?
- Determins the correct encoding for the current platform. (UTF16 or UTF32)
- Apends a
\0
character on the end of the string. - Converts the string into the propper encoding.
- Allocate memory for native access using
Marshal.AllocHGlobal
. - Copy the encoded string into the allocated memory.
public static readonly CString HelloWide = new CString("Hello ❤", CEncoding.Wide);
[UnmanagedCallersOnly]
public static IntPtr Test_ManagedString_Wide()
{
return HelloWide.Ptr;
}
Native
auto fpTest_NativeString_Ansi = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeString"),
STR("Test_NativeString_Ansi")
);
bool success = fpTest_NativeString_Ansi((void*)"Hello Ansi");
LogTest(success, L"Test_NativeString_Ansi");
Managed
[UnmanagedCallersOnly]
public static int Test_NativeString_Ansi(IntPtr stringPtr)
{
return CEncoding.Ascii.GetString(stringPtr) == "Hello Ansi" ? 1 : 0;
}
Native
auto fpTest_NativeString_Wide = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeString"),
STR("Test_NativeString_Wide")
);
bool success = fpTest_NativeString_Wide((void*)L"Hello ❤");
LogTest(success, L"Test_NativeString_Wide");
Managed
[UnmanagedCallersOnly]
public static int Test_NativeString_Wide(IntPtr stringPtr)
{
return CEncoding.Wide.GetString(stringPtr) == "Hello ❤" ? 1 : 0;
}
Native
struct RetArgs
{
bool (*CallbackAnsi)(const char*);
bool (*CallbackWide)(const wchar_t*);
};
typedef void (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn2)(void*);
auto fpTest_NativeString_FunctionPointer = (custom_entry_point_fn2)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeString"),
STR("Test_NativeString_FunctionPointer")
);
RetArgs retArgs;
fpTest_NativeString_FunctionPointer(&retArgs);
{ // Ansi
bool success = retArgs.CallbackAnsi("Hello Ansi");
LogTest(success, L"Test_NativeString_FunctionPointer.CallbackAnsi");
ret &= success;
}
{ // Wide
bool success = retArgs.CallbackWide(L"Hello ❤");
LogTest(success, L"Test_NativeString_FunctionPointer.CallbackWide");
ret &= success;
}
Managed
public delegate bool FunctionPointerCallbackAnsiDelegate(NativeString nstr);
public delegate bool FunctionPointerCallbackWideDelegate(NativeWString nstr);
public static FunctionPointerCallbackAnsiDelegate FunctionPointerCallbackAnsiDelegateInstance =
new FunctionPointerCallbackAnsiDelegate(CallbackAnsi);
public static FunctionPointerCallbackWideDelegate FunctionPointerCallbackWideDelegateInstance =
new FunctionPointerCallbackWideDelegate(CallbackWide);
public static bool CallbackAnsi(NativeString nstr) => nstr.ToString() == "Hello Ansi";
public static bool CallbackWide(NativeWString nstr) => nstr.ToString() == "Hello ❤";
[StructLayout(LayoutKind.Sequential)]
public struct RetArgs
{
public IntPtr CallbackAnsi;
public IntPtr CallbackWide;
}
[UnmanagedCallersOnly]
public static void Test_NativeString_FunctionPointer(IntPtr retArgsPtr)
{
unsafe
{
RetArgs* retArgs = (RetArgs*)retArgsPtr;
retArgs->CallbackAnsi =
Marshal.GetFunctionPointerForDelegate(FunctionPointerCallbackAnsiDelegateInstance);
retArgs->CallbackWide =
Marshal.GetFunctionPointerForDelegate(FunctionPointerCallbackWideDelegateInstance);
}
}
Native
typedef int (*managed_callback_fn)(int);
typedef void* (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)(int);
// Get the managed entry point
auto fpTest_ManagedFunctionPointer_Instance = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedFunctionPointer"),
STR("Test_ManagedFunctionPointer_Instance")
);
// Get the function pointer to the managed method
managed_callback_fn managedCallback = (managed_callback_fn)fpTest_ManagedFunctionPointer_Instance(2);
bool success = managedCallback(6) == 8;
LogTest(success, L"Test_ManagedFunctionPointer_Instance");
Managed
public delegate int FunctionPointerCallbackDelegate(int a);
public class CallableObject
{
public FunctionPointerCallbackDelegate CallbackDelegate;
public IntPtr CallbackFunctionPointer;
private int Member;
public int Callback(int i) => i + Member;
public CallableObject(int member)
{
Member = member;
CallbackDelegate = new FunctionPointerCallbackDelegate(Callback);
CallbackFunctionPointer = Marshal.GetFunctionPointerForDelegate(CallbackDelegate);
}
}
public static CallableObject CallableObjectInstance;
// Enty point for unmanaged code
[UnmanagedCallersOnly]
public static IntPtr Test_ManagedFunctionPointer_Instance(int member)
{
CallableObjectInstance = new CallableObject(member);
return CallableObjectInstance.CallbackFunctionPointer;
}
Native
typedef int (*managed_callback_fn)(int);
typedef void* (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)();
// Get the managed entry point
auto fpTest_ManagedFunctionPointer_Static = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedFunctionPointer"),
STR("Test_ManagedFunctionPointer_Static")
);
// Get the function pointer to the managed function
managed_callback_fn managedCallback = (managed_callback_fn)fpTest_ManagedFunctionPointer_Static();
bool success = managedCallback(5) == 8;
LogTest(success, L"Test_ManagedFunctionPointer_Static");
Managed
public delegate int FunctionPointerCallbackDelegate(int a);
public static FunctionPointerCallbackDelegate FunctionPointerCallbackDelegateStatic = new FunctionPointerCallbackDelegate(Callback);
public static int Callback(int i) => i + 3;
// Enty point for unmanaged code
[UnmanagedCallersOnly]
public static IntPtr Test_ManagedFunctionPointer_Static()
{
return Marshal.GetFunctionPointerForDelegate(FunctionPointerCallbackDelegateStatic);
}
Native
int FEXPORT CallbackFunc(int i)
{
return i * 2;
}
void* fpNativeCallback = (void*)&CallbackFunc;
auto fpTest_NativeFunctionPointer_Checked = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeFunctionPointer"),
STR("Test_NativeFunctionPointer_Checked")
);
bool success = fpTest_NativeFunctionPointer_Checked(fpNativeCallback, 4) == 8;
LogTest(success, L"Test_NativeFunctionPointer_Checked");
Managed
public delegate int FunctionPointerCallbackDelegate(int a);
[UnmanagedCallersOnly]
public static int Test_NativeFunctionPointer_Checked(IntPtr nativeFunctionPtr, int number)
{
var callbackFuncDelegate =
(FunctionPointerCallbackDelegate)Marshal.GetDelegateForFunctionPointer(nativeFunctionPtr,
typeof(FunctionPointerCallbackDelegate));
return callbackFuncDelegate(number);
}
Native
int FEXPORT CallbackFunc(int i)
{
return i * 2;
}
void* fpNativeCallback = (void*)&CallbackFunc;
auto fpTest_NativeFunctionPointer_Unchecked = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeFunctionPointer"),
STR("Test_NativeFunctionPointer_Unchecked")
);
bool success = fpTest_NativeFunctionPointer_Unchecked(fpNativeCallback, 4) == 8;
LogTest(success, L"Test_NativeFunctionPointer_Unchecked");
Managed
[UnmanagedCallersOnly]
public static int Test_NativeFunctionPointer_Unchecked(IntPtr nativeFunctionPtr, int number)
{
unsafe
{
unchecked
{
var callbackFuncPtr = (delegate* unmanaged[Cdecl]<int, int>)nativeFunctionPtr;
return callbackFuncPtr(number);
}
}
}
Native
struct Args
{
int number1;
int number2;
int sum;
char returnMsg[128];
};
bool Run(dotnet_runtime::Library& a_lib)
{
bool ret = true;
typedef void (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)(void*);
auto fpTest_ManagedUnsafe_Struct = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_ManagedUnsafe"),
STR("Test_ManagedUnsafe_Struct")
);
Args args;
args.number1 = 1;
args.number2 = 2;
fpTest_ManagedUnsafe_Struct(&args);
{ //sum
bool success = args.sum == 3;
LogTest(success, L"Test_ManagedUnsafe_Struct.Sum");
ret &= success;
}
{ // ReturnMsg
bool success = cmp(args.returnMsg, (char*)"Hello Ansi");
LogTest(success, L"Test_ManagedUnsafe_Struct.ReturnMsg");
ret &= success;
}
return ret;
}
Managed
[StructLayout(LayoutKind.Sequential)]
public unsafe struct Args
{
public int Number1;
public int number2;
public int Sum;
public fixed byte ReturnMsg[128];
}
[UnmanagedCallersOnly]
public static void Test_ManagedUnsafe_Struct(IntPtr ptr)
{
unsafe
{
Args* args = (Args*) ptr;
args->Sum = args->Number1 + args->number2;
// Get the memory offset of Args.ReturnMsg
var destReturnMsg = IntPtr.Add(ptr, (int)Marshal.OffsetOf(typeof(Args), nameof(Args.ReturnMsg)));
var data = CEncoding.Ascii.GetBytes("Hello Ansi");
// Copy the data to the unmanaged memory
Marshal.Copy(data, 0, (IntPtr)destReturnMsg, data.Length);
}
}
Native
struct args
{
int Arr[8];
int Multiplier;
};
args _args{
1, 2, 3, 4, 5, 6, 7, 8, // Arr[8]
2 // Multiplier
};
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)(args);
auto fpTest_NativeArray_StructFixed = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeArray"),
STR("Test_NativeArray_StructFixed")
);
bool success = fpTest_NativeArray_StructFixed(_args) == 72;
LogTest(success, L"Test_NativeArray_StructFixed");
Managed
[StructLayout(LayoutKind.Sequential)]
public unsafe struct Args
{
public fixed int Arr[8];
public int Multiplier;
}
[UnmanagedCallersOnly]
public static unsafe int Test_NativeArray_StructFixed(Args args)
{
int ret = 0;
for(int i = 0; i < 8; i++)
{
ret += args.Arr[i] * args.Multiplier;
}
return ret;
}
Native
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)(int[], int);
auto fpTest_NativeArray_ArgumentFixed = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeArray"),
STR("Test_NativeArray_ArgumentFixed")
);
bool success = fpTest_NativeArray_ArgumentFixed(_args.Arr, _args.Multiplier) == 72;
LogTest(success, L"Test_NativeArray_ArgumentFixed");
Managed
[UnmanagedCallersOnly]
public static int Test_NativeArray_ArgumentFixed(IntPtr arrPtr, int multiplier)
{
// ArrPointer as argument does not work here!
ArrPointer8<int> arr = new ArrPointer8<int>(arrPtr);
return arr.Sum(el => el * multiplier); ;
}
Native
typedef void* (CORECLR_DELEGATE_CALLTYPE* custom_entry_point_fn)();
typedef int (*managed_callback_fn)(int[], int);
auto fpTest_NativeArray_ArgumentFixed_FunctionPointer = (custom_entry_point_fn)a_lib.GetCustomEntrypoint(
STR("LibNamespace.Test_NativeArray"),
STR("Test_NativeArray_ArgumentFixed_FunctionPointer")
);
managed_callback_fn callback = (managed_callback_fn)fpTest_NativeArray_ArgumentFixed_FunctionPointer();
bool success = callback(_args.Arr, _args.Multiplier) == 72;
LogTest(success, L"Test_NativeArray_ArgumentFixed_FunctionPointer");
Managed
public delegate int FunctionPointerCallbackDelegate(ArrPointer8<int> arr, int multiplier);
public static FunctionPointerCallbackDelegate FunctionPointerCallbackDelegateInstance =
new FunctionPointerCallbackDelegate(Callback);
public static int Callback(ArrPointer8<int> arr, int multiplier) => arr.Sum(el => el * multiplier);
[UnmanagedCallersOnly]
public static IntPtr Test_NativeArray_ArgumentFixed_FunctionPointer()
{
return Marshal.GetFunctionPointerForDelegate(FunctionPointerCallbackDelegateInstance);
}
Native
extern "C"
{
int FEXPORT Test_DllImport_ExternC(int number)
{
return number * 2;
}
}
void* host_handle = dotnet_runtime::get_host_handle();
return fpTest_DllImport_Call(host_handle, 4) == 8;
For posix systems, use the
-export-dynamic
flag for the linker.
Managed
private static IntPtr s_moduleHandle = IntPtr.Zero;
private static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
IntPtr ret = libraryName == "Test_DllImport_LibName" ? s_moduleHandle : NativeLibrary.Load(libraryName, assembly, searchPath);
//Console.WriteLine($"ImportResolver s_moduleHandle={s_moduleHandle} ret={ret}");
return ret;
}
[DllImport("Test_DllImport_LibName")]
public static extern int Test_DllImport_ExternC(int number);
[UnmanagedCallersOnly]
public static int Test_DllImport_Call(IntPtr moduleHandle, int number)
{
s_moduleHandle = moduleHandle;
NativeLibrary.SetDllImportResolver(typeof(Test_DllImport).Assembly, ImportResolver);
return Test_DllImport_ExternC(number);
}
Native
extern "C"
{
int FEXPORT Test_NativeExport_ExternC(int number)
{
return number * 4;
}
}
void* host_handle = dotnet_runtime::get_host_handle();
return fpTest_NativeExport_Call(host_handle, 4) == 16;
For posix systems, use the
-export-dynamic
flag for the linker.
Managed
public delegate int FunctionPointerCallbackDelegate(int a);
[UnmanagedCallersOnly]
public static int Test_NativeExport_Call(IntPtr moduleHandle, int number)
{
var exportedDelegate = ModuleLoader.GetExport<FunctionPointerCallbackDelegate>(moduleHandle, "Test_NativeExport_ExternC");
int result = exportedDelegate(number);
unsafe
{
unchecked
{
var callbackFuncPtr = (delegate* unmanaged[Cdecl]<int, int>)ModuleLoader.GetExport(moduleHandle, "Test_NativeExport_ExternC");
result += callbackFuncPtr(number);
}
}
return result / 2;
}
Native
class ClassLayout
{
private:
int m_iTest = 0;
public:
virtual void AddOne() { this->m_iTest += 1; }
virtual void AddTwo() { this->m_iTest += 2; }
int GetTest() { return m_iTest; }
};
ClassLayout* instance = new ClassLayout();
// Calls AddOne and AddTwo, then overwrites VTable
fpTest_NativeVTable_Call((void*)instance);
Managed
[StructLayout(LayoutKind.Sequential)]
private struct ClassLayout
{
public IntPtr VTable;
public int test;
}
[StructLayout(LayoutKind.Sequential)]
private struct ClassVTable
{
public IntPtr Method_AddOne;
public IntPtr Method_AddTwo;
}
delegate void MethodDelegate(IntPtr thisPtr);
ClassLayout instance = Marshal.PtrToStructure<ClassLayout>(classInstance);
ClassVTable vTable = Marshal.PtrToStructure<ClassVTable>(instance.VTable);
MethodDelegate method_AddOne =
Marshal.GetDelegateForFunctionPointer<MethodDelegate>(vTable.Method_AddOne);
MethodDelegate method_AddTwo =
Marshal.GetDelegateForFunctionPointer<MethodDelegate>(vTable.Method_AddTwo);
method_AddOne(classInstance);
method_AddTwo(classInstance);
Native
class ClassLayout
{
private:
int m_iTest = 0;
public:
virtual void AddOne() { this->m_iTest += 1; }
virtual void AddTwo() { this->m_iTest += 2; }
int GetTest() { return m_iTest; }
};
ClassLayout* instance = new ClassLayout();
// Calls AddOne and AddTwo, then overwrites VTable
fpTest_NativeVTable_Call((void*)instance);
Managed
[StructLayout(LayoutKind.Sequential)]
private struct ClassLayout
{
public IntPtr VTable;
public int test;
}
[StructLayout(LayoutKind.Sequential)]
private struct ClassVTable
{
public IntPtr Method_AddOne;
public IntPtr Method_AddTwo;
}
delegate void MethodDelegate(IntPtr thisPtr);
// This delegate will be the new virtual method
private static readonly unsafe MethodDelegate delegate_SubOne = new MethodDelegate(thisPtr =>
{
var instance = (ClassLayout*)thisPtr;
instance->test -= 1;
});
// Create new unmanaged VTable instance
private static readonly UnmanagedMemory<ClassVTable> OverwrittenVTable = new();
unsafe
{
// Get the new VTable
var vTable = OverwrittenVTable.PtrElem;
// Set the virtual methods with the new managed function pointer (Both the same for simplicity)
vTable->Method_AddTwo = vTable->Method_AddOne =
Marshal.GetFunctionPointerForDelegate(delegate_SubOne);
// Overwrite the VTable reference of the instance
var instance = (ClassLayout*) classInstance;
instance->VTable = (IntPtr) vTable;
}
dotnet_runtime_test is licensed under MIT license. See LICENSE for more details.