Important note: This solution is intended to ease the stress of debugging on Windows only. It is not a viable solution for a release product or non-Windows platform.
So, after a knuckle gnawing afternoon debugging my native plugin for Unity3D, I decided to have a go at resolving an age old problem with Unity; It is not possible to replace a native dll in the Plugins folder without restarting the editor.
Doing some research in the form of a few Google queries, I found the Kernel32.dll methods for loading and unloading native assemblies. Namely LoadLibrary
and FreeLibrary
.
These methods alone, however, would not suffice. Attempts at calling methods via standard platform invoke simply did not work, as Unity/Mono still took over the assembly loading process.
The solution was to manually acquire the pointer to any desired method within my native library by defining its signature as a delegate, using GetProcAddress
and Marshal.GetDelegateForFunctionPointer
to create an instance of that delegate which points to the native assembly method and invoke it using Delegate.DynamicInvoke
.
As you can imagine, this is a tedious process. So to make things simpler, I created a helper class called Native
(see below).
/*
* Native dll invocation helper by Francis R. Griffiths-Keam
* www.runningdimensions.com/blog
*/
using System;
using System.Runtime.InteropServices;
using UnityEngine;
public static class Native
{
public static T Invoke<T, T2>(IntPtr library, params object[] pars)
{
IntPtr funcPtr = GetProcAddress(library, typeof(T2).Name);
if (funcPtr == IntPtr.Zero)
{
Debug.LogWarning("Could not gain reference to method address.");
return default(T);
}
var func = Marshal.GetDelegateForFunctionPointer(GetProcAddress(library, typeof(T2).Name), typeof(T2));
return (T)func.DynamicInvoke(pars);
}
public static void Invoke<T>(IntPtr library, params object[] pars)
{
IntPtr funcPtr = GetProcAddress(library, typeof(T).Name);
if (funcPtr == IntPtr.Zero)
{
Debug.LogWarning("Could not gain reference to method address.");
return;
}
var func = Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(T));
func.DynamicInvoke(pars);
}
[DllImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool FreeLibrary(IntPtr hModule);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
}
So to use this helper, first of all DO NOT build your plugins to the Assets/Plugins folder. Instead, build them to your game project’s root (the same folder as contains the Assets folder).
Then, for each object you wish to make calls to a native dll, follow this format:
using System;
using UnityEngine;
public class Example : MonoBehaviour
{
static IntPtr nativeLibraryPtr;
delegate int MultiplyFloat(float number, float multiplyBy);
delegate void DoSomething(string words);
void Awake()
{
if (nativeLibraryPtr != IntPtr.Zero) return;
nativeLibraryPtr = Native.LoadLibrary("MyNativeLibraryName");
if (nativeLibraryPtr == IntPtr.Zero)
{
Debug.LogError("Failed to load native library");
}
}
void Update()
{
Native.Invoke<DoSomething>(nativeLibraryPtr, "Hello, World!");
int result = Native.Invoke<int, MultiplyFloat>(nativeLibraryPtr, 10, 5); // Should return the number 50.
}
void OnApplicationQuit()
{
if (nativeLibraryPtr == IntPtr.Zero) return;
Debug.Log(Native.FreeLibrary(nativeLibraryPtr)
? "Native library successfully unloaded."
: "Native library could not be unloaded.");
}
}
Take care to ensure your method signature and the parameters you provide match, with the handle provided by Native.LoadLibrary
as the first parameter.
Once you are ready to build the project you will want to replace the helper calls with normal platform invoke calls and move the latest version of your plugin to Assets/Plugins. Alternatively, the #if UNITY_EDITOR
preprocessor argument is helpful for automation of this process.
And there you have it! You can now press play in Unity’s editor, invoke native methods, stop the game, re-build your native plugin and try again!
Feel free to use this code as you please.