Incorporando dll não gerenciado em um dll C # gerenciado

87

Eu tenho uma dll C # gerenciada que usa uma dll C ++ não gerenciada usando DLLImport. Tudo está funcionando muito bem. No entanto, desejo incorporar essa DLL não gerenciada em minha DLL gerenciada, conforme explicado pela Microsoft lá:

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

Portanto, adicionei o arquivo dll não gerenciado ao meu projeto de dll gerenciado, defina a propriedade como 'Recurso incorporado' e modifique o DLLImport para algo como:

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

onde 'Wrapper Engine' é o nome do assembly de minha DLL gerenciada, 'Unmanaged Driver.dll' é a DLL não gerenciada

Quando corro, recebo:

Acesso negado. (Exceção de HRESULT: 0x80070005 (E_ACCESSDENIED))

Vi no MSDN e em http://blogs.msdn.com/suzcook/ que isso deveria ser possível ...

DimaSan
fonte
1
Você pode considerar o BxILMerge para o seu caso
MastAvalons

Respostas:

64

Você pode incorporar a DLL não gerenciada como um recurso se você mesmo extraí-la em um diretório temporário durante a inicialização e carregá-la explicitamente com LoadLibrary antes de usar P / Invoke. Usei essa técnica e funciona bem. Você pode preferir apenas vinculá-lo à montagem como um arquivo separado, como Michael observou, mas ter tudo em um arquivo tem suas vantagens. Esta é a abordagem que usei:

// 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);
JayMcClellan
fonte
O LoadLibrary está usando o DLLImport do kenel32? Debug.Assert está falhando para mim usando o mesmo código no serviço WCF.
Klaus Nji
Esta é uma boa solução, mas seria ainda melhor encontrar uma resolução confiável para os casos em que dois aplicativos tentam gravar no mesmo local ao mesmo tempo. O manipulador de exceção é concluído antes que o outro aplicativo termine de descompactar a DLL.
Robert Važan
Isto é perfeito. A única coisa desnecessária é que o diretório.createdirectory já tem o diretório existe verifique dentro dele
Gaspa79
13

Aqui está minha solução, que é uma versão modificada da resposta de JayMcClellan. Salve o arquivo abaixo em um arquivo 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);
            }
        }

    }
}
Mark Lakata
fonte
2
Mark, isso é muito legal. Para meu uso, descobri que poderia remover o método LoadDll () e chamar LoadLibrary () no final de ExtractEmbeddedDlls (). Isso também me permitiu remover o código de modificação de PATH.
Cameron
9

Eu não sabia que isso é possível - acho que o CLR precisa extrair a DLL nativa incorporada em algum lugar (o Windows precisa ter um arquivo para a DLL carregá-la - não pode carregar uma imagem da memória bruta) e onde quer que está tentando fazer que o processo não tenha permissão.

Algo como o Process Monitor da SysInternals pode lhe dar uma pista se o problema for que a criação do arquivo DLL está falhando ...

Atualizar:


Ah ... agora que li o artigo de Suzanne Cook (a página não apareceu para mim antes), observe que ela não está falando sobre incorporar a DLL nativa como um recurso dentro da DLL gerenciada, mas sim como um recurso vinculado - a DLL nativa ainda precisa ser seu próprio arquivo no sistema de arquivos.

Consulte http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , onde diz:

O arquivo de recurso não é adicionado ao arquivo de saída. Isso difere da opção / resource que incorpora um arquivo de recurso no arquivo de saída.

O que isso parece fazer é adicionar metadados ao assembly que faz com que a DLL nativa faça parte logicamente do assembly (mesmo que seja um arquivo fisicamente separado). Portanto, coisas como colocar o assembly gerenciado no GAC incluirão automaticamente a DLL nativa, etc.

Michael Burr
fonte
Como usar as opções de "linkresource" no Visual Studio? Não consigo encontrar nenhum exemplo.
Alexey Subbota
9

Você pode experimentar Costura.Fody . A documentação diz que ele é capaz de lidar com arquivos não gerenciados. Usei-o apenas para arquivos gerenciados e funciona perfeitamente :)

Matthias
fonte
4

Também é possível simplesmente copiar as DLLs para qualquer pasta e, em seguida, chamar SetDllDirectory para essa pasta. Nenhuma chamada para LoadLibrary é necessária então.

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetDllDirectory(string lpPathName);
Ziriax
fonte
1
Ótima ideia, apenas observe que isso pode ter ramificações de segurança, pois abre para injeção de dll, portanto, em um ambiente de alta segurança, deve ser usado com cautela
yoel halb