Sunday, August 25, 2013

Resolve Assembly By OS Bitness

The AppDomain.CurrentDomain.AssemblyResolve event fires when the CLR tries to load an assembly and fails. This also could happen if the assembly is "OS bitness" dependent and we tried to load a 32-bit assembly in a 64-bit OS and vice-versa.

Instead of maintaining separate setups for different OS system bit, we could solve this by calling the "ResolveAssemblyByOSBitness" method inside the AssemblyResolve event. With this method, both the 32-bit and 64-bit assemblies (must be in same version) are embedded into our project. At runtime, after checking the OS system bit, it'll copy the correct embedded assembly to the system's temporary folder named with it's file name and version (if not already exist). Then it uses "Assembly.LoadFrom" to load the assembly.

Note:
  • This method can load a fully managed assembly or a mixed assembly with unmanaged code.
  • Assembly.LoadFrom would also solve the "Unverifiable code failed policy check" error when loading a mixed assembly.


1. Embed the assembly files:
- In the project, the embedded assembly file must be:
  • put in folder embed\x86 or embed\x64 respectively, and
  • set Build Action to 'Embedded Resource'.
Note: You still need to add reference to either the 32 or 64-bit assembly as normal (base on your development OS) so that the project can be compiled.

2. Create a class named "MyUtil" (or any name you preferred).

3. Copy the "CSCheckOSBitness" code from here and put into the MyUtil class.

Note: Please modify the code to fit into the MyUtil class instead of Program class. Please comment out the Main() method as well as it's not needed here.

4. Add the following method into the MyUtil class (reference):
/// <summary>
/// Copies the contents of input to output. Doesn't close either stream.
/// </summary>
public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8 * 1024];
    int len;
    while ((len = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, len);
    }
}

5. Add the following method into the MyUtil class:
using System.IO;
using System.Reflection;
using System.Diagnostics;

...

/// <summary>
/// Resolves mixed assembly with unmanaged code for 32-bit and 64-bit OS.
/// <para>
/// To be called from the AppDomain.CurrentDomain.AssemblyResolve event.
/// </para>
/// </summary>
/// <param name="sourceAssembly">
/// The assembly that contains the embedded assembly file to resolve.
/// <para>The embedded assembly file must be:</para>
/// <para>- put in folder embed\x86 or embed\x64, and</para>
/// <para>- set Build Action to 'Embedded Resource'.</para>
/// </param>
/// <param name="argsName"><see cref="ResolveEventArgs"/>'s item name.</param>
/// <param name="assemblyName">Assembly name to resolve (case sensitive and includes extension).</param>
/// <param name="assemblyFileVersion">Assembly file version to resolve.</param>
/// <returns>Resolved assembly.</returns>
public static Assembly ResolveAssemblyByOSBitness(Assembly sourceAssembly, string argsName, string assemblyName, string assemblyFileVersion)
{
    // Trim any empty spaces.
    argsName            = argsName.Trim();
    assemblyName        = assemblyName.Trim();
    assemblyFileVersion = assemblyFileVersion.Trim();
    // ==

    // The assembly names must be matched with exact case, else return null.
    if (!Path.GetFileNameWithoutExtension(assemblyName).Equals(new AssemblyName(argsName).Name, StringComparison.Ordinal))
        return null;
    //==

    string osBitness          = Util.Is64BitOperatingSystem() ? "x64" : "x86";

    #region Check entry assembly target platform

    Assembly entryAssembly = Assembly.GetEntryAssembly();
    if (entryAssembly != null)
    {
        if (entryAssembly.GetName().ProcessorArchitecture == ProcessorArchitecture.X86)
            osBitness = "x86";          // Always set to "x86" if target platform is "x86".
    }

    #endregion

    string sourceAssemblyName = sourceAssembly.GetName().Name.Trim();

    string assemblyPath       = Path.Combine(Path.Combine(Path.GetTempPath(), sourceAssemblyName),
        Path.GetFileNameWithoutExtension(assemblyName) + "_" + osBitness + "_" + assemblyFileVersion);

    string assemblyFile       = Path.Combine(assemblyPath, assemblyName);

    if (!File.Exists(assemblyFile))
    {
        if (!Directory.Exists(assemblyPath))
            Directory.CreateDirectory(assemblyPath);

        // Copy the embedded assembly file to the system's temporary folder.
        string resourceName = sourceAssemblyName + ".embed." + osBitness + "." + assemblyName;

        using (Stream stream = sourceAssembly.GetManifestResourceStream(resourceName))
        {
            using (Stream file = File.OpenWrite(assemblyFile))
            {
                CopyStream(stream, file);
            }
        }
        // ==

        // Delete the folder if the assembly's file version not matched, and return null.
        if (!FileVersionInfo.GetVersionInfo(assemblyFile).FileVersion.Equals(assemblyFileVersion))
        {
            Directory.Delete(assemblyPath, true);
            return null;
        }
        // ==
    }

    return Assembly.LoadFrom(assemblyFile);    // Load the assembly from the systems's temporary folder.
}

6. When the application initializes (or at your entry class constructor), register the AppDomain’s AssemblyResolve event:
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
Add the event handler:
internal static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    return MyUtil.ResolveAssemblyByOSBitness(
        Assembly.GetExecutingAssembly(), args.Name, "SampleAssembly.Sample.dll", "1.0.0.0");
}


Example:
This is an example that I used to load a 64-bit SQLite assembly (version 1.0.76) in a 64-bit OS. In the AssemblyResolve event handler, I would use:
return MyUtil.ResolveAssemblyByOSBitness(
    Assembly.GetExecutingAssembly(), args.Name, "System.Data.SQLite.DLL", "1.0.76");
Once this method has been called, the System.Data.SQLite.DLL will be loaded from the System's Temp folder in ...\Temp\MyAssembly\System.Data.SQLite_x64_1.0.76.0.

Note: MyAssembly is the assembly name where the MyUtil class is resided.


Additional Resources:
How to load an assembly at runtime that is located in a folder that is not the bin folder of the application (Method 3: Use the AssemblyResolve event)


If you find this post helpful, would you buy me a coffee?


No comments:

Post a Comment