Встраивание DLL в скомпилированный исполняемый файл

Можно ли встроить уже существующую DLL в скомпилированный исполняемый файл С# (чтобы у вас был только один файл для распространения)? Если это возможно, как можно это сделать?

Обычно я просто оставляю DLL снаружи и заставляю программу установки справиться со всем, но есть несколько человек, которые спрашивают меня об этом, и я, честно говоря, не знаю.

Ответ 1

Я настоятельно рекомендую использовать Costura.Fody - безусловно лучший и самый простой способ встраивания ресурсов в вашу сборку. Он доступен как пакет NuGet.

Install-Package Costura.Fody

После добавления его в проект он автоматически вставляет все ссылки, которые копируются в выходной каталог в вашу основную сборку. Возможно, вы захотите очистить встроенные файлы, добавив цель в свой проект:

Install-CleanReferencesTarget

Вы также сможете указать, следует ли включать pdb, исключать определенные сборки или извлекать сборки "на лету". Насколько мне известно, поддерживаются также неуправляемые сборки.

Обновление

В настоящее время некоторые люди пытаются добавить поддержку DNX.

Ответ 3

Просто щелкните свой проект в 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/

Ответ 4

Да, возможно объединить исполняемые файлы .NET с библиотеками. Для выполнения этой задачи доступны несколько инструментов:

  • ILMerge - это утилита, которая может использоваться для объединения нескольких сборок .NET в одну сборку.
  • Mono mkbundle, пакеты exe и все сборки с libmono в один бинарный пакет.
  • IL-Repack - это FLOSS, альтернативный ILMerge, с некоторыми дополнительными функциями.

Кроме того, это можно комбинировать с Mono Linker, который удаляет неиспользуемый код и, следовательно, уменьшает итоговую сборку.

Другая возможность заключается в использовании . NETZ, который не только позволяет сжимать сборку, но также может упаковывать DLL прямо в Exe. Разница с вышеупомянутыми решениями заключается в том, что .NETZ не объединяет их, они остаются отдельными сборками, но упаковываются в один пакет.

.NETZ - это инструмент с открытым исходным кодом, который сжимает и упаковывает исполняемые файлы Microsoft.NET Framework (EXE, DLL), чтобы сделать их меньше.

Ответ 5

ILMerge может комбинировать сборки с одной сборкой, если сборка имеет только управляемый код. Вы можете использовать приложение командной строки или добавить ссылку на exe и программно объединить. Для версии GUI существует Eazfuscator, а также . Netz, оба из которых являются бесплатными. Платные приложения включают BoxedApp и SmartAssembly,

Если вам нужно объединить сборки с неуправляемым кодом, я бы предложил SmartAssembly. У меня никогда не было икоты с SmartAssembly, но со всеми остальными. Здесь он может встраивать необходимые зависимости в качестве ресурсов в ваш основной 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.

Если сборка полностью неуправляема, вы можете увидеть эту ссылку или this как загрузить такие DLL.

Ответ 6

Отрывок из Джеффри Рихтера очень хорош. Короче говоря, добавьте библиотеку в качестве встроенных ресурсов и добавьте обратный вызов перед чем-либо еще. Вот версия кода (найденная в комментариях его страницы), которую я поставил в начале метода 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);
            }
        };

Ответ 7

Подробнее о @Bobby asnwer выше. Вы можете отредактировать ваш .csproj, чтобы использовать IL-Repack для автоматической упаковки всех файлов в одну сборку при сборке.

  1. Установите пакет nuget ILRepack.MSBuild.Task с помощью Install-Package ILRepack.MSBuild.Task
  2. Отредактируйте раздел 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>

Ответ 8

Вы можете добавить библиотеки DLL в качестве встроенных ресурсов, а затем распакуйте их в каталог приложения при запуске (после проверки, есть ли там там).

Файлы настроек так легко сделать, что я не думаю, что это того стоило.

РЕДАКТИРОВАТЬ: Этот метод будет легко с сборками .NET. С не-NET DLL файлами было бы намного больше работать (вам нужно было бы выяснить, где распаковать файлы и зарегистрировать их и т.д.).

Ответ 9

Отметьте boxedapp

Он может встраивать dll в любое приложение. Написано также на С#, конечно:)

