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

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

通常情况下,我很乐意把DLL留在外面,让设置程序处理一切,但是工作中有几个人问我这个问题,老实说,我不知道。

479062 次浏览

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

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

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

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

另见:如何将C++windows dll合并到C#应用程序exe中?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

在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可以将程序集组合成一个程序集,前提是程序集只有托管代码。你可以使用命令行应用程序,或者添加对exe的引用并以编程方式合并。对于GUI版本,有Eazfucator. Netz,两者都是免费的。付费应用程序包括BoxedApp智能组装

如果您必须将程序集与非托管代码合并,我建议使用智能组装。我从来没有使用智能组装但使用所有其他代码时遇到过问题。在这里,它可以将所需的依赖项作为资源嵌入到您的主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.

如果程序集完全不受管理,您可以看到此链接这个关于如何加载此类dll。

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。

作者: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);
}
};

我强烈建议使用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

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

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

  1. 使用Install-Package ILRepack.MSBuild.Task安装nugetILRepack.MSBuild.任务包
  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>

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

在您的xyz.cs脚本中,在指令之后添加以下行(我的示例用于RenciSSH):

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"

下面的. vbs脚本将拾取ref、res和ico标签以形成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)

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

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

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

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

有关更多详细信息,请参阅我的答案对于这个问题

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

我在这里读到很多答案说使用ILMergeILRepack杰弗里·里彻方法,但这些方法都不适用于wpf应用程序,也不容易使用。

当你有很多DLL时,很难手动将你需要的DLL包含在你的exe中。我发现的最好的方法是Wegged在StackOverflow上解释的

复制粘贴他的答案在这里清晰(所有信贷Wegded


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)让你的MainProgram.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);
}
}

如果您使用的是. NET Core 3.0

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

dotnet发布-r win-x64-c发布 /p: PublishSingleFile=true

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