diff --git a/README.md b/README.md index 252813b..218d111 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/scripts/Attributes.cs b/scripts/Attributes.cs index bae18f9..c4a18e4 100644 --- a/scripts/Attributes.cs +++ b/scripts/Attributes.cs @@ -30,13 +30,32 @@ public class DisableMockingAttribute : Attribute } + /// + /// Such a method must be static and have one of the following signatures: + /// + /// public static void Func() + /// public static void Func(NativeDll dll) + /// public static void Func(NativeDll dll, int mainThreadId) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public class TriggerAttribute : Attribute + { + /// + /// 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. + /// + public bool UseMainThreadQueue = false; + } + /// /// Methods with this attribute are called directly after a native DLL has been loaded. /// Such method must be and either have no parameters or one parameter of type /// which indicates the state of the dll being loaded. Please treat this parameter as readonly. + ///
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public class NativeDllLoadedTriggerAttribute : Attribute + public class NativeDllLoadedTriggerAttribute : TriggerAttribute { } @@ -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 and either have no parameters or one parameter of type /// which indicates the state of the dll being unloaded. Please treat this parameter as readonly. + ///
/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public class NativeDllBeforeUnloadTriggerAttribute : Attribute + public class NativeDllBeforeUnloadTriggerAttribute : TriggerAttribute { } @@ -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 and either have no parameters or one parameter of type /// which indicates the state of the dll being unloaded. Please treat this parameter as readonly. + ///
/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public class NativeDllAfterUnloadTriggerAttribute : Attribute + public class NativeDllAfterUnloadTriggerAttribute : TriggerAttribute { } diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index 8440a3f..84f143f 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -30,10 +30,11 @@ public partial class DllManipulator private static List _mockedNativeFunctions = new List(); private static int _createdDelegateTypes = 0; private static int _lastNativeCallIndex = 0; //Use with synchronization - private static List _customLoadedTriggers = null; - private static List _customBeforeUnloadTriggers = null; - private static List _customAfterUnloadTriggers = null; - + + private static List> _customLoadedTriggers = null; //List of callbacks to run, whether to run them on the main thread. + private static List> _customBeforeUnloadTriggers = null; + private static List> _customAfterUnloadTriggers = null; + /// /// Initialization. /// Finds and mocks relevant native function declarations. @@ -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()); + + if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute))) + RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute()); + + if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute))) + RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute()); } } } @@ -101,18 +101,34 @@ internal static void Initialize(int unityMainThreadId, string assetsPath) LoadAll(); } - private static void RegisterTriggerMethod(MethodInfo method, ref List triggersList) + /// + /// Will unload/forget all dll's and reset the state + /// + public static void Reset() + { + UnloadAll(); + ForgetAllDlls(); + ClearCrashLogs(); + + _customLoadedTriggers?.Clear(); + _customAfterUnloadTriggers?.Clear(); + _customBeforeUnloadTriggers?.Clear(); + } + + private static void RegisterTriggerMethod(MethodInfo method, ref List> 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(2); - triggersList.Add(method); + triggersList = new List>(); + triggersList.Add(new Tuple(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}"); } } @@ -509,17 +525,28 @@ internal static void LoadTargetFunction(NativeFunction nativeFunction, bool igno } } - private static void InvokeCustomTriggers(List triggers, NativeDll dll) + private static void InvokeCustomTriggers(List> 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(); + + // 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()); + methodInfo.Invoke(null, args); } } @@ -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 diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 6679f4f..1245398 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -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 @@ -38,6 +38,8 @@ public class DllManipulatorScript : MonoBehaviour onlyInEditor = true, enableInEditMode = false }; + + public static ConcurrentQueue MainThreadTriggerQueue = new ConcurrentQueue(); private void OnEnable() { @@ -46,7 +48,7 @@ private void OnEnable() { if (EditorApplication.isPlaying) Destroy(gameObject); - else + else if(_singletonInstance != this) enabled = false; //Don't destroy as the user may be editing a Prefab return; } @@ -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; + #else if (Options.onlyInEditor) return; @@ -83,6 +90,32 @@ private void Initialize() initTimer.Stop(); InitializationTime = initTimer.Elapsed; } + + /// + /// Note: also called in edit mode if Options.enableInEditMode is set. + /// + private void Update() + { + InvokeMainThreadQueue(); + } + + /// + /// Executes queued methods. + /// Should be called from the main thread in Update. + /// + 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() { @@ -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; } } diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 1a64aee..fc3d380 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -64,10 +64,24 @@ public class DllManipulatorEditor : Editor private string[] _allKnownAssemblies = null; private DateTime _lastKnownAssembliesRefreshTime; + /// + /// Used to check if the options have changed, in order to set the object as dirty so changes are saved + /// + 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() @@ -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"); @@ -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(); @@ -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 usedDlls) @@ -339,5 +365,12 @@ public static void UnloadAll() { DllManipulator.UnloadAll(); } + + [NativeDllLoadedTrigger(UseMainThreadQueue = true)] + [NativeDllAfterUnloadTrigger(UseMainThreadQueue = true)] + public static void RepaintAll() + { + RepaintAllEditors?.Invoke(); + } } } diff --git a/scripts/Editor/DllManipulatorWindowEditor.cs b/scripts/Editor/DllManipulatorWindowEditor.cs index 681b535..3847a50 100644 --- a/scripts/Editor/DllManipulatorWindowEditor.cs +++ b/scripts/Editor/DllManipulatorWindowEditor.cs @@ -6,11 +6,14 @@ namespace UnityNativeTool.Internal { public class DllManipulatorWindowEditor : EditorWindow { + private static EditorWindow window; + [MenuItem("Window/Dll manipulator")] static void Init() { - var window = GetWindow(); + window = GetWindow(); window.Show(); + DllManipulatorEditor.RepaintAllEditors += window.Repaint; } void OnGUI()