Incorporer une dll non gérée dans une dll C # gérée

87

J'ai une dll C # gérée qui utilise une dll C ++ non gérée à l'aide de DLLImport. Tout fonctionne très bien. Cependant, je souhaite intégrer cette DLL non gérée dans ma DLL gérée, comme l'explique Microsoft ici:

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

J'ai donc ajouté le fichier dll non géré à mon projet de dll géré, définissez la propriété sur `` Ressource intégrée '' et modifiez le DLLImport en quelque chose comme:

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

où 'Wrapper Engine' est le nom d'assembly de ma DLL gérée 'Unmanaged Driver.dll' est la DLL non gérée

Quand je cours, j'obtiens:

L'accès est refusé. (Exception de HRESULT: 0x80070005 (E_ACCESSDENIED))

J'ai vu de MSDN et de http://blogs.msdn.com/suzcook/ que c'est censé être possible ...

DimaSan
la source
1
Vous pouvez envisager BxILMerge pour votre cas
MastAvalons

Réponses:

64

Vous pouvez incorporer la DLL non gérée en tant que ressource si vous l'extrayez vous-même dans un répertoire temporaire lors de l'initialisation et la chargez explicitement avec LoadLibrary avant d'utiliser P / Invoke. J'ai utilisé cette technique et cela fonctionne bien. Vous préférerez peut-être simplement le lier à l'assemblage dans un fichier séparé, comme l'a noté Michael, mais le fait de tout avoir dans un seul fichier présente ses avantages. Voici l'approche que j'ai utilisée:

// 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
la source
LoadLibrary utilise-t-il DLLImport de kenel32? Debug.Assert échoue pour moi en utilisant le même code dans le service WCF.
Klaus Nji
C'est une bonne solution, mais il serait encore mieux de trouver une résolution fiable pour les cas où deux applications tentent d'écrire au même emplacement en même temps. Le gestionnaire d'exceptions se termine avant que l'autre application ait fini de décompresser la DLL.
Robert Važan le
C'est parfait. La seule chose inutile est que directory.createdirectory a déjà le répertoire existe vérifier à l'intérieur de celui
Gaspa79
13

Voici ma solution, qui est une version modifiée de la réponse de JayMcClellan. Enregistrez le fichier ci-dessous dans un fichier 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
la source
2
Mark, c'est vraiment cool. Pour mes utilisations, j'ai trouvé que je pouvais supprimer la méthode LoadDll () et appeler LoadLibrary () à la fin de ExtractEmbeddedDlls (). Cela m'a également permis de supprimer le code de modification de PATH.
Cameron
9

Je ne savais pas que c'était possible - je suppose que le CLR doit extraire la DLL native intégrée quelque part (Windows a besoin d'un fichier pour que la DLL la charge - il ne peut pas charger une image à partir de la mémoire brute), et où il essaie de faire que le processus n'a pas la permission.

Quelque chose comme Process Monitor de SysInternals pourrait vous donner un indice si le problème est que la création du fichier DLL échoue ...

Mettre à jour:


Ah ... maintenant que j'ai pu lire l'article de Suzanne Cook (la page ne m'est pas venue auparavant), notez qu'elle ne parle pas d'incorporer la DLL native en tant que ressource dans la DLL gérée, mais plutôt en tant que ressource liée - la DLL native doit toujours être son propre fichier dans le système de fichiers.

Voir http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , où il est dit:

Le fichier de ressources n'est pas ajouté au fichier de sortie. Cela diffère de l'option / resource qui incorpore un fichier de ressources dans le fichier de sortie.

Ce que cela semble faire, c'est ajouter des métadonnées à l'assembly qui font que la DLL native fasse logiquement partie de l'assembly (même s'il s'agit physiquement d'un fichier distinct). Ainsi, des choses comme mettre l'assembly géré dans le GAC incluront automatiquement la DLL native, etc.

Michael Burr
la source
Comment utiliser les options "linkresource" dans Visual Studio? Je ne trouve aucun exemple.
Alexey Subbota
9

Vous pouvez essayer Costura.Fody . La documentation dit qu'il est capable de gérer des fichiers non gérés. Je ne l'ai utilisé que pour les fichiers gérés, et cela fonctionne comme un charme :)

Matthias
la source
4

On peut également simplement copier les DLL dans n'importe quel dossier, puis appeler SetDllDirectory dans ce dossier. Aucun appel à LoadLibrary n'est alors nécessaire.

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetDllDirectory(string lpPathName);
Ziriax
la source
1
Excellente idée, notez simplement que cela pourrait avoir des ramifications de sécurité, car il s'ouvre pour l'injection de dll, donc dans un environnement de haute sécurité, il doit être utilisé avec prudence
yoel halb