关于c#:将DLL嵌入已编译的可执行文件中

Embedding DLLs in a compiled executable

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

通常情况下,我很酷,只需要把DLL放在外面,让安装程序处理所有的事情,但是有几个人在工作,他们问我这个问题,我真的不知道。


我强烈推荐使用costura.fody——到目前为止,这是将资源嵌入程序集中的最好和最简单的方法。它可以作为Nuget软件包提供。

1
Install-Package Costura.Fody

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

1
Install-CleanReferencesTarget

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

更新

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


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

请参见:如何将C++ Window DLL合并到C应用程序exe中?


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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-easyly-in-a-net-assembly/


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

  • ilmerge是一个实用程序,可用于将多个.NET程序集合并到单个程序集中。
  • mono mkbundle将一个exe和所有带有libmono的程序集打包成一个二进制包。
  • IL重新打包是一种牙线替代ilmerge,具有一些附加功能。

此外,它还可以与mono链接器结合使用,这样可以删除未使用的代码,从而使生成的程序集更小。

另一种可能是使用.netz,它不仅允许压缩程序集,还可以将DLL直接打包到exe中。与上面提到的解决方案不同的是,netz不合并它们,它们保持独立的程序集,但打包成一个包。

.NETZ is a open source tool that compresses and packs the Microsoft .NET Framework executable (EXE, DLL) files in order to make them smaller.


如果程序集只有托管代码,则ilmerge可以将程序集组合为一个程序集。您可以使用命令行应用程序,或者添加对exe的引用并以编程方式进行合并。对于GUI版本,有eazfuscator和.netz,两者都是免费的。付费应用包括boxedapp和smartassembly。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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的位置加载,或者只需读取字节并从内存加载程序集。像这样或那样:

1
2
3
4
5
6
7
8
9
10
    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的信息。


杰弗里·里克特的摘录非常好。简而言之,将库添加为嵌入资源,并在添加任何其他资源之前添加回调。下面是我在控制台应用程序主方法的开始处放置的代码版本(在他页面的注释中找到)(只需确保使用库的任何调用与主方法都不同)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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's asnewr。您可以编辑.csproj以使用il repack在生成时自动将所有文件打包到单个程序集中。

  • Install-Package ILRepack.MSBuild.Task安装nuget ilrepack.msbuild.task包
  • 编辑.csproj的Afterbuild部分
  • 下面是一个简单的示例,它将exampleassemblytomerge.dll合并到项目输出中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- 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>

    检查BOXEDAPP

    它可以将一个DLL嵌入到任何应用程序中。当然,也是用C写的。

    希望它有帮助。


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

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

    编辑:使用.NET程序集可以很容易地使用此技术。对于非.NET DLL来说,这将是一项更大的工作(您必须找出在哪里解压缩文件并注册它们,等等)。


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

    http://madebits.com/netz/help.php单曲


    ilmerge方法和lars-holm-jensen处理assemblyresolve事件对插件主机都不起作用。假设可执行文件H动态加载程序集P,并通过在单独程序集中定义的接口IP访问它。要将IP嵌入H中,需要对LARS代码进行一些修改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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的序列化,请确保为未嵌入到您的程序集中的所有程序集返回空值,从而默认为标准行为。您可以通过以下方式获得这些库的列表:

    1
    2
    3
    4
    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,则返回空值。


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


    Ilmerge做的正是你想要的。


    除了ilmerge,如果您不想麻烦命令行开关,我真的推荐ilmerge gui。这是一个开源项目,真的很好!


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


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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
    enci.SshNet.dll"
    //+res>"
    C:\Program Files (x86)\Microsoft\ILMerge
    enci.SshNet.dll"
    //+ico>"
    C:\Program Files (x86)\Microsoft CAPICOM 2.1.0.2 SDK\Samples\c_sharp\xmldsig
    esources\Traffic.ico"

    ref、res和ico标记将由下面的.vbs脚本获取,以形成csc命令。

    然后在主服务器中添加程序集解析调用程序:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
        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)


    通常,您需要某种形式的后期生成工具来执行程序集合并,就像您描述的那样。有一个名为eazfuscator(eazfuscator.blogspot.com/)的免费工具,它是为字节码管理而设计的,也可以处理程序集合并。您可以将其添加到Visual Studio的后期生成命令行中,以合并程序集,但您的里程数会因任何非三值程序集合并方案中出现的问题而有所不同。

    您也可以检查构建的make untility nant是否具有在构建后合并程序集的能力,但是我对nant自己还不太熟悉,无法确定功能是否是内置的。

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

    或者,如果您不需要自动执行此操作,则有许多工具(如ilmerge)将.NET程序集合并到单个文件中。

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


    在C_中创建一个混合的本机/托管程序集是可能的,但并非那么容易。如果使用VisualC++编译器可以像其他任何东西一样轻松地创建混合程序集,那么使用C++就更容易了。

    除非你有生产混合动力总成的严格要求,否则我同意Musigenesis的观点,这不值得用C_来做麻烦。如果你需要这样做,也许看看移动到C++/CLI。