Using Side-By-Side Assemblies to Load the X64 or X32 Version of a Dll

Using Side-by-Side assemblies to load the x64 or x32 version of a DLL

I created a simple solution that is able to load platform-specific assembly from an executable compiled as AnyCPU. The technique used can be summarized as follows:

  1. Make sure default .NET assembly loading mechanism ("Fusion" engine) can't find either x86 or x64 version of the platform-specific assembly
  2. Before the main application attempts loading the platform-specific assembly, install a custom assembly resolver in the current AppDomain
  3. Now when the main application needs the platform-specific assembly, Fusion engine will give up (because of step 1) and call our custom resolver (because of step 2); in the custom resolver we determine current platform and use directory-based lookup to load appropriate DLL.

To demonstrate this technique, I am attaching a short, command-line based tutorial. I tested the resulting binaries on Windows XP x86 and then Vista SP1 x64 (by copying the binaries over, just like your deployment).

Note 1: "csc.exe" is a C-sharp compiler. This tutorial assumes it is in your path (my tests were using "C:\WINDOWS\Microsoft.NET\Framework\v3.5\csc.exe")

Note 2: I recommend you create a temporary folder for the tests and run command line (or powershell) whose current working directory is set to this location, e.g.

(cmd.exe)
C:
mkdir \TEMP\CrossPlatformTest
cd \TEMP\CrossPlatformTest

Step 1: The platform-specific assembly is represented by a simple C# class library:

// file 'library.cs' in C:\TEMP\CrossPlatformTest
namespace Cross.Platform.Library
{
public static class Worker
{
public static void Run()
{
System.Console.WriteLine("Worker is running");
System.Console.WriteLine("(Enter to continue)");
System.Console.ReadLine();
}
}
}

Step 2: We compile platform-specific assemblies using simple command-line commands:

(cmd.exe from Note 2)
mkdir platform\x86
csc /out:platform\x86\library.dll /target:library /platform:x86 library.cs
mkdir platform\amd64
csc /out:platform\amd64\library.dll /target:library /platform:x64 library.cs

Step 3: Main program is split into two parts. "Bootstrapper" contains main entry point for the executable and it registers a custom assembly resolver in current appdomain:

// file 'bootstrapper.cs' in C:\TEMP\CrossPlatformTest
namespace Cross.Platform.Program
{
public static class Bootstrapper
{
public static void Main()
{
System.AppDomain.CurrentDomain.AssemblyResolve += CustomResolve;
App.Run();
}

private static System.Reflection.Assembly CustomResolve(
object sender,
System.ResolveEventArgs args)
{
if (args.Name.StartsWith("library"))
{
string fileName = System.IO.Path.GetFullPath(
"platform\\"
+ System.Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")
+ "\\library.dll");
System.Console.WriteLine(fileName);
if (System.IO.File.Exists(fileName))
{
return System.Reflection.Assembly.LoadFile(fileName);
}
}
return null;
}
}
}

"Program" is the "real" implementation of the application (note that App.Run was invoked at the end of Bootstrapper.Main):

// file 'program.cs' in C:\TEMP\CrossPlatformTest
namespace Cross.Platform.Program
{
public static class App
{
public static void Run()
{
Cross.Platform.Library.Worker.Run();
}
}
}

Step 4: Compile the main application on command line:

(cmd.exe from Note 2)
csc /reference:platform\x86\library.dll /out:program.exe program.cs bootstrapper.cs

Step 5: We're now finished. The structure of the directory we created should be as follows:

(C:\TEMP\CrossPlatformTest, root dir)
platform (dir)
amd64 (dir)
library.dll
x86 (dir)
library.dll
program.exe
*.cs (source files)

If you now run program.exe on a 32bit platform, platform\x86\library.dll will be loaded; if you run program.exe on a 64bit platform, platform\amd64\library.dll will be loaded. Note that I added Console.ReadLine() at the end of the Worker.Run method so that you can use task manager/process explorer to investigate loaded DLLs, or you can use Visual Studio/Windows Debugger to attach to the process to see the call stack etc.

