Skip to content

Commit ec2d35b

Browse files
Edit options when initialized, Fix cleanup when recompiling (#20)
* 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. * 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. * Update scripts/Editor/DllManipulatorEditor.cs Co-authored-by: mcpiroman <38111589+mcpiroman@users.noreply.github.com> * Review changes Also removed check if singletonInstance in Reinitialize as this should never occur * Small fix * DllManip.Options private set Co-authored-by: mcpiroman <38111589+mcpiroman@users.noreply.github.com>
1 parent dd52d89 commit ec2d35b

3 files changed

Lines changed: 141 additions & 45 deletions

File tree

scripts/DllManipulator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public partial class DllManipulator
3030
public static readonly string[] IGNORED_ASSEMBLY_PREFIXES = { "UnityEngine.", "UnityEditor.", "Unity.", "com.unity.", "Mono." , "nunit."};
3131

3232

33-
public static DllManipulatorOptions Options { get; set; }
33+
public static DllManipulatorOptions Options { get; private set; }
3434
private static int _unityMainThreadId;
3535
private static string _assetsPath;
3636
private static readonly LinkedList<object> _antiGcRefHolder = new LinkedList<object>();
@@ -53,8 +53,11 @@ public partial class DllManipulator
5353
/// If <see cref="DllLoadingMode.Preload"/> option is specified, loads all DLLs specified by these functions.
5454
/// Options have to be configured before calling this method.
5555
/// </summary>
56-
internal static void Initialize(int unityMainThreadId, string assetsPath)
56+
internal static void Initialize(DllManipulatorOptions options, int unityMainThreadId, string assetsPath)
5757
{
58+
// Make a deep copy of the options so we can edit them in DllManipulatorScript independently
59+
Options = new DllManipulatorOptions();
60+
options.CloneTo(Options);
5861
_unityMainThreadId = unityMainThreadId;
5962
_assetsPath = assetsPath;
6063

@@ -123,6 +126,8 @@ public static void Reset()
123126
_customLoadedTriggers?.Clear();
124127
_customAfterUnloadTriggers?.Clear();
125128
_customBeforeUnloadTriggers?.Clear();
129+
130+
Options = null;
126131
}
127132

128133
private static void RegisterTriggerMethod(MethodInfo method, ref List<Tuple<MethodInfo, bool>> triggersList, TriggerAttribute attribute)

scripts/DllManipulatorScript.cs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ private void OnEnable()
5858
DontDestroyOnLoad(gameObject);
5959

6060
if(EditorApplication.isPlaying || Options.enableInEditMode)
61+
{
6162
Initialize();
62-
63+
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
64+
}
65+
6366
// Ensure update is called every frame in edit mode, ExecuteInEditMode only calls Update when the scene changes
6467
if(!EditorApplication.isPlaying && Options.enableInEditMode)
6568
EditorApplication.update += Update;
@@ -80,16 +83,29 @@ private void OnEnable()
8083
#endif
8184
}
8285

83-
private void Initialize()
86+
public void Initialize()
8487
{
8588
var initTimer = System.Diagnostics.Stopwatch.StartNew();
8689

87-
DllManipulator.Options = Options;
88-
DllManipulator.Initialize(Thread.CurrentThread.ManagedThreadId, Application.dataPath);
90+
DllManipulator.Initialize(Options, Thread.CurrentThread.ManagedThreadId, Application.dataPath);
8991

9092
initTimer.Stop();
9193
InitializationTime = initTimer.Elapsed;
9294
}
95+
96+
/// <summary>
97+
/// Will reset the DllManipulator and Initialize it again.
98+
/// Note: Unloads all Dlls, may be a dangerous operation if using preloaded
99+
/// </summary>
100+
public void Reinitialize()
101+
{
102+
DllManipulator.Reset();
103+
104+
#if UNITY_EDITOR
105+
if(EditorApplication.isPlaying || Options.enableInEditMode)
106+
#endif
107+
Initialize();
108+
}
93109

94110
/// <summary>
95111
/// Note: also called in edit mode if Options.enableInEditMode is set.
@@ -110,25 +126,49 @@ public static void InvokeMainThreadQueue()
110126
}
111127

