Search Unity

[SOLVED] Burst and Mono Cecil

Discussion in 'Burst' started by dzamani, Nov 21, 2019.

  1. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    Hello everyone,

    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.

    Thanks!

    PS: Similar issue (very old but that's the only thing I found that looked somehow the same): https://mono-cecil.narkive.com/gXTz...modified-assembly-with-added-custom-attribute
     
    Last edited: Nov 21, 2019
    Singtaa likes this.
  2. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    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?
     
  3. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    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.
     
    Last edited: Nov 21, 2019
  4. lukaszunity

    lukaszunity

    Administrator

    Joined:
    Jun 11, 2014
    Posts:
    461
    If you want to patch assemblies with Cecil before loading, you have to do it in

    https://docs.unity3d.com/ScriptRefe...tionPipeline-assemblyCompilationFinished.html

    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.
     
    OndrejP likes this.
  5. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    So you are saying that even if I use assemblyCompilationFinished, I would be able to patch the assembly only once since Unity will not reload it ?

    It means that I won't be able to code gen with Mono.Cecil in that case.
     
  6. lukaszunity

    lukaszunity

    Administrator

    Joined:
    Jun 11, 2014
    Posts:
    461
    You can patch the assembly again, but then you have to force a assembly reload, which can do with

    https://github.com/Unity-Technologi...r/Mono/InternalEditorUtility.bindings.cs#L202

    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)

    https://www.c-sharpcorner.com/uploadfile/puranindia/reflection-and-reflection-emit-in-C-Sharp/

    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:

    https://docs.unity3d.com/ScriptReference/Compilation.AssemblyBuilder.Build.html
     
  7. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    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.

    I still think the cause is something like this
     
  8. lukaszunity

    lukaszunity

    Administrator

    Joined:
    Jun 11, 2014
    Posts:
    461
    If you think there is a bug, then report a bug with a repro and we will look into it.
     
  9. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    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:

    Code (CSharp):
    1. // This is not working
    2. var listenerPairArrayType = eventManagerType.Module.ImportReference(typeof(ListenerPair<>));
    3. // This is working
    4. var listenerPairArrayType = eventManagerType.Module.GetType(typeof(ListenerPair<>).FullName);
    5. var genericInstanceType = new GenericInstanceType(listenerPairArrayType);
    6. genericInstanceType.GenericArguments.Add(typeDefinition);
    7. var genericInstanceArraryTypeReference = new ArrayType(genericInstanceType);
    8. FieldDefinition fieldDefinition = new FieldDefinition("_" + typeDefinition.FullName + "Listeners", FieldAttributes.Public, genericInstanceArraryTypeReference);
    9. eventManagerType.Fields.Add(fieldDefinition);
    10.  
    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 ?
     
    Last edited: Nov 22, 2019
  10. lukaszunity

    lukaszunity

    Administrator

    Joined:
    Jun 11, 2014
    Posts:
    461
    dzamani likes this.
  11. dzamani

    dzamani

    Joined:
    Feb 25, 2014
    Posts:
    122
    I will take some time to look at that tool, thanks!
     
  12. TheHeftyCoder

    TheHeftyCoder

    Joined:
    Oct 29, 2016
    Posts:
    91
    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?
     
  13. fengkx

    fengkx

    Joined:
    Jun 19, 2014
    Posts:
    1
    I had the same problem when dealing with Mono.Cecil to modifiy Assembly-CSharp.dll in both Unity 5.x and Unity 2018.x. And I found some other questions related to this problem, such as:
    https://forum.unity.com/threads/how-can-i-force-reload-assembly-in-unity-editor.486617/
    https://forum.unity.com/threads/mon...en-modifying-anything-causes-problems.448797/
    https://groups.google.com/forum/#!topic/mono-cecil/wfawmREN6uo

    After doing serveral research and test, I find something interesting as following code demonstrated:
    Code (CSharp):
    1. // 'assembly' variable stands for Assembly-CSharp.dll
    2. // 'MyDebugger' class defined in Assembly-CSharp.dll
    3. // 'Debug' class defined in UnityEngine.dll
    4. // this line can work correctly
    5. MethodDefinition methodRef = assembly.MainModule.GetType(typeof(MyDebugger).FullName).Methods.Single(x => x.Name == "CheckThread");
    6. // this line can only modify assembly successfully once
    7. MethodDefinition methodRef = assembly.MainModule.Import(typeof(MyDebugger).GetMethod("CheckThread", new Type[] { typeof(string) }));
    8. // but this line work correctly
    9. 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).
     
    Last edited: May 10, 2020
  14. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,091
    Is it possible to patch precompiled assemblies like UnityEngine.dll?