ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

我建议您查看 .NETZ 实用程序,它还使用您选择的方案压缩程序集:http://madebits.com/netz/help.php#single
除了 ILMerge,如果您不想为命令行开关操心,我真的推荐 ILMerge-Gui。这是一个开源项目,非常好!

M
Marc Giroux

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

Install-Package Costura.Fody

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

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 文件。
包管理器控制台命令 Install-CleanReferencesTarget 不再有效并且将失败。它在当前版本中是自动化的。同样对于 Visual Studio 2017(使用 MSBuild 15)安装 Nugets Fody 4.2.1Costura.Fody 3.3.3 以成功编译。
M
M. Wiśnicki

只需在 Visual Studio 中右键单击您的项目,选择 Project Properties -> Resources -> Add Resource -> Add Existing File... 并将下面的代码包含到您的 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;
对此有两点评论。一:您应该检查 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("-", "_");
L
Lucas Carneiro

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

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


我对Native DLL merge 很感兴趣,有资料吗?
@BaiyanHuang 看看github.com/boxedapp/bxilmerge,这个想法是为原生 Dll 制作“ILMerge”。
像我这样的 VB NET 开发人员不会对链接中的 C++ 感到害怕。 ILMerge 也很容易用于 VB NET。请参阅此处https://github.com/dotnet/ILMerge。谢谢@Shog9
B
Bobby

是的,可以将 .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,并且不再在网站上链接......所以“完全消失”是夸大其词。很可能不再支持它,但它仍然存在。我更新了链接。
C
Community

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。


请注意,资源的“构建操作”需要设置为“嵌入式资源”。
@Mavamaarten 不一定。如果提前添加到项目的 Resources.resx 中,则不需要这样做。
EAZfuscator 现已商业化。
S
Steve

excerpt by 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);
            }
        };

稍微改变了一下,完成了工作,tnx 伙计!
项目 libz.codeplex.com 使用此过程,但它也会做一些其他事情,例如为您管理事件处理程序和一些不破坏“Managed Extensibility Framework Catalogs”的特殊代码(此过程本身会破坏)
那太棒了!!谢谢@史蒂夫
C
Community

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

使用 Install-Package ILRepack.MSBuild.Task 安装 nuget ILRepack.MSBuild.Task 包 编辑 .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。
M
Marcell Toth

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

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

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

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

有关详细信息,请参阅我的回答 for this question


.NET 5 怎么样,它支持这个吗?
您还需要添加运行时标识符:win10-x64 或 linux-x64,例如:linux-x64
M
MusiGenesis

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

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

编辑:这种技术对于 .NET 程序集很容易。使用非 .NET DLL 会做更多的工作(您必须弄清楚在哪里解压文件并注册它们等等)。


这里有一篇很棒的文章解释了如何做到这一点:codeproject.com/Articles/528178/Load-DLL-From-Embedded-Resource
J
Joy

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

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


A
Ant_222

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 。


很抱歉将其发布为答案而不是评论。我无权评论别人的答案。
L
Ludovic Feltz

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

我在这里阅读了很多答案,说要使用 ILMerge、ILRepack 或 Jeffrey 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 吗?
I
Ivan Ferrer Villa

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


Windows 本身有一个类似的工具,称为 iexpress。 Here's a tutorial
M
Mark Llewellyn

我使用从 .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);
    .

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

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        String resourceName = new AssemblyName(args.Name).Name + ".dll";

        using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
        {
            Byte[] assemblyData = new Byte[stream.Length];
            stream.Read(assemblyData, 0, assemblyData.Length);
            return Assembly.Load(assemblyData);
        }

    }

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

Dim name_,oShell,fso
    Set oShell = CreateObject("Shell.Application")
    Set fso = CreateObject("Scripting.fileSystemObject")

    'TAKE THE VBS SCRIPT NAME AS THE TARGET FILE NAME
    '################################################
    name_ = Split(wscript.ScriptName, ".")(0)

    'GET THE EXTERNAL DLL's AND ICON NAMES FROM THE .CS FILE
    '#######################################################
    Const OPEN_FILE_FOR_READING = 1
    Set objInputFile = fso.OpenTextFile(name_ & ".cs", 1)

    'READ EVERYTHING INTO AN ARRAY
    '#############################
    inputData = Split(objInputFile.ReadAll, vbNewline)

    For each strData In inputData

        if left(strData,7)="//+ref>" then 
            csc_references = csc_references & " /reference:" &         trim(replace(strData,"//+ref>","")) & " "
        end if

        if left(strData,7)="//+res>" then 
            csc_resources = csc_resources & " /resource:" & trim(replace(strData,"//+res>","")) & " "
        end if

        if left(strData,7)="//+ico>" then 
            csc_icon = " /win32icon:" & trim(replace(strData,"//+ico>","")) & " "
        end if
    Next

    objInputFile.Close


    'COMPILE THE FILE
    '################
    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)

C
Chester Ayala

如果您使用的是 .NET Core 3.0

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

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

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


C
Chris Charabaruk

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

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


w
wllmsaccnt

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

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

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

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

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


Eazfuscator 只会调用 IlMerge,AFAIK。
+1 鲍比。我应该记得的。 Eazfucator 为您所做的所有事情都是使用更通用的配置文件抽象对 ILMerge 的实际调用。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