Skip to content

Commit 1f0e9bc

Browse files
authored
Editor GUI Fixes (#15)
* Repaint editor GUIs on shortcut * Editor variable changes saved properly Before changing a variable would not be detected and so not saved/serialized in some cases. * 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. * Allow multiple un/loadTrigger attributes, use action to trigger repaints * Remove SceneManagement, it's not required The scene is automatically set as dirty when the gui target is * 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. * 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) * 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. * Update README.md * Mark scene dirty only if options changed * Bug fix, resetting when not initialized Initialize() is not always called so we should not always reset. * small fix * Review fixes Also RegisterTriggerMethod receives attribute instead of bool, Fix naming in DllManipulatorOptions * Add comments
1 parent 3efdf0d commit 1f0e9bc

6 files changed

Lines changed: 183 additions & 38 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Tool created mainly to solve old problem with reloading [native plugins](https:/
3030
- 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).
3131
- Options are accessible via `DllManipulatorScript` editor or window.
3232
- Although this tool presumably works in built game, it's intended to be used in editor.
33+
- Get callbacks in C# when the dll load state has changed with attributes like `[NativeDllLoadedTrigger]`, see `Attributes.cs` for more information
34+
- Unload and load all DLLs via shortcut `Alt+D` and `Alt+Shfit+D` respectively. Editable in the Shortcut Manager for 2019.1+
3335

3436
## Limitations
3537
- Marshaling parameter attributes other than `[MarshalAs]`, `[In]` and `[Out]` are not supported.

scripts/Attributes.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,32 @@ public class DisableMockingAttribute : Attribute
3030

3131
}
3232

33+
/// <summary>
34+
/// Such a method must be static and have one of the following signatures:
35+
/// <code>
36+
/// public static void Func()
37+
/// public static void Func(NativeDll dll)
38+
/// public static void Func(NativeDll dll, int mainThreadId)
39+
/// </code>
40+
/// </summary>
41+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
42+
public class TriggerAttribute : Attribute
43+
{
44+
/// <summary>
45+
/// Should the method always be executed on the main thread, to allow use of the Unity API.
46+
/// Note: this means the method is not immediately executed but put in a queue, if it is not triggered from the main thread.
47+
/// </summary>
48+
public bool UseMainThreadQueue = false;
49+
}
50+
3351
/// <summary>
3452
/// Methods with this attribute are called directly after a native DLL has been loaded.
3553
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
3654
/// which indicates the state of the dll being loaded. Please treat this parameter as readonly.
55+
/// <br/><inheritdoc cref="TriggerAttribute"/>
3756
/// </summary>
3857
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
39-
public class NativeDllLoadedTriggerAttribute : Attribute
58+
public class NativeDllLoadedTriggerAttribute : TriggerAttribute
4059
{
4160

4261
}
@@ -45,9 +64,10 @@ public class NativeDllLoadedTriggerAttribute : Attribute
4564
/// Methods with this attribute are called directly before a native DLL is going to be unloaded.
4665
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
4766
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
67+
/// <br/><inheritdoc cref="TriggerAttribute"/>
4868
/// </summary>
4969
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
50-
public class NativeDllBeforeUnloadTriggerAttribute : Attribute
70+
public class NativeDllBeforeUnloadTriggerAttribute : TriggerAttribute
5171
{
5272

5373
}
@@ -56,9 +76,10 @@ public class NativeDllBeforeUnloadTriggerAttribute : Attribute
5676
/// Methods with this attribute are called directly after a native DLL has been unloaded.
5777
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
5878
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
79+
/// <br/><inheritdoc cref="TriggerAttribute"/>
5980
/// </summary>
6081
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
61-
public class NativeDllAfterUnloadTriggerAttribute : Attribute
82+
public class NativeDllAfterUnloadTriggerAttribute : TriggerAttribute
6283
{
6384

6485
}

