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


618

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

通常,我很酷,只是把DLL放在外面,让安装程序处理所有事情,但是有几个人在工作,问我这个问题,老实说我不知道​​。


我建议您检查一下.NETZ实用程序,该实用程序还会使用您选择的方案压缩程序集:http
//madebits.com/netz/help.php#single

2
可能,但是最终将得到大型可执行文件(Base64将用于编码dll)。
帕维尔Dyda

除了ILMerge之外,如果您不想打扰命令行开关,我真的建议您使用ILMerge-Gui。这是一个开源项目,真的很好!
tyron

2
@PawełDyda:您可以将原始二进制数据嵌入到PE映像中(请参阅RCDATA)。无需转换(或建议)。
IInspectable '17

Answers:


761

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

Install-Package Costura.Fody

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

Install-CleanReferencesTarget

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

更新资料

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

更新2

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


79
谢谢你的建议。安装软件包即可完成。默认情况下,它甚至压缩程序集。
丹尼尔(Daniel)

9
不想成为“我也是”,但我也很讨厌-这让我头疼很多!谢谢你的推荐!这使我可以将需要重新分发的所有内容打包到单个exe中,并且现在它比原始exe和dll合并的要小...我只使用了几天,所以我不能说我我们已经尽力了,但是除非出现任何不良情况,否则我可以看到它已成为工具箱中的常规工具。就是这样!
mattezell 2014年

19
这很酷。但是有一个缺点:在Windows上生成的程序集不再与Mono Linux二进制兼容。这意味着您不能将程序集直接部署到Linux mono。
泰勒·朗

7
好可爱 如果您使用的是vs2018,请不要忘记FodyWeavers.xml文件位于项目的根目录。
艾伦·迪普

4
作为最后一条注释的补充:将具有以下内容的FodyWeavers.xml添加到您的项目中:<?xml version =“ 1.0” encoding =“ utf-8”?> <Weavers VerifyAssembly =“ true”> <Costura /> </ Weavers>
HHenn,

88

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


6
您可以立即使用此行为。看看我的答案stackoverflow.com/a/20306095/568266
Matthias

4
同样重要的是,要在AshRowe的博客上记录一个非常有用的评论:如果安装了自定义主题,它将尝试解决PresentationFramework.Theme程序集,该程序集会崩溃并烧毁!根据AshRowe的建议,您可以像这样简单地检查dllName是否包含PresentationFramework:if(dllName.ToLower()。Contains(“ presentationframework”))返回null;
YasharBahman 2014年

4
对此有两个评论。一:您应该检查是否bytes为null,如果是,则在此返回null。毕竟,dll可能不在资源中。二:仅当该类本身没有该程序集中的任何内容的“使用”时,此方法才有效。对于命令行工具,我必须将实际的程序代码移至新文件,并制作一个小型的新主程序来执行此操作,然后在旧类中调用原始主程序。
Nyerguds 2014年

2
这种方法的好处是它不依赖安装任何外部库来实现所需的功能。这种方法的缺点是,它仅在涉及托管dll时才有用-互操作dll(至少就我的测试而言)不会触发assemblyresolve事件,即使它们执行了Assembly.Load(<互操作的字节数.dll>)并没有达到预期的效果。stackoverflow.com/questions/13113131/…关于此事,仅是我的2c
XDS

3
以防万一有人遇到我的问题:如果.dll名称中包含连字符(即twenty-two.dll),则这些字符也将被下划线(即twenty_two.dll)取代。您可以将以下代码行更改为:dllName = dllName.Replace(".", "_").Replace("-", "_");
Micah Vertal,2016年

87

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

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


我对本机DLL合并感兴趣,是否有任何资料?
Baiyan Huang 2009年


@BaiyanHuang查看github.com/boxedapp/bxilmerge,这个想法是为本地Dll制作“ ILMerge”。
Artem Razin

