Author Topic: Nov 11, 2017 -- How to edit UnityEngine.dll  (Read 362 times)

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 336
  • -Receive: 1160
  • Posts: 22,121
  • Toronto, Canada
    • View Profile
Nov 11, 2017 -- How to edit UnityEngine.dll
« on: November 11, 2017, 09:41:34 AM »
I'm currently working on performance optimizations for Project 5: Sightseer, and a part of that involved editing the UnityEngine.dll to fix a nasty bug Unity introduced back in ~2014 that they seem to refuse to fix. That bug, is an absurd amount of GC allocations coming from the AttributeHelperEngine.

The bug in question is glaringly obvious to anyone even with a little proficiency in C#, and stems from the lack of caching of expensive GetAttributes calls: https://github.com/MattRix/UnityDecompiled/blob/master/UnityEngine/UnityEngine/AttributeHelperEngine.cs

Amusingly, the bug has been reported to Unity years ago, alongside the code required to fix it (https://fogbugz.unity3d.com/default.asp?746364_pjnmdhk7c9imgdsk)... and yet -- Unity refused to do anything about it, claiming that a future redesign of the system will fix it. Well, guys -- fast forward to several years later -- the bug is still there, all the way in Unity 2017.2, and no actions have been taken to address it.

Here's the thing about closing bugs that are affecting people today hoping for a future redesign to fix it later -- until this "later" comes, the problem will still keep affecting all 4.5 million Unity developers, and it can be (and usually is) years before gets resolved! And if someone submits a bug report with actual code on how to fix it -- why not fix it? Boggles my mind...

Anyway -- this post isn't meant to be a rant about Unity's choices -- I'll do that in another one. Instead, let me explain how you -- the developer -- can fix this problem yourself, to an extent. Fortunately, this particular problem comes from the side of Unity that lives in the UnityEngine.dll file, and C# files are quite easy to modify. The first thing we need to do is make a new C# project in Visual Studio.

I was editing Unity 5.6.4f1 -- so I made the application target .NET Framework 3.5. "The Output type" needs to be a Class Library -- as we need to create a DLL with the edited functions first.

Compile this code into a DLL:
  1. using System;
  2.  
  3. namespace UnityEngine
  4. {
  5.         internal class AttributeHelperEngine
  6.         {
  7.                 private static Type GetParentTypeDisallowingMultipleInclusion (Type type)
  8.                 {
  9.                         return null;
  10.                 }
  11.  
  12.                 private static Type[] GetRequiredComponents (Type klass)
  13.                 {
  14.                         return null;
  15.                 }
  16.         }
  17. }
This simple DLL will not be referencing any Unity classes, so there is no need to reference the UnityEngine.dll. Compile the DLL (I targeted Release) and move it into the solution folder, or somewhere you can find it. I called mine FixUnityEngine.dll. If you choose to fix it by adding caching instead, like in the bug report's suggested fix code, you will need to reference the UnityEngine.dll. Personally, I saw no adverse effects of simply returning 'null' in Sightseer. Worked just fine.

The next step is to create a program that will replace the code in one DLL (UnityEngine.dll) with code from another (FixUnityEngine.dll). Since I no longer needed the code above, I simply commented it out, choosing to reuse the project instead of making a new one -- but if you plan on editing your replacement code you may want to create a separate VS solution.

The API that lets us devs replace C# code is part of Mono.Cecil, but interestingly enough it's actually a part of the Visual Studio installation, at least in the current version (2017). Here's all the code needed to edit the DLL:
  1. using System;
  2. using Mono.Cecil;
  3.  
  4. public class Application
  5. {
  6.         static MethodDefinition Extract (AssemblyDefinition asm, string type, string func)
  7.         {
  8.                 var mod = asm.MainModule;
  9.                 if (mod == null) return null;
  10.  
  11.                 var existingType = mod.GetType(type);
  12.                 if (existingType == null) return null;
  13.  
  14.                 var methods = existingType.Methods;
  15.  
  16.                 foreach (var method in methods)
  17.                 {
  18.                         if (method.Name == func)
  19.                         {
  20.                                 return method;
  21.                         }
  22.                 }
  23.                 return null;
  24.         }
  25.  
  26.         static bool Replace (AssemblyDefinition original, AssemblyDefinition replacement, string type, string func)
  27.         {
  28.                 var method0 = Extract(original, type, func);
  29.                 var method1 = Extract(replacement, type, func);
  30.  
  31.                 if (method0 != null && method1 != null)
  32.                 {
  33.                         method0.Body = method1.Body;
  34.                         Console.WriteLine("Replaced " + type + "." + func);
  35.                         return true;
  36.                 }
  37.  
  38.                 Console.WriteLine("Unable to replace " + type + "." + func);
  39.                 return false;
  40.         }
  41.  
  42.         static int Main (string[] args)
  43.         {
  44.                 var dll0 = "C:/Projects/FixUnityEngine/UnityEngine.dll";
  45.                 var dll1 = "C:/Projects/FixUnityEngine/FixUnityEngine.dll";
  46.  
  47.                 var asm0 = AssemblyDefinition.ReadAssembly(dll0);
  48.                 var asm1 = AssemblyDefinition.ReadAssembly(dll1);
  49.  
  50.                 Replace(asm0, asm1, "UnityEngine.AttributeHelperEngine", "GetParentTypeDisallowingMultipleInclusion");
  51.                 Replace(asm0, asm1, "UnityEngine.AttributeHelperEngine", "GetRequiredComponents");
  52.  
  53.                 asm0.Write("C:/Projects/FixUnityEngine/UnityEngine_edited.dll");
  54.  
  55.                 Console.ReadKey();
  56.                 return 0;
  57.         }
  58. }
You may notice that I'm referencing a local copy of UnityEngine.dll -- I chose to copy it to the project's folder, but you can reference it all the way in Program Files if you like. Its default location is "C:\Program Files\Unity\Editor\Data\Managed\UnityEngine.dll".

So what does the code do? It simply reads the two DLLs and replaces the body of one function with another! In the replacement DLL I kept the same namespace, class name and function names for consistency (but as far as I can tell this isn't actually necessary). In my case though, since I did, the code to perform the replacement ended up being shorter.

Once you compile and run the program, it will spit out an edited version of the DLL (UnityEngine_edited.dll). Simply close all instances of Unity and replace C:\Program Files\Unity\Editor\Data\Managed\UnityEngine.dll with this version. That's it.

Want to test the result? Here's a test program for you:
  1. using UnityEngine;
  2.  
  3. public class Test : MonoBehaviour
  4. {
  5.     private void Update ()
  6.     {
  7.         var go = gameObject;
  8.         if (Input.GetKeyDown(KeyCode.Alpha1))
  9.         {
  10.             for (int i = 0; i < 1000; ++i)
  11.             {
  12.                 var t2 = go.AddComponent<Test2>();
  13.                 Destroy(t2);
  14.             }
  15.         }
  16.     }
  17. }
  1. using UnityEngine;
  2.  
  3. public class Test2 : MonoBehaviour {}
  4.  
The actual GC amounts and timings will vary greatly with project complexity (the more C# scripts you have, the slower the whole process becomes thanks to Unity), but this is what I was seeing before the edit and after:



There's nothing I can do about Unity calling this useless functions, and indeed in my project Unity doing so wastes 0.16 ms per call to AddComponent... but at least the 325 MB of memory allocation is gone. Yay for small victories.