Since I started making middleware for Unity in early 2008, I have been working with Unity and .net assemblies. I have in this time had many opportunities to juggle the two in many different contexts, so I figured a sort of summary of what I learned would make sense.
Why not start off by setting a few facts straight, eh?
- Aside from the extension and a header, .net assemblies have little to do with Windows Dynamic Link Libraries.
- Windows DLLs contain binary code, usually with a very simple interface, while .net assemblies contain .net bytecode (or CIL).
- Unity pro is not required to use .net assemblies.
- Binary plugins require Unity pro (at time of writing) and must be placed in Assets/Plugins. However .net assemblies are just bytecode – just like what Unity turns your scripts into, so none of these requirements apply.
- .net assemblies run on all platforms – including webplayer.
- Again, as we are talking about bytecode which is fed directly to the mono runtime – like the compiled result of your scripts, the same rules go.
- Distributing .net assemblies in stead of script files does not copy protect your code.
- Since the mono runtime has to Just In Time compile your assembly to run on the current platform, the bytecode is quite verbose and while some information is lost, generally it is very easy to decompile and read .net assemblies.
- Remember that this is true for your script source in most project builds as well, since for most platforms Unity builds your scripts to .net assemblies.
Argh! But my kodes!
I will not get into source protection too much, but generally I would not worry too much about it.
Source code in and of itself does not mean that someone can instantly copy and use parts of your work. It takes actual skill and time investment to make use of it, integrated in a different setup from what it was originally designed for. In the end it is my opinion that it would not be worth it compared to doing the same work from scratch.
Consider that the entire source of Half Life 2 was very publicly leaked shortly before its originally scheduled release and how we are all now drowning in games based on it.
If you have client-side security measures which you would like to protect – like in-app purchases or other anti-cheat measures, you may indeed want to consider splitting out and encrypting part of this logic as well as applying some measure of source obfuscation.
A word of caution: While source-level obfuscation is fine and usually sufficient, assembly-level obfuscation can be risky as it generally relies on bugs in the mono runtime in order to function. Bugs which might get fixed at any time – at which point you will need to go back and revisit your security efforts.
And remember: Encryption and obfuscation is just about making it harder for people to do things you don’t want them to. In the end this all has to be understood by processors and even binaries are routinely reverse engineered. The best security is gained by not making what you wish to protect available outside of hardware in your control.
For more information on protecting sensitive client-side information from tampering, see the Unite Asia 2013 “Protecting Your Android Content” talk by Erik Hemming.
Note that Unity does not distribute .net assemblies on all platforms. Some platforms do not run the mono JIT compiler because reasons and for those Unity will in stead perform Ahead Of Time compilation on your code while building.
AOT compilation basically means that in stead of relying on the JIT compiler to at runtime load your bytecode when relevant and compile it for the specific hardware it is running on, the AOT compiler will compile all your assembly to platform-specific binary right up front.
This means that your source code assemblies, any assemblies present in your project as well as the Unity engine assemblies are all jumbled into one binary. In most cases this should disincentivise tampering sufficiently.
At time of writing, Unity employs AOT compiling on iOS and the console platforms.
What is the point then?
Now that you know what assemblies are not, you might be wondering why you should be interested in them at all. And rightly so.
I think it is always healthy to have some understanding of what goes on in the lower layers of your technology stack, but most Unity developers will not need to know about .net assemblies.
However here are some reasons why they are really cool:
- If you are distributing middleware, they allow you to significantly reduce your distribution footprint without compromising your development environment (one assembly vs. a bunch of script files).
- Like their Windows cousins, .net assemblies allow embedded resources, which means you can literally put everything you need – textures, audio, whatever – inside one file.
- Where JIT is available, new assembly can be loaded at runtime.
- So what? So live patching baby!
Since the JIT compiler routinely loads bytecode from disk and compiles and executes it, there is nothing preventing you from having it load bytecode which was not included in your original build.
The class System.Reflection.Assembly lets you load in bytecode from a file or a byte array, after which you can use reflection to find and instantiate types, call functions and all that other good stuff on what is contained in the assembly.
Remember that since Unity employs AOT and not JIT on some platforms, this functionality is not available everywhere.
For an example of loading in new assembly, see the code-in-asset-bundles example from the “Unity Hacks” talk available on the videos page (source included).
A word of caution: If you plan on employing remote patching, make sure to at least sign your transfers in order to avoid man in the middle injection attacks.
Wohoo! Assemble all of the things!
This is all great, but how do you get started building assemblies for all them things? Well, dear reader, there are different approaches:
- If you are already set up with Visual Studio or Mono Develop, you can simply create a new project for a .net assembly using the project creation wizard. In order to access Unity types, simply add the UnityEngine.dll (and if necessary the UnityEditor.dll) assemblies to the project references and off you go.
- Should you be of a different persuasion and if you are not one to shy away from a terminal window, you can use the the gmcs binary as distributed with Unity or the mono SDK to build your assembly by command-line.
Either of these will produce a .dll file with all of your kodes (and potentially resources). Simply drop it anywhere in your assets folder to gain access to the contained classes. The same compile order rules which apply to scripts, also hold true for assemblies.
Any public ScriptableObject or MonoBehaviour derived classes will appear as children of the assembly asset. Simply click the folding widget next to the asset in order to view and be able to drag-drop them.
Ah and if you were too lazy to check out the Unity Hacks talk (you really should, it is full of useful snippets), the trick for putting code in asset bundles is to compile the scripts to an assembly rather than letting Unity compile them and then save the assembly in your assets folder with the .bytes extension rather than .dll.
This makes Unity import the assembly as a TextAsset which is not addressed by Unitys compiler and can be included in asset bundles. At runtime, the bytes property of the asset reference is then passed to the Assembly constructor as described earlier.
And the catch?
Given that we are reaching the end of this article, that would mean it is high time for some dream bursting. This all sounds very easy and optimal to work with, but of-course it is not that simple.
While Unity makes it fairly straight forward to debug your scripts using MonoDevelop, it will by default do absolutely nothing to help you with your assemblies.
To help it on its way, you need to first build your assemblies for debug and secondly copy the generated [AssemblyName].mdb file to your assets folder along side the .dll file.
If you went ahead and looked for the mdb file right as I had ended the above paragraph, you would stand some chance of not finding it. Should you be on Windows, a pdb file rather than an mdb is generated and it is absolutely useless to the Unity debugging runtime.
Should you be stuck with a pdb file, you will need to use the pdb2mdb command-line tool as distributed with the Unity mono runtime or the mono SDK. However running this command by hand every time you need to debug gets old really fast, so I recommend adding it in as a post-build step on your MonoDevelop or VisualStudio project.
For more information on debugging assemblies in Unity and indeed general use of assemblies in Unity, see this excellent article in the Unity documentation: Using Mono DLLs in a Unity Project
And that is all, folks. I hope you find it useful!