Use Mono.Cecil to inject code into Dll in Unity

Keywords: Java Unity Spring iOS

Reprint my station: Original address

Through Mono.Cecil, we can inject code into existing dll by Emit to achieve advanced functions such as AOP.
Unity's code is automatically compiled into two Assemblies under Library Script Assemblies after modification, so I'll try to inject the code into them.

public class Test : MonoBehaviour{

void Start()
{
   InjectMod();
}

void InjectMod () {
   Debug.Log("Heihei asdasd");
}
}

When we bind Test to a scene object, we will find the output "Heihei asdasd" after running, just as we expected.
Then we try to inject code into the function.

private static bool hasGen = false;
[PostProcessBuild(1000)]
private static void OnPostprocessBuildPlayer(BuildTarget buildTarget, string buildPath)
{
   hasGen = false;
}

[PostProcessScene]
public static void TestInjectMothodOnPost()
{
   if (hasGen == true) return;
   hasGen = true;

   TestInjectMothod();
}
[InitializeOnLoadMethod]
public static void TestInjectMothod()
{
   var assembly = AssemblyDefinition.ReadAssembly(@"D:\Documents\Unity5Projects\UnityDllInjector\Library\ScriptAssemblies\Assembly-CSharp.dll");
   var types = assembly.MainModule.GetTypes();
   foreach(var type in types)
   {
      foreach(var Method in type.Methods)
      {
         if(Method.Name == "InjectMod")
         {
            InjectMethod(Method, assembly);
         }
      }
   }
   var writerParameters = new WriterParameters { WriteSymbols = true };
   assembly.Write(@"D:\Documents\Unity5Projects\UnityDllInjector\Library\ScriptAssemblies\Assembly-CSharp.dll", new WriterParameters());
}

Let's first look at TestInjectMothod, which is the function we inject into the editor. Here we need to note that every time we modify the code, the results we inject will be overwritten, so we need to inject every time we modify the code, so we add the label: InitializeOnLoad Method.
This tag means that it is executed at the time of initialization, so it will be executed automatically after compilation.

Then let's look at the first two functions, which exist for injection during packaging, where hasGen is a flag defined to prevent repetitive injection.

Then let's look at our injection method:

private static void InjectMethod(MethodDefinition method, AssemblyDefinition assembly)
{
   var firstIns = method.Body.Instructions.First();
   var worker = method.Body.GetILProcessor();

   //Get the Debug.Log method reference
   var hasPatchRef = assembly.MainModule.Import(
   typeof(Debug).GetMethod("Log", new Type[] { typeof(string) }));
   //Insertion function
   var current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Ldstr, "Inject"));
   current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Call, hasPatchRef));
   //Calculate Offset
   ComputeOffsets(method.Body);
}

In this function, we can see that we first import the functions we need, and then insert them into the front end of the method.

Some tool functions that will be used

/// <summary>
/// Pre-statement insertion Instruction, And return the current statement
/// </summary>
private static Instruction InsertBefore(ILProcessor worker, Instruction target, Instruction instruction)
{
   worker.InsertBefore(target, instruction);
   return instruction;
}

/// <summary>
/// Insert after statement Instruction, And return the current statement
/// </summary>
private static Instruction InsertAfter(ILProcessor worker, Instruction target, Instruction instruction)
{
   worker.InsertAfter(target, instruction);
   return instruction;
}
//Calculating the offset of the injected function
private static void ComputeOffsets(MethodBody body)
{
   var offset = 0;
   foreach (var instruction in body.Instructions)
   {
      instruction.Offset = offset;
      offset += instruction.GetSize();
   }
}

Waiting for compilation to complete and running the program, we found that there was an additional "Inject" before the original statement was output.
But we didn't change anything when we looked at the code, because we only changed the dll, not the source code.

By decompiling software ILSpy, we can decompile the statements in our dll through IL.

The code changes to:

public class Test : MonoBehaviour
{
   private void Start()
   {
      this.InjectMod();
   }

   private void InjectMod()
   {
      Debug.Log("Inject");
      Debug.Log("Heihei asdasd");
   }
}


The injection of success has also achieved our goal.

What's the use of this thing?
In a previous article, Sla's author analyzed Tencent's recent idea of xlua, luapatch.
Probably by adding a [hotfix] to the front of every function that needs a hot patch, hot patches can be made by hot updating the lua code.
This is a non-intrusive way to add additional functionality to our framework, similar to AOP.
For example, our original code is

[hotfix]
void Test()
{
   //DoSomething
}

After injection it becomes

[hotfix]
void Test(){
   //If there is a hot patch
   if(hasPatch()){
      //Load luaPatch and execute
      return;
   }
   //DoSomething
}

That is to say, the original function is completely replaced by lua.

It would be a lot of work to add these codes manually, but it would be a lot easier if we used the way we wrote above.

That is to say, the extra code we want is automatically injected without intrusion into the code.

Perhaps in the near future, there will be many frameworks based on this type, such as Spring in Java and so on. Although the way C# code is generated is much more troublesome than Java, it can also be done! ____________

In the company's IOS version, I also want to add such a way to build the framework, rather than hot updates.

Above.

This article refers to:
http://www.jianshu.com/p/481994e8b7df
https://www.zhihu.com/question/54344452/answer/138990189

Thank you for sharing, so that I can continue to learn new technologies such as small transparency.

By the way, let's celebrate our old age.

Posted by DavidT on Sat, 23 Mar 2019 17:39:52 -0700