Надеюсь, что это поможет.

Ответ 10

Я бы порекомендовал вам проверить утилиту .NETZ, которая также сжимает сборку со схемой по вашему выбору:

http://madebits.com/netz/help.php#single

Ответ 11

Другим продуктом, который может с этим справиться, является SmartAssembly, SmartAssembly.com. Этот продукт в дополнение к объединению всех зависимостей в одну DLL (необязательно) обфускает ваш код, удаляет дополнительные метаданные, чтобы уменьшить результирующий размер файла, а также может фактически оптимизировать IL для повышения производительности во время выполнения. Существует также какая-то глобальная функция обработки/обработки исключений, которая добавляется к вашему программному обеспечению (если это необходимо), что я не нашел времени, чтобы понять, но может быть полезным. Я считаю, что у него также есть API командной строки, поэтому вы можете сделать его частью своего процесса сборки.

Ответ 12

Ни подход 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;
};  

Трюк для обработки повторяющихся попыток разрешить одну и ту же сборку и вернуть существующий вместо создания нового экземпляра.

EDIT: Чтобы он не испортил сериализацию .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.

Ответ 13

ILMerge делает именно то, что вы хотите.

Ответ 14

Помимо ILMerge, если вы не хотите беспокоиться о переключении командной строки, я действительно рекомендую ILMerge-Gui. Это проект с открытым исходным кодом, очень хороший!

Ответ 15

Это может показаться упрощенным, но WinRar предоставляет возможность сжимать кучу файлов в самораспаковывающийся исполняемый файл.
Он имеет множество настраиваемых параметров: финальный значок, извлечение файлов в заданный путь, файл для выполнения после извлечения, пользовательский логотип/тексты для всплывающего окна, отображаемого во время извлечения, всплывающее окно вообще, текст лицензионного соглашения и т.д.
Может быть полезным в некоторых случаях.

Ответ 16

Я использую компилятор csc.exe, вызываемый из .vbs script.

В xyz.cs script добавьте следующие строки после директив (мой пример для SSH Renci):

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 script, чтобы сформировать команду 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 script для соответствия имени файла .cs(например, ssh.vbs ищет ssh.cs); это делает запуск script много раз намного проще, но если вы не идиот, как я, тогда общий script мог бы взять целевой файл .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 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)

Ответ 17

.NET Core 3.0 изначально поддерживает компиляцию в один .exe

Эта функция включена с помощью следующего свойства в файле проекта (.csproj):

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

Это делается без какого-либо внешнего инструмента.

Смотрите мой ответ на этот вопрос для получения дополнительной информации.

Ответ 18

Возможно, но не все так просто, создать гибридную сборку/управляемую сборку на С#. Если бы вы использовали С++, это было бы намного проще, поскольку компилятор Visual С++ может создавать гибридные сборки так же легко, как и все остальное.

Если у вас нет строгого требования к созданию гибридной сборки, я согласен с MusiGenesis, что это не стоит того, чтобы делать с С#. Если вам нужно это сделать, возможно, посмотрите на переход на С++/CLI.

Ответ 19

Как правило, для выполнения слияния сборки, как вы описываете, вам понадобится какая-то форма создания пост-сборки. Существует бесплатный инструмент Eazfuscator (eazfuscator.blogspot.com/), который предназначен для обработки байт-кода, который также обрабатывает слияние сборки. Вы можете добавить это в командную строку post build с Visual Studio, чтобы объединить ваши сборки, но ваш пробег будет зависеть от проблем, возникающих при любых сценариях слияния неконкурентов.

Вы также можете проверить, не удалось ли построить сборку NANT NANT, чтобы объединить сборки после сборки, но я не достаточно знаком с самим NANT, чтобы сказать, встроена ли функция или нет.

Существует также много плагинов Visual Studio, которые будут выполнять слияние сборки как часть создания приложения.

Альтернативно, если вам не нужно, чтобы это было сделано автоматически, существует ряд инструментов, таких как ILMerge, которые будут объединять сборки .net в один файл.

Самая большая проблема, с которой я столкнулась при слиянии сборок, - это использование каких-либо похожих пространств имен. Или, что еще хуже, ссылаться на разные версии одной и той же DLL (мои проблемы обычно были с файлами DLL NUnit).