Question about the implementation of code generation inside Unity, as I understand with Mono Cecil we are able to update assemblies and, as far as I know, Burst rely on that tech in order to generate his optimized version of our code.
There is a Unity limitation that I’m not seeing here and I’m wondering if there is a hidden way to achieve what I want: code is compiled into .dll that Unity load and if we modify that .dll from outside of Unity usual pipeline won’t be doing anything until we restart Unity.
So how does Burst bypass that limitation ?
Solutions where you do the code generation at build time doesn’t work since Burst is also available in editor so there must be something that glue back the generated code to Unity without any restart.
When Unity recompiles scripts, it generates new DLLs and reloads them through App Domain Reload.
I thought Unity reloads App Domain when DLL is modified as well, doesn’t it?
It’s a bit weird, I don’t want to make unfounded assumptions but my tests show that injecting code with Mono.Cecil and calling “myAssemblyDefinition.Write” is breaking something in Unity until you restart it.
More context on my test:
One script inside an asmdef
A Monobehaviour with one field instanciated on Start and then I call a function from that instance that will log a string
One script inside an editor asmdef
On beforeAssemblyReload I find the assembly where the script is and delete everything inside the function that should have been logging something
So without the injection I expect having something logged in the console and with the injection nothing. Then, when I update the string logged to something else without rerunning the injection, I’m expecting to see the logs. Starting the moment when I inject my code, the .dll will not be reloaded by Unity (I checked the decompiled version of the .dll and it should work).
I’m not using the new feature that prevent the DomainReload on playmode.
It feels like I’m either missing something but not sure what and Burst seems to make it work somehow so I’m wondering what they do (not seeing anything fancy in the source code but I may have missed it).
PS: Put a link in my first post to the only similar issue I could find on google.
If you want to patch assemblies with Cecil before loading, you have to do it in
This is invoked after finishes to compile the assembly and before we load it.
Once the assembly has been loaded, it is no longer possible to patch it. Burst works differently, as we explicitly invoke the burst compiled method if/when available instead of regular JIT compiled method.
You can patch the assembly again, but then you have to force a assembly reload, which can do with
Once the assembly gets recompiled, it would have to be patched again. Which I why I suggested to do it in the compilation callback.
You cannot codegen at runtime with Mono.Cecil, for that you can use Reflection.Emit, which only works on JIT (Mono) platforms (Editor, desktop players) and not on AOT (IL2CPP) platforms (consoles, mobile)
If you want to codegen on the fly in the editor, you would use something like Reflection.Emit and then use Mono.Cecil to patch the assemblies for players.
You could also use our AssemblyBuilder API to build an assembly from .cs files outside of the Assets folder:
I think there may be something else, here are my steps:
Open unity
Start playmode and see the logging
Enable patching and trigger an assembly recompilation: I patch the assembly once
Playmode and see that there is no more logging
Disable patching
Change the script to log something else (in theory that should override my patching)
Run playmode and see that the patching is still in effect
I’ve tried to patch again to something else after that and the results did not change, so after the first patching something broke and I can’t figure out what.
I’ve tried the recompilation with RequestScriptReload and nothing changed.
I’m not looking for runtime code gen, I want compile time code gen that also work in editor.
So I’ve removed everything line by line and found what was causing this issue.
I’m not sure if it does count as an Unity issue or not so I hope @lukaszunity will be able to tell if I should report this or not. I will also say that I’m pretty new to Cecil and with not much infos available online, coding with this is not easy.
The issue was this line in my code:
// This is not working
var listenerPairArrayType = eventManagerType.Module.ImportReference(typeof(ListenerPair<>));
// This is working
var listenerPairArrayType = eventManagerType.Module.GetType(typeof(ListenerPair<>).FullName);
var genericInstanceType = new GenericInstanceType(listenerPairArrayType);
genericInstanceType.GenericArguments.Add(typeDefinition);
var genericInstanceArraryTypeReference = new ArrayType(genericInstanceType);
FieldDefinition fieldDefinition = new FieldDefinition("_" + typeDefinition.FullName + "Listeners", FieldAttributes.Public, genericInstanceArraryTypeReference);
eventManagerType.Fields.Add(fieldDefinition);
ListenerPair is a simple generic struct in the module of eventManager. What I want is an array of that type with typeDefinition as the generic argument.
The line which is not working seems to generate an invalid .dll, something that should not compile I suppose. Once that .dll is generated and read by Unity, I won’t be able to override it anymore until I restart Unity.
This was my fault from failing to understand what I’ve needed to use but here is my question:
Shouldn’t Unity be able to tell me that this .dll wasn’t working ?
Hello, I’ve figured I’d ask this here, since this post is a top search result for Unity and Mono Cecil - regarding assemblyCompilationFinished, how do we add the event so that it actually happens without having to recompile manually? (by manually I mean altering a script in IDE and saving)
I tried using InitializeOnLoad, almost immediately realizing that the assembly first has to be loaded in order for the event to actually be added.
So, how does one add these compilation events correctly, for the build process as well as the editor reloading?
After doing serveral research and test, I find something interesting as following code demonstrated:
// 'assembly' variable stands for Assembly-CSharp.dll
// 'MyDebugger' class defined in Assembly-CSharp.dll
// 'Debug' class defined in UnityEngine.dll
// this line can work correctly
MethodDefinition methodRef = assembly.MainModule.GetType(typeof(MyDebugger).FullName).Methods.Single(x => x.Name == "CheckThread");
// this line can only modify assembly successfully once
MethodDefinition methodRef = assembly.MainModule.Import(typeof(MyDebugger).GetMethod("CheckThread", new Type[] { typeof(string) }));
// but this line work correctly
MethodDefinition methodRef = assembly.MainModule.Import(typeof(Debug).GetMethod("Log", new Type[] { typeof(string) }));
It seems that if a type is defined in current assembly which is being injected, we should use GetType instead of Import to access its definition(i.e.,TypeDefinition).