Incorporamento di DLL non gestite in una DLL C # gestita


87

Ho una dll C # gestita che utilizza una dll C ++ non gestita utilizzando DLLImport. Funziona tutto alla grande. Tuttavia, voglio incorporare quella DLL non gestita all'interno della mia DLL gestita come spiegato da Microsoft lì:

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

Quindi ho aggiunto il file dll non gestito al mio progetto dll gestito, ho impostato la proprietà su "Risorsa incorporata" e ho modificato DLLImport in qualcosa di simile:

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

dove "Wrapper Engine" è il nome dell'assembly della mia DLL gestita "Unmanaged Driver.dll" è la DLL non gestita

Quando corro, ottengo:

L'accesso è negato. (Eccezione da HRESULT: 0x80070005 (E_ACCESSDENIED))

Ho visto da MSDN e da http://blogs.msdn.com/suzcook/ che dovrebbe essere possibile ...



1
Puoi prendere in considerazione BxILMerge per il tuo caso
MastAvalons

Risposte:


64

È possibile incorporare la DLL non gestita come risorsa se la si estrae in una directory temporanea durante l'inizializzazione e caricarla esplicitamente con LoadLibrary prima di utilizzare P / Invoke. Ho usato questa tecnica e funziona bene. Potresti preferire collegarlo all'assembly come file separato come ha notato Michael, ma avere tutto in un file ha i suoi vantaggi. Ecco l'approccio che ho usato:

// 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 utilizza DLLImport da kenel32? Debug.Assert non riesce per me utilizzando lo stesso codice all'interno del servizio WCF.
Klaus Nji

Questa è una buona soluzione, ma sarebbe ancora meglio trovare una risoluzione affidabile per i casi in cui due applicazioni tentano di scrivere nella stessa posizione contemporaneamente. Il gestore di eccezioni viene completato prima che l'altra applicazione termini di decomprimere la DLL.
Robert Važan

Questo è perfetto. L'unica cosa non necessaria è che directory.createdirectory ha già la directory esiste controlla al suo interno
Gaspa79

13

Ecco la mia soluzione, che è una versione modificata della risposta di JayMcClellan. Salva il file sottostante in un file 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
Mark, questo è davvero fantastico. Per i miei usi, ho scoperto che potevo rimuovere il metodo LoadDll () e chiamare LoadLibrary () alla fine di ExtractEmbeddedDlls (). Questo mi ha anche permesso di rimuovere il codice di modifica PATH.
Cameron

9

Non sapevo che fosse possibile - immagino che il CLR debba estrarre la DLL nativa incorporata da qualche parte (Windows deve avere un file per la DLL per caricarlo - non può caricare un'immagine dalla memoria raw) e ovunque sta provando a farlo il processo non ha il permesso.

Qualcosa come Process Monitor di SysInternals potrebbe darti un indizio se il problema è che la creazione del file DLL non riesce ...

Aggiornare:


Ah ... ora che ho potuto leggere l'articolo di Suzanne Cook (la pagina non mi è venuta prima), nota che non sta parlando di incorporare la DLL nativa come risorsa all'interno della DLL gestita, ma piuttosto come risorsa collegata : la DLL nativa deve ancora essere il proprio file nel file system.

Vedi http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , dove dice:

Il file di risorse non viene aggiunto al file di output. Questo è diverso dall'opzione / resource che incorpora un file di risorse nel file di output.

Ciò che sembra fare è aggiungere metadati all'assembly che fa sì che la DLL nativa faccia parte logicamente dell'assembly (anche se è fisicamente un file separato). Quindi cose come l'inserimento dell'assembly gestito nella GAC ​​includerà automaticamente la DLL nativa, ecc.


Come utilizzare le opzioni "linkresource" in Visual Studio? Non riesco a trovare alcun esempio.
Alexey Subbota

9

Puoi provare Costura.Fody . La documentazione dice che è in grado di gestire file non gestiti. L'ho usato solo per i file gestiti e funziona a meraviglia :)


4

È anche possibile copiare semplicemente le DLL in qualsiasi cartella e quindi chiamare SetDllDirectory in quella cartella. Non è necessaria alcuna chiamata a LoadLibrary.

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

1
Ottima idea, tieni presente che questo potrebbe avere conseguenze sulla sicurezza, poiché si apre per l'iniezione di dll, quindi in un ambiente ad alta sicurezza dovrebbe essere usato con cautela
yoel halb
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.