How to Reload Native Plugins in Unity

September 2, 2019

If you've ever created a NativePlugins for Unity you may be familiar with a frustrating problem. Once Unity loads a DLL it never unloads it. If you build a new DLL and try to copy it over you'll meet this error message.

File in use error

The fix is to close the Unity Editor, copy the DLL, and relaunch Unity. You have to repeat this process everytime you build a new DLL. For Plugin users this isn't a big deal. For plugin developers it's a huge burden on iteration speed.

This post presents what I think is a decent solution to the problem. It's not a novel solution. Tons of devs have implemented this or something similar. But their solutions are not readily discoverable on Google or GitHub.


I wrote a ~200 line script that loads DLLs on Awake and unloads them in OnDestroy. Attributes are used to auto-fixup delegates on Awake. The syntax to call them is identical to using PInvoke.

Full source code is available on GitHub under a permissive license. The only file you need is NativePluginLoader.cs.

Instructions to use:

  1. Add NativePluginLoader.cs to your project
  2. Add NativePluginLoader component to your scene
  3. Declare an API class and give it a PluginAttr attribute
  4. Declare delegates and give them PluginFunctionAttr attributes.
// my_cool_plugin.h
extern "C" {
    __declspec(dllexport) float sum(float a, float b);
// C#
public static class FooPlugin
    public static Sum sum = null;
    public delegate float Sum(float a, float b);

void CoolFunc() {
    float s = FooPlugin.sum(1.0, 2.0);

Basic Solution

The standard way to call a NativePlugin is through PInvoke.

public static class FooPlugin_PInvoke {
    [DllImport("cpp_example_dll", EntryPoint = "sum")]
    extern static public float sum(float a, float b);

The problem with PInvoke is that it never unloads the DLL. The solution is to manually call LoadLibrary, FreeLibrary, and GetProcAddress.

static class SystemLibrary
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    static public extern IntPtr LoadLibrary(string lpFileName);

    [DllImport("kernel32", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static public extern bool FreeLibrary(IntPtr hModule);

    static public extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

If we manually call these functions for our plugin and plugin functions then we're set! We'll be able to unload our DLL which lets us update the binary without rebooting the editor. Success!

Advanced Solution

The basic solution works. But it may require a lot of tedious boilerplate. Wouldn't it be great if this could all happen automagically? Enter Attributes!

// Goes on class with fields to fixup
public class PluginAttr : System.Attribute
    public string pluginName { get; private set; }

// Goes on static public delegate fields to fixup
public class PluginFunctionAttr : System.Attribute
    public string functionName { get; private set; }

These attributes will allow us to use reflection to automatically find and fixup delegates.

NativePluginLoader.cs declares class NativePluginLoader : MonoBehavior. This singleton handles all loading and unloading.

The code to fixup functions looks roughly like this:

// Loop over all assemblies
foreach (var assembly in assemblies) {
    // Loop over all types
    foreach (var type in assembly.GetTypes()) {
        // Consider types with the attribute PluginAttr
        var typeAttr = type.FindAttribute(typeof(PluginAttr));
        if (!typeAttr)

        // Load the Plugin (this is cached)
        var plugin = LoadLibrary(typeAttr.pluginName);

        // Loop over all fields for type
        foreach (var field in type.GetFields()) {
            // Find static, public fields with PluginFunctionAttr
            var fieldAttr = field.FindAttribute(typeof(PluginFunctionAttr));
            if (!fieldAttr)

            // Get function pointer and store in static delegate field
            var fnPtr = GetProcAddress(plugin, fieldAttr.functionName);
            var fnDelegate = Marshal.GetDelegateForFunctionPointer(fnPtr, field.FieldType);
            field.SetValue(null, fnDelegate);

This code runs once at startup and automatically fixes up every delegate.

That's basically it! OnDestory just loops over stored DLL pointers and calls FreeLibrary. There's a little extra code for error checking, caching, singleton access, and handling script reload.

Once the delegates are set they can be invoked as if they were normal functions. If you integrate this into an existing PInvoke project you shouldn't need to update any call sites.

Other Considerations

I'm pretty happy with this as a general solution. It should "just work" for almost any project.

There are features I considered adding. Such as lazy loading or specifying assemblies to search. Ultimately I decided to publish something simple as a baseline. It's a ~200 line file that can be easily customized if your project has special needs or preferences.

One thing I like about my solution is it has identical syntax to PInvoke. This means it's a drop-in replacement for a PInvoke API. If performance is a concern you can maintain a PInvoke API for standalone builds and only use my fixup path for editor builds.

I'm not a fan of script reload in Unity. It's not worth supporting imho. Unity 2018.2 added a new setting. I strongly recommend setting Editor->Preferences->Script Changes While Playing = Recompile After Finished Playing. ScriptReload causes all NativePlugins to unload and reload. My script handles this via serialization callbacks. But if your plugin has internal state you'll be responsible for restoring it. (Not worth it.)

Right now my code only supports Windows. Supporting other platforms should be trivial. Pull requests welcome.


NativePlugin support in Unity is great. Sometimes it's better to implement something in C/C++ and access it from C# through a C API. Unfortunately, Unity's default behavior makes the creation of that NativePlugin a slow and painful process.

I couldn't find a good solution to this problem on the internet. So I put together one myself. Here it is. It's saved me some pain and suffering and I hope it can help others as well.

Source Code: GitHub