65.9K
CodeProject 正在变化。 阅读更多。
Home

从嵌入式资源加载 DLL

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (114投票s)

2013年1月15日

CPOL

6分钟阅读

viewsIcon

558922

downloadIcon

17688

将 DLL 与应用程序合并到单个 EXE 文件中

 

Content

 

  • 引言
  • 何时以及为何需要嵌入? 
  • 将 DLL 添加为嵌入式资源
  • 开始编码
  • 深入了解 EmbeddedAssembly.cs
  • EmbeddedAssembly.cs 类
  • 相关提示 
  • 替代方案
  • 文章历史

 

引言


通常,当我们添加自定义构建或第三方 DLL 的引用时,它将与我们的主 EXE 应用程序一起分发到同一文件夹中。如果 DLL 不存在于 EXE 应用程序的同一文件夹中,则会抛出异常。在某些情况下,我们希望将 DLL 打包/包含在 EXE 应用程序中。因此,本文介绍了一种从嵌入式资源加载 DLL 的解决方案。 

异常示例(在分发的 EXE 应用程序的同一文件夹中找不到 DLL)

 

何时以及为何需要嵌入?


合并 DLL 通常用于开发便携式软件。 

优点 

  1. 易于在最终用户之间分发。(复制 1 个文件而不是多个文件) 
  2. 最终用户复制软件时,依赖的 DLL 和文件不会丢失或意外遗漏。 
  3. 最终用户不会将文件与其他不相关的文件混淆。 

 

缺点 

 

  1. 增加了开发人员的工作复杂性。开发人员需要采取额外的步骤来合并文件。 
  2. 如果某些组件有新版本,则需要重新打包整个内容。如果 DLL 没有合并,开发人员很容易更改这些部分。(然而,这对最终用户来说将是一件困难的事情,最终用户宁愿下载整个内容而不是手动替换 DLL。) 

 

嵌入 DLL 的概念与大多数软件安装程序相同。大多数安装程序都打包成一个文件。这样做的原因是为了降低复杂性,使其更容易分发。

 

将 DLL 添加为嵌入式资源


本文中用作示例的 DLL 的基本介绍。 

 

    • System.Windows.Forms.Ribbon35.DLL 
    • System.Data.SQLite.DLL
      • - 类型:混合代码/非托管 DLL
      • - .NET 的开源跨平台嵌入式数据库引擎包装器
      • - 阅读更多: http://system.data.sqlite.org 

在开始编码之前,必须将 DLL 添加到项目中。

首先,将 DLL 添加为引用。

 

然后,将同一 DLL 作为文件添加到项目中。右键单击项目名称 > 添加 > 现有项... 

 

同一个 DLL 将在项目的不同文件夹中存在两次。

如下图所示,

对于引用的 DLL,在属性资源管理器中,将 <code>复制本地 = False

对于作为文件添加的 DLL,在属性资源管理器中,将 生成操作 = 嵌入式资源  

 

对 System.Data.SQLite.DLL 执行相同的操作。将其添加到项目文件并作为引用。更改属性,如 System.Windows.Forms.Ribbon35.DLL。 

注意:如果您有任何其他无法引用的非托管/本机 DLL,则无需引用它,只需将非托管 DLL 添加为嵌入式资源。 

开始编码   


从下载中获取 EmbeddedAssembly.cs 并将其添加到项目中。 

或者浏览到本文底部并复制 EmbeddedAssembly.cs 的代码。 

打开 Program.cs 

Program.cs 的初始代码

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

  从嵌入式资源将 DLL 加载到内存中。使用 EmbeddedAssembly.Load 将其加载到内存中。 

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        string resource1 = "MyApp.System.Windows.Forms.Ribbon35.dll";
        string resource2 = "MyApp.System.Data.SQLite.dll";
        EmbeddedAssembly.Load(resource1, "System.Windows.Forms.Ribbon35.dll");
        EmbeddedAssembly.Load(resource2, "System.Data.SQLite.dll");

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

注意资源字符串的格式。示例: 

MyApp.System.Windows.Forms.Ribbon35.dll  