像我这样的VB NET开发人员C++在链接上不必担心。对于VB NET,ILMerge也非常容易工作。见这里https://github.com/dotnet/ILMerge。感谢@ Shog9
Ivan Ferrer Villa

26

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

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

另外,可以将其与Mono Linker结合使用,后者会删除未使用的代码,从而使生成的程序集更小。

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

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


NETZ似乎不见了
Rbjz

哇-我以为我终于找到了,然后阅读了此评论。它似乎完全消失了。有叉子吗?
马菲

好吧,它刚刚移至GitHub,并且不再与网站链接...所以“完全消失”是一种夸大其词。很可能不再支持它,但它仍然存在。我更新了链接。
鲍比

20

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

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

您可以通过将dll嵌入到资源中,然后依靠AppDomain的Assembly来手动进行所有这些操作,而不必担心是否以汇编方式进行管理或以混合模式进行操作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.

如果组件是完全不受管理的,你可以看到这个链接为如何加载的DLL等。


请注意,资源的“生成操作”需要设置为“嵌入式资源”。
Mavamaarten 2014年

@Mavamaarten不一定。如果将其预先添加到项目的Resources.resx中,则无需这样做。
Nyerguds 2014年

2
EAZfuscator现在已经投入商业。
Telemat

16

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

1
换了一点,做完了工作,老兄!
肖恩·埃德曼

项目libz.codeplex.com使用此过程,但它还会做其他一些事情,例如为您管理事件处理程序和一些特殊的代码,以免破坏“ Managed Extensibility Framework Catalogs ”(此过程本身会破坏该过程)
Scott张伯伦2015年

那很棒!!感谢@Steve
Ahmer Afzal,

14

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

  1. 使用以下命令安装nuget ILRepack.MSBuild.Task软件包: Install-Package 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>

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

8

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

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

编辑:.NET程序集将很容易使用此技术。使用非.NET DLL,将需要做更多的工作(您必须弄清楚要在何处解压缩文件并进行注册等等)。


在这里,您可以找到一篇很棒的文章,说明如何执行此操作:codeproject.com/Articles/528178/Load-DLL-From-Embedded-Resource
蓝蓝的

8

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

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


7

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]);  }

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


很抱歉将其发布为答案而不是评论。我无权评论他人的回答。
Ant_222 2013年

5

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

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

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

无需任何外部工具即可完成此操作。

有关更多详细信息,请参见我对这个问题的回答。


3

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


Windows本身有一个类似的工具,称为iexpress。这是一个教程
Ivan Ferrer Villa

2

我使用从.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)
    {
        字符串resourceName = new AssemblyName(args.Name).Name +“ .dll”;

        使用(var stream = Assembly.GetExecutingAssembly()。GetManifestResourceStream(resourceName))
        {
            Byte [] assemblyData =新的Byte [stream.Length];
            stream.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)

    对于inputData中的每个strData

        如果离开(strData,7)=“ // + ref>”然后 
            csc_references = csc_references&“ / reference:”&trim(replace(strData,“ // + ref>”,“”))&“”
        万一

        如果离开(strData,7)=“ // + res>”然后 
            csc_resources = csc_resources&“ / resource:”&trim(replace(strData,“ // + res>”,“”))&“”
        万一

        如果离开(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)

0

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

除非您有严格的要求来生产混合程序集,否则我会同意MusiGenesis的观点,认为使用C#确实不值得这样做。如果您需要这样做,也许可以考虑改用C ++ / CLI。


0

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

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

在构建应用程序的过程中,还有许多Visual Studio插件将执行程序集合并。

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

合并程序集时遇到的最大问题是,如果它们使用任何类似的名称空间。更糟糕的是,请引用同一dll的不同版本(我的问题通常是NUnit dll文件)。


1
Eazfuscator只会致电AFAIK IlMerge。
鲍比(Bobby)

+1鲍比。我应该记得这一点。Eazfucator为您做的所有事情都是使用更通用的配置文件来抽象​​对ILMerge的实际调用。
wllmsaccnt 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.