112128
#if UNITY_EDITOR
129+
private bool _isRecompiling;
130+
/// <summary>
131+
/// Called when Assemblies are reloaded due to recompilation.
132+
/// Called before OnDisable.
133+
/// </summary>
134+
private void OnBeforeAssemblyReload()
135+
{
136+
_isRecompiling = true;
137+
}
138+
113139
private void OnDisable()
114140
{
115-
if(!EditorApplication.isPlaying && Options.enableInEditMode)
141+
if(_singletonInstance == this && !EditorApplication.isPlaying && Options.enableInEditMode)
142+
{
116143
EditorApplication.update -= Update;
144+
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
145+
146+
// When recompiling OnDestroy is not called by default (the object is not really destroyed)
147+
// Manually trigger OnDestroy to clean up if we are disabled because of recompilation
148+
if (_isRecompiling)
149+
{
150+
_isRecompiling = false;
151+
Reset();
152+
}
153+
}
117154
}
118155
#endif
119156

120157
private void OnDestroy()
121158
{
122159
if (_singletonInstance == this)
123-
{
124-
//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.
125-
//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.
126-
//Thankfully thread safety with Lazy mode is not implemented yet.
160+
Reset();
161+
}
127162

128-
if (DllManipulator.Options != null) // Check that we have initialized
129-
DllManipulator.Reset();
130-
_singletonInstance = null;
131-
}
163+
private void Reset()
164+
{
165+
//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.
166+
//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.
167+
//Thankfully thread safety with Lazy mode is not implemented yet.
168+
169+
if (DllManipulator.Options != null) // Check that we have initialized
170+
DllManipulator.Reset();
171+
_singletonInstance = null;
132172
}
133173
}
134174
}

scripts/Editor/DllManipulatorEditor.cs

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace UnityNativeTool.Internal
1515
public class DllManipulatorEditor : Editor
1616
{
1717
private static readonly string INFO_BOX_GUI_CONTENT =
18-
"Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy.";
18+
"Mocks native functions to allow manually un/loading native DLLs. DLLs are always unloaded at OnDestroy. Configuration changes below are always applied at OnEnable.";
1919
private static readonly GUIContent TARGET_ALL_NATIVE_FUNCTIONS_GUI_CONTENT = new GUIContent("All native functions",
2020
"If true, all found native functions will be mocked.\n\n" +
2121
$"If false, you have to select them by using [{nameof(MockNativeDeclarationsAttribute)}] or [{nameof(MockNativeDeclarationAttribute)}].");
@@ -56,10 +56,23 @@ public class DllManipulatorEditor : Editor
5656
private static readonly GUIContent UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT = new GUIContent("Unload all DLLs [dangerous]",
5757
"Use only if you are sure no mocked native calls will be made while DLL is unloaded.");
5858
private static readonly GUIContent UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT = new GUIContent("Unload all DLLs [dangerous]",
59-
"Use only if you are sure no other thread will be call mocked natives.");
59+
"Use only if you are sure no other thread will call mocked natives.");
6060
private static readonly GUIContent UNLOAD_ALL_DLLS_AND_PAUSE_WITH_THREAD_SAFETY_GUI_CONTENT = new GUIContent("Unload all DLLs & Pause [dangerous]",
61-
"Use only if you are sure no other thread will be call mocked natives.");
61+
"Use only if you are sure no other thread will call mocked natives.");
6262
private static readonly TimeSpan ASSEMBLIES_REFRESH_INTERVAL = TimeSpan.FromSeconds(5);
63+
64+
private static readonly GUIContent INITIALIZE_ENABLED_EDIT_MODE_GUI_CONTENT = new GUIContent(
65+
"Apply Changes Now & Initialize",
66+
"Start mocking native functions in edit mode immediately without waiting for OnEnable.");
67+
private static readonly GUIContent REINITIALIZE_WITH_CHANGES_LAZY_GUI_CONTENT = new GUIContent(
68+
"Unload, Apply Changes Now & Reinitialize",
69+
"Changes made to the options above are only applied when play(/edit) mode is entered." +
70+
" Use this to unload all Dlls and initialize with the new changes immediately.");
71+
private static readonly GUIContent REINITIALIZE_WITH_CHANGES_PRELOADED_GUI_CONTENT = new GUIContent(
72+
"Unload, Apply Changes Now & Reinitialize [Dangerous]",
73+
"Changes made to the options above are only applied when play(/edit) mode is entered. " +
74+
"Use this to unload all Dlls and initialize with the new changes immediately. " +
75+
"Use only if you are sure no mocked native calls will be made while DLL is unloaded.");
6376

