diff --git a/scripts/DllManipulator.cs b/scripts/DllManipulator.cs index e61840d..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(); @@ -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..8949088 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; @@ -80,16 +83,29 @@ private void OnEnable() #endif } - private void Initialize() + public 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() + { + DllManipulator.Reset(); + +#if UNITY_EDITOR + if(EditorApplication.isPlaying || Options.enableInEditMode) +#endif + Initialize(); + } /// /// Note: also called in edit mode if Options.enableInEditMode is set. @@ -110,25 +126,49 @@ 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(!EditorApplication.isPlaying && Options.enableInEditMode) + 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; + Reset(); + } + } } #endif 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; - } + 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. + //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 6fd2378..c8c3d2f 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. 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)}]."); @@ -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,69 @@ 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.Initialize(); + } + } + + /// + /// 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 +188,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 +202,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 +215,7 @@ public override void OnInspectorGUI() DrawUsedDlls(usedDlls); } - else if(EditorApplication.isPlaying || t.Options.enableInEditMode) + else { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); @@ -154,25 +224,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) @@ -208,11 +266,6 @@ private void DrawUsedDlls(IList usedDlls) 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); @@ -277,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); @@ -299,8 +352,6 @@ private void DrawOptions(DllManipulatorOptions options) EditorGUI.indentLevel = prevIndent; } - - GUI.enabled = guiEnabledStack.Pop(); } ///