Recently, a customer came up with a straightforward deployment problem:
I want to deploy an assembly that wraps a native DLL in the GAC. Deploying the wrapped native assembly in System32 is to error prone, deploying the wrapped assembly in WinSxS is too much of an overhead. How can I ensure that the managed wrapper can load the native assembly?
I have thought about a solution to this problem some times, but I have to admit, even though my first idea was right, so far I never found the time to prove that my guess really works. Today I took the time to take a closer look at the solution.
Here is was my initial idea:
From inspecting mscorlib with ILDASM, I knew that an assembly can have links to other files.
.assembly mscorlib
{
… attributes ignored here …
.file nometadata sortkey.nlp
.hash = (6E 30 2E 50 36 FB 60 2C 8E 50 C0 83 54 8B CC EA)
.file nometadata sorttbls.nlp
.hash = (F9 04 A8 31 CD FB 23 72 95 63 5D 14 9A A0 66 8E)
… other .file entries ignored here …
.mresource public sortkey.nlp
{
.file sortkey.nlp at 0x00000000
}
.mresource public sorttbls.nlp
{
.file sorttbls.nlp at 0x00000000
}
}
Furthermore you can see that the files are installed together with mscorlib.dll in the GAC:
dir c:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089
4.550.656 mscorlib.dll
262.148 sortkey.nlp
20.320 sorttbls.nlp
… other files ignored here …
My assumption was that if you can install native DLLs in the same way in the GAC, they can be magically found when needed. Creating assemblies with .file references is easy. Assume the following simple native DLLs:
nativedll2.cpp, compile with CL /LD /EHs nativedll2.cpp
#include
using namespace std;
void __declspec(dllexport) fNative2()
{
cout << "fNative2 called." << endl;
}
nativedll.cpp, compile with CL /LD /EHs nativedll.cpp
#include
#include
using namespace std;
#pragma comment(lib, "nativeDll2.lib")
void __declspec(dllimport) fNative2();
void __declspec(dllexport) fNative()
{
cout << "fNative called." << endl;
fNative2();
}
A wrapper assembly for nativedll.cpp can be created with this source code
testasm.dll:
using namespace System;
#pragma comment(lib, "nativedll.lib")
void __declspec(dllimport) fNative();
public ref class Test
{
public:
void DoSth()
{
Console::WriteLine("Test.DoSth called. Calling native DLL now.");
fNative();
}
};
To compile it with the two native DLLs as linked files, use the following command line:
cl /LD /clr testasm.cpp /link /keyfile:..\keyfile.snk /assemblylinkresource:nativedll.dll /assemblylinkresource:nativedll2.dll
Notice that the linker flag /assemblylinkresource adds the .file entries to the assembly manifest:
.file nometadata nativedll.dll
.hash = (EA 16 B4 08 D1 C4 D2 C0 77 CB 00 C7 93 A0 85 4A 41 BC F2 52 )
.file nometadata nativedll2.dll
.hash = (9D D1 39 D0 4C F9 08 7E 33 2F CD D2 8B C3 3E 55 BD E1 81 27 )
.mresource public nativedll.dll
{
.file nativedll.dll at 0x00000000
}
.mresource public nativedll2.dll
{
.file nativedll2.dll at 0x00000000
}
Given that the DLL is signed, it can be placed into the GAC:
Gacutil –i testasm.dll
Notice the .hash metadata for each .file entry. According to my tests, this metadata is not used for assembly validation (e. g. via SN.EXE –v testasm.dll), but during the installation of the assembly into the GAC. If you modify a linked file before adding the assembly to the GAC, GACUtil will report the following error:
Failure adding assembly to the cache: One or more modules were streamed in which did not match those specified by the manifest. The hash of one or more modules found does not match the hash recorded in the manifest.
Inspecting the assembly’s GAC_32 subdirectory after successfully installing it to the GAC shows the following files:
105.472 nativedll.dll
105.472 nativedll2.dll
24.064 testasm.dll
Obviously, the native DLLs are now installed into the GAC. The interesting question is: Are they really loaded from the GAC? The answer is yes. To prove this, use the following simple client application:
Testapp.cpp (should be in a different directory)
using namespace System;
int main()
{
Console::WriteLine("calling Test.DoSth now");
Test t;
t.DoSth();
Console::WriteLine("Press enter to stop app");
Console::ReadLine();
}
You can compile this file with the following command line:
cl /clr testapp.cpp /FU …relative path to testasm.dll …
Executing testapp.exe should work even when nativedll.dll and nativedll2.dll are not in a location that LoadLibrary normally uses; they are loaded from the GAC. If you want to verify this, compile all sources as shown, run testapp.exe and attach a debugger while the app is waiting at Console::ReadLine. In the debugger, you can see where dependent DLLs are loaded from (via the WinDBG command “lm –v” or the VS modules window).