Skip to content

Commit 6b277ed

Browse files
committed
New patching method, bugs and fixes
UnityHotSwap no longer uses entire assembly instrumentation to be able to patch methods. Now, we are forcing methods to be jit-compiled and replace machine code in memory with jumps and returns to get new code running. Wowie! Cleanup and remove a lot of code related to instrumentation and old patching. This is now more stable then ever! As stable as Unity allows it to be, before it tries to reload assemblies on its own and fails miserabely. Fix some bugs related to IL recompiling. Add support for "constrainted." opcode. Fix generic method calls but explicitely disallow generic method emission (DynamicMethod limitation).
1 parent 1bd8151 commit 6b277ed

17 files changed

Lines changed: 683 additions & 223 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ ipch/
3030
[Dd]ebug*/
3131
[Rr]elease*/
3232
Ankh.NoLoad
33-
packages/
33+
packages/
34+
.vs/

README.md

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,82 @@
1-
# Unity hot swap
1+
# Unity Hot Patch
22

33
Swap code in Unity in play mode, without assembly reloading.
44

5-
* [See shorter example on gfycat (10 sec).](http://gfycat.com/HandmadeFastAnole)
6-
* [See longer example on imgrush (40 sec).](https://imgrush.com/TnDsx0wnsqWd/direct)
5+
* [See a video example (45 sec, 25MB mp4).](https://zapu.keybase.pub/Unity/Hotpatching.mp4)
76

8-
(Visual Studio is not required)
7+
(Visual Studio Code is not required)
98

109
## What it does
1110

12-
UnityHotSwap recompiles project's C# code and replaces it while the game is running in the editor. All objects are left untouched, only modified methods are replaced.
11+
UnityHotSwap recompiles project's C# assemblies and patches functions while the game is running in the editor. All objects are left untouched. Only methods that have changed are patched.
1312

1413
Only method bodies can be replaced. Types, fields, method cannot be added or removed (this creates some limitations with lambda expressions and Linq). Method signatures cannot change, even if all invocations of such methods are changed.
1514

16-
It probably does what Visual Studio C++ *Edit and continue* is able to do. Maybe it will do more in the future. Or maybe Unity drops mono altogether and this will become obsolete.
15+
It probably does what Visual Studio C++ *Edit and continue* is able to do. Maybe it will do more in the future. Or maybe Unity drops mono altogether and this will become obsolete. Or maybe Unity rolls its own implementation of similar system and we will never have to exit play mode ever again.
1716

1817
UnityHotSwap is currently in pre-alpha stage and will probably not work for non-toy projects. Small assemblies and simple replacements might work, though, so you are free to play around. But if it breaks, you get to keep all the shiny pieces. Contributions are welcome.
1918

2019
## How to install and use
2120

22-
Extract release to your `Assets` folder.
21+
Extract released package to anywhere in your Asset folder. You should end up with 3 dll files:
2322

24-
Your DLLs should end up as follows:
23+
* `ildynarec.dll`
24+
* `unityhotswap.dll`
25+
* `Mono.Cecil.dll`
2526

26-
* `Dynarec/Editor/ildynarec.dll`
27-
* `Dynarec/Editor/Mono.Cecil.dll`
28-
* `Dynarec/Editor/unityhotswap.dll`
27+
If you are serious about using it, you should probably make them "Editor" only, or move to "Editor/" magic folder.
2928

30-
Open *Tools > Hot-Swap Settings* and set *Hotpatching* to enabled.
31-
32-
Now, while in *Play mode*, you should be able to make changes in your code and apply it by using *Tools > Hot-Swap* (or `ALT+F10`).
29+
Now, while in *Play mode*, you should be able to make changes in your code and apply it by using *Experimental > Hot-patch project* (or `ALT+F10`).
3330

3431
## How does it work
3532

36-
First, UnityHotSwap instruments assemblies and adds private static `DynamicMethod` field for each method that can be replaced at runtime (some limitations apply, e.g. constructors cannot be replaced at the moment). Then, each of those methods is instrumented to check if the associated dynamic method is not null. If it's not, it is invoked and its result is returned. Execution never reaches the original method body. See `Instrument.cs`.
33+
UnityHotSwap does not need assembly instrumentation. If you are not hotpatching in current session, there is zero overhead, and your game plays as is.
3734

38-
When *Hot swap* menu item is used, UnityHotSwap will first build new version of assembly using last compiler parameters from Unity `Temp/` directory. Newly built assembly is then compared to the currently running one. Methods that have changed will be recompiled from static `Mono.Cecil.MethodDefinition` body to runtime `DynamicMethod` using `ILGenerator`. See `Recompiler.cs`.
35+
When *Hot-patch* option is first used, all currently loaded assemblies are inspected and a dictionary of method full names and method body hashes is created. This is used to quickly compare method bodies to bodies in recompiled assemblies.
36+
37+
Then, every assembly is recompiled used mono command line arguments that Unity keeps in `Temp/` folder. UnityHotSwap will look for these settings and invoke mono to compile project assemblies to temporary output dll files. Then these new assemblies are loaded and UnityHotSwap will find methods which bodies have changed compared to currently running methods. See `UnityPlayModePatching.cs`.
3938

40-
Some Unity trickery is needed for all this to work. When *Play mode* is activated, UnityHotSwap will try to disable assembly reloading using `EditorApplication.LockReloadAssemblies();` and `EditorPrefs.SetBool("kAutoRefresh", false);`. Those settings are restored when *Play mode* is exited. There is an `InitializeOnLoad` class with static constructor that triggers the instrumentation. It is guaranteed to be invoked at some point after Unity discovers code modification and recompiles assembly.
39+
DynamicMethod is emitted using `ILGenerator` and method body from `Mono.Cecil.MethodDefinition`. Method body from new assembly is essentially "retargetted" to run within currently loaded runtime. See `Recompiler.cs`
40+
41+
To swap currently loaded method with newly generated DynamicMethod, both methods are forced to be jit-compiled using `RuntimeHelpers.PrepareMethod`. Then a `jump` or `push+ret` code "gadget" is inserted into memory at the address of the original method, making it immediately continue execution at the new method's code after it's called. See `Hotpatcher.cs`
42+
43+
Some Unity trickery is needed for all this to work. When *Play mode* is activated, UnityHotSwap will try to disable assembly reloading using `EditorApplication.LockReloadAssemblies();`. Those settings are restored when *Play mode* is exited.
4144

4245
## Known issues
4346

44-
IL recompilation is an interesting process and is definitely not feature complete. Mono is a lot more forgiving in terms of `DynamicMethod` generation so sometimes invalid code will crash the entire editor. Please do not report bugs to Unity when this happens, as they will be useless to them. Report them here instead. Thanks!
47+
IL recompilation is an involved process and is definitely not feature complete. Mono is a lot more forgiving in terms of `DynamicMethod` generation so sometimes invalid code will crash the entire editor. Please do not report bugs to Unity when this happens, as they will be useless to them. Report them here instead. Thanks!
4548

46-
Behavior of virtual and override methods is... not tested.
49+
Some code will fail to recompile with various exceptions. You can try to reproduce with minimal function that fails to be compiled and file an issue. Thanks!
4750

48-
Sometimes Unity will recompile and reload assemblies after code changes even though we do our best to disable this behavior.
51+
Behavior of virtual and override methods is not tested. Generic methods are not supported, this is a limitation of DynamicMethod generation. Calling generic method from new code is fine, though.
4952

50-
Sometimes assembly will not get instrumented. Errors "method is not hotpatchable" is a result of this.
53+
Sometimes Unity will recompile and reload assemblies after code changes even though we do our best to disable this behavior. When Unity starts ignoring `LockReloadAssemblies`, your best bet is to restart the editor. This tends to happen when *Pause* is used while in Play mode.
5154

5255
## How to contribute
5356

54-
Fork and clone this repository. I use Microsoft Visual Studio 2013 but whatever should be fine, this is not very huge codebase. Fix dependencies to `UnityEngine.dll` and `UnityEditor.dll`. Have Visual Studio download nuget packages (nunit).
57+
Fork and clone this repository. I use Microsoft Visual Studio 2017 but whatever should be fine, this is not very huge codebase. Fix dependencies to `UnityEngine.dll` and `UnityEditor.dll`. Have Visual Studio download nuget packages (nunit).
5558

56-
If this project gets any traction I'll figure this out.
59+
If this project gets traction, I'll figure this out.
5760

5861
## License
5962

6063
GPLv2.
6164

62-
Since you are not linking this library in your final product (but merely using it during development), license of your product does not matter and you are not required to release anything. But any released changes to the library itself must include source code.
65+
Since you should not be linking this library in your final product (but merely using it during development), license of your product does not matter and you are not required to release anything. But any released changes to the library itself must include source code.
6366

6467
## Contact
6568

66-
I can be reached on twitter [@taluhunusa](https://twitter.com/taluhunusa) or by e-mail: michal at zapu.net
69+
I can be reached on Keybase https://keybase.io/zapu or by e-mail: michal at zapu.net
70+
71+
72+
***
73+
74+
## Historical purposes - how it used to work in 2015
75+
76+
*See above for current writeup, this is not how things work anymore.*
77+
78+
First, UnityHotSwap instruments assemblies and adds private static `DynamicMethod` field for each method that can be replaced at runtime (some limitations apply, e.g. constructors cannot be replaced at the moment). Then, each of those methods is instrumented to check if the associated dynamic method is not null. If it's not, it is invoked and its result is returned. Execution never reaches the original method body. See `Instrument.cs`.
79+
80+
When *Hot swap* menu item is used, UnityHotSwap will first build new version of assembly using last compiler parameters from Unity `Temp/` directory. Newly built assembly is then compared to the currently running one. Methods that have changed will be recompiled from static `Mono.Cecil.MethodDefinition` body to runtime `DynamicMethod` using `ILGenerator`. See `Recompiler.cs`.
81+
82+
Some Unity trickery is needed for all this to work. When *Play mode* is activated, UnityHotSwap will try to disable assembly reloading using `EditorApplication.LockReloadAssemblies();` and `EditorPrefs.SetBool("kAutoRefresh", false);`. Those settings are restored when *Play mode* is exited. There is an `InitializeOnLoad` class with static constructor that triggers the instrumentation. It is guaranteed to be invoked at some point after Unity discovers code modification and recompiles assembly.

ildynarec/CecilExtensions.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,5 @@ public static string GetNormalizedFullName(this MethodReference method) {
2828

2929
return str;
3030
}
31-
32-
public static MethodDefinition GetMethodByName(this TypeDefinition type, string name) {
33-
return type.Methods.FirstOrDefault(method => method.Name == name);
34-
}
3531
}
3632
}

ildynarec/Debug.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static void Silence() {
3434
}
3535

3636
public static void Log(string str) {
37+
Trace(str);
3738
LogAction(str);
3839
}
3940

0 commit comments

Comments
 (0)