Embedding, Streaming, and Loading Assemblies

 

OK, this might not be such a big deal for some people, but have you ever had a third party .NET dll that you wanted to distribute with your .exe, but you'd like to just distribute just one exe and not two seperate files (And you don't want to write an installer)?  Well, you can actually do this by embedding the .dll file as a resource, and then loading it after listening for the AppDomain.CurrentDomain.AssemblyResolve event.

 

It's not rocket science, but I found it interesting.

 

 

/*========================================================================

 

  File:    Embed.cs

 

  Summary: This file shows an example of how an assembly may be loaded from

  an embedded resource instead of from disk.

 

  Instructions for use:

  1.  Include the Embed.cs file in the project or assembly in which you wish to

  'embed' an assembly.

  2.  As early in the startup code as possible ( preferably in Main() ), make

  a call to Dennany.Embed.Init().  This will register Embed.LoadComponentAssembly()

  as the callback method for the AssemblyResolve event. 

  3.  Add the 'dynamic' assembly to your project as an Embedded Resource.

  This may be done from the menu:

  Project -> Add Existing Item (Browse to appropriate .dll file here)

  Make certain that you set the properties of the .dll to "Embedded Resource"!

  (This may be done from the Solution Explorer by right-clicking on the .dll file,

  selecting properties, and setting the "Build Action" to "Embedded Resource.")

 

  Special Note:  Because of the way the CLR loads assemblies, you may not reference

  a type in one of these embedded assemblies before the call to Embed.Init(). 

  You may also not reference a type in one of these embedded assemblies in the same method

  that the call to Embed.Init() is made.

 

 

Copyright (c) 2003, Jerry Dennany

========================================================================*/

 

using System;

using System.Reflection;

using System.Reflection.Emit;

using System.Resources;

using System.IO;

using System.Diagnostics;

 

namespace Dennany {

      public sealed class Embed {

           

            // this class is static, and should not be instantiated.

            private Embed() {}

 

 

            // Init is the only public callable method of this class.  It should be called once

            // (and only once!)

            // to register the LoadComponentAssembly method as the event handler to call should the

            // AssemblyResolve event get fired.

            public static void Init(){

                  ResolveEventHandler loadAssembly = new ResolveEventHandler(LoadComponentAssembly);

                  AppDomain.CurrentDomain.AssemblyResolve += loadAssembly;

            }

 

            // This method is called when an assembly fails to load via probing path, GAC, etc.

            // Note that we return null to the CLR if we have no assembly to load.  This is

            // expected behavior, so that the CLR may notify the rest of the application that

            // it could not load the assembly in question.

            static Assembly LoadComponentAssembly(Object sender, ResolveEventArgs args) {

           

                  // We'll use this reference fairly often in the future...

                  Assembly assembly = Assembly.GetExecutingAssembly();

                 

                  // Get the requested assembly's simple name (no namespace info or file extension)

                  string simpleName = args.Name.Substring(0, args.Name.IndexOf(',') );

                   

                  string dllImageResourceName = getResourceLibName( simpleName, assembly );

                 

                  return streamFromResource(dllImageResourceName, assembly);

                       

            }

 

            private static string getResourceLibName(string simpleLibName){

                  return getResourceLibName( simpleLibName, Assembly.GetExecutingAssembly() );

            }

 

            // We will go through the list of resources in the assembly and using the

            // simpleLibName, we will find if the dll resource is embedded in the assembly

            // Note that we return null on purpose if we didn't find anything.

            // This is because we also want to return null to the CLR if we have no assembly to load.

            private static string getResourceLibName(string simpleLibName, Assembly assembly) {

                  if ( simpleLibName == null || assembly == null ) return null;

 

                  simpleLibName += ".dll"; // assume that the file ends in this extension.

                  string dllImageResourceName = null;

                 

                  // We will iterate through the list of resources in this assembly,

                  // looking for the name of the assembly that failed to load from disk

                  foreach (string resourceName in assembly.GetManifestResourceNames()) {

                        if (resourceName.Length < simpleLibName.Length) continue;

                             

                        // if the simpleName and resourceName end the same (we drop namespace info here),

                        // then this should be the embedded assembly that we are looking for.

                        if (String.Compare(simpleLibName,

                              0,

                              resourceName,

                              (resourceName.Length - simpleLibName.Length),

                              simpleLibName.Length,

                              true) == 0 ) {

                             

                              dllImageResourceName = resourceName;

                        }

                  }          

                  return dllImageResourceName; 

            }

 

            private static Assembly streamFromResource(string dllImageResourceName){

                  return streamFromResource(dllImageResourceName, Assembly.GetExecutingAssembly() );

            }

 

            // this is the 'workhorse' of the class.  Once we've got a resource name in the assembly,

            // we stream the resource to a byte array, and load the Assembly from the byte array.

           

