From 9263c102b78c8fdf7ad8371fd5424516a78c153c Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Mon, 16 Mar 2020 22:20:39 +0100 Subject: [PATCH 01/14] Repaint editor GUIs on shortcut --- scripts/Editor/DllManipulatorEditor.cs | 12 ++++++++++++ scripts/Editor/DllManipulatorWindowEditor.cs | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 69b58ae..5a07706 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -322,6 +322,8 @@ static string GetFirstAssemblyToList(string[] allAssemblies) public static void LoadAllShortcut() { DllManipulator.LoadAll(); + RepaintAll(); + DllManipulatorWindowEditor.RepaintAll(); } #if UNITY_2019_1_OR_NEWER @@ -332,6 +334,16 @@ public static void LoadAllShortcut() public static void UnloadAll() { DllManipulator.UnloadAll(); + RepaintAll(); + DllManipulatorWindowEditor.RepaintAll(); + } + + public static void RepaintAll() + { + var editors = Resources.FindObjectsOfTypeAll(); + if(editors == null) return; + foreach (var editor in editors) + editor.Repaint(); } } } \ No newline at end of file diff --git a/scripts/Editor/DllManipulatorWindowEditor.cs b/scripts/Editor/DllManipulatorWindowEditor.cs index 681b535..d275ef8 100644 --- a/scripts/Editor/DllManipulatorWindowEditor.cs +++ b/scripts/Editor/DllManipulatorWindowEditor.cs @@ -6,13 +6,20 @@ 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(); } + public static void RepaintAll() + { + if(window) window.Repaint(); + } + void OnGUI() { var dllManipulator = FindObjectOfType(); From 88a8895c40ebf0a679b4cd7d29ea0eda47e2b947 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Sat, 21 Mar 2020 15:01:49 +0100 Subject: [PATCH 02/14] Editor variable changes saved properly Before changing a variable would not be detected and so not saved/serialized in some cases. --- scripts/Editor/DllManipulatorEditor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 5a07706..06853f8 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -8,6 +8,8 @@ #endif using System.IO; using System; +using UnityEditor.SceneManagement; +using UnityEngine.SceneManagement; namespace UnityNativeTool.Internal { @@ -139,6 +141,12 @@ public override void OnInspectorGUI() var time = t.InitializationTime.Value; EditorGUILayout.LabelField($"Initialized in: {(int)time.TotalSeconds}.{time.Milliseconds.ToString("D3")}s"); } + + if (GUI.changed) + { + EditorUtility.SetDirty(target); + EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); + } } private void DrawUsedDlls(IList usedDlls) From 34a6b01c839e2a023b2278fa89f0de792ec1f670 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Sat, 21 Mar 2020 15:15:27 +0100 Subject: [PATCH 03/14] Use callback attributes to repaint editors This would not work with the `upm-support` branch #14 as the attributes are not searched for in the attribute that these scripts are in by default. --- scripts/Editor/DllManipulatorEditor.cs | 6 ++---- scripts/Editor/DllManipulatorWindowEditor.cs | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 06853f8..99b7896 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -330,8 +330,6 @@ static string GetFirstAssemblyToList(string[] allAssemblies) public static void LoadAllShortcut() { DllManipulator.LoadAll(); - RepaintAll(); - DllManipulatorWindowEditor.RepaintAll(); } #if UNITY_2019_1_OR_NEWER @@ -342,10 +340,10 @@ public static void LoadAllShortcut() public static void UnloadAll() { DllManipulator.UnloadAll(); - RepaintAll(); - DllManipulatorWindowEditor.RepaintAll(); } + [NativeDllLoadedTrigger] + [NativeDllAfterUnloadTrigger] public static void RepaintAll() { var editors = Resources.FindObjectsOfTypeAll(); diff --git a/scripts/Editor/DllManipulatorWindowEditor.cs b/scripts/Editor/DllManipulatorWindowEditor.cs index d275ef8..74a17f6 100644 --- a/scripts/Editor/DllManipulatorWindowEditor.cs +++ b/scripts/Editor/DllManipulatorWindowEditor.cs @@ -15,6 +15,8 @@ static void Init() window.Show(); } + [NativeDllLoadedTrigger] + [NativeDllAfterUnloadTrigger] public static void RepaintAll() { if(window) window.Repaint(); From 74c1c96f19b432179a9780be3f3e0ac42a71aff8 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Tue, 24 Mar 2020 14:48:28 +0100 Subject: [PATCH 04/14] Allow multiple un/loadTrigger attributes, use action to trigger repaints --- scripts/DllManipulator.cs | 19 +++++++++---------- scripts/Editor/DllManipulatorEditor.cs | 10 +++++----- scripts/Editor/DllManipulatorWindowEditor.cs | 8 +------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index 2b4c5b4..eb25eaf 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -81,17 +81,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))) + else { - RegisterTriggerMethod(method, ref _customLoadedTriggers); - } - else if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute))) - { - RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers); - } - else if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute))) - { - RegisterTriggerMethod(method, ref _customAfterUnloadTriggers); + if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute))) + RegisterTriggerMethod(method, ref _customLoadedTriggers); + + if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute))) + RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers); + + if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute))) + RegisterTriggerMethod(method, ref _customAfterUnloadTriggers); } } } diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 99b7896..47535ba 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -61,11 +61,14 @@ public class DllManipulatorEditor : Editor private bool _showTargetAssemblies = true; private string[] _allKnownAssemblies = null; private DateTime _lastKnownAssembliesRefreshTime; - + + public static event Action RepaintAllEditors = delegate {}; + public DllManipulatorEditor() { EditorApplication.pauseStateChanged += _ => Repaint(); EditorApplication.playModeStateChanged += _ => Repaint(); + RepaintAllEditors += Repaint; } public override void OnInspectorGUI() @@ -346,10 +349,7 @@ public static void UnloadAll() [NativeDllAfterUnloadTrigger] public static void RepaintAll() { - var editors = Resources.FindObjectsOfTypeAll(); - if(editors == null) return; - foreach (var editor in editors) - editor.Repaint(); + RepaintAllEditors.Invoke(); } } } \ No newline at end of file diff --git a/scripts/Editor/DllManipulatorWindowEditor.cs b/scripts/Editor/DllManipulatorWindowEditor.cs index 74a17f6..3847a50 100644 --- a/scripts/Editor/DllManipulatorWindowEditor.cs +++ b/scripts/Editor/DllManipulatorWindowEditor.cs @@ -13,13 +13,7 @@ static void Init() { window = GetWindow(); window.Show(); - } - - [NativeDllLoadedTrigger] - [NativeDllAfterUnloadTrigger] - public static void RepaintAll() - { - if(window) window.Repaint(); + DllManipulatorEditor.RepaintAllEditors += window.Repaint; } void OnGUI() From 87dcb2ab6fde2a1a1712ec37ea1277a1b2df138f Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Tue, 24 Mar 2020 15:01:21 +0100 Subject: [PATCH 05/14] Remove SceneManagement, it's not required The scene is automatically set as dirty when the gui target is --- scripts/Editor/DllManipulatorEditor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 47535ba..530080e 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -8,8 +8,6 @@ #endif using System.IO; using System; -using UnityEditor.SceneManagement; -using UnityEngine.SceneManagement; namespace UnityNativeTool.Internal { @@ -148,7 +146,6 @@ public override void OnInspectorGUI() if (GUI.changed) { EditorUtility.SetDirty(target); - EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); } } From b478c7b9c109b38877500c52df393ecaf8b22c55 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Wed, 8 Apr 2020 20:31:25 +0200 Subject: [PATCH 06/14] Fixes to gui with enableInEdit mode Admittedly this should have been in #12. However, here are the fixes anyways. GUI buttons to un/load show in edit mode if enableInEditMode is true. Disabling and re-enabling the DllManipulatorScript works now. --- scripts/DllManipulatorScript.cs | 2 +- scripts/Editor/DllManipulatorEditor.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 6679f4f..8013983 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -46,7 +46,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; } diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 3165390..132465d 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -117,9 +117,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"); @@ -130,7 +130,7 @@ public override void OnInspectorGUI() DrawUsedDlls(usedDlls); } - else if(EditorApplication.isPlaying) + else if(EditorApplication.isPlaying || t.Options.enableInEditMode) { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); @@ -139,7 +139,7 @@ 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(); From 1a33f4d83b03d12b79d0ca74c50e2b7de29218d6 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Thu, 16 Apr 2020 22:29:54 +0200 Subject: [PATCH 07/14] Properly reset custom triggers (fix duplicates) Previously triggers would not be cleared properly and so be duplicated. (Static variables seem to persist between entering/exiting playmode) --- scripts/DllManipulator.cs | 14 ++++++++++++++ scripts/DllManipulatorScript.cs | 4 +--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index 1290b40..8eca69a 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -100,6 +100,20 @@ internal static void Initialize(int unityMainThreadId, string assetsPath) LoadAll(); } + /// + /// 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) { var parameters = method.GetParameters(); diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 8013983..aa8923a 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -92,9 +92,7 @@ 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(); + DllManipulator.Reset(); _singletonInstance = null; } } From 39c4fce825d7f1c4572d183ec8666593d3840f13 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Thu, 16 Apr 2020 22:37:42 +0200 Subject: [PATCH 08/14] Custom triggers optional execute on main thread Allow custom triggers to be executed on the main thread in a queue. We need this internally to repaint the editor GUI as it uses the Unity API. --- README.md | 1 + scripts/Attributes.cs | 17 +++++-- scripts/DllManipulator.cs | 61 ++++++++++++++++++-------- scripts/DllManipulatorScript.cs | 18 ++++++++ scripts/Editor/DllManipulatorEditor.cs | 4 +- 5 files changed, 78 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 252813b..493b41d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ 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 ## 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..98bc1b0 100644 --- a/scripts/Attributes.cs +++ b/scripts/Attributes.cs @@ -30,13 +30,24 @@ public class DisableMockingAttribute : Attribute } + [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. + /// For consistent behaviour, the method is put in the queue even if it is 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 { } @@ -47,7 +58,7 @@ public class NativeDllLoadedTriggerAttribute : Attribute /// 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 { } @@ -58,7 +69,7 @@ public class NativeDllBeforeUnloadTriggerAttribute : Attribute /// 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 8eca69a..22e1bba 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -30,10 +31,12 @@ 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; + private static ConcurrentQueue> _mainThreadTriggerQueue = new ConcurrentQueue>(); + /// /// Initialization. /// Finds and mocks relevant native function declarations. @@ -84,13 +87,13 @@ internal static void Initialize(int unityMainThreadId, string assetsPath) else { if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute))) - RegisterTriggerMethod(method, ref _customLoadedTriggers); + RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute().UseMainThreadQueue); if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute))) - RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers); + RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute().UseMainThreadQueue); if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute))) - RegisterTriggerMethod(method, ref _customAfterUnloadTriggers); + RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute().UseMainThreadQueue); } } } @@ -113,19 +116,20 @@ public static void Reset() _customAfterUnloadTriggers?.Clear(); _customBeforeUnloadTriggers?.Clear(); } - - private static void RegisterTriggerMethod(MethodInfo method, ref List triggersList) + + private static void RegisterTriggerMethod(MethodInfo method, ref List> triggersList, bool runOnMainThread) { 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, runOnMainThread)); } 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 or one parameter of type {nameof(NativeDll)} or two of type {nameof(NativeDll)} and int. Violation on method {method.Name} in {method.DeclaringType.FullName}"); } } @@ -522,20 +526,41 @@ 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*/) + _mainThreadTriggerQueue.Enqueue(new Tuple(methodInfo, args)); else - triggerMethod.Invoke(null, Array.Empty()); + methodInfo.Invoke(null, args); } } + /// + /// Executes queued methods. + /// Should be called from the main thread in Update. + /// + public static void InvokeMainThreadQueue() + { + while (_mainThreadTriggerQueue.TryDequeue(out var action)) + action.Item1.Invoke(null, action.Item2); + } + /// /// Logs native function's call to file. If that file exists, it is overwritten. One file is maintained for each thread. /// Note: This method is being called by dynamically generated code. Be careful when changing its signature. diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index aa8923a..786f686 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -57,6 +57,10 @@ private void OnEnable() if(EditorApplication.isPlaying || Options.enableInEditMode) Initialize(); + + if(!EditorApplication.isPlaying && Options.enableInEditMode) + EditorApplication.update += Update; + #else if (Options.onlyInEditor) return; @@ -83,6 +87,20 @@ private void Initialize() initTimer.Stop(); InitializationTime = initTimer.Elapsed; } + + /// + /// Note: also called in edit mode if Options.enableInEditMode is set. + /// + private void Update() + { + DllManipulator.InvokeMainThreadQueue(); + } + + private void OnDisable() + { + if(!EditorApplication.isPlaying && Options.enableInEditMode) + EditorApplication.update -= Update; + } private void OnDestroy() { diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 132465d..857a691 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -348,8 +348,8 @@ public static void UnloadAll() DllManipulator.UnloadAll(); } - [NativeDllLoadedTrigger] - [NativeDllAfterUnloadTrigger] + [NativeDllLoadedTrigger(UseMainThreadQueue = true)] + [NativeDllAfterUnloadTrigger(UseMainThreadQueue = true)] public static void RepaintAll() { RepaintAllEditors.Invoke(); From 5f7b79c9fb9958f8b5dfd7e62665de55a472c34e Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Thu, 16 Apr 2020 22:45:41 +0200 Subject: [PATCH 09/14] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 493b41d..218d111 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Tool created mainly to solve old problem with reloading [native plugins](https:/ - 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. From 5bda99031c315006d66ea14aa8c7c949c7dabd6e Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Fri, 17 Apr 2020 09:58:08 +0200 Subject: [PATCH 10/14] Mark scene dirty only if options changed --- scripts/DllManipulator.cs | 27 ++++++++++++++++++++++++++ scripts/Editor/DllManipulatorEditor.cs | 17 +++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index 22e1bba..479d948 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -692,6 +692,33 @@ public class DllManipulatorOptions public bool mockAllNativeFunctions; public bool onlyInEditor; public bool enableInEditMode; + + public DllManipulatorOptions CloneTo(DllManipulatorOptions other) + { + other.dllPathPattern = dllPathPattern; + other.assemblyNames = (string[]) assemblyNames.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.assemblyNames.SequenceEqual(assemblyNames) && + 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/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 857a691..8de1fae 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -63,6 +63,11 @@ public class DllManipulatorEditor : Editor private bool _showTargetAssemblies = true; private string[] _allKnownAssemblies = null; private DateTime _lastKnownAssembliesRefreshTime; + + /// + /// To check if the options have change in order to set the object as dirty + /// + private DllManipulatorOptions _prevOptions = new DllManipulatorOptions(); public static event Action RepaintAllEditors = delegate {}; @@ -72,6 +77,11 @@ public DllManipulatorEditor() EditorApplication.playModeStateChanged += _ => Repaint(); RepaintAllEditors += Repaint; } + + private void Awake() + { + ((DllManipulatorScript)target).Options.CloneTo(_prevOptions); + } public override void OnInspectorGUI() { @@ -147,9 +157,14 @@ public override void OnInspectorGUI() 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) { - EditorUtility.SetDirty(target); + if (!t.Options.Equals(_prevOptions)) + { + t.Options.CloneTo(_prevOptions); + EditorUtility.SetDirty(target); + } } } From 66c60cff8798f8b368a605a39fdc473d3cdef46a Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Fri, 17 Apr 2020 11:13:34 +0200 Subject: [PATCH 11/14] Bug fix, resetting when not initialized Initialize() is not always called so we should not always reset. --- scripts/DllManipulatorScript.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 786f686..4cf29b6 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -110,7 +110,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.Reset(); + if (DllManipulator.Options != null) // Check that we have initialized + DllManipulator.Reset(); _singletonInstance = null; } } From a0dc185babf73065dad5935be87e2de2f8d5811a Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Sat, 25 Apr 2020 09:42:14 +0200 Subject: [PATCH 12/14] small fix --- scripts/DllManipulatorScript.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 4cf29b6..64bb27a 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -96,11 +96,13 @@ private void Update() DllManipulator.InvokeMainThreadQueue(); } +#if UNITY_EDITOR private void OnDisable() { if(!EditorApplication.isPlaying && Options.enableInEditMode) EditorApplication.update -= Update; } +#endif private void OnDestroy() { From e5edbf47b6172d05e0730cbde88dffbd80cd2454 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Thu, 30 Apr 2020 21:02:09 +0200 Subject: [PATCH 13/14] Review fixes Also RegisterTriggerMethod receives attribute instead of bool, Fix naming in DllManipulatorOptions --- scripts/Attributes.cs | 14 +++++++++-- scripts/DllManipulator.cs | 33 +++++++++----------------- scripts/DllManipulatorScript.cs | 18 +++++++++++--- scripts/Editor/DllManipulatorEditor.cs | 2 +- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/scripts/Attributes.cs b/scripts/Attributes.cs index 98bc1b0..c4a18e4 100644 --- a/scripts/Attributes.cs +++ b/scripts/Attributes.cs @@ -30,13 +30,20 @@ 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. - /// For consistent behaviour, the method is put in the queue even if it is triggered from the main thread. + /// 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; } @@ -45,6 +52,7 @@ public class TriggerAttribute : Attribute /// 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 : TriggerAttribute @@ -56,6 +64,7 @@ public class NativeDllLoadedTriggerAttribute : TriggerAttribute /// 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 : TriggerAttribute @@ -67,6 +76,7 @@ public class NativeDllBeforeUnloadTriggerAttribute : TriggerAttribute /// 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 : TriggerAttribute diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index 479d948..84f143f 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -35,7 +34,6 @@ public partial class DllManipulator 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; - private static ConcurrentQueue> _mainThreadTriggerQueue = new ConcurrentQueue>(); /// /// Initialization. @@ -87,13 +85,13 @@ internal static void Initialize(int unityMainThreadId, string assetsPath) else { if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute))) - RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute().UseMainThreadQueue); + RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute()); if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute))) - RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute().UseMainThreadQueue); + RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute()); if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute))) - RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute().UseMainThreadQueue); + RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute()); } } } @@ -117,7 +115,7 @@ public static void Reset() _customBeforeUnloadTriggers?.Clear(); } - private static void RegisterTriggerMethod(MethodInfo method, ref List> triggersList, bool runOnMainThread) + 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) @@ -125,11 +123,12 @@ private static void RegisterTriggerMethod(MethodInfo method, ref List>(); - triggersList.Add(new Tuple(method, runOnMainThread)); + triggersList.Add(new Tuple(method, attribute.UseMainThreadQueue)); } else { - Debug.LogError($"Trigger method must either take no parameters or one parameter of type {nameof(NativeDll)} or two of type {nameof(NativeDll)} and int. 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}"); } } @@ -544,23 +543,13 @@ private static void InvokeCustomTriggers(List> triggers, args = Array.Empty(); // Execute now or queue to the main thread - if (useMainThreadQueue /*&& Thread.CurrentThread.ManagedThreadId != _unityMainThreadId*/) - _mainThreadTriggerQueue.Enqueue(new Tuple(methodInfo, args)); + if (useMainThreadQueue && Thread.CurrentThread.ManagedThreadId != _unityMainThreadId) + DllManipulatorScript.MainThreadTriggerQueue.Enqueue(() => methodInfo.Invoke(null, args)); else methodInfo.Invoke(null, args); } } - /// - /// Executes queued methods. - /// Should be called from the main thread in Update. - /// - public static void InvokeMainThreadQueue() - { - while (_mainThreadTriggerQueue.TryDequeue(out var action)) - action.Item1.Invoke(null, action.Item2); - } - /// /// Logs native function's call to file. If that file exists, it is overwritten. One file is maintained for each thread. /// Note: This method is being called by dynamically generated code. Be careful when changing its signature. @@ -696,7 +685,7 @@ public class DllManipulatorOptions public DllManipulatorOptions CloneTo(DllManipulatorOptions other) { other.dllPathPattern = dllPathPattern; - other.assemblyNames = (string[]) assemblyNames.Clone(); + other.assemblyPaths = (string[]) assemblyPaths.Clone(); other.loadingMode = loadingMode; other.posixDlopenFlags = posixDlopenFlags; other.threadSafe = threadSafe; @@ -712,7 +701,7 @@ public DllManipulatorOptions CloneTo(DllManipulatorOptions other) public bool Equals(DllManipulatorOptions other) { - return other.dllPathPattern == dllPathPattern && other.assemblyNames.SequenceEqual(assemblyNames) && + 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 && diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 64bb27a..5723c80 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() { @@ -93,7 +95,17 @@ private void Initialize() /// private void Update() { - DllManipulator.InvokeMainThreadQueue(); + 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 diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 8de1fae..5db40a6 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -367,7 +367,7 @@ public static void UnloadAll() [NativeDllAfterUnloadTrigger(UseMainThreadQueue = true)] public static void RepaintAll() { - RepaintAllEditors.Invoke(); + RepaintAllEditors?.Invoke(); } } } From 05fc35cf8f2431de8f7556633518a09217866658 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Fri, 1 May 2020 11:30:06 +0200 Subject: [PATCH 14/14] Add comments --- scripts/DllManipulatorScript.cs | 1 + scripts/Editor/DllManipulatorEditor.cs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 5723c80..1245398 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -60,6 +60,7 @@ 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; diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 5db40a6..fc3d380 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -65,7 +65,7 @@ public class DllManipulatorEditor : Editor private DateTime _lastKnownAssembliesRefreshTime; /// - /// To check if the options have change in order to set the object as dirty + /// 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(); @@ -80,6 +80,7 @@ public DllManipulatorEditor() 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); } @@ -162,6 +163,8 @@ public override void OnInspectorGUI() { 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); }