在已编译的可执行文件中嵌入 DLL

本文关键字:DLL 可执行文件 编译 | 更新日期: 2023-09-27 17:47:22

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

通常,我很酷地将 DLL 留在外面并让安装程序处理所有事情,但是工作中有几个人问过我这个问题,老实说我不知道。

在已编译的可执行文件中嵌入 DLL

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

Install-Package Costura.Fody

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

Install-CleanReferencesTarget

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

更新

目前,有些人正在尝试添加对 DNX 的支持。

更新 2

对于最新的Fody版本,您需要拥有MSBuild 16(因此Visual Studio 2019)。Fody 版本 4.2.1 将执行 MSBuild 15。(参考:Fody 仅在 MSBuild 16 及更高版本上受支持。当前版本: 15)

只需在 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/

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

另请参阅:如何将C++ Windows dll 合并到 C# 应用程序 exe 中?

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

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

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

另一种可能性是使用 .NETZ,它不仅允许压缩程序集,还可以将 dll 直接打包到 exe 中。与上述解决方案的区别在于。NETZ不会合并它们,它们仍然是单独的组件,而是打包到一个包中。

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

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

如果必须将程序集与非托管代码合并,我建议使用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 时它会被删除,但至少这是一个麻烦。应用数据是不错的位置)。另请注意,您每次都必须写入字节,您不能从位置加载,因为 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.

如果程序集是完全非托管的,您可以看到此链接或有关如何加载此类 dll 的此链接。

>.NET Core 3.0 本身支持编译为单个.exe

通过在

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

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

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

有关更多详细信息,请参阅我对此问题的回答。

Jeffrey Richter的摘录非常好。简而言之,将库添加为嵌入式资源,并在其他任何内容之前添加回调。这是我放在控制台应用程序的 Main 方法开头的代码版本(可在他页面的评论中找到)(只需确保使用这些库的任何调用都与 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);
            }
        };

扩展上面的@Bobby。 可以编辑 .csproj,以便在生成时使用 IL-Repack 自动将所有文件打包到单个程序集中。

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

下面是将 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>

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

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

当您有很多 DLL 时,可能很难手动将您需要的 DLL 包含在您的 exe 中。我发现的最好的方法是由Wegged在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)使主要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);
    }
}

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

但是,安装文件非常容易制作,我认为这不值得。

编辑:使用.NET程序集,此技术很容易。 使用 non-.NET DLL,这将需要更多的工作(您必须弄清楚在哪里解压缩文件并注册它们等等)。

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

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

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

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

如果您使用的是.NET Core 3.0

可以使用具有 PublishSingleFile 属性的 dotnet 发布命令执行此操作:

dotnet publish -r win-x64 -c Release/p:PublishSingleFile=true

唯一的缺点是你最终会得到一个巨大的EXE文件。

我使用从.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)    {        字符串资源名称 = 新程序集名称(参数。名称)。名称 + ".dll";        using (var stream = Assembly.GetExecutingAssembly()。GetManifestResourceStream(resourceName))        {            Byte[] assemblyData = new Byte[stream.长度];            流。Read(assemblyData, 0, assemblyData.Length);            返回 Assembly.Load(assemblyData);        }    }

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

    点name_,oShell,fso    Set oShell = CreateObject("Shell.Application")    Set fso = CreateObject("Scripting.fileSystemObject")    '将 VBS 脚本名称作为目标文件名    '################################################    name_ = Split(wscript.脚本名称,".")(0)    '从.CS文件中获取外部 DLL 和图标名称    '#######################################################    常量 OPEN_FILE_FOR_READING = 1    设置 objInputFile = fso。OpenTextFile(name_ & ".cs", 1)    '将所有内容读入数组    '#############################    inputData = Split(objInputFile.ReadAll, vbNewline)    对于输入数据中的每个 strData        如果 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", 2WScript.Quit(0)

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

除非你对生产混合程序集有严格的要求,否则我同意MusiGenesis的观点,即这并不值得麻烦地使用C#。如果需要这样做,也许可以考虑迁移到 C++/CLI。

通常,

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

您还可以检查构建是否使构建不可靠 NANT 能够在构建后合并程序集,但我本人对 NANT 不够熟悉,无法说该功能是否内置。

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

或者,如果不需要自动执行此操作,可以使用许多工具(如 ILMerge)将 .net 程序集合并到单个文件中。

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

试试这个:

https://github.com/ytk2128/dll-merger

在这里,您可以合并所有 32 位 DLL/exe - 即使它不是".net"dll - 所以对我来说比 ilmerge 更好,例如......