【问题标题】:Embedding DLLs in a compiled executable在已编译的可执行文件中嵌入 DLL
【发布时间】:2011-05-01 21:46:10
【问题描述】:

是否可以将预先存在的 DLL 嵌入到已编译的 C# 可执行文件中(这样您只有一个文件可以分发)?如果可以的话,怎么做呢?

通常情况下,我很乐意将 DLL 留在外面并让安装程序处理所有事情,但有几个工作人员问过我这个问题,我真的不知道。

【问题讨论】:

标签: c# .net dll merge linker


【解决方案1】:

我强烈建议使用Costura.Fody - 迄今为止在您的程序集中嵌入资源的最佳和最简单的方法。它以 NuGet 包的形式提供。

Install-Package Costura.Fody

将其添加到项目后,它会自动将复制到输出目录的所有引用嵌入到您的 main 程序集中。您可能希望通过向项目添加目标来清理嵌入的文件:

Install-CleanReferencesTarget

您还可以指定是否包含 pdb、排除某些程序集或动态提取程序集。据我所知,也支持非托管程序集。

更新

目前有人尝试添加support for DNX

更新 2

对于最新的 Fody 版本,您需要拥有 MSBuild 16(即 Visual Studio 2019)。 Fody 4.2.1 版将执行 MSBuild 15。(参考:Fody is only supported on MSBuild 16 and above. Current version: 15

【讨论】:

  • 感谢您提出这个很棒的建议。安装软件包,你就完成了。它甚至默认压缩程序集。
  • 讨厌成为“我也是”,但我也是——这让我省了很多麻烦!谢谢你的推荐!这使我能够将我需要重新分发的所有内容打包到一个 exe 中,它现在比原始 exe 和 dll 的组合要小......我只使用了几天,所以我不能说我'我已经完成了它的步伐,但除非出现任何不好的东西,我可以看到它成为我工具箱中的常规工具。它只是工作!
  • 这很酷。但有一个缺点:在 Windows 上生成的程序集不再与单 Linux 二进制兼容。这意味着,您不能直接将程序集部署到 Linux mono。
  • 这太可爱了!如果您使用的是 vs2018,请不要忘记 FodyWeavers.xml 文件位于项目的根目录下。
  • 作为对上一条评论的补充:将具有以下内容的 FodyWeavers.xml 添加到您的项目中:
【解决方案2】:

只需在 Visual Studio 中右键单击您的项目,选择项目属性 -> 资源 -> 添加资源 -> 添加现有文件... 并将以下代码包含到您的 App.xaml.cs 或等效代码中。

public App()
{
    AppDomain.CurrentDomain.AssemblyResolve +=new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string dllName = args.Name.Contains(',') ? args.Name.Substring(0, args.Name.IndexOf(',')) : args.Name.Replace(".dll","");

    dllName = dllName.Replace(".", "_");

    if (dllName.EndsWith("_resources")) return null;

    System.Resources.ResourceManager rm = new System.Resources.ResourceManager(GetType().Namespace + ".Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());

    byte[] bytes = (byte[])rm.GetObject(dllName);

    return System.Reflection.Assembly.Load(bytes);
}

这是我的原始博客文章: http://codeblog.larsholm.net/2011/06/embed-dlls-easily-in-a-net-assembly/

【讨论】:

  • 您可以立即使用此行为。看看我的回答stackoverflow.com/a/20306095/568266
  • 同样重要的是要注意 AshRowe 在您的博客上发表的一条非常有用的评论:如果您安装了自定义主题,它将尝试解决崩溃和烧毁的 PresentationFramework.Theme 程序集!根据 AshRowe 的建议,您可以像这样简单地检查 dllName 是否包含 PresentationFramework: if (dllName.ToLower().Contains("presentationframework")) return null;
  • 两个cmets。一:你应该检查bytes是否为null,如果是,则在此处返回null。毕竟,资源中的 dll 可能 not 。二:这只适用于该类本身没有对该程序集的任何内容的“使用”。对于命令行工具,我必须将我的实际程序代码移动到一个新文件中,并创建一个小的新主程序来执行此操作,然后在旧类中调用原始主程序。
  • 好的,所以您的答案适用于 WPF。我让它与 Winforms 一起工作。按照您所说的添加资源后,只需将 AppDomain.CurrentDomain.AssemblyResolve +=new ResolveEventHandler(CurrentDomain_AssemblyResolve); 行放在 Form 构造函数中的 InitializeComponent(); 行之前。然后将整个System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) 方法放在任何地方。编译并运行。而已。它使您的解决方案比得分最高的答案更容易,因为无需下载第三方实用程序。
  • 以防万一有人遇到我的问题:如果.dll 名称包含任何连字符(即twenty-two.dll),它们也将被替换为下划线(即twenty_two.dll)。你可以把这行代码改成这样:dllName = dllName.Replace(".", "_").Replace("-", "_");
【解决方案3】:

如果它们实际上是托管程序集,则可以使用ILMerge。对于本机 DLL,您需要做更多的工作。

另见: How can a C++ windows dll be merged into a C# application exe?

【讨论】:

【解决方案4】:

是的,可以将 .NET 可执行文件与库合并。有多种工具可用于完成工作:

  • ILMerge 是一个实用程序,可用于将多个 .NET 程序集合并为一个程序集。
  • Mono mkbundle,将一个 exe 和所有带有 libmono 的程序集打包到一个二进制包中。
  • IL-Repack 是 ILMerge 的 FLOSS 替代品,具有一些附加功能。

此外,这可以与Mono Linker 结合使用,它确实会删除未使用的代码,从而使生成的程序集更小。

另一种可能是使用.NETZ,它不仅可以压缩程序集,还可以将dll直接打包到exe中。与上述解决方案的不同之处在于 .NETZ 不会合并它们,它们保持独立的程序集,而是打包到一个包中。

.NETZ 是一个开源工具,它压缩和打包 Microsoft .NET Framework 可执行文件(EXE、DLL)以使它们更小。

【讨论】:

  • NETZ 好像没了
  • 哇 - 我以为我终于找到了它,然后我读了这个评论。它似乎完全消失了。有分叉吗?
  • 嗯,它刚刚搬到了 GitHub,并且不再在网站上链接......所以“完全消失”是夸大其词。很可能不再支持它,但它仍然存在。我更新了链接。
【解决方案5】:

ILMerge 可以将程序集合并为一个程序集,前提是该程序集只有托管代码。您可以使用命令行应用程序,或添加对 exe 的引用并以编程方式合并。对于 GUI 版本,有 Eazfuscator.Netz 两者都是免费的。付费应用包括BoxedAppSmartAssembly

如果您必须将程序集与非托管代码合并,我建议SmartAssembly。我从来没有对SmartAssembly 打嗝,但对所有其他人。在这里,它可以将所需的依赖项作为资源嵌入到您的主 exe 中。

您可以手动完成所有这些操作,无需担心程序集是托管还是混合模式,方法是将 dll 嵌入到您的资源中,然后依赖 AppDomain 的程序集ResolveHandler。这是采用最坏情况的一站式解决方案,即带有非托管代码的程序集。

static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
    {
        string assemblyName = new AssemblyName(args.Name).Name;
        if (assemblyName.EndsWith(".resources"))
            return null;

        string dllName = assemblyName + ".dll";
        string dllFullPath = Path.Combine(GetMyApplicationSpecificPath(), dllName);

        using (Stream s = Assembly.GetEntryAssembly().GetManifestResourceStream(typeof(Program).Namespace + ".Resources." + dllName))
        {
            byte[] data = new byte[stream.Length];
            s.Read(data, 0, data.Length);

            //or just byte[] data = new BinaryReader(s).ReadBytes((int)s.Length);

            File.WriteAllBytes(dllFullPath, data);
        }

        return Assembly.LoadFrom(dllFullPath);
    };
}

