From 20e9645cf06e2344796842c25c0d0135d897563b Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Fri, 1 May 2020 17:45:08 +0200 Subject: [PATCH 1/6] Allow editing options when initialized, DllManip has a separate Options copy to DllManipScript When DllManipulator is initialized we pass a copy of the current DllManipulatorScript options. This allows us to keep editing the options when the manipulator is initialized/dlls are loaded as we are editing a copy. This is particularly useful when using enableInEditMode. Changes are always applied at OnEnable as before. To allow modifying without play/stop a DllManipulatorScript.Reinitialize() has been added which can be called via GUI (see DetectOptionChanges) which unloads dlls and initializes with new options. GUI is only shown when there are changes. --- scripts/DllManipulator.cs | 7 +- scripts/DllManipulatorScript.cs | 23 +++++- scripts/Editor/DllManipulatorEditor.cs | 100 +++++++++++++++++++------ 3 files changed, 103 insertions(+), 27 deletions(-) diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index e61840d..d25ae55 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -53,8 +53,11 @@ public partial class DllManipulator /// If option is specified, loads all DLLs specified by these functions. /// Options have to be configured before calling this method. /// - internal static void Initialize(int unityMainThreadId, string assetsPath) + internal static void Initialize(DllManipulatorOptions options, int unityMainThreadId, string assetsPath) { + // Make a deep copy of the options so we can edit them in DllManipulatorScript independently + Options = new DllManipulatorOptions(); + options.CloneTo(Options); _unityMainThreadId = unityMainThreadId; _assetsPath = assetsPath; @@ -123,6 +126,8 @@ public static void Reset() _customLoadedTriggers?.Clear(); _customAfterUnloadTriggers?.Clear(); _customBeforeUnloadTriggers?.Clear(); + + Options = null; } private static void RegisterTriggerMethod(MethodInfo method, ref List> triggersList, TriggerAttribute attribute) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 2acbafd..50f379c 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -84,12 +84,29 @@ private void Initialize() { var initTimer = System.Diagnostics.Stopwatch.StartNew(); - DllManipulator.Options = Options; - DllManipulator.Initialize(Thread.CurrentThread.ManagedThreadId, Application.dataPath); + DllManipulator.Initialize(Options, Thread.CurrentThread.ManagedThreadId, Application.dataPath); initTimer.Stop(); InitializationTime = initTimer.Elapsed; } + + /// + /// Will reset the DllManipulator and Initialize it again. + /// Note: Unloads all Dlls, may be a dangerous operation if using preloaded + /// + public void Reinitialize() + { + if(_singletonInstance != this) + return; + + if (DllManipulator.Options != null) + DllManipulator.Reset(); + +#if UNITY_EDITOR + if(EditorApplication.isPlaying || Options.enableInEditMode) +#endif + Initialize(); + } /// /// Note: also called in edit mode if Options.enableInEditMode is set. @@ -112,7 +129,7 @@ public static void InvokeMainThreadQueue() #if UNITY_EDITOR private void OnDisable() { - if(!EditorApplication.isPlaying && Options.enableInEditMode) + if(_singletonInstance == this && !EditorApplication.isPlaying && Options.enableInEditMode) EditorApplication.update -= Update; } #endif diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 6fd2378..ef3a344 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -15,7 +15,7 @@ namespace UnityNativeTool.Internal public class DllManipulatorEditor : Editor { private static readonly string INFO_BOX_GUI_CONTENT = - "Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy."; + "Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy. Changes below are always applied at OnEnable."; private static readonly GUIContent TARGET_ALL_NATIVE_FUNCTIONS_GUI_CONTENT = new GUIContent("All native functions", "If true, all found native functions will be mocked.\n\n" + $"If false, you have to select them by using [{nameof(MockNativeDeclarationsAttribute)}] or [{nameof(MockNativeDeclarationAttribute)}]."); @@ -56,10 +56,23 @@ public class DllManipulatorEditor : Editor private static readonly GUIContent UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT = new GUIContent("Unload all DLLs [dangerous]", "Use only if you are sure no mocked native calls will be made while DLL is unloaded."); private static readonly GUIContent UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT = new GUIContent("Unload all DLLs [dangerous]", - "Use only if you are sure no other thread will be call mocked natives."); + "Use only if you are sure no other thread will call mocked natives."); private static readonly GUIContent UNLOAD_ALL_DLLS_AND_PAUSE_WITH_THREAD_SAFETY_GUI_CONTENT = new GUIContent("Unload all DLLs & Pause [dangerous]", - "Use only if you are sure no other thread will be call mocked natives."); + "Use only if you are sure no other thread will call mocked natives."); private static readonly TimeSpan ASSEMBLIES_REFRESH_INTERVAL = TimeSpan.FromSeconds(5); + + private static readonly GUIContent INITIALIZE_ENABLED_EDIT_MODE_GUI_CONTENT = new GUIContent( + "Apply Changes Now & Initialize", + "Start mocking native functions in edit mode immediately without waiting for OnEnable."); + private static readonly GUIContent REINITIALIZE_WITH_CHANGES_LAZY_GUI_CONTENT = new GUIContent( + "Unload, Apply Changes Now & Reinitialize", + "Changes made to the options above are only applied when play(/edit) mode is entered." + + " Use this to unload all Dlls and initialize with the new changes immediately."); + private static readonly GUIContent REINITIALIZE_WITH_CHANGES_PRELOADED_GUI_CONTENT = new GUIContent( + "Unload, Apply Changes Now & Reinitialize [Dangerous]", + "Changes made to the options above are only applied when play(/edit) mode is entered. " + + "Use this to unload all Dlls and initialize with the new changes immediately. " + + "Use only if you are sure no mocked native calls will be made while DLL is unloaded."); private bool _showLoadedLibraries = true; private bool _showTargetAssemblies = true; @@ -93,12 +106,67 @@ public override void OnInspectorGUI() EditorGUILayout.HelpBox(INFO_BOX_GUI_CONTENT, MessageType.Info); DrawOptions(t.Options); + + DetectOptionChanges(t); + EditorGUILayout.Space(); + DrawCurrentState(t); + } + + /// + /// Detects whether the have changed, both relative to the previous + /// options and the if we are currently initialized. + /// + /// The OnInspectorGUI target + private void DetectOptionChanges(DllManipulatorScript t) + { + // 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); + } + } + + // Allow Reinitializing DllManipulator if there are changes + if (DllManipulator.Options != null && !t.Options.Equals(DllManipulator.Options)) + { + if (DllManipulator.Options.loadingMode == DllLoadingMode.Preload) + { + if (GUILayout.Button(REINITIALIZE_WITH_CHANGES_PRELOADED_GUI_CONTENT)) + t.Reinitialize(); + } + else if(GUILayout.Button(REINITIALIZE_WITH_CHANGES_LAZY_GUI_CONTENT)) + t.Reinitialize(); + } + + // When enabling enableInEditMode for the first time, allow immediately initializing without waiting for OnEnable + if(DllManipulator.Options == null && t.Options.enableInEditMode && !EditorApplication.isPlaying && + GUILayout.Button(INITIALIZE_ENABLED_EDIT_MODE_GUI_CONTENT)) + { + t.Reinitialize(); + } + } + + /// + /// Draws GUI related to the current state of the DllManipulator. + /// Buttons to load/unload Dlls as well as details about which Dlls are loaded + /// + /// The OnInspectorGUI target + private void DrawCurrentState(DllManipulatorScript t) + { + if (DllManipulator.Options == null) // Exit if we have not initialized DllManipulator + return; + var usedDlls = DllManipulator.GetUsedDllsInfos(); if (usedDlls.Count != 0) { - if(t.Options.loadingMode == DllLoadingMode.Preload && usedDlls.Any(d => !d.isLoaded)) + if(DllManipulator.Options.loadingMode == DllLoadingMode.Preload && usedDlls.Any(d => !d.isLoaded)) { if (EditorApplication.isPaused) { @@ -118,7 +186,7 @@ public override void OnInspectorGUI() if (EditorApplication.isPlaying && !EditorApplication.isPaused) { bool pauseAndUnloadAll; - if(t.Options.threadSafe) + if(DllManipulator.Options.threadSafe) pauseAndUnloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_AND_PAUSE_WITH_THREAD_SAFETY_GUI_CONTENT); else pauseAndUnloadAll = GUILayout.Button("Unload all DLLs & Pause"); @@ -132,9 +200,9 @@ public override void OnInspectorGUI() bool unloadAll; - if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.Options.threadSafe) + if(DllManipulator.Options.threadSafe) unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT); - else if ((EditorApplication.isPlaying && !EditorApplication.isPaused || t.Options.enableInEditMode) && t.Options.loadingMode == DllLoadingMode.Preload) + else if (DllManipulator.Options.loadingMode == DllLoadingMode.Preload && (EditorApplication.isPlaying && !EditorApplication.isPaused || DllManipulator.Options.enableInEditMode)) unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT); else unloadAll = GUILayout.Button("Unload all DLLs"); @@ -145,7 +213,7 @@ public override void OnInspectorGUI() DrawUsedDlls(usedDlls); } - else if(EditorApplication.isPlaying || t.Options.enableInEditMode) + else { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); @@ -154,25 +222,13 @@ public override void OnInspectorGUI() GUILayout.EndHorizontal(); } - if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.InitializationTime != null) + if (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) @@ -210,8 +266,6 @@ private void DrawOptions(DllManipulatorOptions options) { var guiEnabledStack = new Stack(); guiEnabledStack.Push(GUI.enabled); - if (EditorApplication.isPlaying) - GUI.enabled = false; options.onlyInEditor = EditorGUILayout.Toggle(ONLY_IN_EDITOR, options.onlyInEditor); options.enableInEditMode = EditorGUILayout.Toggle(ENABLE_IN_EDIT_MODE, options.enableInEditMode); From 80ea4fee12f95f4b34e22383b06d4499b3a987cf Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Fri, 1 May 2020 18:01:00 +0200 Subject: [PATCH 2/6] Fix OnDestroy not being called when recompiling When recompiling only `OnDisable` is called, not `OnDestroy`. Now gets the callback for when the assembly is being reloaded, just when finished with compiling. This is called before `OnDisable`, so I just set a flag `_isRecompiling`. Previously the native function `UnityPluginUnload` was not being called causing various bugs. --- scripts/DllManipulatorScript.cs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 50f379c..5268fa9 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -58,8 +58,11 @@ private void OnEnable() DontDestroyOnLoad(gameObject); if(EditorApplication.isPlaying || Options.enableInEditMode) + { Initialize(); - + AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; + } + // 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; @@ -127,10 +130,31 @@ public static void InvokeMainThreadQueue() } #if UNITY_EDITOR + private bool _isRecompiling; + /// + /// Called when Assemblies are reloaded due to recompilation. + /// Called before OnDisable. + /// + private void OnBeforeAssemblyReload() + { + _isRecompiling = true; + } + private void OnDisable() { if(_singletonInstance == this && !EditorApplication.isPlaying && Options.enableInEditMode) + { EditorApplication.update -= Update; + AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; + + // When recompiling OnDestroy is not called by default (the object is not really destroyed) + // Manually trigger OnDestroy to clean up if we are disabled because of recompilation + if (_isRecompiling) + { + _isRecompiling = false; + OnDestroy(); + } + } } #endif From 247f2cda89e5263332907125219b8d8a17104821 Mon Sep 17 00:00:00 2001 From: Roger Barton <42413282+rogerbarton@users.noreply.github.com> Date: Wed, 6 May 2020 09:39:02 +0200 Subject: [PATCH 3/6] Update scripts/Editor/DllManipulatorEditor.cs Co-authored-by: mcpiroman <38111589+mcpiroman@users.noreply.github.com> --- scripts/Editor/DllManipulatorEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index ef3a344..6b77d9d 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -15,7 +15,7 @@ namespace UnityNativeTool.Internal public class DllManipulatorEditor : Editor { private static readonly string INFO_BOX_GUI_CONTENT = - "Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy. Changes below are always applied at OnEnable."; + "Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy. Configuration changes below are always applied at OnEnable."; private static readonly GUIContent TARGET_ALL_NATIVE_FUNCTIONS_GUI_CONTENT = new GUIContent("All native functions", "If true, all found native functions will be mocked.\n\n" + $"If false, you have to select them by using [{nameof(MockNativeDeclarationsAttribute)}] or [{nameof(MockNativeDeclarationAttribute)}]."); From 698117a6fe103027f741a5afa117dd636677764f Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Wed, 6 May 2020 10:24:04 +0200 Subject: [PATCH 4/6] Review changes Also removed check if singletonInstance in Reinitialize as this should never occur --- scripts/DllManipulatorScript.cs | 29 +++++++++++++------------- scripts/Editor/DllManipulatorEditor.cs | 13 +++++------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index 5268fa9..a17aca9 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -83,7 +83,7 @@ private void OnEnable() #endif } - private void Initialize() + public void Initialize() { var initTimer = System.Diagnostics.Stopwatch.StartNew(); @@ -99,11 +99,7 @@ private void Initialize() /// public void Reinitialize() { - if(_singletonInstance != this) - return; - - if (DllManipulator.Options != null) - DllManipulator.Reset(); + DllManipulator.Reset(); #if UNITY_EDITOR if(EditorApplication.isPlaying || Options.enableInEditMode) @@ -152,7 +148,7 @@ private void OnDisable() if (_isRecompiling) { _isRecompiling = false; - OnDestroy(); + Reset(); } } } @@ -161,15 +157,18 @@ private void OnDisable() private void OnDestroy() { if (_singletonInstance == this) - { - //Note on threading: Because we don't wait for other threads to finish, we might be stealing function delegates from under their nose if Unity doesn't happen to close them yet. - //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. + Reset(); + } - if (DllManipulator.Options != null) // Check that we have initialized - DllManipulator.Reset(); - _singletonInstance = null; - } + public void Reset() + { + //Note on threading: Because we don't wait for other threads to finish, we might be stealing function delegates from under their nose if Unity doesn't happen to close them yet. + //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. + + if (DllManipulator.Options != null) // Check that we have initialized + DllManipulator.Reset(); + _singletonInstance = null; } } } \ No newline at end of file diff --git a/scripts/Editor/DllManipulatorEditor.cs b/scripts/Editor/DllManipulatorEditor.cs index 6b77d9d..c8c3d2f 100644 --- a/scripts/Editor/DllManipulatorEditor.cs +++ b/scripts/Editor/DllManipulatorEditor.cs @@ -142,14 +142,16 @@ private void DetectOptionChanges(DllManipulatorScript t) t.Reinitialize(); } else if(GUILayout.Button(REINITIALIZE_WITH_CHANGES_LAZY_GUI_CONTENT)) + { t.Reinitialize(); + } } // When enabling enableInEditMode for the first time, allow immediately initializing without waiting for OnEnable if(DllManipulator.Options == null && t.Options.enableInEditMode && !EditorApplication.isPlaying && GUILayout.Button(INITIALIZE_ENABLED_EDIT_MODE_GUI_CONTENT)) { - t.Reinitialize(); + t.Initialize(); } } @@ -264,9 +266,6 @@ private void DrawUsedDlls(IList usedDlls) private void DrawOptions(DllManipulatorOptions options) { - var guiEnabledStack = new Stack(); - guiEnabledStack.Push(GUI.enabled); - options.onlyInEditor = EditorGUILayout.Toggle(ONLY_IN_EDITOR, options.onlyInEditor); options.enableInEditMode = EditorGUILayout.Toggle(ENABLE_IN_EDIT_MODE, options.enableInEditMode); @@ -331,14 +330,14 @@ private void DrawOptions(DllManipulatorOptions options) options.posixDlopenFlags = (PosixDlopenFlags)EditorGUILayout.EnumPopup(POSIX_DLOPEN_FLAGS_GUI_CONTENT, options.posixDlopenFlags); #endif - guiEnabledStack.Push(GUI.enabled); + var guiEnabled = GUI.enabled; if (options.loadingMode != DllLoadingMode.Preload) { options.threadSafe = false; GUI.enabled = false; } options.threadSafe = EditorGUILayout.Toggle(THREAD_SAFE_GUI_CONTENT, options.threadSafe); - GUI.enabled = guiEnabledStack.Pop(); + GUI.enabled = guiEnabled; options.enableCrashLogs = EditorGUILayout.Toggle(CRASH_LOGS_GUI_CONTENT, options.enableCrashLogs); @@ -353,8 +352,6 @@ private void DrawOptions(DllManipulatorOptions options) EditorGUI.indentLevel = prevIndent; } - - GUI.enabled = guiEnabledStack.Pop(); } /// From 5091e0ccc741ff0be0635431a6f9b006c2dc1714 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Wed, 6 May 2020 11:50:45 +0200 Subject: [PATCH 5/6] Small fix --- scripts/DllManipulatorScript.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/DllManipulatorScript.cs b/scripts/DllManipulatorScript.cs index a17aca9..8949088 100644 --- a/scripts/DllManipulatorScript.cs +++ b/scripts/DllManipulatorScript.cs @@ -160,7 +160,7 @@ private void OnDestroy() Reset(); } - public void Reset() + private void Reset() { //Note on threading: Because we don't wait for other threads to finish, we might be stealing function delegates from under their nose if Unity doesn't happen to close them yet. //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. From 50efd28c19fc98f73a77b09867919c03cf113c19 Mon Sep 17 00:00:00 2001 From: Roger Barton Date: Wed, 6 May 2020 11:58:08 +0200 Subject: [PATCH 6/6] DllManip.Options private set --- scripts/DllManipulator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index d25ae55..602940c 100644 --- a/scripts/DllManipulator.cs +++ b/scripts/DllManipulator.cs @@ -30,7 +30,7 @@ public partial class DllManipulator public static readonly string[] IGNORED_ASSEMBLY_PREFIXES = { "UnityEngine.", "UnityEditor.", "Unity.", "com.unity.", "Mono." , "nunit."}; - public static DllManipulatorOptions Options { get; set; } + public static DllManipulatorOptions Options { get; private set; } private static int _unityMainThreadId; private static string _assetsPath; private static readonly LinkedList _antiGcRefHolder = new LinkedList();