When program.exe is run, our custom assembly resolver is attached to current appdomain. As soon as .NET starts loading the Program class, it sees a dependency on 'library' assembly, so it tries loading it. However, no such assembly is found (because we've hidden it in platform/* subdirectories). Luckily, our custom resolver knows our trickery and based on the current platform it tries loading the assembly from appropriate platform/* subdirectory.

Load x64 or a x86 DLL depending upon the platform?

If we are talking about unmanaged DLLs, declare the p/invokes like this:

[DllImport("DllName.dll")]
static extern foo();

Note that we are not specifying a path to the DLL, just its name, which I presume is the same for both 32 and 64 bit versions.

Then, before you call any of your p/invokes, load the library into your process. Do that by p/invoking to the LoadLibrary API function. At this point you will determine whether your process is 32 or 64 bit and build the full path to the DLL accordingly. That full path is what you pass to LoadLibrary.

Now, when you call your p/invokes for the library, they will be resolved by the module that you have just loaded.

For managed assemblies then you can use Assembly.LoadFile to specify the path of the assembly. This can be a little tricky to orchestrate, but this excellent article shows you how: Automatically Choose 32 or 64 Bit Mixed Mode DLLs. There are a lot of details relating to mixed mode and the native DLL dependencies that are probably not relevant to you. The key is the AppDomain.CurrentDomain.AssemblyResolve event handler.

Late Binding to dll based on CPU architecture

If the target DLLs only vary by the target architecture and the assemblies are not strongly named, an interface is not necessary.

My suggestion is to name them *_64.dll and *_86.dll respectively and pick one to compile against.

At runtime, all you need to do is System.Reflection.Assembly.LoadFile the correct file.

32 or 64 bit DLL loading from .Net managed code

P/Invoke uses LoadLibrary to load DLLs, and if there is already a library loaded with a given name, LoadLibrary will return it. So if you can give both versions of the DLL the same name, but put them in different directories, you can do something like this just once before your first call to a function from scilexer.dll, without needing to duplicate your extern declarations:

    string platform = IntPtr.Size == 4 ? "x86" : "x64";
string dll = installDir + @"\lib-" + platform + @"\scilexer.dll";
if (LoadLibrary(dll) == IntPtr.Zero)
throw new IOException("Unable to load " + dll + ".");

Is it possible to have win32 and x64 versions of native DLL in one file?

This is not possible. The bitness of the code is indicated in the header of the DLL. The Machine field in the IMAGE_FILE_HEADER structure. There can be only one header. This is never a problem in practice, you simply deploy the right file with the installer.

How can I determine if a .NET assembly was built for x86 or x64?

Look at System.Reflection.AssemblyName.GetAssemblyName(string assemblyFile).

You can examine assembly metadata from the returned AssemblyName instance:

Using PowerShell:


[36] C:\> [reflection.assemblyname]::GetAssemblyName("${pwd}\Microsoft.GLEE.dll") | fl

Name : Microsoft.GLEE
Version : 1.0.0.0
CultureInfo :
CodeBase : file:///C:/projects/powershell/BuildAnalyzer/...
EscapedCodeBase : file:///C:/projects/powershell/BuildAnalyzer/...
ProcessorArchitecture : MSIL
Flags : PublicKey
HashAlgorithm : SHA1
VersionCompatibility : SameMachine
KeyPair :
FullName : Microsoft.GLEE, Version=1.0.0.0, Culture=neut...

Here, ProcessorArchitecture identifies the target platform.

  • Amd64: A 64-bit processor based on the x64 architecture.
  • Arm: An ARM processor.
  • IA64: A 64-bit Intel Itanium processor only.
  • MSIL: Neutral with respect to processor and bits-per-word.
  • X86: A 32-bit Intel processor, either native or in the Windows on Windows environment on a 64-bit platform (WoW64).
  • None: An unknown or unspecified combination of processor and bits-per-word.

I'm using PowerShell in this example to call the method.



Related Topics



Leave a reply



Submit