scripts/DllManipulator.cs

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ public partial class DllManipulator
3030
private static List<NativeFunction> _mockedNativeFunctions = new List<NativeFunction>();
3131
private static int _createdDelegateTypes = 0;
3232
private static int _lastNativeCallIndex = 0; //Use with synchronization
33-
private static List<MethodInfo> _customLoadedTriggers = null;
34-
private static List<MethodInfo> _customBeforeUnloadTriggers = null;
35-
private static List<MethodInfo> _customAfterUnloadTriggers = null;
36-
33+
34+
private static List<Tuple<MethodInfo, bool>> _customLoadedTriggers = null; //List of callbacks to run, whether to run them on the main thread.
35+
private static List<Tuple<MethodInfo, bool>> _customBeforeUnloadTriggers = null;
36+
private static List<Tuple<MethodInfo, bool>> _customAfterUnloadTriggers = null;
37+
3738
/// <summary>
3839
/// Initialization.
3940
/// Finds and mocks relevant native function declarations.
@@ -81,17 +82,16 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
8182
if (Options.mockAllNativeFunctions || method.IsDefined(typeof(MockNativeDeclarationAttribute)) || method.DeclaringType.IsDefined(typeof(MockNativeDeclarationsAttribute)))
8283
MockNativeFunction(method);
8384
}
84-
else if(method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
85-
{
86-
RegisterTriggerMethod(method, ref _customLoadedTriggers);
87-
}
88-
else if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
89-
{
90-
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers);
91-
}
92-
else if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
85+
else
9386
{
94-
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers);
87+
if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
88+
RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute<NativeDllLoadedTriggerAttribute>());
89+
90+
if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
91+
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute<NativeDllBeforeUnloadTriggerAttribute>());
92+
93+
if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
94+
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute<NativeDllAfterUnloadTriggerAttribute>());
9595
}
9696
}
9797
}
@@ -101,18 +101,34 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
101101
LoadAll();
102102
}
103103

104-
private static void RegisterTriggerMethod(MethodInfo method, ref List<MethodInfo> triggersList)
104+
/// <summary>
105+
/// Will unload/forget all dll's and reset the state
106+
/// </summary>
107+
public static void Reset()
108+
{
109+
UnloadAll();
110+
ForgetAllDlls();
111+
ClearCrashLogs();
112+
113+
_customLoadedTriggers?.Clear();
114+
_customAfterUnloadTriggers?.Clear();
115+
_customBeforeUnloadTriggers?.Clear();
116+
}
117+
118+
private static void RegisterTriggerMethod(MethodInfo method, ref List<Tuple<MethodInfo, bool>> triggersList, TriggerAttribute attribute)
105119
{
106120
var parameters = method.GetParameters();
107-
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll))
121+
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll)
122+
|| parameters.Length == 2 && parameters[0].ParameterType == typeof(NativeDll) && parameters[1].ParameterType == typeof(int))
108123
{
109124
if (triggersList == null)
110-
triggersList = new List<MethodInfo>(2);
111-
triggersList.Add(method);
125+
triggersList = new List<Tuple<MethodInfo, bool>>();
126+
triggersList.Add(new Tuple<MethodInfo, bool>(method, attribute.UseMainThreadQueue));
112127
}
113128
else
114129
{
115-
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}");
130+
Debug.LogError($"Trigger method must either take no parameters, one parameter of type {nameof(NativeDll)} or one of type {nameof(NativeDll)} and one int. " +
131+
$"See the TriggerAttribute for more details. Violation on method {method.Name} in {method.DeclaringType.FullName}");
116132
}
117133
}
118134

@@ -509,17 +525,28 @@ internal static void LoadTargetFunction(NativeFunction nativeFunction, bool igno
509525
}
510526
}
511527

512-
private static void InvokeCustomTriggers(List<MethodInfo> triggers, NativeDll dll)
528+
private static void InvokeCustomTriggers(List<Tuple<MethodInfo, bool>> triggers, NativeDll dll)
513529
{
514530
if (triggers == null)
515531
return;
516532

517-
foreach(var triggerMethod in triggers)
533+
foreach(var (methodInfo, useMainThreadQueue) in triggers)
518534
{
519-
if (triggerMethod.GetParameters().Length == 1)
520-
triggerMethod.Invoke(null, new object[] { dll });
535+
object[] args;
536+
537+
// Determine args for method
538+
if (methodInfo.GetParameters().Length == 2)
539+
args = new object[] { dll, _unityMainThreadId };
540+
else if (methodInfo.GetParameters().Length == 1)
541+
args = new object[] { dll };
542+
else
543+
args = Array.Empty<object>();
544+
545+
// Execute now or queue to the main thread
546+
if (useMainThreadQueue && Thread.CurrentThread.ManagedThreadId != _unityMainThreadId)
547+
DllManipulatorScript.MainThreadTriggerQueue.Enqueue(() => methodInfo.Invoke(null, args));
521548
else
522-
triggerMethod.Invoke(null, Array.Empty<object>());
549+
methodInfo.Invoke(null, args);
523550
}
524551
}
525552

