是否可以将预先存在的DLL嵌入到已编译的C#可执行文件中(以便仅分发一个文件)?如果有可能,人们将如何去做呢?
通常,我很酷,只是把DLL放在外面,让安装程序处理所有事情,但是有几个人在工作,问我这个问题,老实说我不知道。
是否可以将预先存在的DLL嵌入到已编译的C#可执行文件中(以便仅分发一个文件)?如果有可能,人们将如何去做呢?
通常,我很酷,只是把DLL放在外面,让安装程序处理所有事情,但是有几个人在工作,问我这个问题,老实说我不知道。
Answers:
我强烈建议使用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)
只需在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/
bytes
为null,如果是,则在此返回null。毕竟,dll可能不在资源中。二:仅当该类本身没有该程序集中的任何内容的“使用”时,此方法才有效。对于命令行工具,我必须将实际的程序代码移至新文件,并制作一个小型的新主程序来执行此操作,然后在旧类中调用原始主程序。
.dll
名称中包含连字符(即twenty-two.dll
),则这些字符也将被下划线(即twenty_two.dll
)取代。您可以将以下代码行更改为:dllName = dllName.Replace(".", "_").Replace("-", "_");
如果它们实际上是托管程序集,则可以使用ILMerge。对于本机DLL,您还有更多工作要做。
C++
在链接上不必担心。对于VB NET,ILMerge也非常容易工作。见这里https://github.com/dotnet/ILMerge。感谢@ Shog9
是的,可以将.NET可执行文件与库合并。有多种工具可以完成工作:
另外,可以将其与Mono Linker结合使用,后者会删除未使用的代码,从而使生成的程序集更小。
另一种可能性是使用.NETZ,它不仅允许压缩程序集,而且可以将dll直接打包到exe中。与上述解决方案的区别在于.NETZ不会合并它们,它们保持单独的程序集,但打包到一个程序包中。
.NETZ是一种开源工具,可压缩和打包Microsoft .NET Framework可执行文件(EXE,DLL),以使其更小。
ILMerge可以将程序集组合到一个程序集中,前提是该程序集只有托管代码。您可以使用命令行应用程序,也可以添加对exe的引用并以编程方式合并。对于GUI版本,有Eazfuscator和.Netz都是免费的。付费应用包括BoxedApp和SmartAssembly。
如果必须将程序集与非托管代码合并,建议使用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.
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的答案上扩展。您可以编辑.csproj以使用IL-Repack在构建时将所有文件自动打包到单个程序集中。
Install-Package ILRepack.MSBuild.Task
这是一个将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>
您可以将这些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]); }
如果传递的程序集不属于,则返回null IncludedAssemblies
。
通过在项目文件(.csproj)中使用以下属性来启用此功能:
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
无需任何外部工具即可完成此操作。
有关更多详细信息,请参见我对这个问题的回答。
听起来似乎很简单,但是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);
.
...并将解析器本身添加到类中的某个位置:
静态程序集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)
在C#中创建混合本机/托管程序集是可能的,但并非那么简单。如果您使用的是C ++,它会容易得多,因为Visual C ++编译器可以像其他方法一样轻松地创建混合程序集。
除非您有严格的要求来生产混合程序集,否则我会同意MusiGenesis的观点,认为使用C#确实不值得这样做。如果您需要这样做,也许可以考虑改用C ++ / CLI。
通常,您将需要某种形式的后期构建工具来执行您所描述的程序集合并。有一个名为Eazfuscator的免费工具(eazfuscator.blogspot.com/),该工具专为字节码处理而设计,还可以处理程序集合并。您可以将其添加到Visual Studio的后期构建命令行中以合并程序集,但是由于在任何非平凡的程序集合并方案中都会出现问题,因此里程会有所不同。
您还可以检查构建是否完整,NANT在构建后是否具有合并程序集的能力,但是我自己对NANT不够熟悉,无法说出该功能是否内置。
在构建应用程序的过程中,还有许多Visual Studio插件将执行程序集合并。
另外,如果您不需要自动完成此操作,则可以使用ILMerge之类的许多工具将.net程序集合并到一个文件中。
合并程序集时遇到的最大问题是,如果它们使用任何类似的名称空间。更糟糕的是,请引用同一dll的不同版本(我的问题通常是NUnit dll文件)。