Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Tool created mainly to solve old problem with reloading [native plugins](https:/
- If something is not working, first check out available options (and read their descriptions), then [report an issue](https://github.com/mcpiroman/UnityNativeTool/issues/new).
- Options are accessible via `DllManipulatorScript` editor or window.
- Although this tool presumably works in built game, it's intended to be used in editor.
- Get callbacks in C# when the dll load state has changed with attributes like `[NativeDllLoadedTrigger]`, see `Attributes.cs` for more information
- Unload and load all DLLs via shortcut `Alt+D` and `Alt+Shfit+D` respectively. Editable in the Shortcut Manager for 2019.1+

## Limitations
- Marshaling parameter attributes other than `[MarshalAs]`, `[In]` and `[Out]` are not supported.
Expand Down
27 changes: 24 additions & 3 deletions scripts/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,32 @@ public class DisableMockingAttribute : Attribute

}

/// <summary>
/// Such a method must be static and have one of the following signatures:
/// <code>
/// public static void Func()
/// public static void Func(NativeDll dll)
/// public static void Func(NativeDll dll, int mainThreadId)
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class TriggerAttribute : Attribute
{
/// <summary>
/// Should the method always be executed on the main thread, to allow use of the Unity API.
/// Note: this means the method is not immediately executed but put in a queue, if it is not triggered from the main thread.
/// </summary>
public bool UseMainThreadQueue = false;
}

/// <summary>
/// Methods with this attribute are called directly after a native DLL has been loaded.
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
/// which indicates the state of the dll being loaded. Please treat this parameter as readonly.
/// <br/><inheritdoc cref="TriggerAttribute"/>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class NativeDllLoadedTriggerAttribute : Attribute
public class NativeDllLoadedTriggerAttribute : TriggerAttribute
{

}
Expand All @@ -45,9 +64,10 @@ public class NativeDllLoadedTriggerAttribute : Attribute
/// Methods with this attribute are called directly before a native DLL is going to be unloaded.
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
/// <br/><inheritdoc cref="TriggerAttribute"/>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class NativeDllBeforeUnloadTriggerAttribute : Attribute
public class NativeDllBeforeUnloadTriggerAttribute : TriggerAttribute
{

}
Expand All @@ -56,9 +76,10 @@ public class NativeDllBeforeUnloadTriggerAttribute : Attribute
/// Methods with this attribute are called directly after a native DLL has been unloaded.
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
/// <br/><inheritdoc cref="TriggerAttribute"/>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class NativeDllAfterUnloadTriggerAttribute : Attribute
public class NativeDllAfterUnloadTriggerAttribute : TriggerAttribute
{

}
Expand Down
102 changes: 78 additions & 24 deletions scripts/DllManipulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ public partial class DllManipulator
private static List<NativeFunction> _mockedNativeFunctions = new List<NativeFunction>();
private static int _createdDelegateTypes = 0;
private static int _lastNativeCallIndex = 0; //Use with synchronization
private static List<MethodInfo> _customLoadedTriggers = null;
private static List<MethodInfo> _customBeforeUnloadTriggers = null;
private static List<MethodInfo> _customAfterUnloadTriggers = null;


private static List<Tuple<MethodInfo, bool>> _customLoadedTriggers = null; //List of callbacks to run, whether to run them on the main thread.
private static List<Tuple<MethodInfo, bool>> _customBeforeUnloadTriggers = null;
private static List<Tuple<MethodInfo, bool>> _customAfterUnloadTriggers = null;

/// <summary>
/// Initialization.
/// Finds and mocks relevant native function declarations.
Expand Down Expand Up @@ -81,17 +82,16 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
if (Options.mockAllNativeFunctions || method.IsDefined(typeof(MockNativeDeclarationAttribute)) || method.DeclaringType.IsDefined(typeof(MockNativeDeclarationsAttribute)))
MockNativeFunction(method);
}
else if(method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
{
RegisterTriggerMethod(method, ref _customLoadedTriggers);
}
else if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
{
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers);
}
else if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
else
{
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers);
if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute<NativeDllLoadedTriggerAttribute>());

if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute<NativeDllBeforeUnloadTriggerAttribute>());

if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute<NativeDllAfterUnloadTriggerAttribute>());
}
}
}
Expand All @@ -101,18 +101,34 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
LoadAll();
}

private static void RegisterTriggerMethod(MethodInfo method, ref List<MethodInfo> triggersList)
/// <summary>
/// Will unload/forget all dll's and reset the state
/// </summary>
public static void Reset()
{
UnloadAll();
ForgetAllDlls();
ClearCrashLogs();

_customLoadedTriggers?.Clear();
_customAfterUnloadTriggers?.Clear();
_customBeforeUnloadTriggers?.Clear();
}