这里的关键是将字节写入文件并从其位置加载。为了避免先有鸡还是先有蛋的问题,您必须确保在访问程序集之前声明处理程序,并且不要在加载(程序集解析)部分中访问程序集成员(或实例化任何必须处理程序集的东西)。还要注意确保GetMyApplicationSpecificPath() 不是任何临时目录,因为临时文件可能会被其他程序或您自己尝试擦除(不是说它会在您的程序访问 dll 时被删除,但至少它是一个麻烦。 AppData 位置很好)。另请注意,您每次都必须写入字节,您不能从位置加载,因为 dll 已经存在于那里。

对于托管 dll,您不需要写入字节,而是直接从 dll 的位置加载,或者只需读取字节并从内存中加载程序集。像这样左右:

    using (Stream s = Assembly.GetEntryAssembly().GetManifestResourceStream(typeof(Program).Namespace + ".Resources." + dllName))
    {
        byte[] data = new byte[stream.Length];
        s.Read(data, 0, data.Length);
        return Assembly.Load(data);
    }

    //or just

    return Assembly.LoadFrom(dllFullPath); //if location is known.

如果程序集完全不受管理,您可以查看linkthis 了解如何加载此类 dll。

【讨论】:

  • 注意Resource的“Build Action”需要设置为“Embedded Resource”。
  • @Mavamaarten 不一定。如果是提前添加到项目的Resources.resx中,则不需要这样做。
  • EAZfuscator 现已商用。