此字符串是项目中嵌入式资源的地址。 

MyApp 是项目名称,后跟 DLL 文件名。如果 DLL 添加到文件夹中,则文件夹名称必须包含在资源字符串中。示例

MyApp.NewFolder1.System.Windows.Forms.Ribbon35.dll 

DLL 不与应用程序一起分发,当应用程序无法定位 DLL 时,它会引发 AppDomain.CurrentDomain.AssemblyResolve 事件。AssemblyResolve 请求缺少的 DLL。然后,我们将告诉我们的应用程序在内存中查找 DLL。使用 EmbeddedAssembly.Get 从内存中检索 DLL 并将其传递给 AssemblyResolve 来处理其余部分。  

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        string resource1 = "MyApp.System.Windows.Forms.Ribbon35.dll";
        string resource2 = "MyApp.System.Data.SQLite.dll";
        EmbeddedAssembly.Load(resource1, "System.Windows.Forms.Ribbon35.dll");
        EmbeddedAssembly.Load(resource2, "System.Data.SQLite.dll");
 
        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
 
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);S
        Application.Run(new Form1());
    }
 
    static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        return EmbeddedAssembly.Get(args.Name);
    }
}

就这样。  

深入了解 EmbeddedAssembly.cs 


在这里,我们将了解 EmbeddedAssembly.cs 在幕后做了什么。 

有两种常见的 DLL 类型将与 .NET 应用程序一起使用

 

  • 托管 DLL
  • 非托管 DLL 或混合代码 DLL 

 

托管 DLL 完全由 .NET 语言编写(即 C#、VB.NET)
非托管 DLL 将由其他语言编写(即 C、C++)

System.Data.SQLite.DLL 是混合代码。它是 C 和 C# 的组合。因此,它可以被视为非托管 DLL。    

加载托管 DLL

从嵌入式资源加载托管 DLL 的过程很简单。 

 

static class Program
{
    [STAThread]
    static void Main()
    {
        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
    static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        return Load();
    }
} 

 

public static Assembly Load()
{
    byte[] ba = null;
    string resource = "MyApp.System.Windows.Forms.Ribbon35.dll";
    Assembly curAsm = Assembly.GetExecutingAssembly();
    using (Stream stm = curAsm.GetManifestResourceStream(resource))
    {
        ba = new byte[(int)stm.Length];
        stm.Read(ba, 0, (int)stm.Length);
 
        return Assembly.Load(ba);
    }
}

加载非托管 DLL

非托管 DLL 不能像托管 DLL 那样直接从 stream/byte[] 加载。 

 

  1. 首先将 DLL 从嵌入式资源加载到 byte[] 中。
  2. byte[] 写入物理文件并存储在临时文件夹中。
  3. 使用 Assembly.LoadFile() 将文件加载到内存中。  

 

步骤在下面的代码注释中解释: 

 

public static Assembly Load()
{
    // Get the byte[] of the DLL
    byte[] ba = null;
    string resource = "MyApp.System.Data.SQLite.dll";
    Assembly curAsm = Assembly.GetExecutingAssembly();
    using (Stream stm = curAsm.GetManifestResourceStream(resource))
    {
        ba = new byte[(int)stm.Length];
        stm.Read(ba, 0, (int)stm.Length);
    }
 
    bool fileOk = false;
    string tempFile = "";
 
    using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
    {
        // Get the hash value of the Embedded DLL
        string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);
 
        // The full path of the DLL that will be saved
        tempFile = Path.GetTempPath() + "System.Data.SQLite.dll";
 
        // Check if the DLL is already existed or not?
        if (File.Exists(tempFile))
        {
            // Get the file hash value of the existed DLL
            byte[] bb = File.ReadAllBytes(tempFile);
            string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);
 
            // Compare the existed DLL with the Embedded DLL
            if (fileHash == fileHash2)
            {
                // Same file
                fileOk = true;
            }
            else
            {
                // Not same
                fileOk = false;
            }
        }
        else
        {
            // The DLL is not existed yet
            fileOk = false;
        }
    }
 
    // Create the file on disk
    if (!fileOk)
    {
        System.IO.File.WriteAllBytes(tempFile, ba);
    }
    
    // Load it into memory    
    return Assembly.LoadFile(tempFile);
} 

