将非托管dll嵌入托管C# dll

92

我有一个托管的C# dll,它使用DLLImport使用未管理的C++ dll。一切都很顺利。 然而,我想按照Microsoft的说明将那个未管理的DLL嵌入到我的托管DLL中:

http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.dllimportattribute.dllimportattribute.aspx

所以我将非托管的dll文件添加到我的托管dll项目中,将属性设置为“嵌入式资源”,并将DLLImport修改为类似以下内容:

[DllImport("Unmanaged Driver.dll, Wrapper Engine, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null",
CallingConvention = CallingConvention.Winapi)]

其中'Wrapper Engine'是我的托管DLL的程序集名称,'Unmanaged Driver.dll'是非托管DLL

运行时我得到:

访问被拒绝。(来自HRESULT的异常:0x80070005(E_ACCESSDENIED))

我从MSDN和http://blogs.msdn.com/suzcook/中了解到这应该是可能的...


1
你可以考虑使用BxILMerge来解决你的问题。 - MastAvalons
5个回答

66

如果您在初始化期间将未管理的DLL提取到临时目录中,并在使用P/Invoke之前显式加载它,则可以将其作为资源嵌入。我已经使用过这种技术,它很有效。您可能更喜欢像Michael指出的那样将其链接到程序集作为单独的文件,但将其全部放在一个文件中也有其优点。以下是我使用的方法:

// Get a temporary directory in which we can store the unmanaged DLL, with
// this assembly's version number in the path in order to avoid version
// conflicts in case two applications are running at once with different versions
string dirName = Path.Combine(Path.GetTempPath(), "MyAssembly." +
  Assembly.GetExecutingAssembly().GetName().Version.ToString());
if (!Directory.Exists(dirName))
  Directory.CreateDirectory(dirName);
string dllPath = Path.Combine(dirName, "MyAssembly.Unmanaged.dll");

// Get the embedded resource stream that holds the Internal DLL in this assembly.
// The name looks funny because it must be the default namespace of this project
// (MyAssembly.) plus the name of the Properties subdirectory where the
// embedded resource resides (Properties.) plus the name of the file.
using (Stream stm = Assembly.GetExecutingAssembly().GetManifestResourceStream(
  "MyAssembly.Properties.MyAssembly.Unmanaged.dll"))
{
  // Copy the assembly to the temporary file
  try
  {
    using (Stream outFile = File.Create(dllPath))
    {
      const int sz = 4096;
      byte[] buf = new byte[sz];
      while (true)
      {
        int nRead = stm.Read(buf, 0, sz);
        if (nRead < 1)
          break;
        outFile.Write(buf, 0, nRead);
      }
    }
  }
  catch
  {
    // This may happen if another process has already created and loaded the file.
    // Since the directory includes the version number of this assembly we can
    // assume that it's the same bits, so we just ignore the excecption here and
    // load the DLL.
  }
}

// We must explicitly load the DLL here because the temporary directory 
// is not in the PATH.
// Once it is loaded, the DllImport directives that use the DLL will use
// the one that is already loaded into the process.
IntPtr h = LoadLibrary(dllPath);
Debug.Assert(h != IntPtr.Zero, "Unable to load library " + dllPath);

LoadLibrary是使用来自kernel32的DLLImport吗?在WCF服务中使用相同代码时,Debug.Assert对我失败了。 - Klaus Nji
这是一个不错的解决方案,但如果能找到可靠的解决方法来处理两个应用程序同时尝试写入同一位置的情况,那就更好了。异常处理程序在另一个应用程序完成解压缩 DLL 之前就已经完成了。 - Robert Važan
这很完美。唯一不必要的是,directory.createdirectory已经在其内部具有目录存在检查。 - Gaspa79
我讨厌这个方法,它看起来像一个奇怪的黑客(我的意思是,保存临时文件,以不同的方式读取并忘记它),但我找不到更好的方法。 而且捕获所有异常只是一般般,如果我们想忽略这种文件存在的情况,我认为捕获“IOException”就足够了。尽管如此,这是一个很好的简单示例,谢谢。 - Adam Sałata

17

这是我的解决方案,是JayMcClellan答案的修改版。请将以下文件保存到class.cs文件中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.ComponentModel;

namespace Qromodyn
{
    /// <summary>
    /// A class used by managed classes to managed unmanaged DLLs.
    /// This will extract and load DLLs from embedded binary resources.
    /// 
    /// This can be used with pinvoke, as well as manually loading DLLs your own way. If you use pinvoke, you don't need to load the DLLs, just
    /// extract them. When the DLLs are extracted, the %PATH% environment variable is updated to point to the temporary folder.
    ///
    /// To Use
    /// <list type="">
    /// <item>Add all of the DLLs as binary file resources to the project Propeties. Double click Properties/Resources.resx,
    /// Add Resource, Add Existing File. The resource name will be similar but not exactly the same as the DLL file name.</item>
    /// <item>In a static constructor of your application, call EmbeddedDllClass.ExtractEmbeddedDlls() for each DLL that is needed</item>
    /// <example>
    ///               EmbeddedDllClass.ExtractEmbeddedDlls("libFrontPanel-pinv.dll", Properties.Resources.libFrontPanel_pinv);
    /// </example>
    /// <item>Optional: In a static constructor of your application, call EmbeddedDllClass.LoadDll() to load the DLLs you have extracted. This is not necessary for pinvoke</item>
    /// <example>
    ///               EmbeddedDllClass.LoadDll("myscrewball.dll");
    /// </example>
    /// <item>Continue using standard Pinvoke methods for the desired functions in the DLL</item>
    /// </list>
    /// </summary>
    public class EmbeddedDllClass
    {
        private static string tempFolder = "";