6477
private bool _showLoadedLibraries = true;
6578
private bool _showTargetAssemblies = true;
@@ -93,12 +106,69 @@ public override void OnInspectorGUI()
93106
EditorGUILayout.HelpBox(INFO_BOX_GUI_CONTENT, MessageType.Info);
94107

95108
DrawOptions(t.Options);
109+
110+
DetectOptionChanges(t);
111+
96112
EditorGUILayout.Space();
97113

114+
DrawCurrentState(t);
115+
}
116+
117+
/// <summary>
118+
/// Detects whether the <see cref="DllManipulatorScript.Options"/> have changed, both relative to the previous
119+
/// options and the <see cref="DllManipulator.Options"/> if we are currently initialized.
120+
/// </summary>
121+
/// <param name="t">The OnInspectorGUI target</param>
122+
private void DetectOptionChanges(DllManipulatorScript t)
123+
{
124+
// Set the target as dirty so changes can be saved, if there are changes
125+
if (GUI.changed)
126+
{
127+
if (!t.Options.Equals(_prevOptions))
128+
{
129+
// If the options have changed then update the _prevOptions and notify there are changes to be saved
130+
// CloneTo is used to ensure a deep copy is made
131+
t.Options.CloneTo(_prevOptions);
132+
EditorUtility.SetDirty(target);
133+
}
134+
}
135+
136+
// Allow Reinitializing DllManipulator if there are changes
137+
if (DllManipulator.Options != null && !t.Options.Equals(DllManipulator.Options))
138+
{
139+
if (DllManipulator.Options.loadingMode == DllLoadingMode.Preload)
140+
{
141+
if (GUILayout.Button(REINITIALIZE_WITH_CHANGES_PRELOADED_GUI_CONTENT))
142+
t.Reinitialize();
143+
}
144+
else if(GUILayout.Button(REINITIALIZE_WITH_CHANGES_LAZY_GUI_CONTENT))
145+
{
146+
t.Reinitialize();
147+
}
148+
}
149+
150+
// When enabling enableInEditMode for the first time, allow immediately initializing without waiting for OnEnable
151+
if(DllManipulator.Options == null && t.Options.enableInEditMode && !EditorApplication.isPlaying &&
152+
GUILayout.Button(INITIALIZE_ENABLED_EDIT_MODE_GUI_CONTENT))
153+
{
154+
t.Initialize();
155+
}
156+
}
157+
158+
/// <summary>
159+
/// Draws GUI related to the current state of the DllManipulator.
160+
/// Buttons to load/unload Dlls as well as details about which Dlls are loaded
161+
/// </summary>
162+
/// <param name="t">The OnInspectorGUI target</param>
163+
private void DrawCurrentState(DllManipulatorScript t)
164+
{
165+
if (DllManipulator.Options == null) // Exit if we have not initialized DllManipulator
166+
return;
167+
98168
var usedDlls = DllManipulator.GetUsedDllsInfos();
99169
if (usedDlls.Count != 0)
100170
{
101-
if(t.Options.loadingMode == DllLoadingMode.Preload && usedDlls.Any(d => !d.isLoaded))
171+
if(DllManipulator.Options.loadingMode == DllLoadingMode.Preload && usedDlls.Any(d => !d.isLoaded))
102172
{
103173
if (EditorApplication.isPaused)
104174
{
@@ -118,7 +188,7 @@ public override void OnInspectorGUI()
118188
if (EditorApplication.isPlaying && !EditorApplication.isPaused)
119189
{
120190
bool pauseAndUnloadAll;
121-
if(t.Options.threadSafe)
191+
if(DllManipulator.Options.threadSafe)
122192
pauseAndUnloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_AND_PAUSE_WITH_THREAD_SAFETY_GUI_CONTENT);
123193
else
124194
pauseAndUnloadAll = GUILayout.Button("Unload all DLLs & Pause");
@@ -132,9 +202,9 @@ public override void OnInspectorGUI()
132202

133203

134204
bool unloadAll;
135-
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.Options.threadSafe)
205+
if(DllManipulator.Options.threadSafe)
136206
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT);
137-
else if ((EditorApplication.isPlaying && !EditorApplication.isPaused || t.Options.enableInEditMode) && t.Options.loadingMode == DllLoadingMode.Preload)
207+
else if (DllManipulator.Options.loadingMode == DllLoadingMode.Preload && (EditorApplication.isPlaying && !EditorApplication.isPaused || DllManipulator.Options.enableInEditMode))
138208
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT);
139209
else
140210
unloadAll = GUILayout.Button("Unload all DLLs");
@@ -145,7 +215,7 @@ public override void OnInspectorGUI()
145215

146216
DrawUsedDlls(usedDlls);
147217
}
148-
else if(EditorApplication.isPlaying || t.Options.enableInEditMode)
218+
else
149219
{
150220
GUILayout.BeginHorizontal();
151221
GUILayout.FlexibleSpace();
@@ -154,25 +224,13 @@ public override void OnInspectorGUI()
154224
GUILayout.EndHorizontal();
155225
}
156226

