Osadzanie niezarządzanej biblioteki dll w zarządzanej bibliotece dll C #


87

Mam zarządzaną bibliotekę dll C #, która używa niezarządzanej biblioteki dll C ++ przy użyciu biblioteki DLLImport. Wszystko działa świetnie. Jednak chcę osadzić tę niezarządzaną bibliotekę DLL w mojej zarządzanej bibliotece DLL, jak wyjaśniono tam przez firmę Microsoft:

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

Dodałem więc niezarządzany plik dll do mojego zarządzanego projektu dll, ustawiłem właściwość na `` Zasób osadzony '' i zmodyfikowałem plik DLLImport na coś takiego:

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

gdzie „Wrapper Engine” to nazwa zestawu mojej zarządzanej biblioteki DLL „Unmanaged Driver.dll” to niezarządzana biblioteka DLL

Kiedy biegam, otrzymuję:

Odmowa dostępu. (Wyjątek od HRESULT: 0x80070005 (E_ACCESSDENIED))

Widziałem z MSDN i http://blogs.msdn.com/suzcook/ , co powinno być możliwe ...



1
Możesz rozważyć BxILMerge w swoim przypadku
MastAvalons

Odpowiedzi:


64

Możesz osadzić niezarządzaną bibliotekę DLL jako zasób, jeśli wyodrębnisz ją samodzielnie do katalogu tymczasowego podczas inicjowania i załadujesz ją jawnie za pomocą LoadLibrary przed użyciem P / Invoke. Użyłem tej techniki i działa dobrze. Możesz po prostu połączyć go z zestawem jako oddzielnym plikiem, jak zauważył Michael, ale posiadanie tego wszystkiego w jednym pliku ma swoje zalety. Oto podejście, które zastosowałem:

// 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);

Czy LoadLibrary używa biblioteki DLLImport firmy kenel32? Debug.Assert kończy się niepowodzeniem przy użyciu tego samego kodu w ramach usługi WCF.
Klaus Nji

To dobre rozwiązanie, ale jeszcze lepiej byłoby znaleźć niezawodne rozwiązanie dla przypadków, w których dwie aplikacje próbują pisać w tym samym miejscu w tym samym czasie. Procedura obsługi wyjątków kończy działanie, zanim inna aplikacja zakończy rozpakowywanie biblioteki DLL.
Robert Važan

To jest doskonałe. Jedyną niepotrzebną rzeczą jest to, że
katalog.createdirectory

13

Oto moje rozwiązanie, które jest zmodyfikowaną wersją odpowiedzi JayMcClellana. Zapisz poniższy plik w pliku 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, to jest naprawdę fajne. W moich zastosowaniach stwierdziłem, że mogę usunąć metodę LoadDll () i wywołać LoadLibrary () na końcu ExtractEmbeddedDlls (). Pozwoliło mi to również na usunięcie kodu modyfikującego PATH.
Cameron

9

Nie wiedziałem, że jest to możliwe - zgaduję, że CLR musi gdzieś wyodrębnić osadzoną natywną bibliotekę DLL (system Windows musi mieć plik, aby DLL, aby ją załadować - nie może załadować obrazu z pamięci surowej) i gdziekolwiek próbuje to zrobić, że proces nie ma pozwolenia.

Coś w rodzaju Process Monitor firmy SysInternals może dać ci wskazówkę, jeśli problem polega na tym, że tworzenie pliku DLL kończy się niepowodzeniem ...

Aktualizacja:


Ach ... teraz, gdy mogłem przeczytać artykuł Suzanne Cook (strona nie pojawiła się wcześniej dla mnie), zauważ, że nie mówi ona o osadzeniu natywnej biblioteki DLL jako zasobu wewnątrz zarządzanej biblioteki DLL, ale raczej jako zasób połączony - natywna biblioteka DLL nadal musi być własnym plikiem w systemie plików.

Zobacz http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , gdzie jest napisane:

Plik zasobów nie jest dodawany do pliku wyjściowego. Różni się to od opcji / resource, która powoduje osadzenie pliku zasobów w pliku wyjściowym.

Wydaje się, że to powoduje dodanie metadanych do zestawu, co powoduje, że natywna biblioteka DLL jest logicznie częścią zestawu (nawet jeśli jest fizycznie oddzielnym plikiem). Więc takie rzeczy, jak umieszczenie zarządzanego zestawu w GAC, będą automatycznie zawierać natywną bibliotekę DLL itp.


Jak używać opcji „linkresource” w programie Visual Studio? Nie mogę znaleźć żadnych przykładów.
Alexey Subbota

9

Możesz spróbować Costura.Fody . Dokumentacja mówi, że jest w stanie obsłużyć niezarządzane pliki. Użyłem go tylko do zarządzanych plików i działa jak marzenie :)


4

Można również po prostu skopiować biblioteki DLL do dowolnego folderu, a następnie wywołać SetDllDirectory do tego folderu. Nie jest wtedy potrzebne wywołanie LoadLibrary.

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

1
Świetny pomysł, tylko uwaga, że może to mieć konsekwencje dla bezpieczeństwa, ponieważ otwierają się do dll iniekcji, a więc w dużej środowisku bezpieczeństwa należy stosować ostrożnie
yoel HALB
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.