            private static Assembly streamFromResource(string dllImageResourceName, Assembly assembly){

                  if ( dllImageResourceName == null || assembly == null ) return null;

                 

                  Stream imageStream;

                  imageStream = assembly.GetManifestResourceStream(dllImageResourceName);

                  long bytestreamMaxLength = imageStream.Length;

           

                  byte[] buffer = new byte[bytestreamMaxLength];

                  imageStream.Read(buffer,0,(int)bytestreamMaxLength);

                 

                  return  AssemblyBuilder.Load(buffer);

            }

      }

}
// EOF

 

12 Comments

  • That's rocket science to ME! Very smart stuff there, Jerry.

  • cool stuff. one question though... what about Dynamic loading of DLLs? i mean, if the DLLs are embedded in the exe, wouldn't it mean that all the embedded DLLs get loaded into the memory when the EXE is invoked?

  • shahid,



    No, what happens is that when the CLR cannot find the assembly using the probing paths, etc, then it fires the AssemblyResolve event, passing the name of the assembly that the runtime is looking for. This code registers to be called upon the AssemblyResolve event being fired, and then scans the list of embedded resources within itself. Only if it finds a resource of the same name as what is requested will it attempt to load this resource as an assembly.

    It does NOT load the assemblies when you make the call to Embed.Init(). This merely registers us to be called if any assembly cannot be found by the runtime.



    If you are asking about this increasing the footprint of the executable, then yes, of course this is the case.

  • Jerry,



    Thanks a lot for the clarification. When we say, the footprint of the EXE increases, arent we also saying that the memory used by the EXE is more than what the application would otherwise use?

  • shahid,



    Yes, the memory used by the EXE is more than what the application would have used had the dll been included separately. I should have been more clear about that.

  • Hey Jerry, great work here.. I'm trying to embed some assemblies (dlls) in my winexe application and I don't have VS.Net. I've been searching everywhere and I've tried 100 different ways of compiling the code, creating resources, etc. and can't seem to get it right. I have your code included which seems to be working fine. I've tried adding the dlls straight to a .resources file and then embeding it with csc.exe /res: and when I try to run it without the seperate dlls present I get 'file or assembly name MyAssembly, or one of it's dependencies was not found.' Any instruction on the proper way to accomplish this without having VS.Net ? TIA





    Will

  • Will - try using something like:



    csc.exe main.cs embed.cs /resource:Test.dll /reference:Test.dll





    This works. Also, make certain that you call Dennany.Embed.Init() before you use anything in the embedded DLL. The call to Init() may not be in the same method that the embedded DLL is first used.



    If you still need assistance, use the contact link on my blog, and I'll send you an example project.

  • First off, nice work. I am having a problem though. It loads the assemblies just fine, but can't call methods in them:



    System.MissingMethodException: Method not found: blahblahblah



    I'm calling a static method in the assembly. Works fine if its not embedded. The embedded assembly gets the namespace 'project_name' so the full name is project_name.assembly_name.dll. The code tries to call assembly_name.class_name.static_method, but fails. I think that needs to call project_name.assembly_name.class_name.static_method for it to work. However, that won't compile since is does not exist at compile time.



    Any suggestions?



  • Are you making the call to Dennany.Embed.Init()? if so, are you making the call in the same method that your first call to the assembly is taking place? For example, this will fail:



    -----------------------



    // my method

    public void foo() {



    Dennany.Embed.Init();





    // this won't work, because of the way the loader

    // loads assemblies. The call to Init() should happen

    // before we even get to foo().



    namespacename.classname.staticmethodname();



    }



    ----------------



    If you still have more problems, email me using the contact page on my weblog.

  • Ha cool that you linkied this in. Jerry, you have to be VERY careful with what you are doing above. Technically speaking two loaded dynamic instances of the same assembly are not actually the same. If multiple libraries or executables reference the same assembly name, you are simply loading it multiple times (each time a new assembly). The Hashtable keyed on the simple name is there to prevent this occurence.



    If you had multiply referenced or interreferenced libraries you could run into some big problems.



    Also note, this only works for dynamic assemblies. Any assembly that is referenced using a /reference line is loaded into the process before Main is even called, so you never get a chance to trap the resolve events. The DelayRunner code in the linked article is there to overcome this issue since we can set up the resolver before we ever run our target executable.



    Great work though, I should have looked a bit harder and referenced your work as prior art.

  • Actually - this does also work for assemblies that use /ref. You just may not use a class or type in the assembly directly in Main().



    I do admit the above code has a problem with loading assemblies multiple times - it was meant to be an example of a technique, and not meant for use in an actualy product.

  • Hello,
    I have an executable file created in c# 2003, and i would like to add a resource file to it in runtime or using some kind of command.
    Is that possible?

Comments have been disabled for this content.