C# wrapper library around unmanaged DLL requires u

2019-08-07 21:11发布

问题:

Usually when referencing a managed DLL that wraps an unmanaged DLL with PInvoke, you have to reference those two DLLs separately - the managed one as a standard <Reference/> in your csproj and the unmanaged one as linked <content/> (as outlined here). However, I recently came across a managed wrapper library which not only auto-copies the unmanaged DLL along with it during the build, but actually produces a build error when the unmanaged DLL is not present in the same directory! This is the Microsoft.Z3 library, which has a managed DLL (Microsoft.Z3.dll) wrapping an unmanaged DLL (libz3.dll) with PInvoke so you can use the library in C#.

If you put the two Z3 DLLs together in a directory, referencing only Microsoft.Z3.dll, then compile your project with msbuild, you'll get both DLLs in the output directory without referencing libz3.dll at all! Looking in the output produced by msbuild /verbosity:diag, I see the following references to libz3.dll:

Primary reference "Microsoft.Z3, Version=4.7.1.0, Culture=neutral, PublicKeyToken=9c8d792caae602a2". (TaskId:9)
      Resolved file path is "C:\Users\ahelwer\source\test\Framework\lib\z3\Microsoft.Z3.dll". (TaskId:9)
      Reference found at search path location "{HintPathFromItem}". (TaskId:9)
      Found embedded scatter file "libz3.dll". (TaskId:9)
      The ImageRuntimeVersion for this reference is "v4.0.30319". (TaskId:9)

...

Output Item(s): 
      _ReferenceScatterPaths=
          C:\Users\ahelwer\source\test\Framework\lib\z3\libz3.dll
                  CopyLocal=true
                  FusionName=
                  HintPath=lib\z3\Microsoft.Z3.dll
                  OriginalItemSpec=C:\Users\ahelwer\source\test\Framework\lib\z3\Microsoft.Z3.dll
                  ResolvedFrom={HintPathFromItem}
                  Version=4.7.1.0 (TaskId:9)

Which somehow leads to it being copied:

Task "Copy" (TaskId:22)
  Task Parameter:
      SourceFiles=
          C:\Users\ahelwer\source\test\Framework\lib\z3\Microsoft.Z3.dll
                  CopyLocal=true
                  FusionName=Microsoft.Z3, Version=4.7.1.0, Culture=neutral, PublicKeyToken=9c8d792caae602a2
                  HintPath=lib\z3\Microsoft.Z3.dll
                  ImageRuntime=v4.0.30319
                  OriginalItemSpec=Microsoft.Z3
                  ReferenceSourceTarget=ResolveAssemblyReference
                  ResolvedFrom={HintPathFromItem}
                  Version=4.7.1.0
          C:\Users\ahelwer\source\test\Framework\lib\z3\libz3.dll
                  CopyLocal=true
                  FusionName=
                  HintPath=lib\z3\Microsoft.Z3.dll
                  OriginalItemSpec=C:\Users\ahelwer\source\test\Framework\lib\z3\Microsoft.Z3.dll
                  ResolvedFrom={HintPathFromItem}
                  Version=4.7.1.0 (TaskId:22)
  Task Parameter:
      DestinationFiles=
          bin\Debug\Microsoft.Z3.dll
                  CopyLocal=true
                  FusionName=Microsoft.Z3, Version=4.7.1.0, Culture=neutral, PublicKeyToken=9c8d792caae602a2
                  HintPath=lib\z3\Microsoft.Z3.dll
                  ImageRuntime=v4.0.30319
                  OriginalItemSpec=Microsoft.Z3
                  ReferenceSourceTarget=ResolveAssemblyReference
                  ResolvedFrom={HintPathFromItem}
                  Version=4.7.1.0
          bin\Debug\libz3.dll
                  CopyLocal=true
                  FusionName=
                  HintPath=lib\z3\Microsoft.Z3.dll
                  OriginalItemSpec=C:\Users\ahelwer\source\test\Framework\lib\z3\Microsoft.Z3.dll
                  ResolvedFrom={HintPathFromItem}
                  Version=4.7.1.0 (TaskId:22)

It gets more mysterious, because if I take libz3.dll out of the directory, the build fails with the following error:

"C:\Users\ahelwer\source\test\Framework\FrameworkTest.csproj" (default target) (1) ->
(_CopyFilesMarkedCopyLocal target) ->
  C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\bin\Microsoft.Common.CurrentVersion.targe
ts(4358,5): error MSB3030: Could not copy the file "C:\Users\ahelwer\source\test\Framework\lib\z3\libz3.dll" because it
 was not found. [C:\Users\ahelwer\source\test\Framework\FrameworkTest.csproj]

Even if I reference libz3.dll in the standard way with <content/> in my csproj!

Questions:

  1. What is special about Microsoft.Z3.dll that enables it to require libz3.dll be in the same directory during build? Was it compiled with certain flags?
  2. How can I add this effect to a managed wrapper library of my own?
  3. Is there any way to remove this effect from Microsoft.Z3.dll, or would I have to recompile it in a different way?

回答1:

This is what's CSC (the C# compiler) calls a "link resource". It works with any type of file.

So for example, if you have this kind of code in a DLL project:

using System.Runtime.InteropServices;

namespace Microsoft.Z3
{
    public static class DoSomething
    {
        [DllImport("libz3.dll")]
        public static extern int ReturnValue(int value);
    }
}

And this C code exported from a Windows DLL:

#include "stdafx.h"

STDAPI ReturnValue(HRESULT value)
{
    return value;
}

you can build the .NET DLL like this:

"<path to csc.exe>\csc.exe" DoSomething.cs -out:Microsoft.Z3.dll -target:library -linkresource:<path to libz3.dll>\libz3.dll

Now, when you reference this new Microsoft.Z3.dll, it will behave the same way as the real Z3 thing, it will copy the libz3.dll aside automatically.

Note AFAIK, Visual Studio has no support for this linkresource stuff.

Plus one other drawback is if you want to support multiple bitnesses, you'll have to ship two .NET DLL, one for x64 and one for x86, each one embedding its native counterpart (or you'll have to duplicate all DllImport stuff etc.).