Skip to content

Commit e737ee3

Browse files
committed
Merge branch 'master' into upm-support
2 parents b5d22b2 + edc68d8 commit e737ee3

6 files changed

Lines changed: 196 additions & 41 deletions

File tree

README.md

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

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

scripts/Attributes.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,33 @@ public class DisableMockingAttribute : Attribute
3131
}
3232

3333
/// <summary>
34-
/// Methods with this attribute are called directly after a native DLL has been loaded.
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+
51+
/// <summary>
52+
/// Methods with this attribute are called directly after a native DLL has been loaded. Native functions can be used within such a method.
53+
/// This is called after <c>UnityPluginLoad</c>.<br/>
3554
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
36-
/// which indicates the state of the dll being loaded. Please treat this parameter as readonly.
55+
/// which indicates the state of the dll being loaded. Please treat this parameter as readonly.<br/>
56+
/// Preloaded: only called once all native methods have been loaded.
57+
/// <br/><inheritdoc cref="TriggerAttribute"/>
3758
/// </summary>
3859
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
39-
public class NativeDllLoadedTriggerAttribute : Attribute
60+
public class NativeDllLoadedTriggerAttribute : TriggerAttribute
4061
{
4162

4263
}
@@ -45,9 +66,10 @@ public class NativeDllLoadedTriggerAttribute : Attribute
4566
/// Methods with this attribute are called directly before a native DLL is going to be unloaded.
4667
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
4768
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
69+
/// <br/><inheritdoc cref="TriggerAttribute"/>
4870
/// </summary>
4971
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
50-
public class NativeDllBeforeUnloadTriggerAttribute : Attribute
72+
public class NativeDllBeforeUnloadTriggerAttribute : TriggerAttribute
5173
{
5274

5375
}
@@ -56,9 +78,10 @@ public class NativeDllBeforeUnloadTriggerAttribute : Attribute
5678
/// Methods with this attribute are called directly after a native DLL has been unloaded.
5779
/// Such method must be <see langword="static"/> and either have no parameters or one parameter of type <see cref="NativeDll"/>
5880
/// which indicates the state of the dll being unloaded. Please treat this parameter as readonly.
81+
/// <br/><inheritdoc cref="TriggerAttribute"/>
5982
/// </summary>
6083
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
61-
public class NativeDllAfterUnloadTriggerAttribute : Attribute
84+
public class NativeDllAfterUnloadTriggerAttribute : TriggerAttribute
6285
{
6386

6487
}

scripts/DllManipulator.cs

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ public partial class DllManipulator
4242
private static List<NativeFunction> _mockedNativeFunctions = new List<NativeFunction>();
4343
private static int _createdDelegateTypes = 0;
4444
private static int _lastNativeCallIndex = 0; //Use with synchronization
45-
private static List<MethodInfo> _customLoadedTriggers = null;
46-
private static List<MethodInfo> _customBeforeUnloadTriggers = null;
47-
private static List<MethodInfo> _customAfterUnloadTriggers = null;
48-
45+
46+
private static List<Tuple<MethodInfo, bool>> _customLoadedTriggers = null; //List of callbacks to run, whether to run them on the main thread.
47+
private static List<Tuple<MethodInfo, bool>> _customBeforeUnloadTriggers = null;
48+
private static List<Tuple<MethodInfo, bool>> _customAfterUnloadTriggers = null;
49+
4950
/// <summary>
5051
/// Initialization.
5152
/// Finds and mocks relevant native function declarations.
@@ -91,17 +92,16 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
9192
if (Options.mockAllNativeFunctions || method.IsDefined(typeof(MockNativeDeclarationAttribute)) || method.DeclaringType.IsDefined(typeof(MockNativeDeclarationsAttribute)))
9293
MockNativeFunction(method);
9394
}
94-
else if(method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
95-
{
96-
RegisterTriggerMethod(method, ref _customLoadedTriggers);
97-
}
98-
else if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
95+
else
9996
{
100-
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers);
101-
}
102-
else if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
103-
{
104-
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers);
97+
if (method.IsDefined(typeof(NativeDllLoadedTriggerAttribute)))
98+
RegisterTriggerMethod(method, ref _customLoadedTriggers, method.GetCustomAttribute<NativeDllLoadedTriggerAttribute>());
99+
100+
if (method.IsDefined(typeof(NativeDllBeforeUnloadTriggerAttribute)))
101+
RegisterTriggerMethod(method, ref _customBeforeUnloadTriggers, method.GetCustomAttribute<NativeDllBeforeUnloadTriggerAttribute>());
102+
103+
if (method.IsDefined(typeof(NativeDllAfterUnloadTriggerAttribute)))
104+
RegisterTriggerMethod(method, ref _customAfterUnloadTriggers, method.GetCustomAttribute<NativeDllAfterUnloadTriggerAttribute>());
105105
}
106106
}
107107
}
@@ -111,18 +111,34 @@ internal static void Initialize(int unityMainThreadId, string assetsPath)
111111
LoadAll();
112112
}
113113