@@ -654,6 +681,33 @@ public class DllManipulatorOptions
654681
public bool mockAllNativeFunctions;
655682
public bool onlyInEditor;
656683
public bool enableInEditMode;
684+
685+
public DllManipulatorOptions CloneTo(DllManipulatorOptions other)
686+
{
687+
other.dllPathPattern = dllPathPattern;
688+
other.assemblyPaths = (string[]) assemblyPaths.Clone();
689+
other.loadingMode = loadingMode;
690+
other.posixDlopenFlags = posixDlopenFlags;
691+
other.threadSafe = threadSafe;
692+
other.enableCrashLogs = enableCrashLogs;
693+
other.crashLogsDir = crashLogsDir;
694+
other.crashLogsStackTrace = crashLogsStackTrace;
695+
other.mockAllNativeFunctions = mockAllNativeFunctions;
696+
other.onlyInEditor = onlyInEditor;
697+
other.enableInEditMode = enableInEditMode;
698+
699+
return other;
700+
}
701+
702+
public bool Equals(DllManipulatorOptions other)
703+
{
704+
return other.dllPathPattern == dllPathPattern && other.assemblyPaths.SequenceEqual(assemblyPaths) &&
705+
other.loadingMode == loadingMode && other.posixDlopenFlags == posixDlopenFlags &&
706+
other.threadSafe == threadSafe && other.enableCrashLogs == enableCrashLogs &&
707+
other.crashLogsDir == crashLogsDir && other.crashLogsStackTrace == crashLogsStackTrace &&
708+
other.mockAllNativeFunctions == mockAllNativeFunctions && other.onlyInEditor == onlyInEditor &&
709+
other.enableInEditMode == enableInEditMode;
710+
}
657711
}
658712

659713
public enum DllLoadingMode

scripts/DllManipulatorScript.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
2-
using System.Reflection;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
34
using System.Threading;
4-
using System.Linq;
55
using UnityEngine;
66
using UnityNativeTool.Internal;
77
#if UNITY_EDITOR
@@ -38,6 +38,8 @@ public class DllManipulatorScript : MonoBehaviour
3838
onlyInEditor = true,
3939
enableInEditMode = false
4040
};
41+
42+
public static ConcurrentQueue<Action> MainThreadTriggerQueue = new ConcurrentQueue<Action>();
4143

4244
private void OnEnable()
4345
{
@@ -46,7 +48,7 @@ private void OnEnable()
4648
{
4749
if (EditorApplication.isPlaying)
4850
Destroy(gameObject);
49-
else
51+
else if(_singletonInstance != this)
5052
enabled = false; //Don't destroy as the user may be editing a Prefab
5153
return;
5254
}
@@ -57,6 +59,11 @@ private void OnEnable()
5759

5860
if(EditorApplication.isPlaying || Options.enableInEditMode)
5961
Initialize();
62+
63+
// Ensure update is called every frame in edit mode, ExecuteInEditMode only calls Update when the scene changes
64+
if(!EditorApplication.isPlaying && Options.enableInEditMode)
65+
EditorApplication.update += Update;
66+
6067
#else
6168
if (Options.onlyInEditor)
6269
return;
@@ -83,6 +90,32 @@ private void Initialize()
8390
initTimer.Stop();
8491
InitializationTime = initTimer.Elapsed;
8592
}
93+
94+
/// <summary>
95+
/// Note: also called in edit mode if Options.enableInEditMode is set.
96+
/// </summary>
97+
private void Update()
98+
{
99+
InvokeMainThreadQueue();
100+
}
101+
102+
/// <summary>
103+
/// Executes queued methods.
104+
/// Should be called from the main thread in Update.
105+
/// </summary>
106+
public static void InvokeMainThreadQueue()
107+
{
108+
while (MainThreadTriggerQueue.TryDequeue(out var action))
109+
action();
110+
}
111+
112+
#if UNITY_EDITOR
113+
private void OnDisable()
114+
{
115+
if(!EditorApplication.isPlaying && Options.enableInEditMode)
116+
EditorApplication.update -= Update;
117+
}
118+
#endif
86119

