When loading unmanaged / native libraries from managed .NET code, the normal way is to use the “Platform Invoke (P/Invoke)” mechanism.
1[DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi)]
2static extern int DllGetVersion(ref DLLVERSIONINFO pdvi);
P/Invoke has an annoying limitation: You can not load an x86 (32bit) library into a 64bit process and vice versa. This is especially problematic when your application is compiled with the “AnyCPU”-flag - which lets the .NET runtime decide which architecture to use at runtime - and there is only one 32bit version of a specific DLL. If recompiling the library against a different architecture is not an option, you have to find another solution.
Since we can’t load 32bit code into our 64bit process, the idea is to create a separate executable for this task. It would somehow load a library, invoke a specific function and pass the results back to the caller. So this is what the Legacy Wrapper (Link points to GitHub) does. See this example:
1// Define delegate matching API function
2private delegate int GetSystemMetrics(int index);
3
4// Create new WrapperClient
5// Remember to ensure a call to the Dispose()-Method!
6using (var client = new WrapperClient())
7{
8 // Make calls providing library name, function name, and parameters
9 int x = (int)client.Invoke<GetSystemMetrics>("User32.dll", "GetSystemMetrics", new object[] { 0 });
10 int y = (int)client.Invoke<GetSystemMetrics>("User32.dll", "GetSystemMetrics", new object[] { 1 });
11}
How does this work? The Legacy Wrapper consists of an executable (the “server”) and a class library (the “client”). With creating a new WrapperClient
instance, the library would at first create a random token and pass it as an argument when starting the “server” executable.
1string token = Guid.NewGuid().ToString();
2
3// Pass token to child process
4Process.Start("LegacyWrapper", token);
The executable will use this token to create a new named pipe and wait for incoming connections:
1static void Main(string[] args)
2{
3 if (args.Length < 1) return;
4
5 string token = args[0];
6
7 // Create new named pipe with token from client
8 using (var pipe = new NamedPipeServerStream(token, PipeDirection.InOut, 1, PipeTransmissionMode.Message))
9 {
10 pipe.WaitForConnection();
Now, the client will send binary serialized information about the desired library to the server:
1var info = new CallData
2{
3 Library = library,
4 ProcedureName = function,
5 Parameters = args,
6 Delegate = typeof(T),
7};
8var formatter = new BinaryFormatter();
9// Write request to server
10formatter.Serialize(_pipe, info);
If the called DLL function returns any result, the server will eventually write it to the pipe - after that, the Invoke<T>()
method returns. If an exception occurs, it will also be serialized and re-thrown client side.
Named pipes are by default public and visible to anyone who knows its name. The following screenshot was taken using Process Explorer:
This includes read / write access to all visible pipes. So at the moment you should NOT use the Legacy Wrapper for cryptographic / security purposes, as anyone could connect to the open pipe and read your keys, ciphertext etc.
Check out the GitHub repository for more technical details. Feel free to submit any suggestions/issues and contribute to the project.