private static void RegisterTriggerMethod(MethodInfo method, ref List<Tuple<MethodInfo, bool>> triggersList, TriggerAttribute attribute)
{
var parameters = method.GetParameters();
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll))
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll)
|| parameters.Length == 2 && parameters[0].ParameterType == typeof(NativeDll) && parameters[1].ParameterType == typeof(int))
{
if (triggersList == null)
triggersList = new List<MethodInfo>(2);
triggersList.Add(method);
triggersList = new List<Tuple<MethodInfo, bool>>();
triggersList.Add(new Tuple<MethodInfo, bool>(method, attribute.UseMainThreadQueue));
}
else
{
Debug.LogError($"Trigger method must either take no parameters or one parameter of type {nameof(NativeDll)}. Violation on method {method.Name} in {method.DeclaringType.FullName}");
Debug.LogError($"Trigger method must either take no parameters, one parameter of type {nameof(NativeDll)} or one of type {nameof(NativeDll)} and one int. " +
$"See the TriggerAttribute for more details. Violation on method {method.Name} in {method.DeclaringType.FullName}");
}
}

Expand Down Expand Up @@ -509,17 +525,28 @@ internal static void LoadTargetFunction(NativeFunction nativeFunction, bool igno
}
}

private static void InvokeCustomTriggers(List<MethodInfo> triggers, NativeDll dll)
private static void InvokeCustomTriggers(List<Tuple<MethodInfo, bool>> triggers, NativeDll dll)
{
if (triggers == null)
return;

foreach(var triggerMethod in triggers)
foreach(var (methodInfo, useMainThreadQueue) in triggers)
{
if (triggerMethod.GetParameters().Length == 1)
triggerMethod.Invoke(null, new object[] { dll });
object[] args;

// Determine args for method
if (methodInfo.GetParameters().Length == 2)
args = new object[] { dll, _unityMainThreadId };
else if (methodInfo.GetParameters().Length == 1)
args = new object[] { dll };
else
args = Array.Empty<object>();

// Execute now or queue to the main thread
if (useMainThreadQueue && Thread.CurrentThread.ManagedThreadId != _unityMainThreadId)
DllManipulatorScript.MainThreadTriggerQueue.Enqueue(() => methodInfo.Invoke(null, args));
else
triggerMethod.Invoke(null, Array.Empty<object>());
methodInfo.Invoke(null, args);
}
}

Expand Down Expand Up @@ -654,6 +681,33 @@ public class DllManipulatorOptions
public bool mockAllNativeFunctions;
public bool onlyInEditor;
public bool enableInEditMode;

public DllManipulatorOptions CloneTo(DllManipulatorOptions other)
{
other.dllPathPattern = dllPathPattern;
other.assemblyPaths = (string[]) assemblyPaths.Clone();
other.loadingMode = loadingMode;
other.posixDlopenFlags = posixDlopenFlags;
other.threadSafe = threadSafe;
other.enableCrashLogs = enableCrashLogs;
other.crashLogsDir = crashLogsDir;
other.crashLogsStackTrace = crashLogsStackTrace;
other.mockAllNativeFunctions = mockAllNativeFunctions;
other.onlyInEditor = onlyInEditor;
other.enableInEditMode = enableInEditMode;

return other;
}

public bool Equals(DllManipulatorOptions other)
{
return other.dllPathPattern == dllPathPattern && other.assemblyPaths.SequenceEqual(assemblyPaths) &&
other.loadingMode == loadingMode && other.posixDlopenFlags == posixDlopenFlags &&
other.threadSafe == threadSafe && other.enableCrashLogs == enableCrashLogs &&
other.crashLogsDir == crashLogsDir && other.crashLogsStackTrace == crashLogsStackTrace &&
other.mockAllNativeFunctions == mockAllNativeFunctions && other.onlyInEditor == onlyInEditor &&
other.enableInEditMode == enableInEditMode;
}
}

public enum DllLoadingMode
Expand Down
44 changes: 38 additions & 6 deletions scripts/DllManipulatorScript.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Reflection;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Linq;
using UnityEngine;
using UnityNativeTool.Internal;
#if UNITY_EDITOR
Expand Down Expand Up @@ -38,6 +38,8 @@ public class DllManipulatorScript : MonoBehaviour
onlyInEditor = true,
enableInEditMode = false
};

public static ConcurrentQueue<Action> MainThreadTriggerQueue = new ConcurrentQueue<Action>();