114-
private static void RegisterTriggerMethod(MethodInfo method, ref List<MethodInfo> triggersList)
114+
/// <summary>
115+
/// Will unload/forget all dll's and reset the state
116+
/// </summary>
117+
public static void Reset()
118+
{
119+
UnloadAll();
120+
ForgetAllDlls();
121+
ClearCrashLogs();
122+
123+
_customLoadedTriggers?.Clear();
124+
_customAfterUnloadTriggers?.Clear();
125+
_customBeforeUnloadTriggers?.Clear();
126+
}
127+
128+
private static void RegisterTriggerMethod(MethodInfo method, ref List<Tuple<MethodInfo, bool>> triggersList, TriggerAttribute attribute)
115129
{
116130
var parameters = method.GetParameters();
117-
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll))
131+
if (parameters.Length == 0 || parameters.Length == 1 && parameters[0].ParameterType == typeof(NativeDll)
132+
|| parameters.Length == 2 && parameters[0].ParameterType == typeof(NativeDll) && parameters[1].ParameterType == typeof(int))
118133
{
119134
if (triggersList == null)
120-
triggersList = new List<MethodInfo>(2);
121-
triggersList.Add(method);
135+
triggersList = new List<Tuple<MethodInfo, bool>>();
136+
triggersList.Add(new Tuple<MethodInfo, bool>(method, attribute.UseMainThreadQueue));
122137
}
123138
else
124139
{
125-
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}");
140+
Debug.LogError($"Trigger method must either take no parameters, one parameter of type {nameof(NativeDll)} or one of type {nameof(NativeDll)} and one int. " +
141+
$"See the TriggerAttribute for more details. Violation on method {method.Name} in {method.DeclaringType.FullName}");
126142
}
127143
}
128144

@@ -142,6 +158,11 @@ public static void LoadAll()
142158
{
143159
LoadTargetFunction(nativeFunction, false);
144160
}
161+
162+
// Notify that the dll and its functions have been loaded in preload mode
163+
// This here allows use of native functions in the triggers
164+
if(Options.loadingMode == DllLoadingMode.Preload)
165+
InvokeCustomTriggers(_customLoadedTriggers, dll);
145166
}
146167
}
147168
}
@@ -491,8 +512,12 @@ internal static void LoadTargetFunction(NativeFunction nativeFunction, bool igno
491512
else
492513
{
493514
dll.loadingError = false;
494-
InvokeCustomTriggers(_customLoadedTriggers, dll);
495515
LowLevelPluginManager.OnDllLoaded(dll);
516+
517+
// Call the custom triggers once UnityPluginLoad has been called
518+
// For Lazy mode call the triggers immediately, preload waits until all functions are loaded (in LoadAll)
519+
if(Options.loadingMode == DllLoadingMode.Lazy)
520+
InvokeCustomTriggers(_customLoadedTriggers, dll);
496521
}
497522
}
498523

@@ -519,17 +544,28 @@ internal static void LoadTargetFunction(NativeFunction nativeFunction, bool igno
519544
}
520545
}
521546

