- Home /
Injecting Code With Mono.Cecil
I'm working on a networking system for Unity which uses Cecil to inject code into the assemblies. Currently I have the following code to do this via an Editor script:
[InitializeOnLoad]
static class AssemblyPostProcessor
{
static AssemblyPostProcessor()
{
// ...
// Inject code into assemblies if needed
// ...
if( anyAssembliesWereChanged )
{
UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
}
}
}
It seemed to be working - if I decompile the assemblies with ILSpy they show the injected code is there. However, when I run the game in the editor, it behaves as if the assembly has not been updated. Not just that the injected code isn't there, but changes I've made to scripts have not been applied either.
If I close the Unity editor and open it again, then the updated code appears to run. Is there any way I can get eliminate this step? It would be nice to have at least a semi Unity-approved way of using Cecil in Unity projects.
The assemblies are in two places in your project folder. One set is in "ProjectFolder/Assets" (I think the real ones), the others are in "ProjectFolder/Library/ScriptAssemblies" ($$anonymous$$aybe backups?). Could it be possible that line 12 reloads the wrong set?
The only assemblies I can find are in Library/ScriptAssemblies, they're the ones I'm changing
Correction, the first set is in the ProjectFolder. I'm not sure why I wrote /Assets.... The Library is the local cache folder, that is why I assume they're the backups.
Did some tests: I deleted all assemblies from both locations. The assemblies in Library are created when Unity compiles the scripts on startup, the ones in the project's folder are created when you try to open a script via Unity (and $$anonymous$$onoDevelop is started).
I dunno, maybe you need to modify both sets?
I'm still not finding any assemblies in the project folder, just various project files for visual studio.
Answer by spectre1989 · Oct 09, 2013 at 09:01 AM
JB Evain told me on twitter
simply moving the code to a DLL that you can post process before copying to the Assets folder works
It's not ideal, but it's a solution.
UPDATE: I got it working without the DLL solution! The problem was that my code which processed the assembly had a bug, where I was importing a type or a function into the same module which defined it, so it was causing a cyclic reference. Once I fixed that, it seems to be working fine so far! This is excellent, as it means I get some cool parts of the workflow back such as auto compilation, and more importantly when I double click on a message in the console it makes visual studio bring up the relevant .cs file and go to the correct line.
Hey I am trying to use cecil to inject a simple function call into a method based on an attribute tag. I am not familiar with cecil and have been scouring through its not very existent documentation as I'm sure anyone trying to figure it out has. It's doubly confounding as I am finding the mono.cecil.dll in the Unity folder doesn't even have all the functions that some of the examples use. So any help you could offer would be highly appreciated. Could you share more example code of how you figured this out? Even just the proper methods to use to get one assembly loaded and injected with just one Log line would be very helpful. Also are you using the mono.cecil.dll from the Unity folder? And if so which folder did you you take it from as I find there is about a dozen mono.cecil.dll spread all throughout the Unity Application directory.
Well partially never $$anonymous$$d, I hacked my way through this and got it working. Here is some example code if anyone wants to see it a little more filled in.
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using $$anonymous$$ono.Cecil;
using $$anonymous$$ono.Cecil.Cil;
[InitializeOnLoad]
static class AssemblyPostProcessor
{
static AssemblyPostProcessor()
{
var assembly = AssemblyDefinition.ReadAssembly(Application.dataPath+"/../Library/ScriptAssemblies/Assembly-CSharp.dll");
foreach (var module in assembly.$$anonymous$$odules)
{
foreach (var type in module.Types)
{
if (type.Has$$anonymous$$ethods)
{
$$anonymous$$ethodReference injectTest$$anonymous$$ethod = type.$$anonymous$$ethods[0];
foreach (var method in type.$$anonymous$$ethods)
{
if (method.Name == "InjectTest")
{
injectTest$$anonymous$$ethod = method;
}
}
foreach (var method in type.$$anonymous$$ethods)
{
if (HasAttribute("NRTime", method.CustomAttributes) )
{
ILProcessor ilProcessor = method.Body.GetILProcessor();
ilProcessor.InsertBefore(method.Body.Instructions[0], ilProcessor.Create(OpCodes.Call, injectTest$$anonymous$$ethod ));
ilProcessor.InsertBefore(method.Body.Instructions[0], ilProcessor.Create(OpCodes.Ldarg_0));
}
}
}
}
module.Write(Application.dataPath+"/../Library/ScriptAssemblies/Assembly-CSharp.dll");
}
// if( anyAssembliesWereChanged )
// {
// UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
// }
}
private static bool HasAttribute(string attributeName, IEnumerable<CustomAttribute> customAttributes)
{
return GetAttributeByName(attributeName, customAttributes) != null;
}
private static CustomAttribute GetAttributeByName(string attributeName, IEnumerable<CustomAttribute> customAttributes)
{
foreach (var attribute in customAttributes)
if (attribute.AttributeType.FullName == attributeName)
return attribute;
return null;
}
}
However if you have figured out a better way to do something that how I am doing it, it'd be nice to be informed of it.
I am however still trying to figure out how you deal with this "anyAssemblyWereChanged" value. I am finding you must someway inject a flag into the assembly to mark it as injected and to avoid next time. All other methods that come to $$anonymous$$d might break in some way, unless you've come up with something different? What is the smartest way to tag the whole assembly as injected?
Hi there. I don't use the cecil dll from the Unity folder, I built an up to date one from the source code and used that ins$$anonymous$$d. As for "anyAssembliesWereChanged", you could use a static variable in your AssemblyPostProcessor class that gets set to true when you alter an assembly. The only trouble might be working out if you've already processed an assembly. One way might be if you had things tagged with an attribute that tells the post processor to do some stuff to a class or function, and then the post processor could remove the attribute when it was done. The other way I've used is to just have the post processor apply an attribute to the entire assembly to mark it as "already processed".
Answer by PrefabEvolution · Sep 05, 2014 at 11:01 AM
Hi there. Another way to catch file changed event is using FileSystemWatcher. The benefit of this way is that you will get fileChanged event even if you make player build.
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
[InitializeOnLoad]
static class AssemblyPostProcessor
{
//Keep list of watchers to prevent GC collect
static public List<FileSystemWatcher> watchers = new List<FileSystemWatcher>();
static AssemblyPostProcessor()
{
Debug.Log("Init AssemblyPostProcessor");
RigisterOnFileChanged(Application.dataPath + "/../Library/ScriptAssemblies/Assembly-CSharp.dll", DoInject);
}
static void DoInject(string path)
{
Debug.Log("Inject dll " + path);
//Inject code here...
Debug.Log("Inject dll Done");
}
static public void RigisterOnFileChanged(string path, System.Action<string> action)
{
Debug.Log("Watch: " + Path.GetFullPath(path));
var fileSystemWatcher =
new FileSystemWatcher(Path.GetDirectoryName(path))
{
EnableRaisingEvents = true
};
fileSystemWatcher.Changed +=
(o, e) =>
{
Debug.Log("File changed:" + e.FullPath);
if(Path.GetFullPath(e.FullPath) == Path.GetFullPath(path))
{
action(path);
}
};
watchers.Add(fileSystemWatcher);
}
}
Your answer
Follow this Question
Related Questions
A node in a childnode? 1 Answer
Build a game from Illegal Unity 0 Answers
Wait for reload to finish? 1 Answer
How can I fix the Unity Bug 1 Answer