private void OnEnable()
{
Expand All @@ -46,7 +48,7 @@ private void OnEnable()
{
if (EditorApplication.isPlaying)
Destroy(gameObject);
else
else if(_singletonInstance != this)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows the user to properly toggle enabled in edit mode.

enabled = false; //Don't destroy as the user may be editing a Prefab
return;
}
Expand All @@ -57,6 +59,11 @@ private void OnEnable()

if(EditorApplication.isPlaying || Options.enableInEditMode)
Initialize();

// Ensure update is called every frame in edit mode, ExecuteInEditMode only calls Update when the scene changes
if(!EditorApplication.isPlaying && Options.enableInEditMode)
EditorApplication.update += Update;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doens't just having [ExecuteInEditMode] make the Update be invoke in editor? Or do I misunderstand sth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ExecuteInEditMode "Update is only called when something in the Scene changed" - Docs. So we need to manually add this to cover all cases.


#else
if (Options.onlyInEditor)
return;
Expand All @@ -83,6 +90,32 @@ private void Initialize()
initTimer.Stop();
InitializationTime = initTimer.Elapsed;
}

/// <summary>
/// Note: also called in edit mode if Options.enableInEditMode is set.
/// </summary>
private void Update()
{
InvokeMainThreadQueue();
}

/// <summary>
/// Executes queued methods.
/// Should be called from the main thread in Update.
/// </summary>
public static void InvokeMainThreadQueue()
{
while (MainThreadTriggerQueue.TryDequeue(out var action))
action();
}

#if UNITY_EDITOR
private void OnDisable()
{
if(!EditorApplication.isPlaying && Options.enableInEditMode)
EditorApplication.update -= Update;
}
#endif

private void OnDestroy()
{
Expand All @@ -92,9 +125,8 @@ private void OnDestroy()
//On Preloaded mode this leads to NullReferenceException, but on Lazy mode the DLL and function would be just reloaded so we would up with loaded DLL after game exit.
//Thankfully thread safety with Lazy mode is not implemented yet.

DllManipulator.UnloadAll();
DllManipulator.ForgetAllDlls();
DllManipulator.ClearCrashLogs();
if (DllManipulator.Options != null) // Check that we have initialized
DllManipulator.Reset();
_singletonInstance = null;
}
}
Expand Down
41 changes: 37 additions & 4 deletions scripts/Editor/DllManipulatorEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,24 @@ public class DllManipulatorEditor : Editor
private string[] _allKnownAssemblies = null;
private DateTime _lastKnownAssembliesRefreshTime;

/// <summary>
/// Used to check if the options have changed, in order to set the object as dirty so changes are saved
/// </summary>
private DllManipulatorOptions _prevOptions = new DllManipulatorOptions();

public static event Action RepaintAllEditors = delegate {};

public DllManipulatorEditor()
{
EditorApplication.pauseStateChanged += _ => Repaint();
EditorApplication.playModeStateChanged += _ => Repaint();
RepaintAllEditors += Repaint;
}

private void Awake()
{
// Immediately copy the Options to the previous so we don't need to check for null later
((DllManipulatorScript)target).Options.CloneTo(_prevOptions);
}

public override void OnInspectorGUI()
Expand Down Expand Up @@ -114,9 +128,9 @@ public override void OnInspectorGUI()


bool unloadAll;
if(EditorApplication.isPlaying && t.Options.threadSafe)
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.Options.threadSafe)
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT);
else if (EditorApplication.isPlaying && !EditorApplication.isPaused && t.Options.loadingMode == DllLoadingMode.Preload)
else if ((EditorApplication.isPlaying && !EditorApplication.isPaused || t.Options.enableInEditMode) && t.Options.loadingMode == DllLoadingMode.Preload)
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT);
else
unloadAll = GUILayout.Button("Unload all DLLs");
Expand All @@ -127,7 +141,7 @@ public override void OnInspectorGUI()

DrawUsedDlls(usedDlls);
}
else if(EditorApplication.isPlaying)
else if(EditorApplication.isPlaying || t.Options.enableInEditMode)
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
Expand All @@ -136,13 +150,25 @@ public override void OnInspectorGUI()
GUILayout.EndHorizontal();
}

if(EditorApplication.isPlaying && t.InitializationTime != null)
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.InitializationTime != null)
{
EditorGUILayout.Space();
EditorGUILayout.Space();
var time = t.InitializationTime.Value;
EditorGUILayout.LabelField($"Initialized in: {(int)time.TotalSeconds}.{time.Milliseconds.ToString("D3")}s");
}

// Set the target as dirty so changes can be saved, if there are changes
if (GUI.changed)
{
if (!t.Options.Equals(_prevOptions))
{
// If the options have changed then update the _prevOptions and notify there are changes to be saved
// CloneTo is used to ensure a deep copy is made
t.Options.CloneTo(_prevOptions);
EditorUtility.SetDirty(target);
}
}
}

private void DrawUsedDlls(IList<NativeDllInfo> usedDlls)
Expand Down Expand Up @@ -339,5 +365,12 @@ public static void UnloadAll()
{
DllManipulator.UnloadAll();
}

[NativeDllLoadedTrigger(UseMainThreadQueue = true)]
[NativeDllAfterUnloadTrigger(UseMainThreadQueue = true)]
public static void RepaintAll()
{
RepaintAllEditors?.Invoke();
}
}
}
Loading