Встраивание DLL в скомпилированный исполняемый файл
Знаете, я нигде не видел хорошего ответа на этот вопрос. Можно ли встроить уже существующую DLL в скомпилированный исполняемый файл C# (чтобы у вас был только один файл для распространения)? Если это возможно, как можно это сделать?
Обычно я просто оставляю DLL снаружи и заставляю программу установки справиться со всем, но на работе было несколько человек, которые спрашивали меня об этом, и я, честно говоря, не знаю.
23 ответа
Я настоятельно рекомендую использовать Costura.Fody - безусловно, лучший и самый простой способ встраивания ресурсов в вашу сборку. Он доступен в виде пакета NuGet.
Install-Package Costura.Fody
После добавления его в проект он автоматически вставит все ссылки, скопированные в выходной каталог, в основную сборку. Возможно, вы захотите очистить встроенные файлы, добавив цель в ваш проект:
Install-CleanReferencesTarget
Вы также сможете указать, следует ли включать файлы pdb, исключать определенные сборки или извлекать сборки на лету. Насколько я знаю, поддерживаются и неуправляемые сборки.
Обновить
В настоящее время некоторые люди пытаются добавить поддержку DNX.
Просто щелкните правой кнопкой мыши свой проект в 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. Для нативных DLL у вас будет немного больше работы.
Смотрите также: Как можно объединить dll Windows C++ в exe приложения C#?
Да, можно объединять исполняемые файлы.NET с библиотеками. Есть несколько инструментов, доступных для выполнения работы:
- ILMerge - это утилита, которая может использоваться для объединения нескольких сборок.NET в одну сборку.
- Mono mkbundle, упаковывает exe и все сборки с libmono в один двоичный пакет.
- IL-Repack является альтернативой FLOSS для ILMerge, с некоторыми дополнительными функциями.
Кроме того, это может быть объединено с Mono Linker, который удаляет неиспользуемый код и, следовательно, делает полученную сборку меньше.
Другой возможностью является использование .NETZ, который не только позволяет сжимать сборку, но также может упаковать dll-файлы прямо в exe. Отличие от вышеупомянутых решений состоит в том, что.NETZ не объединяет их, они остаются отдельными сборками, но упакованы в один пакет.
.NETZ - это инструмент с открытым исходным кодом, который сжимает и упаковывает исполняемые файлы Microsoft .NET Framework (EXE, DLL), чтобы сделать их меньше.
ILMerge может объединять сборки в одну сборку, если сборка содержит только управляемый код. Вы можете использовать приложение командной строки или добавить ссылку на исполняемый файл и программно объединить. Для версии с графическим интерфейсом есть Eazfuscator, а также .Netz, которые бесплатны. Платные приложения включают BoxedApp и SmartAssembly.
Если вам нужно объединить сборки с неуправляемым кодом, я бы предложил SmartAssembly. У меня никогда не было икоты с SmartAssembly, но со всеми остальными. Здесь он может встраивать необходимые зависимости в качестве ресурсов в ваш основной исполняемый файл.
Вы можете сделать все это вручную, не беспокоясь о том, что сборка управляется или в смешанном режиме, добавив 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.
Если сборка полностью неуправляемая, вы можете увидеть эту ссылку или это о том, как загрузить такие библиотеки.
.NET Core 3.0 изначально поддерживает компиляцию в один EXE-файл.
Эта функция активируется при использовании следующего свойства в файле проекта (.csproj):
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
Это делается без каких-либо внешних инструментов.
См. Мой ответ на этот вопрос для получения дополнительной информации.
Отрывок Джеффри Рихтера очень хорош. Короче говоря, добавьте библиотеку как встроенные ресурсы и добавьте обратный вызов, прежде всего. Вот версия кода (найдена в комментариях на его странице), которую я поместил в начале метода 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);
}
};
Чтобы расширить на @ ответ Бобби выше. Вы можете отредактировать ваш.csproj, чтобы использовать IL-Repack для автоматической упаковки всех файлов в одну сборку при сборке.
- Установите пакет nuget ILRepack.MSBuild.Task с помощью
Install-Package ILRepack.MSBuild.Task
- Отредактируйте раздел AfterBuild вашего.csproj
Вот простой пример, который объединяет 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 (никаких действий вручную не требуется, все делается при компиляции)
Я читал здесь много ответов, в которых говорилось об использовании методов ILMerge, ILRepack или Джеффри Ритчера, но ни один из них не работал с приложениями WPF и не был простым в использовании.
Когда у вас много DLL, может быть сложно вручную включить ту, которая вам нужна в исполняемый файл. Лучший метод, который я нашел, был объяснен Wegged здесь, на Stackru.
Копия Wegged здесь его ответ для ясности (все кредиты 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);
}
}
Вы можете добавить библиотеки DLL в качестве встроенных ресурсов, а затем при запуске программы распаковать их в каталог приложения (после проверки, чтобы убедиться, что они там уже есть).
Установочные файлы настолько просты в создании, что я не думаю, что это того стоит.
РЕДАКТИРОВАТЬ: Этот метод будет легко с сборками.NET. С не-.NET DLL было бы намного больше работы (вам нужно было бы выяснить, где распаковать файлы, зарегистрировать их и так далее).
SmartAssembly, http://www.smartassembly.com/, также может справиться с этим элегантно. Этот продукт, в дополнение к объединению всех зависимостей в одну DLL, (необязательно) запутывает ваш код, удаляет дополнительные метаданные для уменьшения размера получаемого файла, а также может фактически оптимизировать IL для повышения производительности во время выполнения. Существует также некоторая глобальная функция обработки исключений / отчетности, которую она добавляет к вашему программному обеспечению (при желании), которую я не нашел времени, чтобы понять, но мог бы быть полезен. Я считаю, что он также имеет API командной строки, поэтому вы можете сделать его частью вашего процесса сборки.
Проверьте boxedapp
Он может встроить DLL в любое приложение. Написано в C# тоже, конечно:)
Надеюсь, поможет.
Ни подход ILMerge, ни обработка события AssemblyResolve Ларсом Холмом Дженсеном не будут работать для хоста плагина. Например, исполняемый файл H загружает сборку P динамически и обращается к ней через IP- интерфейс, определенный в отдельной сборке. Чтобы встроить IP в H, нужно немного изменить код Ларса:
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
,
Я бы порекомендовал вам проверить утилиту.NETZ, которая также сжимает сборку по выбранной вами схеме:
Это может показаться упрощенным, но WinRar дает возможность сжать кучу файлов в самораспаковывающийся исполняемый файл.
Он имеет множество настраиваемых опций: значок финала, извлечение файлов по заданному пути, файл для выполнения после извлечения, настраиваемый логотип / текст для всплывающего окна, отображаемого во время извлечения, вообще без всплывающего окна, текст лицензионного соглашения и т. Д.
Может быть полезно в некоторых случаях.
Помимо ILMerge, если вы не хотите беспокоиться о переключателях командной строки, я действительно рекомендую ILMerge-Gui. Это проект с открытым исходным кодом, действительно хорошо!
Если вы используете .NET Core 3.0
Это можно сделать с помощью команды публикации dotnet со свойством PublishSingleFile:
dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
Единственным недостатком является то, что вы получите один EXE-файл огромного размера.
Я использую компилятор csc.exe, вызываемый из скрипта.vbs.
В вашем скрипте 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) { 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); возврат Assembly.Load(assemblyData); } }
Я называю скрипт vbs в соответствии с именем файла.cs (например, ssh.vbs ищет файл ssh.cs); это делает запуск сценария много раз намного проще, но если вы не такой идиот, как я, то универсальный сценарий может выбрать целевой файл.cs из перетаскивания:
Тусклый name_,oShell,fso Set oShell = CreateObject("Shell.Application") Set fso = CreateObject("Scripting.fileSystemObject") ПРИНЯТЬ НАЗВАНИЕ СЦЕНАРИИ VBS В КАЧЕСТВЕ НАЗВАНИЯ ФАЙЛА "################################################ name_ = Split(wscript.ScriptName, ".")(0) 'ПОЛУЧИТЕ ВНЕШНИЕ DLL И НАЗВАНИЯ ИКОНЫ ИЗ ФАЙЛА.CS "####################################################### Const OPEN_FILE_FOR_READING = 1 Установите objInputFile = fso.OpenTextFile(name_ & ".cs", 1) "ВСЕ ЧИТАЙТЕ В Массив "############################# inputData = Split(objInputFile.ReadAll, vbNewline) Для каждого strData In inputData если слева (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)
Попробуй это:
https://github.com/ytk2128/dll-слияние
здесь вы можете объединить все 32-битные dll / exe - даже не ".net" dll - так что для меня лучше, чем, например, ilmerge ...
Как правило, вам понадобится инструмент для пост-сборки, чтобы выполнить слияние сборки, как вы описываете. Существует бесплатный инструмент под названием Eazfuscator (eazfuscator.blogspot.com/), предназначенный для манипулирования байт-кодом, который также обрабатывает объединение сборок. Вы можете добавить это в командную строку после сборки с помощью Visual Studio для объединения ваших сборок, но ваш пробег будет отличаться из-за проблем, которые возникнут в любых сценариях слияния нетривиальных сборок.
Вы также можете проверить, имеет ли сборка до тех пор, пока NANT не сможет объединять сборки после сборки, но я сам не достаточно знаком с NANT, чтобы сказать, встроена ли функциональность или нет.
Существует также множество плагинов Visual Studio, которые выполняют слияние сборок как часть сборки приложения.
В качестве альтернативы, если вам не нужно, чтобы это делалось автоматически, есть ряд инструментов, таких как ILMerge, которые объединят сборки.net в один файл.
Самая большая проблема, с которой я столкнулся при объединении сборок, заключается в том, используют ли они какие-либо похожие пространства имен. Или, что еще хуже, ссылаются на разные версии одной и той же dll (мои проблемы обычно были с dll-файлами NUnit).
Я опробовал это решение в проекте кода, который встраивает DLL: http://www.codeproject.com/Articles/528178/Load-DLL-From-Embedded-Resource
И это работало просто отлично.
Возможно, но не все так просто, создать гибридную нативную / управляемую сборку в C#. Если бы вы использовали C++, это было бы намного проще, поскольку компилятор Visual C++ может создавать гибридные сборки так же легко, как и все остальное.
Если у вас нет строгих требований к созданию гибридной сборки, я бы согласился с MusiGenesis, что на самом деле это не стоит делать с C#. Если вам нужно сделать это, возможно, обратите внимание на переход на C++/CLI.