Динамическое подключение C# DLL к C++
Ууу, сегодня brainstorm тема, специально для тру-програмеров. Не так давно мне понадобилось подключить dll, написанную на C#, к основной программе, которая была написана на unmanaged C++. Оказалось, что сделать это не так-то просто.
На одном из зарубежных сайтов мне удалось найти что-то вроде хака, который позволяет небольшим изменением кода dll-ки сделать ее легко экпортируемой. Без всяких там регистраций dll'ки в системе.
Метод не Бог весть что, но для простых дллок работает и в целом заслуживает того, чтобы я о нем написал.
Вступление
Этот пост — модифицированный перевод статьи с CSharpHelp.com.
Предложенный метод работает не только для C# и С++. Тем же самым способом можно добиться взаимодействия любых unmanaged языков и dll, написанных с использованием .Net Framework.
В целом, можно связать managed DLL и unmanaged программу, используя технологию COM. Но может случится так, что язык, на котором написана ваша основная програма, не поддерживает COM. Или у вас есть любая другая причина не использовать эту технологию, включая нежелание
.
Из необходимых инструментов — вам непременно понадобится дизассемблер и ассемблер для языка IL. Обычно, он есть в пакете со средой разработки от Microsoft или в каком-нибудь SDK (вроде Microsoft .NET Framework Software Development Kit). Проверить, есть ли у вас необходимые программы, можно в консоле. Запустите ее, напишите сначала ILAsm и нажмите Enter, а потом Ildasm. Если программы на компьютере есть, вы увидите какой-нибудь вывод в окне консоли.
Код основного приложения может быть на любом языке, который поддерживает динамическое (явное) подключение DLL. Я буду использовать C++, автор в оригинальной статье по своим причинам использовал Blitz3D.
Приступаем
Для начала давайте создадим C# DLL'ку с одной простой функцией, которую мы и будем вызывать в основной программе. Она будет называться SayHello ().
В Visual Studio создайте новый проект Class Library и в основной файл допишите нашу функцию. В итоге получится что-то вроде такого:
using System;
namespace HelloWorldDll<br />
{<br />
public class HelloWorldClass<br />
{<br />
public static string SayHello(string name)<br />
{<br />
return ("Hello " + name);<br />
}<br />
}<br />
}
Ничего сложного, так? Неймспейс HelloWorldDll, класс HelloWorldClass и один метод у класса — SayHello.
Все, что наша функция делает — это принимает строковое значение, добавляет его к строке "Hello " и возвращает результирующую строку.
Обратите внимание, что класс объявлен как PUBLIC. Хотя автор оригинальной статьи утверждает, что это НЕ обязательно и нет никакой роли ни у public, ни у static, у некоторых людей были проблемы с использованием функций без этих ключевых слов.
В функции используется передача параметров и есть возвращаемое значение только для того, чтобы показать, что передача параметров не представляет особой проблемы. Вы можете использовать любые базовые типы данных вроде byte, short, long, int, string, uint, ulong. И, да, вы даже можете передать структуру в функцию. Просто убедитесь, что поля в структурах совпадают.
Пример:
// C#<br />
public struct SomeStruct<br />
{<br />
public int X, Y, Z;<br />
public string Name;<br />
public object Obj;<br />
}//C++<br />
Struct SomeType{<br />
int X, Y, Z;<br />
string Name;<br />
object Obj;<br />
};
Приведенный пример будет работать, если вы передадите SomeType в функцию в качестве параметра. Единственное ограничение — функция на .NET не может возвращать структуру в return. В этом случае, передайте структуру в функцию как параметр и заполните ее.
Вот так:
// НЕПРАВИЛЬНО<br />
public SomeStruct DoStuff()<br />
{<br />
SomeStruct obj= new SomeStruct();<br />
obj.X= 0;<br />
obj.Y= 0;<br />
obj.Name= "Эта строка никогда не вернется";<br />
obj.Obj= null;<br />
return obj;<br />
}
// ПРАВИЛЬНО<br />
// ref = Передача SomeStruct по ссылке (ByRef в VB)<br />
public void SomeStruct(ref SomeStruct obj)<br />
{<br />
obj.X= 0;<br />
obj.Y= 0;<br />
obj.Name= "Эта строка вернется.";<br />
obj.Obj= null;<br />
}
Теперь, когда мы с этим разобрались, вернемся к основному коду нашей дллки. Скомпилируйте ее.
Теперь у вас должен быть файл HelloWorldDll.dll.
Погружение
Теперь у нас есть managed дллка и нам надо связать ее с обычной unmanaged программой. И мы не хотим использовать COM. Главная интрига — как же мы это сделаем? Ответ находится в волшебном мире IL. Пожалуй, сначала немного теории.
MSIL (MicroSoft Intermediate Language) или IL — это фактически язык ассемблера на .NET Framework. Он похож на Java Bytecode, по крайней мере функции у них одинаковые. IL — это низкоуровневый код, в который преобразуются все .Net языки, такие как C#, VB.NET, C++. Затем уже при запуске программы, он на ходу компилируется в машинный код для процессора и выполняется.
Именно IL код обеспечивает взаимодействие вышеназванных языков между собой и теоретически делает программу на .NET платформонезависимой. Все Exe и Dll, полученные после компиляции .Net языков, содержат не настоящий машинный код, а именно код IL + избыточное описание содержимого, которое называется MetaData.
Вот поэтому мы не можем подключить dll обычными способами — на самом деле там просто нет экспортируемых функций. Вы можете проверить это утилитой DependencyWalker — она покажет, что C# dll ничего не экпортирует.
Сейчас нам нужно достать IL код. Для этого есть небольшая утилита, которая поставляется с .NET Framework SDK — IldAsm.exe. Как подсказывает ее название, она декомпилирует .Net сборку в IL код. Да, вы можете декомпилировать любое .Net приложение обратно в чистый IL код. Хорошо это или плохо? Вам решать. Существует несколько программ, которые усложняют разбор декомпилированного кода. В то же время без возможности простой декомпиляции этой статьи бы не существовало.
К делу.
Нам надо декомпилировать нашу DLL в IL код, отредактировать ее немного, а затем скопилировать обратно в DLL. Чтобы сделать это, откроем командную строку и в директории, где находится наша DLL, напишем:
c:\somedir\ildasm /OUT:HelloWorldDll.il HelloWorldDll.dll
Это создаст два новых файла:
- HelloWorldDll.il
- HelloWorldDll.res
Res файл можно удалять, для нашей дллки он не нужен. Обычно он содержит информацию о ресурсах внутри дллки и мог бы понадобиться, если бы библиотека содержала формы или контролсы, но сейчас HelloWorldDll.res можно спокойно удалить.
Откройте файл HelloWorldDll.il в текстовом редакторе. Содержимое похоже на хаос, но на самом деле не все так плохо. Вы можете захотеть почистить код, убрав пустые строки и ненужные комментарии (они начинаются с «//»), но это совсем не обязательно. Код и так без проблем скомпилируется обратно в dll, просто после чистки вам будет легче его читать.
Ниже — то как выглядит наш IL код после чистки:
.assembly extern mscorlib<br />
{<br />
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )<br />
.ver 1:0:3300:0<br />
}<br />
.assembly HelloWorld<br />
{<br />
.hash algorithm 0x00008004<br />
.ver 0:0:0:0<br />
}<br />
.module HelloWorld.dll<br />
.imagebase 0x00400000<br />
.subsystem 0x00000003<br />
.file alignment 512<br />
.corflags 0x00000001.namespace HelloWorldDll<br />
{<br />
.class public auto ansi beforefieldinit HelloWorldClass<br />
extends [mscorlib]System.Object{}<br />
}.namespace HelloWorldDll<br />
{<br />
.class public auto ansi beforefieldinit HelloWorldClass<br />
extends [mscorlib]System.Object<br />
{<br />
.method public hidebysig static string SayHello(string name)<br />
cil managed<br />
{<br />
.maxstack 2<br />
.locals init (string V_0)<br />
IL_0000: ldstr "Hello "<br />
IL_0005: ldarg.0<br />
IL_0006: call string [mscorlib]System.String::Concat<br />
(string, string)<br />
IL_000b: stloc.0<br />
IL_000c: br.s IL_000eIL_000e: ldloc.0<br />
IL_000f: ret<br />
}.method public hidebysig specialname rtspecialname<br />
instance void .ctor() cil managed<br />
{<br />
.maxstack 1<br />
IL_0000: ldarg.0<br />
IL_0001: call instance void [mscorlib]System.Object::.ctor()<br />
IL_0006: ret<br />
}<br />
}<br />
}
Разве это не прекрасно?
Как вы видите, IL выглядит как настоящий язык программирования. По сути это и есть настоящий язык программирования, поскольку вы можете писать программы прямо в IL, если хотите. В отличие от обычного языка ассемблера, IL использует стеки. Никаких регистриров, все делается через стек и кучу.
Начнем править файл.
Флаг corflags
Для начала нужно заменить строку:
.corflags 0×00000001
на
.corflags 0×00000002
Зачем? Это флаг, часть заголовка Common Language Runtime, который говорит ОС, что та имеет дело со сборкой .NET, а не c обычным исполняемым файлом.
По-умолчанию, этот флаг всегда равен COMIMAGE_FLAGS_ILONLY (0×00000001), что говорит о том, что сборка содержит только IL код. Никаких вставок unmanaged кода в Exe/Dll нет.
COMIMAGE_FLAGS_ILONLY (0×00000001)
Приложение содержит только IL код без каких либо вставок нативного unmanaged кода, кроме стартовой заглушки. Поскольку операционные системы, которые умеют работать с Common Language Runtime (такие как Windows XP), игнориуют стартовую заглушку, можно считать, что файл содержит только чистый IL код. Однако, использование этого флага может вызвать некоторые проблемы связанные с IlAsm компилятором под Windows Xp. Если этот флаг установлен Windows Xp игнорирует не только стартовую заглушку, но и секцию .reloc.
Важная часть здесь — это то, что Windows XP игнорирует не только стартовую заглушку, но и .reloc секцию. Это означает, что функции, которые мы экспортируем как unmanaged код, не будут правильно загружены. Для того, чтобы решить эту проблему, мы должны поменять этот флаг на COMIMAGE_FLAGS_32BITSREQUIRED (0×00000002).
COMIMAGE_FLAGS_32BITSREQUIRED (0×00000002)
Этот флаг устанавливается, когда исполняемый файл содержит unmanaged код или секция .recloc не пуста.
Добавление функций
Далее мы должны зарезервировать место в нашей DLL для хранения адреса фукнции. Во время выполнения модуля, оно будет заполнено действительным адресом функции, сейчас же нам надо просто выделить место.
В IL файле может быть специальная секция, которая называется v-Table. Она предназначена для того, чтобы обеспечить экспорт managed методов для unmanaged кода. Содержимое этой таблицы определяется другим полем — VTableFixups. Идея в том, что v-Table хранит ключ завязанный на определенный метод. Когда программа запускается, благодаря VTableFixups, этот ключ заменяется на реальный адрес метода или «переходника», который обеспечивает соответствие типов.
Мы и воспользуемся этим принципом, чтобы обеспечить взаимодействие нашей дллки и программы. Более детально узнать о v-table можно в оригинальной статье и в этой книге.
Вот код IL-файла, который должен получиться у вас в результате:
.assembly extern mscorlib<br />
{<br />
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )<br />
.ver 1:0:3300:0<br />
}<br />
.assembly HelloWorld<br />
{<br />
.hash algorithm 0x00008004<br />
.ver 0:0:0:0<br />
}<br />
.module HelloWorld.dll<br />
.imagebase 0x00400000<br />
.subsystem 0x00000003<br />
.file alignment 512// ### ИЗМЕНЕНО #### -> Изменили флаг, чтобы модуль нормально работал с WinXP<br />
.corflags 0x00000002// ### ИЗМЕНЕНО #### -> Создана запись в таблице V-Table,<br />
// которая содержит информацию о нашей функции<br />
.vtfixup [1] int32 fromunmanaged at VT_01</p>
<p>// ### ИЗМЕНЕНО #### -> Создана запись данных, которая будет хранить<br />
// виртуальный адрес на нашу функцию<br />
.data VT_01 = int32(0)</p>
<p>.namespace HelloWorldDll<br />
{<br />
.class public auto ansi beforefieldinit HelloWorldClass<br />
extends [mscorlib]System.Object{}<br />
}</p>
<p>.namespace HelloWorldDll<br />
{<br />
.class public auto ansi beforefieldinit HelloWorldClass<br />
extends [mscorlib]System.Object<br />
{<br />
.method public hidebysig static string<br />
SayHello(string name) cil managed<br />
{<br />
// ### ИЗМЕНЕНО #### -> Мы указали, какую VTable нужно использовать для функции<br />
.vtentry 1 : 1</p>
<p>// ### ИЗМЕНЕНО #### -> Экспортируем метод в unmanaged код<br />
// с именем метода - "SayHello"<br />
.export [1] as SayHello</p>
<p>.maxstack 2<br />
.locals init (string V_0)<br />
IL_0000: ldstr "Hello "<br />
IL_0005: ldarg.0<br />
IL_0006: call string [mscorlib]System.String::Concat(string, string)<br />
IL_000b: stloc.0<br />
IL_000c: br.s IL_000e</p>
<p>IL_000e: ldloc.0<br />
IL_000f: ret<br />
}</p>
<p>.method public hidebysig specialname rtspecialname<br />
instance void .ctor() cil managed<br />
{<br />
.maxstack 1<br />
IL_0000: ldarg.0<br />
IL_0001: call instance void [mscorlib]System.Object::.ctor()<br />
IL_0006: ret<br />
}<br />
}<br />
}</p>
<p>
Завершаем
Наша DLL готова. Осталось ее скомпилировать. Сделать это можно следующей командой:
c:\somedir\ilasm /OUT:HelloWorldDll.dll HelloWorldDll.il /DLL
После этого вы получите DLL с экпортируемыми в unmanaged код функциями — их можно будет увидеть в DependencyWalker и без проблем использовать в С++ программе.
Метод, конечно интересный, хотя возможно и несколько корявый. Мне, например, не удалось обеспечить модуль нормальной отладкой.
Ну, а вобще, можно пользоваться. По крайней мере, вы получили массу полезной информации о внутренней структуре .Net приложения.