522-
private static void InvokeCustomTriggers(List<MethodInfo> triggers, NativeDll dll)
547+
private static void InvokeCustomTriggers(List<Tuple<MethodInfo, bool>> triggers, NativeDll dll)
523548
{
524549
if (triggers == null)
525550
return;
526551

527-
foreach(var triggerMethod in triggers)
552+
foreach(var (methodInfo, useMainThreadQueue) in triggers)
528553
{
529-
if (triggerMethod.GetParameters().Length == 1)
530-
triggerMethod.Invoke(null, new object[] { dll });
554+
object[] args;
555+
556+
// Determine args for method
557+
if (methodInfo.GetParameters().Length == 2)
558+
args = new object[] { dll, _unityMainThreadId };
559+
else if (methodInfo.GetParameters().Length == 1)
560+
args = new object[] { dll };
561+
else
562+
args = Array.Empty<object>();
563+
564+
// Execute now or queue to the main thread
565+
if (useMainThreadQueue && Thread.CurrentThread.ManagedThreadId != _unityMainThreadId)
566+
DllManipulatorScript.MainThreadTriggerQueue.Enqueue(() => methodInfo.Invoke(null, args));
531567
else
532-
triggerMethod.Invoke(null, Array.Empty<object>());
568+
methodInfo.Invoke(null, args);
533569
}
534570
}
535571

@@ -664,6 +700,33 @@ public class DllManipulatorOptions
664700
public bool mockAllNativeFunctions;
665701
public bool onlyInEditor;
666702
public bool enableInEditMode;
703+
704+
public DllManipulatorOptions CloneTo(DllManipulatorOptions other)
705+
{
706+
other.dllPathPattern = dllPathPattern;
707+
other.assemblyPaths = (string[]) assemblyPaths.Clone();
708+
other.loadingMode = loadingMode;
709+
other.posixDlopenFlags = posixDlopenFlags;
710+
other.threadSafe = threadSafe;
711+
other.enableCrashLogs = enableCrashLogs;
712+
other.crashLogsDir = crashLogsDir;
713+
other.crashLogsStackTrace = crashLogsStackTrace;
714+
other.mockAllNativeFunctions = mockAllNativeFunctions;
715+
other.onlyInEditor = onlyInEditor;
716+
other.enableInEditMode = enableInEditMode;
717+
718+
return other;
719+
}
720+
721+
public bool Equals(DllManipulatorOptions other)
722+
{
723+
return other.dllPathPattern == dllPathPattern && other.assemblyPaths.SequenceEqual(assemblyPaths) &&
724+
other.loadingMode == loadingMode && other.posixDlopenFlags == posixDlopenFlags &&
725+
other.threadSafe == threadSafe && other.enableCrashLogs == enableCrashLogs &&
726+
other.crashLogsDir == crashLogsDir && other.crashLogsStackTrace == crashLogsStackTrace &&
727+
other.mockAllNativeFunctions == mockAllNativeFunctions && other.onlyInEditor == onlyInEditor &&
728+
other.enableInEditMode == enableInEditMode;
729+
}
667730
}
668731

669732
public enum DllLoadingMode

scripts/DllManipulatorScript.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
3-
using System.Reflection;
44
using System.Threading;
5-
using System.Linq;
65
using UnityEngine;
76
using UnityNativeTool.Internal;
87
#if UNITY_EDITOR
@@ -39,6 +38,8 @@ public class DllManipulatorScript : MonoBehaviour
3938
onlyInEditor = true,
4039
enableInEditMode = false
4140
};
41+
42+
public static ConcurrentQueue<Action> MainThreadTriggerQueue = new ConcurrentQueue<Action>();
4243

4344
private void OnEnable()
4445
{
@@ -47,7 +48,7 @@ private void OnEnable()
4748
{
4849
if (EditorApplication.isPlaying)
4950
Destroy(gameObject);
50-
else
51+
else if(_singletonInstance != this)
5152
enabled = false; //Don't destroy as the user may be editing a Prefab
5253
return;
5354
}
@@ -58,6 +59,11 @@ private void OnEnable()
5859

5960
if(EditorApplication.isPlaying || Options.enableInEditMode)
6061
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+
6167
#else
6268
if (Options.onlyInEditor)
6369
return;
@@ -84,6 +90,32 @@ private void Initialize()
8490
initTimer.Stop();
8591
InitializationTime = initTimer.Elapsed;
8692
}
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
87119

88120
private void OnDestroy()
89121
{
@@ -93,9 +125,8 @@ private void OnDestroy()
93125
//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.
94126
//Thankfully thread safety with Lazy mode is not implemented yet.
95127

96-
DllManipulator.UnloadAll();
97-
DllManipulator.ForgetAllDlls();
98-
DllManipulator.ClearCrashLogs();
128+
if (DllManipulator.Options != null) // Check that we have initialized
129+
DllManipulator.Reset();
99130
_singletonInstance = null;
100131
}
101132
}

0 commit comments

Comments
 (0)