【解决方案6】:

excerpt by Jeffrey Richter 非常好。简而言之,将库添加为嵌入式资源并在其他任何内容之前添加回调。这是我在控制台应用程序的 Main 方法开头放置的代码版本(在他的页面的 cmets 中找到)(只需确保使用该库的任何调用与 Main 的方法不同)。

AppDomain.CurrentDomain.AssemblyResolve += (sender, bargs) =>
        {
            String dllName = new AssemblyName(bargs.Name).Name + ".dll";
            var assem = Assembly.GetExecutingAssembly();
            String resourceName = assem.GetManifestResourceNames().FirstOrDefault(rn => rn.EndsWith(dllName));
            if (resourceName == null) return null; // Not found, maybe another handler will find it
            using (var stream = assem.GetManifestResourceStream(resourceName))
            {
                Byte[] assemblyData = new Byte[stream.Length];
                stream.Read(assemblyData, 0, assemblyData.Length);
                return Assembly.Load(assemblyData);
            }
        };

【讨论】:

  • 稍作改动,成功了,tnx 伙计!
  • 项目libz.codeplex.com 使用这个过程,但它也会做一些其他的事情,比如为你管理事件处理程序和一些特殊代码不破坏“Managed Extensibility Framework Catalogs”(它自己这个过程会打破)
  • 太好了!!谢谢@史蒂夫
【解决方案7】:

扩展上面的@Bobby's asnwer。您可以编辑 .csproj 以使用 IL-Repack 在构建时自动将所有文件打包到一个程序集中。

  1. 使用Install-Package ILRepack.MSBuild.Task 安装 nuget ILRepack.MSBuild.Task 包
  2. 编辑 .csproj 的 AfterBuild 部分

这是一个将 ExampleAssemblyToMerge.dll 合并到您的项目输出中的简单示例。

<!-- ILRepack -->
<Target Name="AfterBuild" Condition="'$(Configuration)' == 'Release'">

   <ItemGroup>
    <InputAssemblies Include="$(OutputPath)\$(AssemblyName).exe" />
    <InputAssemblies Include="$(OutputPath)\ExampleAssemblyToMerge.dll" />
   </ItemGroup>

   <ILRepack 
    Parallel="true"
    Internalize="true"
    InputAssemblies="@(InputAssemblies)"
    TargetKind="Exe"
    OutputFile="$(OutputPath)\$(AssemblyName).exe"
   />
</Target>

【讨论】:

  • IL-Repack 的语法已更改,请检查链接的 github 存储库 (github.com/peters/ILRepack.MSBuild.Task) 上的 README.md。这种方式是唯一对我有用的方式,并且我能够使用通配符来匹配我想要包含的所有 dll。
【解决方案8】:

.NET Core 3.0 原生支持编译成单个 .exe

通过在项目文件 (.csproj) 中使用以下属性来启用该功能:

    <PropertyGroup>
        <PublishSingleFile>true</PublishSingleFile>
    </PropertyGroup>

这是在没有任何外部工具的情况下完成的。

查看我的回答for this question了解更多详情。

【讨论】:

  • .NET 5 怎么样,它支持这个吗?
  • 您还需要添加运行时标识符:win10-x64 或 linux-x64 如:linux-x64
【解决方案9】:

您可以将 DLL 作为嵌入式资源添加,然后让您的程序在启动时将它们解压到应用程序目录中(在检查它们是否已经存在之后)。

不过,设置文件很容易制作,我认为这不值得。

编辑:使用 .NET 程序集可以轻松实现此技术。使用非 .NET DLL 会做更多的工作(您必须弄清楚在哪里解压文件并注册它们等等)。

【讨论】:

【解决方案10】:

另一个可以优雅地处理这个问题的产品是 SmartAssembly,SmartAssembly.com。除了将所有依赖项合并到一个 DLL 之外,该产品还将(可选地)混淆您的代码,删除额外的元数据以减小生成的文件大小,并且实际上还可以优化 IL 以提高运行时性能。

它还为您的软件(如果需要)添加了某种可能有用的全局异常处理/报告功能。我相信它还有一个命令行 API,因此您可以将其作为构建过程的一部分。