157-
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.InitializationTime != null)
227+
if (t.InitializationTime != null)
158228
{
159229
EditorGUILayout.Space();
160230
EditorGUILayout.Space();
161231
var time = t.InitializationTime.Value;
162232
EditorGUILayout.LabelField($"Initialized in: {(int)time.TotalSeconds}.{time.Milliseconds.ToString("D3")}s");
163233
}
164-
165-
// Set the target as dirty so changes can be saved, if there are changes
166-
if (GUI.changed)
167-
{
168-
if (!t.Options.Equals(_prevOptions))
169-
{
170-
// If the options have changed then update the _prevOptions and notify there are changes to be saved
171-
// CloneTo is used to ensure a deep copy is made
172-
t.Options.CloneTo(_prevOptions);
173-
EditorUtility.SetDirty(target);
174-
}
175-
}
176234
}
177235

178236
private void DrawUsedDlls(IList<NativeDllInfo> usedDlls)
@@ -208,11 +266,6 @@ private void DrawUsedDlls(IList<NativeDllInfo> usedDlls)
208266

209267
private void DrawOptions(DllManipulatorOptions options)
210268
{
211-
var guiEnabledStack = new Stack<bool>();
212-
guiEnabledStack.Push(GUI.enabled);
213-
if (EditorApplication.isPlaying)
214-
GUI.enabled = false;
215-
216269
options.onlyInEditor = EditorGUILayout.Toggle(ONLY_IN_EDITOR, options.onlyInEditor);
217270
options.enableInEditMode = EditorGUILayout.Toggle(ENABLE_IN_EDIT_MODE, options.enableInEditMode);
218271

@@ -277,14 +330,14 @@ private void DrawOptions(DllManipulatorOptions options)
277330
options.posixDlopenFlags = (PosixDlopenFlags)EditorGUILayout.EnumPopup(POSIX_DLOPEN_FLAGS_GUI_CONTENT, options.posixDlopenFlags);
278331
#endif
279332

280-
guiEnabledStack.Push(GUI.enabled);
333+
var guiEnabled = GUI.enabled;
281334
if (options.loadingMode != DllLoadingMode.Preload)
282335
{
283336
options.threadSafe = false;
284337
GUI.enabled = false;
285338
}
286339
options.threadSafe = EditorGUILayout.Toggle(THREAD_SAFE_GUI_CONTENT, options.threadSafe);
287-
GUI.enabled = guiEnabledStack.Pop();
340+
GUI.enabled = guiEnabled;
288341

289342
options.enableCrashLogs = EditorGUILayout.Toggle(CRASH_LOGS_GUI_CONTENT, options.enableCrashLogs);
290343

@@ -299,8 +352,6 @@ private void DrawOptions(DllManipulatorOptions options)
299352

300353
EditorGUI.indentLevel = prevIndent;
301354
}
302-
303-
GUI.enabled = guiEnabledStack.Pop();
304355
}
305356

306357
/// <summary>

0 commit comments

Comments
 (0)