        /// <summary>
        /// Extract DLLs from resources to temporary folder
        /// </summary>
        /// <param name="dllName">name of DLL file to create (including dll suffix)</param>
        /// <param name="resourceBytes">The resource name (fully qualified)</param>
        public static void ExtractEmbeddedDlls(string dllName, byte[] resourceBytes)
        {
            Assembly assem = Assembly.GetExecutingAssembly();
            string[] names = assem.GetManifestResourceNames();
            AssemblyName an = assem.GetName();

            // The temporary folder holds one or more of the temporary DLLs
            // It is made "unique" to avoid different versions of the DLL or architectures.
            tempFolder = String.Format("{0}.{1}.{2}", an.Name, an.ProcessorArchitecture, an.Version);

            string dirName = Path.Combine(Path.GetTempPath(), tempFolder);
            if (!Directory.Exists(dirName))
            {
                Directory.CreateDirectory(dirName);
            }

            // Add the temporary dirName to the PATH environment variable (at the head!)
            string path = Environment.GetEnvironmentVariable("PATH");
            string[] pathPieces = path.Split(';');
            bool found = false;
            foreach (string pathPiece in pathPieces)
            {
                if (pathPiece == dirName)
                {
                    found = true;
                    break;
                }
            }
            if (!found)
            {
                Environment.SetEnvironmentVariable("PATH", dirName + ";" + path);
            }

            // See if the file exists, avoid rewriting it if not necessary
            string dllPath = Path.Combine(dirName, dllName);
            bool rewrite = true;
            if (File.Exists(dllPath)) {
                byte[] existing = File.ReadAllBytes(dllPath);
                if (resourceBytes.SequenceEqual(existing))
                {
                    rewrite = false;
                }
            }
            if (rewrite)
            {
                File.WriteAllBytes(dllPath, resourceBytes);
            }
        }

        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr LoadLibrary(string lpFileName);

        /// <summary>
        /// managed wrapper around LoadLibrary
        /// </summary>
        /// <param name="dllName"></param>
        static public void LoadDll(string dllName)
        {
            if (tempFolder == "")
            {
                throw new Exception("Please call ExtractEmbeddedDlls before LoadDll");
            }
            IntPtr h = LoadLibrary(dllName);
            if (h == IntPtr.Zero)
            {
                Exception e = new Win32Exception();
                throw new DllNotFoundException("Unable to load library: " + dllName + " from " + tempFolder, e);
            }
        }

    }
}

2
马克,这真的很酷。对于我的用途,我发现我可以删除LoadDll()方法,并在ExtractEmbeddedDlls()的末尾调用LoadLibrary()。这也使我能够删除修改路径的代码。 - Cameron

10

您可以尝试使用Costura.Fody。文档显示它能够处理非托管文件。我只使用它来处理托管文件,它运行得非常好:)


10

我之前并不知道这是可能的 - 我猜CLR需要将嵌入式本地DLL提取到某个地方(Windows需要有一个文件来加载它 - 它无法从原始内存加载图像),而无论它试图在哪里执行此操作,该进程都没有权限。

类似于SysInternals的Process Monitor的工具可以给您一个提示,如果创建DLL文件失败导致了问题 ...

更新:


啊...既然我已经能够阅读Suzanne Cook的文章(之前页面对我无法显示),请注意她谈论的不是将本地DLL作为资源嵌入托管DLL中,而是作为链接资源 - 本地DLL仍然需要成为文件系统中的自己的文件。

参见http://msdn.microsoft.com/en-us/library/xawyf94k.aspx,其中说:

 

资源文件未添加到输出文件中。这与/embed资源选项不同,后者会在输出文件中嵌入一个资源文件。

这似乎是向程序集添加元数据,使得本地DLL在逻辑上成为程序集的一部分(即使它在物理上是一个单独的文件)。 因此,像将托管程序集放入全局程序集缓存中将自动包括本地DLL等内容。


如何在Visual Studio中使用“linkresource”选项?我找不到任何示例。 - Alexey Subbota

4

你也可以将DLL文件复制到任何文件夹中,然后调用SetDllDirectory来指定该文件夹。这样就不需要调用LoadLibrary了。

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetDllDirectory(string lpPathName);

3
好主意,只是要注意可能会有安全风险,因为它打开了DLL注入的可能性,所以在高安全性环境中应该谨慎使用。 - yoel halb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接