EmbeddedAssembly.cs 将预加载所需的 DLL 并将其存储在 Dictionary 中。当应用程序的 AssemblyResolve 事件触发时,EmbeddedAssembly.Get() 将返回请求的 DLL。 

EmbeddedAssembly.cs 类


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;

public class EmbeddedAssembly
{
    // Version 1.3

    static Dictionary<string, Assembly> dic = null;

    public static void Load(string embeddedResource, string fileName)
    {
        if (dic == null)
            dic = new Dictionary<string, Assembly>();

        byte[] ba = null;
        Assembly asm = null;
        Assembly curAsm = Assembly.GetExecutingAssembly();

        using (Stream stm = curAsm.GetManifestResourceStream(embeddedResource))
        {
            // Either the file is not existed or it is not mark as embedded resource
            if (stm == null)
                throw new Exception(embeddedResource + " is not found in Embedded Resources.");

            // Get byte[] from the file from embedded resource
            ba = new byte[(int)stm.Length];
            stm.Read(ba, 0, (int)stm.Length);
            try
            {
                asm = Assembly.Load(ba);

                // Add the assembly/dll into dictionary
                dic.Add(asm.FullName, asm);
                return;
            }
            catch
            {
                // Purposely do nothing
                // Unmanaged dll or assembly cannot be loaded directly from byte[]
                // Let the process fall through for next part
            }
        }

        bool fileOk = false;
        string tempFile = "";

        using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
        {
            string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);;
            
            tempFile = Path.GetTempPath() + fileName;

            if (File.Exists(tempFile))
            {
                byte[] bb = File.ReadAllBytes(tempFile);
                string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);

                if (fileHash == fileHash2)
                {
                    fileOk = true;
                }
                else
                {
                    fileOk = false;
                }
            }
            else
            {
                fileOk = false;
            }
        }

        if (!fileOk)
        {
            System.IO.File.WriteAllBytes(tempFile, ba);
        }
        
        asm = Assembly.LoadFile(tempFile);

        dic.Add(asm.FullName, asm);
    }

    public static Assembly Get(string assemblyFullName)
    {
        if (dic == null || dic.Count == 0)
            return null;

        if (dic.ContainsKey(assemblyFullName))
            return dic[assemblyFullName];

        return null;
    }
}

相关提示


提示 1:在另一个程序集内获取程序集由 Thierry Maurel 建议) 

如果 DLL 嵌入到另一个 DLL 中,则可以使用

Assembly a1 = GetType ().Assembly; 

获取本地 dll 程序集,而不是可执行程序集。

替代方案


ILMerge 

Mergebin - 将您的非托管和托管 DLL 合并到一个便捷包中 

文章 - 从非托管代码调用托管代码,反之亦然 

SmartAssembly(商业版) 

Costuraherves 建议) 

将所有额外文件/DLL 打包为存档文件 - Zip/CAB/Tar由 Fred Flams 建议

  • 将 DLL 打包到存档文件中。在应用程序启动时,将存档文件解包到内存或磁盘上。
  • 主 EXE 将更轻。将存档文件与主 EXE 的根名相同地分发。最终用户将知道这两个文件(EXE 和存档文件)必须一起使用。 

将 DLL 转换为加密类并在运行时从类加载   

Vitevic 程序集嵌入器

在同一解决方案中构建时自动合并所有项目的所有源文件。

LibZ

BoxedApp

  • http://www.boxedapp.com
  • BoxedApp SDK 模拟文件,因此开发人员创建一个虚拟文件(伪文件)——一个磁盘上不存在的“文件”,但进程的工作方式就像这个文件真的存在一样。BoxedApp 拦截文件调用以实现此目的。
  • User-11642949 建议

文章历史 


  • 2013 年 1 月 16 日 - 初次发布。 

 

© . All rights reserved.