Downloading the hydra

by AngryAnt on January 5th, 2010

So I’ve seen some people asking how you would go about downloading executable code from an external location, reading it into memory and executing it in ones running application.

Now some readers might go "eh? why the heck would someone want to do that?". Well first off, its cool™, but seeing as that is rarely an argument which convinces decision makers, here’s a few others:

  • Patching. The ability to modify the behaviour of your downloaded executable after distribution, without requiring re-download.
  • Highly dynamic online content. Say you’re building a virtual world or something. You might want to have the ability to add more complex behaviours to downloaded content – more than what a data-driven approach allows for.
  • Or how about expanding the capabilities of user generated content?

There. Those are my alibis and I’m sticking to them.

Now this example is quick and simple so as to not bloat the post too much. Regardless of your use, you’ll likely be wanting to add in for instance some cache functionality for real-world use.

Right. So this example contains three pieces of eight, uh, code:

  • The loader utility – responsible for downloading and making available the remote code.
  • The assembly – you know, the one we’re downloading.
  • An example handler. For easy re-use, I’ve designed this example to allow for easy re-use of the loader utility by externalising handling of the assemblies via messages. This example handler shows some techniques for accessing the data of loaded assemblies.

Using the example:

  1. Download and save /Assets/WWWAssemblyLoader.cs, /Assets/NewBehaviourScript.cs and /MyAssembly.cs.
  2. Download and install the mono runtime (or the .net equivalent on Windnows – not tested, but should work just fine).
  3. Build the assembly from the terminal – using the build command supplied at the end of this post.
  4. Upload the assembly to some host (save the URL for later).
  5. Open up your Unity project.
  6. Add the WWWAssemblyLoader and NewBehaviourScript scripts to a GameObject and set the URL property of the first to that of your uploaded assembly.
  7. Press play.
  8. Profit!

And now the codes!

/Assets/WWWAssemblyLoader.cs

using UnityEngine;
using System.Collections;
using System.Reflection;

public class WWWAssemblyLoader : MonoBehaviour
{
    public string m_AssemblyURL;
    private string m_ErrorString = "";
    private WWW m_WWW;
    private bool m_Complete = true;

    public void Start ()
    {
        if (m_AssemblyURL != "")
        {
            ReloadAssembly (m_AssemblyURL);
        }
    }

    public string AssemblyURL
    {
        get
        {
            return m_AssemblyURL;
        }
        set
        {
            if (m_AssemblyURL != value)
            {
                ReloadAssembly (value);
            }
        }
    }

    public float Progress
    {
        get
        {
            return m_Complete ? 1.0f : m_WWW.progress;
        }
    }

    public string Error
    {
        get
        {
            return m_ErrorString;
        }
    }

    public void ReloadAssembly (string url)
    {
        m_Complete = false;
        m_ErrorString = "";
        m_AssemblyURL = url;
        m_WWW = new WWW (m_AssemblyURL);
    }

    public void Update ()
    {
        if (!m_Complete)
        {
            if (m_WWW.error != null)
            {
                m_ErrorString = m_WWW.error;
                m_Complete = true;
                SendMessage ("OnAssemblyLoadFailed", m_AssemblyURL);
            }
            else if (m_WWW.isDone)
            {
                Assembly assembly = LoadAssembly ();
                m_Complete = true;
                if (assembly != null)
                {
                    Debug.Log ("Done");
                    SendMessage ("OnAssemblyLoaded", new WWWAssembly (m_AssemblyURL, assembly));
                }
                else
                {
                    Debug.Log ("Failed");
                    SendMessage ("OnAssemblyLoadFailed", m_AssemblyURL);
                }
            }
        }
    }

    private Assembly LoadAssembly ()
    {
        try
        {
            return Assembly.Load (m_WWW.bytes);
        }
        catch (System.Exception e)
        {
            m_ErrorString = e.ToString ();
            return null;
        }
    }
}

public class WWWAssembly
{
    private string m_URL;
    private Assembly m_Assembly;

    public string URL
    {
        get
        {
            return m_URL;
        }
    }

    public Assembly Assembly
    {
        get
        {
            return m_Assembly;
        }
    }

    public WWWAssembly (string url, Assembly assembly)
    {
        m_URL = url;
        m_Assembly = assembly;
    }
}

/Assets/NewBehaviourScript.cs

using UnityEngine;
using System.Collections;
using System.Reflection;