【讨论】:

    【解决方案11】:

    ILMerge 方法和 Lars Holm Jensen 处理 AssemblyResolve 事件都不适用于插件主机。假设可执行文件 H 动态加载程序集 P 并通过在单独程序集中定义的接口 IP 访问它。要将 IP 嵌入到 H 中,需要对 Lars 的代码稍作修改:

    Dictionary<string, Assembly> loaded = new Dictionary<string,Assembly>();
    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
    {   Assembly resAssembly;
        string dllName = args.Name.Contains(",") ? args.Name.Substring(0, args.Name.IndexOf(',')) : args.Name.Replace(".dll","");
        dllName = dllName.Replace(".", "_");
        if ( !loaded.ContainsKey( dllName ) )
        {   if (dllName.EndsWith("_resources")) return null;
            System.Resources.ResourceManager rm = new System.Resources.ResourceManager(GetType().Namespace + ".Properties.Resources", System.Reflection.Assembly.GetExecutingAssembly());
            byte[] bytes = (byte[])rm.GetObject(dllName);
            resAssembly = System.Reflection.Assembly.Load(bytes);
            loaded.Add(dllName, resAssembly);
        }
        else
        {   resAssembly = loaded[dllName];  }
        return resAssembly;
    };  
    

    处理重复尝试解析相同程序集并返回现有程序集而不是创建新实例的技巧。

    编辑: 为避免破坏 .NET 的序列化,请确保为所有未嵌入您的程序集返回 null,从而默认为标准行为。您可以通过以下方式获取这些库的列表:

    static HashSet<string> IncludedAssemblies = new HashSet<string>();
    string[] resources = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames();
    for(int i = 0; i < resources.Length; i++)
    {   IncludedAssemblies.Add(resources[i]);  }
    

    如果传递的程序集不属于IncludedAssemblies,则返回null。

    【讨论】:

    • 很抱歉将其发布为答案而不是评论。我无权评论他人的回答。
    【解决方案12】:

    以下方法不要使用外部工具自动包含所有需要的DLL(无需手动操作,一切都在编译时完成)

    我在这里阅读了很多答案,说要使用 ILMergeILRepackJeffrey Ritcher 方法,但这些方法都不适用于 WPF 应用程序 也不好用。

    当您有很多 DLL 时,可能很难在 exe 中手动包含您需要的那个。 Wegged here on StackOverflow 解释了我发现的最佳方法

    为了清楚起见,复制粘贴他的答案(所有功劳归于Wegged


    1) 将此添加到您的 .csproj 文件中:

    <Target Name="AfterResolveReferences">
      <ItemGroup>
        <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
          <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
        </EmbeddedResource>
      </ItemGroup>
    </Target>
    

    2) 让您的 Main Program.cs 看起来像这样:

    [STAThreadAttribute]
    public static void Main()
    {
        AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
        App.Main();
    }
    

    3) 添加OnResolveAssembly 方法:

    private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
    {
        Assembly executingAssembly = Assembly.GetExecutingAssembly();
        AssemblyName assemblyName = new AssemblyName(args.Name);
    
        var path = assemblyName.Name + ".dll";
        if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false) path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
    
        using (Stream stream = executingAssembly.GetManifestResourceStream(path))
        {
            if (stream == null) return null;
    
            var assemblyRawBytes = new byte[stream.Length];
            stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
            return Assembly.Load(assemblyRawBytes);
        }
    }
    

    【讨论】:

    • 您能解释一下CultureInfo 上的测试,是否有一些en-usfr-fr 子文件夹?这是DestinationSubDirectory 吗?
    【解决方案13】:

    这听起来可能很简单,但 WinRar 提供了将一堆文件压缩为自解压可执行文件的选项。
    它有许多可配置选项:最终图标、将文件提取到给定路径、提取后要执行的文件、提取过程中显示的自定义徽标/弹出窗口文本、根本没有弹出窗口、许可协议文本等。
    在某些情况下可能有用。

    【讨论】:

    【解决方案14】:

    我使用从 .vbs 脚本调用的 csc.exe 编译器。

    在您的 xyz.cs 脚本中,在指令后添加以下行(我的示例是 Renci SSH):

    using System;
    using Renci;//FOR THE SSH
    using System.Net;//FOR THE ADDRESS TRANSLATION
    using System.Reflection;//FOR THE Assembly
    
    //+ref>"C:\Program Files (x86)\Microsoft\ILMerge\Renci.SshNet.dll"
    //+res>"C:\Program Files (x86)\Microsoft\ILMerge\Renci.SshNet.dll"
    //+ico>"C:\Program Files (x86)\Microsoft CAPICOM 2.1.0.2 SDK\Samples\c_sharp\xmldsig\resources\Traffic.ico"
    

    ref、res 和 ico 标签将被下面的 .vbs 脚本拾取以形成 csc 命令。

    然后在 Main 中添加程序集解析器调用者:

    public static void Main(string[] args)
    {
        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
        .
    

    ...并将解析器本身添加到类中的某个位置:

    静态程序集 CurrentDomain_AssemblyResolve(对象发送者,ResolveEventArgs args) { String resourceName = new AssemblyName(args.Name).Name + ".dll"; 使用 (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { 字节[] 组装数据 = 新字节[流.长度]; 流.Read(assemblyData, 0, assemblyData.Length); 返回 Assembly.Load(assemblyData); } }

    我命名 vbs 脚本以匹配 .cs 文件名(例如 ssh.vbs 查找 ssh.cs);这使得多次运行脚本变得容易得多,但如果你不是像我这样的白痴,那么通用脚本可以从拖放中获取目标 .cs 文件:

    暗淡名称_,oShell,fso 设置 oShell = CreateObject("Shell.Application") 设置 fso = CreateObject("Scripting.fileSystemObject") '将 VBS 脚本名作为目标文件名 '############################################### name_ = Split(wscript.ScriptName, ".")(0) '从 .CS 文件中获取外部 DLL 和图标名称 '################################################ ###### 常量 OPEN_FILE_FOR_READING = 1 设置 objInputFile = fso.OpenTextFile(name_ & ".cs", 1) '将所有内容读入数组 '############################# inputData = Split(objInputFile.ReadAll, vbNewline) 对于每个 strData 中的 inputData 如果 left(strData,7)="//+ref>" 那么 csc_references = csc_references & " /reference:" & trim(replace(strData,"//+ref>","")) & " " 万一 如果 left(strData,7)="//+res>" 那么 csc_resources = csc_resources & " /resource:" & trim(replace(strData,"//+res>","")) & " " 万一 如果 left(strData,7)="//+ico>" 那么 csc_icon = " /win32icon:" & trim(replace(strData,"//+ico>","")) & " " 万一 下一个 objInputFile.Close '编译文件 '################ oShell.ShellExecute "c:\windows\microsoft.net\framework\v3.5\csc.exe", "/warn:1 /target:exe " & csc_references & csc_resources & csc_icon & " " & name_ & ".cs" , "", "runas", 2 WScript.Quit(0)

    【讨论】:

      【解决方案15】:

      在 C# 中创建混合本机/托管程序集是可能的,但并非那么容易。如果您使用的是 C++,它会容易得多,因为 Visual C++ 编译器可以像其他任何东西一样轻松地创建混合程序集。

      除非您对生成混合程序集有严格的要求,否则我同意 MusiGenesis 的观点,即这不值得用 C# 来做这件事。如果您需要这样做,或许可以考虑改用 C++/CLI。

      【讨论】:

        【解决方案16】:

        通常,您需要某种形式的后期构建工具来执行您所描述的程序集合并。有一个名为 Eazfuscator (eazfuscator.blogspot.com/) 的免费工具,它专为字节码处理而设计,也可以处理程序集合并。您可以使用 Visual Studio 将其添加到构建后的命令行中以合并您的程序集,但是由于在任何非平凡的程序集合并方案中都会出现问题,您的工作量会有所不同。

        您还可以检查构建直到 NANT 是否有能力在构建后合并程序集,但我自己对 NANT 还不够熟悉,无法说出该功能是否内置。

        还有许多 Visual Studio 插件将执行程序集合并作为构建应用程序的一部分。

        或者,如果您不需要自动完成此操作,可以使用 ILMerge 等许多工具将 .net 程序集合并到一个文件中。

        我在合并程序集时遇到的最大问题是它们是否使用任何类似的命名空间。或者更糟糕的是,引用同一个 dll 的不同版本(我的问题通常与 NUnit dll 文件有关)。

        【讨论】:

        • Eazfuscator 只会调用 IlMerge,AFAIK。
        • +1 鲍比。我应该记得的。 Eazfucator 为您所做的所有事情都是使用更通用的配置文件抽象对 ILMerge 的实际调用。
        猜你喜欢
        • 2020-10-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多