是否可以将预先存在的 DLL 嵌入到已编译的 C# 可执行文件中(这样您只有一个要分发的文件)?如果有可能,人们将如何去做呢?
通常,我很乐意将 DLL 留在外面并让安装程序处理所有事情,但是有几个工作人员问过我这个问题,老实说我不知道。
我强烈建议使用 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)
只需在 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/
bytes
是否为 null,如果是,则在此处返回 null。毕竟,资源中的 dll 可能 not 。二:这只适用于该类本身没有对该程序集的任何内容的“使用”。对于命令行工具,我必须将我的实际程序代码移动到一个新文件中,并创建一个小的新主程序来执行此操作,然后在旧类中调用原始主程序。
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("-", "_");
如果它们实际上是托管程序集,则可以使用 ILMerge。对于本机 DLL,您需要做更多的工作。
另请参阅: How can a C++ windows dll be merged into a C# application exe?
C++
感到害怕。 ILMerge 也很容易用于 VB NET。请参阅此处https://github.com/dotnet/ILMerge。谢谢@Shog9
是的,可以将 .NET 可执行文件与库合并。有多种工具可用于完成工作:
ILMerge 是一个实用程序,可用于将多个 .NET 程序集合并为一个程序集。
Mono mkbundle,将一个 exe 和所有带有 libmono 的程序集打包到一个二进制包中。
IL-Repack 是 ILMerge 的 FLOSS 替代品,具有一些附加功能。
此外,这可以与 Mono Linker 结合使用,它确实会删除未使用的代码,从而使生成的程序集更小。
另一种可能性是使用.NETZ,它不仅可以压缩程序集,还可以将 dll 直接打包到 exe 中。与上述解决方案的不同之处在于 .NETZ 不会合并它们,它们保持独立的程序集,但被打包到一个包中。
.NETZ 是一个开源工具,它压缩和打包 Microsoft .NET Framework 可执行文件(EXE、DLL)以使它们更小。
ILMerge 可以将程序集合并为一个程序集,前提是该程序集只有托管代码。您可以使用命令行应用程序,或添加对 exe 的引用并以编程方式合并。对于 GUI 版本,有 Eazfuscator 和 .Netz,两者都是免费的。付费应用程序包括 BoxedApp 和 SmartAssembly。
如果您必须将程序集与非托管代码合并,我建议您使用 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.
如果程序集完全不受管理,您可以查看此 link 或 this 以了解如何加载此类 dll。
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);
}
};
在上面的 @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>
.NET Core 3.0 原生支持编译为单个 .exe
该功能通过在项目文件 (.csproj) 中使用以下属性来启用:
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
这是在没有任何外部工具的情况下完成的。
有关详细信息,请参阅我的回答 for this question。
您可以将 DLL 作为嵌入式资源添加,然后让您的程序在启动时将它们解压缩到应用程序目录中(在检查它们是否已经存在之后)。
但是,设置文件很容易制作,我认为这不值得。
编辑:这种技术对于 .NET 程序集很容易。使用非 .NET DLL 会做更多的工作(您必须弄清楚在哪里解压文件并注册它们等等)。
另一个可以优雅地处理这个问题的产品是 SmartAssembly,位于 SmartAssembly.com。除了将所有依赖项合并到一个 DLL 之外,该产品还将(可选地)混淆您的代码,删除额外的元数据以减小生成的文件大小,并且实际上还可以优化 IL 以提高运行时性能。
它还向您的软件(如果需要)添加了某种可能有用的全局异常处理/报告功能。我相信它还有一个命令行 API,因此您可以将其作为构建过程的一部分。
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 。
以下方法不要使用外部工具,并自动包含所有需要的 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-us
或 fr-fr
子文件夹?这是 DestinationSubDirectory
吗?
这听起来可能很简单,但 WinRar 提供了将一堆文件压缩为自解压可执行文件的选项。它有很多可配置的选项:最终图标、将文件提取到给定路径、提取后要执行的文件、提取过程中显示的自定义徽标/弹出窗口文本、根本没有弹出窗口、许可协议文本等。在某些情况下可能很有用.
我使用从 .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)
如果您使用的是 .NET Core 3.0
您可以使用具有 PublishSingleFile 属性的 dotnet publish 命令执行此操作:
dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
唯一的缺点是您最终会得到一个巨大的 EXE 文件。
在 C# 中创建混合本机/托管程序集是可能的,但并非那么容易。如果您使用 C++,它会容易得多,因为 Visual C++ 编译器可以像其他任何东西一样轻松地创建混合程序集。
除非您对生成混合程序集有严格的要求,否则我同意 MusiGenesis 的观点,即这不值得用 C# 来做这件事。如果您需要这样做,或许可以考虑改用 C++/CLI。
通常,您需要某种形式的后期构建工具来执行您所描述的程序集合并。有一个名为 Eazfuscator (eazfuscator.blogspot.com/) 的免费工具,它专为字节码处理而设计,同时也处理程序集合并。您可以使用 Visual Studio 将其添加到构建后的命令行中以合并您的程序集,但是由于在任何非平凡的程序集合并方案中会出现问题,您的工作量会有所不同。
您还可以检查 build make untility NANT 是否能够在构建后合并程序集,但我自己对 NANT 还不够熟悉,无法说出该功能是否内置。
还有许多 Visual Studio 插件将执行程序集合并作为构建应用程序的一部分。
或者,如果您不需要自动完成此操作,有许多工具(如 ILMerge)会将 .net 程序集合并到一个文件中。
我在合并程序集时遇到的最大问题是它们是否使用任何类似的命名空间。或者更糟糕的是,引用同一个 dll 的不同版本(我的问题通常与 NUnit dll 文件有关)。
不定期副业成功案例分享
Install-CleanReferencesTarget
不再有效并且将失败。它在当前版本中是自动化的。同样对于 Visual Studio 2017(使用 MSBuild 15)安装 NugetsFody 4.2.1
和Costura.Fody 3.3.3
以成功编译。