public class NewBehaviourScript : MonoBehaviour
{
    private string m_MessageString = "Waiting for assembly";

    void OnAssemblyLoaded (WWWAssembly loadedAssembly)
    {
        m_MessageString = "Assembly " + loadedAssembly.URL + "\n";

        System.Type type = loadedAssembly.Assembly.GetType ("MyClass");

        FieldInfo field = type.GetField ("myString");
        m_MessageString += (field.GetValue (null) as string) + "\n";

        object instance = loadedAssembly.Assembly.CreateInstance ("MyClass");
        MethodInfo method = type.GetMethod ("LogMyString");
        m_MessageString += "Return value: " + method.Invoke (instance, null).ToString ();
    }

    void OnAssemblyLoadFailed (string url)
    {
        m_MessageString = "Failed to load assembly at " + url;
    }

    void OnGUI ()
    {
        GUILayout.BeginArea (new Rect (0.0f, 0.0f, Screen.width, Screen.height));
            GUILayout.FlexibleSpace ();
            GUILayout.BeginHorizontal ();
                GUILayout.FlexibleSpace ();
                GUILayout.Box (m_MessageString);
                GUILayout.FlexibleSpace ();
            GUILayout.EndHorizontal ();
            GUILayout.FlexibleSpace ();
        GUILayout.EndArea ();
    }
}

/MyAssembly.cs

using UnityEngine;

public class MyClass
{
    public static string myString = "This is my string from my class in my assembly";

    public int LogMyString ()
    {
        Debug.Log (myString);
        return 2 + 2;
    }
}

The assembly compile terminal command
mcs -target:library -out:MyAssembly.dll -r:/Applications/Unity/Unity.app/Contents/Frameworks/UnityEngine.dll MyAssembly.cs

