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

时间:2008-10-09 23:11:54

标签: c# .net dll merge linker

你知道,我在任何地方都没有看到一个好的答案。是否可以将预先存在的DLL嵌入到已编译的C#可执行文件中(这样您只能分发一个文件)?如果有可能的话,怎么去做呢?

通常情况下,我很乐意将DLL放在外面并让安装程序处理所有内容,但是有几个人在工作,他们问我这个,老实说我不知道​​。

20 个答案:

答案 0 :(得分:682)

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

Install-Package Costura.Fody

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

Install-CleanReferencesTarget

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

<强>更新

目前,有些人正在尝试添加support for DNX

答案 1 :(得分:81)

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

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

答案 2 :(得分:77)

在Visual Studio中右键单击您的项目,选择Project Properties - &gt;资源 - &gt;添加资源 - &gt;添加现有文件... 并将以下代码包含在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/

答案 3 :(得分:25)

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

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

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

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

  

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

答案 4 :(得分:18)

如果程序集只有托管代码,

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。

答案 5 :(得分:11)

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

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

答案 6 :(得分:11)

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

  1. 使用Install-Package ILRepack.MSBuild.Task
  2. 安装nuget ILRepack.MSBuild.Task包
  3. 编辑.csproj的AfterBuild部分
  4. 这是一个将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>
    

答案 7 :(得分:8)

检查boxedapp

它可以在任何应用中嵌入一个dll。当然也用C#编写:)

希望它有所帮助。

答案 8 :(得分:7)

我建议您查看.NETZ实用程序,它还可以使用您选择的方案压缩程序集:

http://madebits.com/netz/help.php#single

答案 9 :(得分:7)

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

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

编辑:使用.NET程序集这种技术很容易。使用非.NET DLL会有更多的工作(你必须找出解压缩文件的位置并注册它们等等)。

答案 10 :(得分:6)

另一个可以优雅处理这个问题的产品是SmartAssembly,SmartAssembly.com。除了将所有依赖项合并到单个DLL中之外,该产品还可以(可选地)对代码进行模糊处理,删除额外的元数据以减少生成的文件大小,还可以实际优化IL以提高运行时性能。它还为您的软件添加了某种全局异常处理/报告功能(如果需要),我没有花时间去理解,但可能很有用。我相信它还有一个命令行API,因此您可以将其作为构建过程的一部分。

答案 11 :(得分:6)

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 :(得分:4)

ILMerge完全符合您的要求。

答案 13 :(得分:3)

除了ILMerge,如果你不想打扰命令行开关,我真的推荐ILMerge-Gui。这是一个开源项目,非常好!

答案 14 :(得分:2)

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

答案 15 :(得分:2)

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

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

getRuns()

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

有关更多详细信息,请参见我的答案for this question

答案 16 :(得分:2)

以下方法请勿使用外部工具,并且自动包括所有需要的DLL (无需手动操作,所有操作均在编译时完成)

我在这里读到很多答案,说要使用 ILMerge ILRepack Jeffrey Ritcher 方法,但是这些方法都不与一起使用WPF应用程序也不容易使用。

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

答案 17 :(得分:1)

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

答案 18 :(得分:0)

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

您还可以检查构建是否具有直接性NANT是否能够在构建后合并程序集,但我不熟悉NANT自己说明功能是否内置。

还有许多Visual Studio插件将在构建应用程序时执行程序集合。

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

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

答案 19 :(得分:0)

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

除非你有严格要求生产混合装配,否则我同意MusiGenesis认为这对C#来说并不值得。如果您需要这样做,或许请转而使用C ++ / CLI。