Einbetten einer nicht verwalteten DLL in eine verwaltete C # -Dll

87

Ich habe eine verwaltete C # -Dll, die eine nicht verwaltete C ++ - DLL mit DLLImport verwendet. Alles funktioniert super. Ich möchte diese nicht verwaltete DLL jedoch in meine verwaltete DLL einbetten, wie von Microsoft dort erläutert:

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

Also habe ich die nicht verwaltete DLL-Datei zu meinem verwalteten DLL-Projekt hinzugefügt, die Eigenschaft auf 'Embedded Resource' gesetzt und den DLLImport wie folgt geändert:

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

Dabei ist 'Wrapper Engine' der Assemblyname meiner verwalteten DLL. 'Unmanaged Driver.dll' ist die nicht verwaltete DLL

Wenn ich renne, bekomme ich:

Der Zugriff wird verweigert. (Ausnahme von HRESULT: 0x80070005 (E_ACCESSDENIED))

Ich habe von MSDN und von http://blogs.msdn.com/suzcook/ gesehen, dass das möglich sein soll ...

DimaSan
quelle
1
Sie können BxILMerge für Ihren Fall in Betracht ziehen
MastAvalons

Antworten:

64

Sie können die nicht verwaltete DLL als Ressource einbetten, wenn Sie sie während der Initialisierung selbst in ein temporäres Verzeichnis extrahieren und vor der Verwendung von P / Invoke explizit mit LoadLibrary laden. Ich habe diese Technik verwendet und sie funktioniert gut. Sie können es vorziehen, es einfach als separate Datei mit der Assembly zu verknüpfen, wie Michael bemerkte, aber alles in einer Datei zu haben, hat seine Vorteile. Hier ist der Ansatz, den ich verwendet habe:

// 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
quelle
Verwendet LoadLibrary DLLImport von kenel32? Debug.Assert schlägt fehl, wenn ich denselben Code innerhalb des WCF-Dienstes verwende.
Klaus Nji
Dies ist eine gute Lösung, aber es wäre noch besser, eine zuverlässige Lösung für Fälle zu finden, in denen zwei Anwendungen gleichzeitig versuchen, an denselben Speicherort zu schreiben. Der Ausnahmebehandler wird abgeschlossen, bevor die andere Anwendung das Entpacken der DLL abgeschlossen hat.
Robert Važan
Dies ist perfekt. Das einzig Unnötige ist, dass in directory.createdirectory das Verzeichnis bereits vorhanden ist. Überprüfen Sie darin
Gaspa79
13

Hier ist meine Lösung, die eine modifizierte Version von JayMcClellans Antwort ist. Speichern Sie die folgende Datei in einer class.cs-Datei.

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
quelle
2
Mark, das ist wirklich cool. Für meine Zwecke stellte ich fest, dass ich die LoadDll () -Methode entfernen und LoadLibrary () am Ende von ExtractEmbeddedDlls () aufrufen konnte. Dadurch konnte ich auch den PATH-Änderungscode entfernen.
Cameron
9

Ich wusste nicht, dass dies möglich ist - ich würde vermuten, dass die CLR die eingebettete native DLL irgendwo extrahieren muss (Windows benötigt eine Datei, damit die DLL sie laden kann - sie kann kein Bild aus dem Rohspeicher laden) und wo auch immer Es wird versucht, dass der Prozess keine Berechtigung hat.

So etwas wie Process Monitor von SysInternals gibt Ihnen möglicherweise einen Hinweis, wenn das Pronblem lautet, dass das Erstellen der DLL-Datei fehlschlägt ...

Aktualisieren:


Ah ... jetzt, da ich den Artikel von Suzanne Cook lesen konnte (die Seite ist mir vorher nicht erschienen), beachte, dass sie nicht über das Einbetten der nativen DLL als Ressource in die verwaltete DLL spricht, sondern vielmehr als verknüpfte Ressource - Die native DLL muss weiterhin eine eigene Datei im Dateisystem sein.

Siehe http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , wo es heißt:

Die Ressourcendatei wird nicht zur Ausgabedatei hinzugefügt. Dies unterscheidet sich von der Option / resource, mit der eine Ressourcendatei in die Ausgabedatei eingebettet wird.

Dies scheint der Assembly Metadaten hinzuzufügen, die dazu führen, dass die native DLL logisch Teil der Assembly ist (obwohl es sich physisch um eine separate Datei handelt). Dinge wie das Einfügen der verwalteten Assembly in den GAC enthalten also automatisch die native DLL usw.

Michael Burr
quelle
Wie verwende ich "Linkresource" -Optionen in Visual Studio? Ich kann keine Beispiele finden.
Alexey Subbota
9

Sie können Costura.Fody ausprobieren . Die Dokumentation besagt, dass nicht verwaltete Dateien verarbeitet werden können. Ich habe es nur für verwaltete Dateien verwendet und es funktioniert wie ein Zauber :)

Matthias
quelle
4

Man könnte die DLLs auch einfach in einen beliebigen Ordner kopieren und dann SetDllDirectory in diesen Ordner aufrufen . Dann ist kein Aufruf von LoadLibrary erforderlich.

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetDllDirectory(string lpPathName);
Ziriax
quelle
1
Tolle Idee, beachten Sie nur, dass dies Sicherheitsauswirkungen haben kann, da es sich für die DLL-Injektion öffnet. In einer Hochsicherheitsumgebung sollte es daher mit Vorsicht verwendet werden
yoel halb