87120
private void OnDestroy()
88121
{
@@ -92,9 +125,8 @@ private void OnDestroy()
92125
//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.
93126
//Thankfully thread safety with Lazy mode is not implemented yet.
94127

95-
DllManipulator.UnloadAll();
96-
DllManipulator.ForgetAllDlls();
97-
DllManipulator.ClearCrashLogs();
128+
if (DllManipulator.Options != null) // Check that we have initialized
129+
DllManipulator.Reset();
98130
_singletonInstance = null;
99131
}
100132
}

scripts/Editor/DllManipulatorEditor.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,24 @@ public class DllManipulatorEditor : Editor
6464
private string[] _allKnownAssemblies = null;
6565
private DateTime _lastKnownAssembliesRefreshTime;
6666

67+
/// <summary>
68+
/// Used to check if the options have changed, in order to set the object as dirty so changes are saved
69+
/// </summary>
70+
private DllManipulatorOptions _prevOptions = new DllManipulatorOptions();
71+
72+
public static event Action RepaintAllEditors = delegate {};
73+
6774
public DllManipulatorEditor()
6875
{
6976
EditorApplication.pauseStateChanged += _ => Repaint();
7077
EditorApplication.playModeStateChanged += _ => Repaint();
78+
RepaintAllEditors += Repaint;
79+
}
80+
81+
private void Awake()
82+
{
83+
// Immediately copy the Options to the previous so we don't need to check for null later
84+
((DllManipulatorScript)target).Options.CloneTo(_prevOptions);
7185
}
7286

7387
public override void OnInspectorGUI()
@@ -114,9 +128,9 @@ public override void OnInspectorGUI()
114128

115129

116130
bool unloadAll;
117-
if(EditorApplication.isPlaying && t.Options.threadSafe)
131+
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.Options.threadSafe)
118132
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_WITH_THREAD_SAFETY_GUI_CONTENT);
119-
else if (EditorApplication.isPlaying && !EditorApplication.isPaused && t.Options.loadingMode == DllLoadingMode.Preload)
133+
else if ((EditorApplication.isPlaying && !EditorApplication.isPaused || t.Options.enableInEditMode) && t.Options.loadingMode == DllLoadingMode.Preload)
120134
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT);
121135
else
122136
unloadAll = GUILayout.Button("Unload all DLLs");
@@ -127,7 +141,7 @@ public override void OnInspectorGUI()
127141

128142
DrawUsedDlls(usedDlls);
129143
}
130-
else if(EditorApplication.isPlaying)
144+
else if(EditorApplication.isPlaying || t.Options.enableInEditMode)
131145
{
132146
GUILayout.BeginHorizontal();
133147
GUILayout.FlexibleSpace();
@@ -136,13 +150,25 @@ public override void OnInspectorGUI()
136150
GUILayout.EndHorizontal();
137151
}
138152

139-
if(EditorApplication.isPlaying && t.InitializationTime != null)
153+
if((EditorApplication.isPlaying || t.Options.enableInEditMode) && t.InitializationTime != null)
140154
{
141155
EditorGUILayout.Space();
142156
EditorGUILayout.Space();
143157
var time = t.InitializationTime.Value;
144158
EditorGUILayout.LabelField($"Initialized in: {(int)time.TotalSeconds}.{time.Milliseconds.ToString("D3")}s");
145159
}
160+
161+
// Set the target as dirty so changes can be saved, if there are changes
162+
if (GUI.changed)
163+
{
164+
if (!t.Options.Equals(_prevOptions))
165+
{
166+
// If the options have changed then update the _prevOptions and notify there are changes to be saved
167+
// CloneTo is used to ensure a deep copy is made
168+
t.Options.CloneTo(_prevOptions);
169+
EditorUtility.SetDirty(target);
170+
}
171+
}
146172
}
147173

148174
private void DrawUsedDlls(IList<NativeDllInfo> usedDlls)
@@ -339,5 +365,12 @@ public static void UnloadAll()
339365
{
340366
DllManipulator.UnloadAll();
341367
}
368+
369+
[NativeDllLoadedTrigger(UseMainThreadQueue = true)]
370+
[NativeDllAfterUnloadTrigger(UseMainThreadQueue = true)]
371+
public static void RepaintAll()
372+
{
373+
RepaintAllEditors?.Invoke();
374+
}
342375
}
343376
}

0 commit comments

Comments
 (0)