forked from mcpiroman/UnityNativeTool
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DllManipulatorEditor.cs
361 lines (310 loc) · 17.5 KB
/
DllManipulatorEditor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Compilation;
#if UNITY_2019_1_OR_NEWER
using UnityEditor.ShortcutManagement;
#endif
using System.IO;
using System;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace UnityNativeTool.Internal
{
[CustomEditor(typeof(DllManipulatorScript))]
public class DllManipulatorEditor : Editor
{
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)}].");
private static readonly GUIContent TARGET_ONLY_EXECUTING_ASSEMBLY_GUI_CONTENT = new GUIContent("Only executing assembly",
"If true, native functions will be mocked only in assembly that contains DllManipulator (usually Assembly-CSharp)");
private static readonly GUIContent ONLY_IN_EDITOR = new GUIContent("Only in editor",
"Whether to run only inside editor (which is recommended).");
private static readonly GUIContent ENABLE_IN_EDIT_MODE = new GUIContent("Enable in Edit Mode",
"Should the DLLs also be mocked in edit mode. " +
"Turning this off when not needed improves performance when entering edit mode. " +
"Changed are currently only visible on the next time edit mode is entered.");
private static readonly GUIContent TARGET_ASSEMBLIES_GUI_CONTENT = new GUIContent("Target assemblies",
"Choose from which assemblies to mock native functions");
private static readonly GUIContent DLL_PATH_PATTERN_GUI_CONTENT = new GUIContent("DLL path pattern",
"Available macros:\n\n" +
$"{DllManipulator.DLL_PATH_PATTERN_DLL_NAME_MACRO} - name of DLL as specified in [DllImport] attribute.\n\n" +
$"{DllManipulator.DLL_PATH_PATTERN_ASSETS_MACRO} - assets folder of current project.\n\n" +
$"{DllManipulator.DLL_PATH_PATTERN_PROJECT_MACRO} - project folder i.e. one above Assets.");
private static readonly GUIContent DLL_LOADING_MODE_GUI_CONTENT = new GUIContent("DLL loading mode",
"Specifies how DLLs and functions will be loaded.\n\n" +
"Lazy - All DLLs and functions are loaded each time they are called, if not loaded yet. This allows them to be easily unloaded and loaded within game execution.\n\n" +
"Preloaded - Slight performance benefit over Lazy mode. All declared DLLs and functions are loaded at startup (OnEnable()) and not reloaded later. Mid-execution it's not safe to unload them unless game is paused.");
private static readonly GUIContent POSIX_DLOPEN_FLAGS_GUI_CONTENT = new GUIContent("dlopen flags",
"Flags used in dlopen() P/Invoke on Linux and OSX systems. Has minor meaning unless library is large.");
private static readonly GUIContent THREAD_SAFE_GUI_CONTENT = new GUIContent("Thread safe",
"Ensures synchronization required for native calls from any other than Unity main thread. Overhead might be few times higher, with uncontended locks.\n\n" +
"Only in Preloaded mode.");
private static readonly GUIContent CRASH_LOGS_GUI_CONTENT = new GUIContent("Crash logs",
"Logs each native call to file. In case of crash or hang caused by native function, you can than see what function was that, along with arguments and, optionally, stack trace.\n\n" +
"In multi-threaded scenario there will be one file for each thread and you'll have to guess the right one (call index will be a hint).\n\n" +
"Note that existence of log files doesn't mean the crash was caused by any tracked native function.\n\n" +
"Overhead is HIGH (on poor PC there might be just few native calls per update to disturb 60 fps.)");
private static readonly GUIContent CRASH_LOGS_DIR_GUI_CONTENT = new GUIContent("Logs directory",
"Path to directory in which crash logs will be stored. You can use macros as in DLL path. Note that this file(s) will usually exist during majority of game execution.");
private static readonly GUIContent CRASH_LOGS_STACK_TRACE_GUI_CONTENT = new GUIContent("Stack trace",
"Whether to include stack trace in crash log.\n\n" +
"Overhead is about 4 times higher.");
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.");
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.");
private static readonly TimeSpan ASSEMBLIES_REFRESH_INTERVAL = TimeSpan.FromSeconds(1);
private bool _showLoadedLibraries = true;
private bool _showTargetAssemblies = true;
private string[] _allKnownAssemblies = null;
private DateTime _lastKnownAssembliesRefreshTime;
public DllManipulatorEditor()
{
EditorApplication.pauseStateChanged += _ => Repaint();
EditorApplication.playModeStateChanged += _ => Repaint();
}
public override void OnInspectorGUI()
{
var t = (DllManipulatorScript)this.target;
DrawOptions(t.Options);
EditorGUILayout.Space();
var usedDlls = DllManipulator.GetUsedDllsInfos();
if (usedDlls.Count != 0)
{
if(t.Options.loadingMode == DllLoadingMode.Preload && usedDlls.Any(d => !d.isLoaded))
{
if (EditorApplication.isPaused)
{
if (GUILayout.Button("Load all DLLs & Unpause"))
{
DllManipulator.LoadAll();
EditorApplication.isPaused = false;
}
}
if (GUILayout.Button("Load all DLLs"))
DllManipulator.LoadAll();
}
if (usedDlls.Any(d => d.isLoaded))
{
if (EditorApplication.isPlaying && !EditorApplication.isPaused)
{
bool pauseAndUnloadAll;
if(t.Options.threadSafe)
pauseAndUnloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_AND_PAUSE_WITH_THREAD_SAFETY_GUI_CONTENT);
else
pauseAndUnloadAll = GUILayout.Button("Unload all DLLs & Pause");
if(pauseAndUnloadAll)
{
EditorApplication.isPaused = true;
DllManipulator.UnloadAll();
}
}
bool unloadAll;
if(EditorApplication.isPlaying && 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)
unloadAll = GUILayout.Button(UNLOAD_ALL_DLLS_IN_PLAY_PRELOADED_GUI_CONTENT);
else
unloadAll = GUILayout.Button("Unload all DLLs");
if(unloadAll)
DllManipulator.UnloadAll();
}
DrawUsedDlls(usedDlls);
}
else if(EditorApplication.isPlaying)
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField("No DLLs to mock");
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
if(EditorApplication.isPlaying && t.InitializationTime != null)
{
EditorGUILayout.Space();
EditorGUILayout.Space();
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<NativeDllInfo> usedDlls)
{
_showLoadedLibraries = EditorGUILayout.Foldout(_showLoadedLibraries, "Mocked DLLs");
if (_showLoadedLibraries)
{
var prevIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel += 1;
bool isFirstDll = true;
foreach (var dll in usedDlls)
{
if (!isFirstDll)
EditorGUILayout.Space();
var stateAttributes = new List<string>
{
dll.isLoaded ? "LOADED" : "NOT LOADED"
};
if (dll.loadingError)
stateAttributes.Add("LOAD ERROR");
if (dll.symbolError)
stateAttributes.Add("SYMBOL ERRROR");
var state = string.Join(" | ", stateAttributes);
EditorGUILayout.LabelField($"[{state}] {dll.name}");
EditorGUILayout.LabelField(dll.path);
isFirstDll = false;
}
EditorGUI.indentLevel = prevIndent;
}
}
private void DrawOptions(DllManipulatorOptions options)
{
var guiEnabledStack = new Stack<bool>();
guiEnabledStack.Push(GUI.enabled);
if (EditorApplication.isPlaying)
GUI.enabled = false;
options.mockAllNativeFunctions = EditorGUILayout.Toggle(TARGET_ALL_NATIVE_FUNCTIONS_GUI_CONTENT, options.mockAllNativeFunctions);
if (EditorGUILayout.Toggle(TARGET_ONLY_EXECUTING_ASSEMBLY_GUI_CONTENT, options.assemblyPaths.Length == 0))
{
options.assemblyPaths = new string[0];
}
else
{
var prevIndent1 = EditorGUI.indentLevel;
EditorGUI.indentLevel++;
if (_allKnownAssemblies == null || _lastKnownAssembliesRefreshTime + ASSEMBLIES_REFRESH_INTERVAL < DateTime.Now)
{
var playerCompiledAssemblies = CompilationPipeline.GetAssemblies(AssembliesType.Player)
.Select(a => PathUtils.NormallizeUnityAssemblyPath(a.outputPath));
var editorCompiledAssemblies = CompilationPipeline.GetAssemblies(AssembliesType.Editor)
.Select(a => PathUtils.NormallizeUnityAssemblyPath(a.outputPath));
var assemblyAssets = Resources.FindObjectsOfTypeAll<PluginImporter>()
.Where(p => !p.isNativePlugin)
.Select(p => PathUtils.NormallizeUnityAssemblyPath(p.assetPath));
string[] defaultAssemblyPrefixes = { "UnityEngine.", "UnityEditor.", "Unity.", "com.unity.", "Mono." , "nunit."};
_allKnownAssemblies = playerCompiledAssemblies
.Concat(assemblyAssets)
.Concat(editorCompiledAssemblies)
.OrderBy(path => Array.FindIndex(defaultAssemblyPrefixes, p => path.Substring(path.LastIndexOf('/') + 1).StartsWith(p)))
.ToArray();
_lastKnownAssembliesRefreshTime = DateTime.Now;
}
if (options.assemblyPaths.Length == 0)
{
var first = GetFirstAssemblyToList(_allKnownAssemblies);
if(first != null)
options.assemblyPaths = new[] { first };
}
_showTargetAssemblies = EditorGUILayout.Foldout(_showTargetAssemblies, TARGET_ASSEMBLIES_GUI_CONTENT);
if (_showTargetAssemblies)
{
var prevIndent2 = EditorGUI.indentLevel;
EditorGUI.indentLevel++;
var selectedAssemblies = options.assemblyPaths.Where(p => _allKnownAssemblies.Any(a => PathUtils.DllPathsEqual(a, p))).ToList();
var notSelectedAssemblies = _allKnownAssemblies.Except(selectedAssemblies).ToArray();
DrawList(selectedAssemblies, i =>
{
var values = new[] { selectedAssemblies[i] }
.Concat(notSelectedAssemblies)
.Select(a => a.Substring(a.LastIndexOf('/') + 1))
.ToArray();
var selectedIndex = EditorGUILayout.Popup(0, values);
return selectedIndex == 0 ? selectedAssemblies[i] : notSelectedAssemblies[selectedIndex - 1];
}, notSelectedAssemblies.Length > 0, () => notSelectedAssemblies[0]);
options.assemblyPaths = selectedAssemblies.ToArray();
EditorGUI.indentLevel = prevIndent2;
}
EditorGUI.indentLevel = prevIndent1;
}
options.onlyInEditor = EditorGUILayout.Toggle(ONLY_IN_EDITOR, options.onlyInEditor);
options.enableInEditMode = EditorGUILayout.Toggle(ENABLE_IN_EDIT_MODE, options.enableInEditMode);
options.dllPathPattern = EditorGUILayout.TextField(DLL_PATH_PATTERN_GUI_CONTENT, options.dllPathPattern);
options.loadingMode = (DllLoadingMode)EditorGUILayout.EnumPopup(DLL_LOADING_MODE_GUI_CONTENT, options.loadingMode);
#if UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX
options.posixDlopenFlags = (PosixDlopenFlags)EditorGUILayout.EnumPopup(POSIX_DLOPEN_FLAGS_GUI_CONTENT, options.posixDlopenFlags);
#endif
guiEnabledStack.Push(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();
options.enableCrashLogs = EditorGUILayout.Toggle(CRASH_LOGS_GUI_CONTENT, options.enableCrashLogs);
if (options.enableCrashLogs)
{
var prevIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel += 1;
options.crashLogsDir = EditorGUILayout.TextField(CRASH_LOGS_DIR_GUI_CONTENT, options.crashLogsDir);
options.crashLogsStackTrace = EditorGUILayout.Toggle(CRASH_LOGS_STACK_TRACE_GUI_CONTENT, options.crashLogsStackTrace);
EditorGUI.indentLevel = prevIndent;
}
GUI.enabled = guiEnabledStack.Pop();
}
private void DrawList<T>(IList<T> elements, Func<int, T> drawElement, bool canAddNewElement, Func<T> getNewElement)
{
int indexToRemove = -1;
for (int i = 0; i < elements.Count; i++)
{
EditorGUILayout.BeginHorizontal();
elements[i] = drawElement(i);
if (GUILayout.Button("X", GUILayout.Width(20)))
{
indexToRemove = i;
}
EditorGUILayout.EndHorizontal();
}
if (indexToRemove != -1)
elements.RemoveAt(indexToRemove);
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
var prevGuiEnabled = GUI.enabled;
GUI.enabled = prevGuiEnabled && canAddNewElement;
if (GUILayout.Button("Add", GUILayout.Width(40)))
elements.Add(getNewElement());
GUI.enabled = prevGuiEnabled;
GUILayout.EndHorizontal();
}
static string GetFirstAssemblyToList(string[] allAssemblies)
{
return allAssemblies.FirstOrDefault(a => PathUtils.DllPathsEqual(a, typeof(DllManipulator).Assembly.Location))
?? allAssemblies.FirstOrDefault();
}
#if UNITY_2019_1_OR_NEWER
[Shortcut("Tools/Load All Dlls", KeyCode.D, ShortcutModifiers.Alt | ShortcutModifiers.Shift)]
#else
[MenuItem("Tools/Load All Dlls #&d")]
#endif
public static void LoadAllShortcut()
{
DllManipulator.LoadAll();
}
#if UNITY_2019_1_OR_NEWER
[Shortcut("Tools/Unload All Dlls", KeyCode.D, ShortcutModifiers.Alt)]
#else
[MenuItem("Tools/Unload All Dlls &d")]
#endif
public static void UnloadAll()
{
DllManipulator.UnloadAll();
}
[NativeDllLoadedTrigger]
[NativeDllAfterUnloadTrigger]
public static void RepaintAll()
{
var editors = Resources.FindObjectsOfTypeAll<DllManipulatorEditor>();
if(editors == null) return;
foreach (var editor in editors)
editor.Repaint();
}
}
}