Menyematkan dll yang tidak dikelola ke dll C # terkelola

87

Saya memiliki dll C # terkelola yang menggunakan dll C ++ tidak terkelola menggunakan DLLImport. Semuanya bekerja dengan baik. Namun, saya ingin menyematkan DLL yang tidak terkelola itu di dalam DLL terkelola saya seperti yang dijelaskan oleh Microsoft di sana:

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

Jadi saya menambahkan file dll yang tidak dikelola ke proyek dll yang saya kelola, mengatur properti ke 'Sumber Daya Tertanam' dan memodifikasi DLLImport menjadi seperti:

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

di mana 'Mesin Pembungkus' adalah nama rakitan dari DLL terkelola saya 'Driver.dll' adalah DLL yang tidak terkelola

Saat saya berlari, saya mendapatkan:

Akses ditolak. (Pengecualian dari HRESULT: 0x80070005 (E_ACCESSDENIED))

Saya melihat dari MSDN dan dari http://blogs.msdn.com/suzcook/ itu seharusnya mungkin ...

DimaSan
sumber
1
Anda dapat mempertimbangkan BxILMerge untuk kasus Anda
MastAvalons

Jawaban:

64

Anda dapat menyematkan DLL yang tidak dikelola sebagai sumber daya jika Anda mengekstraknya sendiri ke direktori sementara selama inisialisasi, dan memuatnya secara eksplisit dengan LoadLibrary sebelum menggunakan P / Invoke. Saya telah menggunakan teknik ini dan berhasil dengan baik. Anda mungkin lebih memilih untuk hanya menautkannya ke assembly sebagai file terpisah seperti yang dicatat Michael, tetapi memiliki semuanya dalam satu file memiliki kelebihan. Inilah pendekatan yang saya gunakan:

// 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
sumber
Apakah LoadLibrary menggunakan DLLImport dari kenel32? Debug.Assert gagal untuk saya menggunakan kode yang sama dalam layanan WCF.
Klaus Nji
Ini adalah solusi yang baik, tetapi akan lebih baik lagi untuk menemukan resolusi yang andal untuk kasus-kasus ketika dua aplikasi mencoba untuk menulis ke lokasi yang sama pada waktu yang sama. Penangan pengecualian selesai sebelum aplikasi lain selesai membongkar DLL.
Robert Važan
Ini sempurna. Hanya hal yang tidak perlu adalah bahwa directory.createdirectory sudah memiliki direktori tersebut, periksa di dalamnya
Gaspa79
13

Inilah solusi saya, yang merupakan versi modifikasi dari jawaban JayMcClellan. Simpan file di bawah ini ke dalam 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);
            }
        }

    }
}
Mark Lakata
sumber
2
Mark, ini sangat keren. Untuk penggunaan saya, saya menemukan saya bisa menghapus metode LoadDll (), dan memanggil LoadLibrary () di akhir ExtractEmbeddedDlls (). Ini juga memungkinkan saya untuk menghapus kode pengubah PATH.
Cameron
9

Saya tidak sadar ini mungkin - saya kira CLR perlu mengekstrak DLL asli yang disematkan di suatu tempat (Windows perlu memiliki file untuk DLL untuk memuatnya - tidak dapat memuat gambar dari memori mentah), dan di mana pun itu mencoba melakukan bahwa proses tersebut tidak memiliki izin.

Sesuatu seperti Monitor Proses dari SysInternals mungkin memberi Anda petunjuk jika pronblemnya adalah bahwa membuat file DLL gagal ...

Memperbarui:


Ah ... sekarang setelah saya bisa membaca artikel Suzanne Cook (halaman tidak muncul untuk saya sebelumnya), perhatikan bahwa dia tidak berbicara tentang menyematkan DLL asli sebagai sumber daya di dalam DLL terkelola, melainkan sebagai sumber daya tertaut - DLL asli masih harus berupa filenya sendiri dalam sistem file.

Lihat http://msdn.microsoft.com/en-us/library/xawyf94k.aspx , di mana dikatakan:

File sumber daya tidak ditambahkan ke file keluaran. Ini berbeda dari opsi / resource yang menyematkan file resource di file output.

Apa yang tampaknya dilakukan adalah menambahkan metadata ke rakitan yang menyebabkan DLL asli secara logis menjadi bagian dari rakitan (meskipun secara fisik merupakan file terpisah). Jadi hal-hal seperti menempatkan rakitan terkelola ke dalam GAC akan secara otomatis menyertakan DLL asli, dll.

Michael Burr
sumber
Bagaimana cara menggunakan opsi "linkresource" di Visual Studio? Saya tidak dapat menemukan contoh apa pun.
Alexey Subbota
9

Anda dapat mencoba Costura.Fody . Dokumentasi mengatakan, bahwa itu mampu menangani file yang tidak terkelola. Saya hanya menggunakannya untuk file yang dikelola, dan berfungsi seperti pesona :)

Matthias
sumber
4

Seseorang juga bisa menyalin DLL ke folder mana pun, dan kemudian memanggil SetDllDirectory ke folder itu. Tidak diperlukan panggilan ke LoadLibrary.

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool SetDllDirectory(string lpPathName);
Ziriax
sumber
1
Ide bagus, perhatikan saja bahwa ini mungkin memiliki konsekuensi keamanan, karena terbuka untuk injeksi dll, jadi dalam lingkungan keamanan yang tinggi harus digunakan dengan hati
yoel halb