17 Comments
  1. Nerosis permalink

    I’m having some trouble getting the compilation to work on Windows. I’m using gmcs instead of mcs because the version of Mono that Unity installs doesn’t have mcs.

    My command line looks like this:

    C:\compile>”C:\Program Files (x86)\Unity\Editor\Data\MonoCompiler.framework\gmcs.exe” -target:library -out:MyAssembly.dll -r:”C:\Program Files (x86)\Unity\Editor\Data\Frameworks\UnityEngine.dll” cRotateScript.cs

    I get the following error:

    Unhandled Exception: System.TypeLoadException: Could not load type ‘UnityEngine.RequireComponent’ from assembly ‘UnityEngine, Version=0.0.0.0, Culture=neutral,
    PublicKeyToken=null’ because the format is invalid.
    at System.Reflection.Assembly._GetExportedTypes()
    at System.Reflection.Assembly.GetExportedTypes()
    at Mono.CSharp.RootNamespace.ComputeNamespaces(Assembly assembly)
    at Mono.CSharp.GlobalRootNamespace.AddAssemblyReference(Assembly a)
    at Mono.CSharp.Driver.LoadAssembly(String assembly, String alias, Boolean soft)
    at Mono.CSharp.Driver.LoadReferences()
    at Mono.CSharp.Driver.MainDriver(String[] args)
    at Mono.CSharp.Driver.Main(String[] args)

    Any ideas what would cause this?

  2. I’ve not tested this with the version of gmcs shipped with Unity – only the one downloadable from mono-project.com. Did you have similar difficulties getting this to work with the provided example script?

  3. Ashkan permalink

    is there any way to compile scripts into asset bundles? i told many people that it’s one of the advantages of unity. in documentation it said “you can compile anything in your project in an asset bundle”. you should add more description to it.
    another question: can you tell me how do you add these code panels to your blog?

    thank you

  4. Its possible that you could somehow include an assembly in an assetbundle as a data resource – haven’t tested that idea. However just adding in scripts will not include the built result in the bundle, but merely a reference to the compiled result already included in your build.

    The code pastes are generated using TextMates built in bundles. Bundles->TextMate->Create HTML From Document. I included the CSS of the C# source highlight in my blog design files and then paste in the contents of the body section of the generated HTML document when I need source in a post.

  5. Nerosis permalink

    I got the compilation working through mcs by downloading Mono as you suggested. I’m still getting the same exception that I posted above when I try to compile through gmcs. Weird. :-( The dynamic code loading and execution works well, and it’s definately Coolâ„¢. :-)

    One further question, if you don’t mind me delving a bit deeper into the topic:

    Ok, so we can load an external DLL and instantiate classes in it using reflection. Now say one of those classes (let’s call it Foo) inherits from MonoBehaviour. Is it possible to attach an instance of Foo as a component to a GameObject at runtime? I’ve experimented with GameObject.AddComponent(”Foo”), but Unity says it can’t find Foo. The assembly containing Foo is loaded into the same AppDomain as the other Unity scripts – does it need to be registered somewhere?

  6. Unity unfortunately does not yet support MonoBehaviours and ScriptableObjects in external assemblies. Remember that your external code is still not a first class citizen in your Unity runtime, but an external assembly – just as if you had dropped the assembly into your assets folder, so the same rules and regulations apply.

  7. Nerosis permalink

    That’s unfortunate. I’m going to have to try to work around that. I suppose one option is replace at runtime each user-created MonoBehavior on a GameObject with a stub class that holds a reference to a MonoBehaviour instantiated from the external assembly, and transparently passes function calls like Start() and Update() through to the instance of the external class.

  8. Indeed. That’s what I do in Path and Behave. Create stub MonoBehaviours and friends referencing and forwarding to WannabeMonoBehaviours in the assembly.

  9. Nerosis permalink

    Oh. Wow. Sweet! Would you object if I read through the Path and Behave code to get a better idea of how you did it? I’m not sure how that would fit under your licence agreement, but I have no intention of copying your code – I’ll write it myself from scratch after having a look at what you’ve done. The modules I’m writing will be used in a commercial project if management agrees to it, but I’m hoping to get permission to release them as open source back to the Unity community if they look if they could be used outside of our project.

    Thank you for all of the invaluable assistance. :-)

  10. There’s a GPL version of Path now – you can just check that out :)

    It is hosted on github and a link is available from the Path download section.

  11. Ashkan permalink

    is there any possibility for unity to support compiled scripts in asset bundles? if yes we can make a ticket for it. it’s the best possible solution for patching.
    hey AngryAnt what are the best learning resources for advanced .NET programming. learning things like stub classes , reflections, singleton patterns and factory classes?

  12. If you’re interested in compiled scripts in assetbundles, you should go to http://feedback.unity3d.com and upvote any existing request on that or create a new one.

    Of what you mentioned, only reflection is really .net specific. I just went to msdn and started reading up on the classes in the System.Reflection namespace. Regarding design patterns, I would recommend you get a good book on that – always handy to have around.

  13. bill permalink

    Cough sputter spurt !!! code injection!!! Hacks gage wheeze belch fart.

  14. @bill
    Sure, if you’re worried that someone would write malware to intercept the assembly stream and re-route something different to the webplayer then you probably should write at least some basic checksum security.

    Though the only risk you would be running is someone changing the behaviour of your webplayer – which has consequences if you have server structure blindly trusting it. Neither client nor server machine can be compromised this way, as the executed code is still sandboxed by the mono runtime.

  15. Samir permalink

    Great article AngryAnt. I got an external dll loading and running just fine. I had trouble with one thing — hoping to get some advice on how to do this.

    I am building my external dll in visual studio. I want this external dll (call it minigame.dll) to reference classes in my main unity project (Assets/Code/). In visual studio I set up the scripts in Assets/Code to build to a dll, gameloader.dll, and made the minigame project reference that. This all compiles fine and dandy.

    The problem is when I load the minigame assembly at runtime, it cannot find gameloader.dll, because Unity knows nothing about it. Unity has built the scripts into it’s own Assembly-Csharp.dll.

    I also tried making my minigame project reference Assembly-Csharp.dll. That gave me a similar error when loading the minigame.dll at runtime, about not being able to find the Assembly-Csharp.dll (but it had some obfuscated name for the dll).

    Any ideas of how I can accomplish this?

  16. How come you need to reference stuff in the Unity project directly? That does kind of limit the idea of the assembly being perfectly external. Why not in stead define some shared abstract classes or similar and via those register from the Unity project with the assembly when its loaded.

  17. Samir permalink

    Just wanted to update you. Your advice worked! Had to move a lot of code out of the core Unity project into a shared dll, and that did the trick.

    One annoying problem now is since I’m using Visual Studio / MSBuild to build my shared dll, I am losing line numbers in my stack traces when there is an exception in the shared dll. Has anyone figured out a way around this? Does the shared dll have to be built with the same version of mono that unity runs in order to get line numbers?

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS

Spam protection by WP Captcha-Free