Это НЕ очередная статья о том что такое P/Invoke.
Итак, допустим в сферическом C# проекте необходимо использовать какую-либо технологию, отсутствующую в .NET, и все что у нас есть это Windows SDK 8.1 в котором имеется лишь набор заголовочных файлов для C/С++. Придется объявлять кучу типов, проверять корректность выравнивания структур и писать различные обертки. Это большое количество рутинной работы, и риск допустить ошибку. Можно конечно написать парсер заголовочных файлов… Тут просто и понятно все кроме количества требуемых на это человекочасов. Поэтому этот вариант отбрасываем и постараемся как либо иначе свести к минимуму количество необходимых действий для взаимодействия с unmanaged кодом.
Кроме того, полученный в результате код не будет зависеть от разрядности процесса, будет сохранена строгая типизация, будет применено автоматическое тестирование.
Взаимодействие Managed и Unmanaged кода.
Как известно, в .NET существует 2 основных способа взаимодействия с unmanaged кодом:
- С++/CLI: Можно написать враппер – обернуть unmanaged вызовы в managed методы, вручную преобразовывать native структуры, строки и массивы в managed объекты. Бесспорно это максимально гибко, но недостатков больше.
Во-первых это куча кода, в том числе unmanaged, соответственно потенциальный риск допустить ошибку (без багов пишут только боги и лжецы).
Во-вторых полученные сборки гвоздями приколочены к архитектуре – x64, x86 и.т.п., соответственно если у нас весь проект AnyCPU то придется собирать врапперы под несколько платформ и тащить их все с собой, распаковывая при установке или загружая при запуске сборку соответствующую конфигурации.
В-третьих это C++, а он не нужен. - P/Invoke и COM: Множество компонентов windows реализовано с использованием COM. В общем случае .net приемлемо работает с этой технологией. Необходимые интерфейсы и структуры можно либо объявлять вручную самостоятельно, либо, при наличии библиотеки типов, импортировать их оттуда автоматически с использованием специальной утилиты tlbimp.
А вызывать экспортируемые функции из динамических библиотек можно объявив extern методы с атрибутом DllImport. Есть даже целый сайт где выложены объявления для основных winapi функций.
Остановимся подробнее на библиотеках типов. Библиотеки типов, как можно догадаться из названия, содержат информацию о типах, и получаются путем компиляции IDL – interface definition language – языка синтаксис которого чертовски схож с С. Библиотеки типов обычно поставляются либо в виде отдельных файлов с расширением .tlb либо встроены в ту же DLL где находятся описываемые объекты. Упомянутая выше утилита tlbimp генерирует из библиотек типов специальную interop-сборку содержащую необходимые объявления для .NET.
Поскольку синтаксис IDL схож объявлениями в заголовочных файлах языка C, то первая мысль которая приходит в голову – а не сгенерировать ли каким-либо образом библиотеку типов чтобы в дальнейшем импортировать ее в .net проект? Если в IDL файл можно скопировать все необходимые объявления из заголовочных файлов практически как есть, не задумываясь о конвертировании всяких там DWORD в uint, то это как раз то что нужно. Но есть ряд проблем: во-первых IDL не все поддерживает, а во-вторых tlbimp не все импортирует. В частности:
- В IDL нельзя использовать указатели на функции
- В IDL нельзя объявлять битовые поля
- tlbimp не использует unsafe-код, поэтому на выходе подавляющее число указателей будут представлены нетипизированным IntPtr
- Если в качестве аргумента в метод передается структура по ссылке, то tlbimp объявит такой аргумент как ref. И если в теории подразумевается, что туда на самом деле передавать надо адрес массива, то мы идем лесом. Конечно можно передать как ref нулевой элемент pinned-массива, оно даже будет работать, но выглядит такое несколько по-индусски. В любом случае из-за ref мы не сможем передать нулевой указатель если аргумент вдруг опциональный
- Указатели на C-style null-terminated строки (а ля LPWSTR) tlbimp преобразует в string, и если вдруг нехороший COM объект вздумает что то записать в этот кусок памяти, приложение скажет “кря”
- tlbimp импортирует только интерфейсы и структуры. Методы из DLL придется объявлять вручную
- tlbimp генерирует сборку но не код. Хотя это и не так критично
Все проблемы с tlbimp решаются легко – мы не будем использовать эту утилиту, а напишем свою. А вот с IDL дело обстоит сложнее – придется шаманить. Предупреждаю сразу: поскольку библиотека типов будет являться лишь промежуточным звеном, то забудем о совместимости с какими-либо стандартами, хорошим тоном и.т.п. и будем хранить в ней все в том виде в котором удобнее нам.
IDL
Я не буду подробно останавливаться на описании этого языка, а лишь вкратце перечислю ключевые элементы IDL которые будут использованы. Полное описание IDL есть в msdn
Основной блок в IDL файле это library. Все типы, которые находится внутри него, будут включены в библиотеку. Типы объявленные вне блока library будут включены только если на них ссылается кто-либо из блока library. По хорошему блок library должен иметь имя и уникальный идентификатор. Есть и ряд других атрибутов, но нам ничего из этого не нужно.
[uuid(00000000-0000-0000-0000-000000000001)]
library Import
{
}
Но если все-таки необходимо принудительно включить тип объявленный вне блока, то можно внутри library написать
typedef MY_TYPE MY_TYPE;
Внутри блока идут объявления типов. Нам понадобятся struct, union, enum, interface и module. Первые три абсолютно то же что и в С, поэтому не будем на них подробно останавливаться. Следует отметить только одну особенность, заключающуюся в том, что при таком объявлении:
typedef struct tagTEST
{
int i;
} TEST;
именем структуры будет tagTEST, а TEST это alias который будет в итоге заменен именем. Поскольку во многих заголовочных файлах в объявлениях структур присутствуют различные мерзкие префиксы, то во избежание бардака в именах лучше принять какие-нибудь меры. А в целом, в IDL как и в C можно создавать любое количество alias-ов директивой typedef.
Для объявления интерфейсов используется блок interface. Внутри этого блока функции:
[uuid(38BF1A5B-65EE-4C5C-9BC3-0D8BE47E8A1F)]
interface IXAudio2MasteringVoice : IXAudio2Voice
{
HRESULT GetChannelMask(DWORD* pChannelmask);
};
Все довольно очевидно. Из атрибутов в нашем случае важен только uuid, являющийся идентификатором интерфейса.
Еще есть блок module. В нем можно, к примеру, размещать функции из DLL, или какие-нибудь константы.
[dllname("kernel32.dll")]
module NativeMethods_kernel32
{
const UINT DONT_RESOLVE_DLL_REFERENCES = 0x00000001;
[entry("RtlMoveMemory")]
void RtlMoveMemory(
void *Destination,
const void *Source,
SIZE_T Length);
}
Здесь важны атрибуты dllname и entry, указывающие откуда будет загружаться метод. В качестве entry можно указывать ordinal функции вместо имени.
Объявления в IDL
Составим список того что надо брать из заголовочного файла:
- Структуры и объединения, в.т.ч. с битовыми полями
- Перечисления
- Объявления функций импортируемых из DLL
- Интерфейсы
- Константы (макросы объявленные с помощью #define)
- Указатели на функции
- Alias-ы типов объявленные через typedef (т.е. всякие там DWORD-ы и.т.п.)
Теперь надо определиться как это все копировать в IDL.
- Структуры и объединения: Копируем как есть, при желании убирая только лишние префиксы из имен.
- Перечисления: Аналогично структурам.
- Объявления функций импортируемых из DLL: Копируем как есть в блок module для соответствующей DLL. Очевидно, что для каждой DLL понадобится создать хотя бы по одному блоку module.
- Константы (объявленные через #define): Тут конечно не очень хорошо получается – придется добавлять тип, т.е. константа из примера выше это на самом деле
#define DONT_RESOLVE_DLL_REFERENCES 0x00000001
вариантов немного – макросы то естественно никак не могут попасть в библиотеку типов.
Другая проблема это всякие структуры вроде GUID-ов объявленных с помощью DEFINE_GUID. Ну если быть точным, то фактически это никакие не константы, а глобальные переменные, но используются обычно в качестве констант. Тут увы никак. GUID-ы то мы еще можем в виде строк объявить, но со всем остальным придется иметь дело вручную. - Alias-ы типов объявленные через typedef (т.е. всякие там DWORD-ы и.т.п.): Копируем как есть.
- Интерфейсы: Поскольку ни C ни C++ не поддерживают интерфейсы, то в большинстве заголовочных файлов они объявлены через условную компиляцию двумя способами – как класс для C++ с __declspec(uuid(x)) в том или ином виде и как структура со списком указателей на функции для C. Нас интересуют объявления для C++. Они выглядят обычно так:
MIDL_INTERFACE("0c733a30-2a1c-11ce-ade5-00aa0044773d") ISequentialStream : public IUnknown { public: virtual /* [local] */ HRESULT STDMETHODCALLTYPE Read( /* [annotation] */ _Out_writes_bytes_to_(cb, *pcbRead) void *pv, /* [annotation][in] */ _In_ ULONG cb, /* [annotation] */ _Out_opt_ ULONG *pcbRead) = 0; virtual /* [local] */ HRESULT STDMETHODCALLTYPE Write( /* [annotation] */ _In_reads_bytes_(cb) const void *pv, /* [annotation][in] */ _In_ ULONG cb, /* [annotation] */ _Out_opt_ ULONG *pcbWritten) = 0; };
Необходимо почистить отсюда все лишнее, чтобы интерфейс выглядел так:
[uuid(0c733a30-2a1c-11ce-ade5-00aa0044773d)] interface ISequentialStream : IUnknown { HRESULT Read( void *pv, ULONG cb, ULONG *pcbRead); HRESULT Write( void const *pv, ULONG cb, ULONG *pcbWritten); };
При желании можно не трогать комментарии, а SAL-аннотации спрятать в атрибут [annotation(…)].
Да, ряд операций проделывать все-таки приходится, но ключевой момент, как и основная суть статьи, здесь в том что мы не трогаем аргументы функций и возвращаемые значения. Т.е. даже несмотря на то что исходное объявление несколько изменяется, можно с достаточной уверенностью гарантировать его корректность, так как все типы и indirection level указателей остаются неизменными. Если что то забудем почистить, то оно не скомпилируется, но если скомпилируется то результат будет корректен поскольку “сигнатуры” не меняются. - Указатели на функции: Здесь начинаются костыли. Объявим интерфейс с одним методом, а при конвертации библиотеки типов такие интерфейсы будем преобразовывать в делегаты. Таким образом по-прежнему не будем трогать аргументы, да и остальной код использующий этот указатель не будет выдавать ошибок компиляции.
Т.е. к примеру это:typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
будет выглядеть так:
[uuid(C17B0B13-6E49-4268-B699-2D083BAE88F9) interface WNDPROC : __Delegate { LRESULT WNDPROC(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); }
В данном случае __Delegate это объявленный нами пустой интерфейс по которому мы будем отличать такой “указатель на функцию” от обычных интерфейсов. Атрибут uuid содержит случайное значение (чтобы не конфликтовать ни с чем), просто без него не скомпилируется. Можно конечно было бы заменить все указатели на функции на void*, но благодаря такому хаку мы сохраним строгую типизацию, например поле WNDPROC lpfnWndProc у структуры WNDCLASSEX в библиотеке типов будет также строго типизированным, а нам нужна информация только об имени типа и indirection level указателей, потому тот факт, что это интерфейс значения не имеет.
- Битовые поля: Хотя это и относится к структурам, я вынес их в отдельный пункт поскольку здесь тоже придется хитрить. Надо к каждому каким-либо образом привязать информацию о числе бит. Например, можно сделать это с помощью массивов. А чтобы при конвертации библиотеки типов понять, что это битовое поле, добавить какой-нибудь ненужный атрибут. Например это:
struct DWRITE_LINE_BREAKPOINT { UINT8 breakConditionBefore : 2; UINT8 breakConditionAfter : 2; UINT8 isWhitespace : 1; UINT8 isSoftHyphen : 1; UINT8 padding : 2; };
объявим так:
typedef struct DWRITE_LINE_BREAKPOINT { [replaceable] UINT8 breakConditionBefore[2]; [replaceable] UINT8 breakConditionAfter[2]; [replaceable] UINT8 isWhitespace[1]; [replaceable] UINT8 isSoftHyphen[1]; [replaceable] UINT8 padding[2]; } DWRITE_LINE_BREAKPOINT;
И для простоты условимся что если в структуре есть битовые поля то обычных полей там быть не должно. Тогда такие объявления:
typedef struct TEST { int i1 : 1; int i2 : 31; float f1; } TEST;
Надо будет преобразовать в:
typedef struct TEST { struct { int i1 : 1; int i2 : 31; }; float f1; } TEST;
Но битовые поля это очень большая редкость, потому в принципе их можно бы было и вообще не поддерживать, а заменять на базовый тип и уже в C# вручную делать все остальное:
typedef struct TEST { int i; float f1; } TEST;
Вышеизложенного должно быть достаточно чтобы перенести в IDL информацию обо всем что может понадобиться при работе с native библиотеками. Конечно здесь не учитываются различные классы и шаблоны для C++, но во всяком случае процентов девяносто пять содержимого заголовочных файлов от Windows API таким образом перенести можно. Несмотря на наличие нескольких грязных хаков, копирование в IDL все равно проще, быстрее и безопаснее чем написание врапперов на CLI или ручного объявления типов в .NET.
Объявления в С#
Рассмотрим теперь как это все должно выглядеть в C#.
Генерировать мы будем unsafe код. Во-первых для строгой типизации указателей, во-вторых, чтобы не гонять данные туда-сюда всяческими там Marshal.PtrToStructure. Не столько из-за ловли блох на производительности, а просто потому что с расово-верными указателями код получается тупо проще. Маршалинг сложных типов иначе лаконично не сделать — это будут тонны кода. Я пробовал все варианты и очень долго пытался найти универсальный способ не использующий unsafe код. Его нет, и отказ от unsafe это палки себе в колеса – надежнее и безопаснее код не станет, а проблем добавится.
Разницу лучше всего видно когда надо в функцию передать структуру содержащую указатель на другую структуру, или на строку, или вообще рекурсивную ссылку. А если в unmanaged коде один указатель затем будет заменен на другой и надо чтобы эти изменения отразились на исходной структуре в managed коде… тут даже custom marshaling не особо поможет. Да, и кстати атрибут MarshalAs не нужен и использоваться не будет.
Кроме того, использование импортированных объявлений будет максимально приближено к таковому в С, что возможно сможет облегчить перенос уже написанного кода. Следует сразу отметить что чтобы в C# получить адрес переменной, она должна иметь blittable-тип. Все наши структуры будут соответствовать этим требованиям. Поля с массивами объявим как fixed, для строк будем использовать char*/byte*, но вот тип bool не является blittable, поэтому в нашем случае для его представления будет использоваться структура с int полем и implicit операторами для приведения от/к bool. На массивах внутри структур надо остановиться чуть подробнее. Есть ограничения: во-первых ключевое слово fixed применимо только к массивам примитивных типов, поэтому массивы структур так не объявить, а во-вторых поддерживаются только одномерные массивы. Обычные массивы (с атрибутом MarshalAs и опцией SizeConst) хоть и могут содержать структуры, но они не являются blittable-типом, кроме того они также могут быть только одномерными. Чтобы решить этот вопрос, для массивов мы будем создавать специальные структуры с private полями по числу элементов. Такие структуры будут иметь indexer property для доступа к элементам, а также implicit операторы для копирования из/в managed массивы. Псевдомногомерность будет обеспечиваться через доступ по нескольким индексам. Т.е. матрица 4х4 это будет структура с 16 полями, а indexer property будет брать адрес первого элемента и высчитывать смещение по такой формуле: индекс1 * длина1 + индекс2, где длина1 равна 4, а оба индекса – числа от 0 до 3.
- Структуры и объединения: Структуры как структуры, ничего особенного. Для объединений LayoutKind.Explicit и FieldOffset(0) для всех полей. Особо следует отметить безымянные поля со структурами и объединениями. Дело в том что библиотеки типов такое не поддерживают, вместо этого им будут назначены сгенерированые имена, начинающиеся на __MIDL__.
Структураtypedef struct TEST { struct { int i; }; } TEST;
На самом деле будет чем то таким:
typedef struct TEST { struct __MIDL___MIDL_itf_Win32_0001_0001_0001 { int i; } __MIDL____MIDL_itf_Win32_0001_00010000; } TEST;
Соответственно если импортировать в C# как есть, то получим следующее:
[StructLayout(LayoutKind.Sequential)] public unsafe struct TEST { [StructLayout(LayoutKind.Sequential)] public unsafe struct __MIDL___MIDL_itf_Win32_0001_0001_0001 { public int i; } public __MIDL___MIDL_itf_Win32_0001_0001_0001 __MIDL____MIDL_itf_Win32_0001_00010000; }
В принципе и черт бы с ним, но доступ к полю i в C выполняется напрямую, как будто это поле основной структуры, т.е. myVar.i, а здесь будет жутковатое myVar. __MIDL____MIDL_itf_Win32_0001_00010000.i. Не годится, поэтому для таких случаев будем генерировать свойства для доступа напрямую к полям вложенных безымянных структур:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public unsafe struct TEST { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public unsafe struct __MIDL___MIDL_itf_Win32_0001_0001_0001 { public int i; } public __MIDL___MIDL_itf_Win32_0001_0001_0001 __MIDL____MIDL_itf_Win32_0001_00010000; public int i { get { return __MIDL____MIDL_itf_Win32_0001_00010000.i; } set { __MIDL____MIDL_itf_Win32_0001_00010000.i = value; } } }
Возможно такой подход не лишен недостатков, но это позволяет добиться максимального соответствия объявлений и корректно обрабатывать к примеру такие структуры:
typedef struct TEST { union { struct { int i1; int i2; }; struct { float f1; float f2; }; }; char c1; } TEST;
Доступ напрямую через свойства позволит работать со структурой почти точно так же как в С. Исключением является только случай когда необходим адрес вложенных полей, тогда придется все-таки указывать полный путь.
- Перечисления. Тут все просто, лишь незначительные различия в синтаксисе.
- Битовые поля. Выглядеть они будут так – целочисленная private переменная (тип зависит от того какого суммарно размера структура с битовыми полями) и сгенерированные свойства выполняющие битовые операции для чтения/установки только соответствующих бит:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] public unsafe struct DWRITE_LINE_BREAKPOINT { private byte __bit_field_value; public byte breakConditionBefore { get { return (byte)((__bit_field_value >> 8) & 3); } set { __bit_field_value = (byte)((value & 3) << 8); } } public byte breakConditionAfter { get { return (byte)((__bit_field_value >> 8) & 3); } set { __bit_field_value = (byte)((value & 3) << 8); } } ... }
- Объявления функций импортируемых из DLL: Как обычно, static extern методы с атрибутом DllImport в классе NativeMethods
- Alias-ы типов объявленные через typedef: Если в IDL случайно не затесались никакие лишние атрибуты то alias-ы будут заменены на сам тип при компиляции библиотеки типов (см. тут). А если все таки они туда попадут, то вместо них подставим тип который они представляют.
- Константы: константы в классе NativeConstants. Строки или числа.
- Указатели на функции (которые в виде специальных интерфейсов): Генерируем 2 основных типа: делегат и структуру, которая будет представлять собой сам указатель. В структуре одно private-поле имеющее тип void*. А через оператор implicit неявно приводить типы от/к делегату путем вызова Marshal.GetFunctionPointerForDelegate и Marshal.GetDelegateForFunctionPointer
- Интерфейсы: Тут казалось бы все просто – объявил интерфейс с атрибутом ComImport и дело в шляпе, и в классе Marshal навалом методов для дополнительной функциональности.
А вот нет, это работает только для COM-интерфейсов. А нам запросто могут вернуть нечто не наследующее IUnknown. Например IXAudio2Voice. И вот тут-то стандартные механизмы .NET скажут вам “кря”. Ну не страшно, в запасе есть хитрый ход конем – будем генерировать таблицы виртуальных методов сами и вызывать их через Marshal.GetFunctionPointerForDelegate и Marshal.GetDelegateForFunctionPointer. Здесь нет ничего особенного – интерфейсы будут представлены структурами, внутри которых есть private структуры с набором указателей. Для каждой функции интерфейса у основной структуры генерируется метод, вызывающий соответствующий указатель через Marshal.GetDelegateForFunctionPointer. А также набор implicit операторов чтобы поддержать приведение типов в случае наследования интерфейсов. Пример занял бы слишком много места чтобы привести его здесь, поэтому все можно посмотреть в приложенном архиве.
Утилита для преобразования
С теорией на этом все. Переходим к практике.
За преобразование IDL в библиотеку типов будет отвечать компилятор midl входящий в комплект Windows SDK.
За преобразование библиотеки типов в C# код будет отвечать собственная утилита (но из нее же будем запускать и компилятор).
Начну со второго. Для чтения содержимого библиотеки типов используются стандартные интерфейсы ITypeLib2 и ITypeInfo2. Документацию можно посмотреть здесь. Они же используются и в утилите tlbimp. Реализация конвертера ничего интересного из себя не представляет, поэтому больше про него рассказывать нечего. Исходный код в приложенном архиве (и да, я знаю, что существуют библиотеки для генерации C# кода, но без них проще).
Теперь о компиляции IDL.
Скопируем файлы компилятора в отдельную папку. Во-первых потому что придется их модифицировать, а во-вторых чтобы отвязаться от Windows 8.1 SDK и не прописывать нигде никаких абсолютных путей вида C:Program Files (x86)блаблабла.
Понадобятся следующие файлы:
C:Program Files (x86)Microsoft Visual Studio 12.0VCbinamd641033clui.dll
C:Program Files (x86)Microsoft Visual Studio 12.0VCbinamd64c1.dll
C:Program Files (x86)Microsoft Visual Studio 12.0VCbinamd64cl.exe
C:Program Files (x86)Microsoft Visual Studio 12.0VCbinamd64mspdb120.dll
C:Program Files (x86)Windows Kits8.1binx64midl.exe
C:Program Files (x86)Windows Kits8.1binx64midlc.exe
Все кроме clui.dll сваливаем в одну кучу. А clui.dll должен располагаться в подпапке 1033.
Процесс midl.exe запускает другой процесс – midlc.exe, который и выполняет всю работу.
Компилятор требует обязательное наличие файла с именем oaidl.idl где-либо в пределах досягаемости, с объявленым там интерфейсом IUnknown. Для удобства настройки создадим копию этого файла и скопируем туда основные объявления из исходного oaidl.idl и файлов на которые он ссылается. Хотя можно ограничиться и лишь интерфейсом IUnknown, а остальные объявления добавлять уже по мере использования. Разместим полученный файл рядом с компилятором.
Необходимо это затем, что часть системных типов придется немного подправить. К примеру BOOL и BOOLEAN нам нужны в виде структур с одним полем чтобы не возиться с int и byte, а поддержать приведение такой структуры к bool (который как уже было упомянуто выше, не является blittable типом и поэтому не может быть напрямую использован). Также надо там же объявить базовый интерфейс для типов обозначающих указатели на функции.
Исправление багов в компиляторе Обход ограничений компилятора
Бочкой дегтя была следующая особенность: http://support.microsoft.com/default.aspx?scid=kb;en-us;220137. Microsoft позиционирует это как feature. С одной стороны логично – основное предназначение библиотек типов это OLE Automation, что подразумевает поддержку регистронезависимых языков. С другой стороны реализация мягко говоря странная – между именами аргументов и именами методов или типов нет никакой связи, для чего использовать один глобальный список строк вместо отдельных списков для имен типов, отдельных списков для имен методов в каждом типе и.т.п.? В любом случае, нас такой “by design” не устраивает, ибо результатом является чудовищная помойка в именах, да и с автоматическим тестированием (см. ниже) будут проблемы, поскольку для этого необходимо точное соответствие имен тем что в исходных файлах.
Регистронезависимое сравнение строк обычно даже самые отъявленные индусы редко станут писать с нуля, потому с большой долей вероятности используется API-функция.
Вооружившись отладчиком наблюдаем практическое подтверждение описанного в KB220137 поведения:
Внутри компилятора есть глобальный словарь в который добавляются строки с именами. Если в файле хоть раз попалась строка “msg” (к примеру в качестве аргумента в какой-либо функции), то она будет добавлена в словарь. Если в дальнейшем в исходном файле попадется строка “Msg” (к примеру имя структуры), то выполнится проверка наличия этой строки в словаре с помощью CompareStringA и флагом NORM_IGNORECASE. Проверка вернет результат что строки одинаковы, текст “Msg” будет проигнорирован и компилятор в библиотеку типов в обоих случаях (и имя аргумента и имя структуры) запишет “msg”, хотя по факту они никак не связаны. Эта логика выполняется в зависимости от значения глобальной переменной.
Кроме того, для создания файла с библиотекой типов используются COM-объекты из oleaut32.dll (ICreateTypeLib, ICreateTypeInfo и.т.п.), которые также используют CompareStringA для проверки повторяющихся имен. К примеру, функция ICreateTypeInfo::SetVarName вернет результат TYPE_E_AMBIGUOUSNAME при попытке добавить поле в структуру отличающееся только регистром от существующего. Хотя там похоже глобальных словарей нет и такие проверки выполняются только для полей и методов в пределах содержащего их типа.
Из вышеизложенного становится очевидной задача – перехватить вызов CompareStringA и убрать из аргумента dwCmpFlags флаг NORM_IGNORECASE.
Midlc.exe импортирует CompareStringA из kernel32.dll, которая в свою очередь вызывает CompareStringA из kernelbase.dll, а oleaut32.dll использует сразу CompareStringA из kernelbase.dll. Поскольку подменить системную библиотеку не получится, будем перехватывать в рантайме.
Делается это элементарно: надо внедрить свой код в процесс и, получив адрес функции, модифицировать код так, чтобы передать управление в перехватчик, где выполнить необходимые операции и передать управление обратно. Для этого можно воспользоваться к примеру этой библиотекой: http://www.codeproject.com/Articles/44326/MinHook-The-Minimalistic-x86-x64-API-Hooking-Libra (В приложенном архиве слегка модифицированный вариант – код переписан на нормальный язык и почищен от лишней функциональности).
Для внедрения в процесс создадим DLL и модифицируем таблицы импорта файла midlc.exe чтобы при запуске он загружал нашу библиотеку. Инициализация перехватчика будет выполняться в точке входа DllMain.
Модифицировать таблицы импорта можно и вручную, но лучше воспользоваться готовыми утилитами, к примеру вот http://www.ntcore.com/exsuite.php. В утилите CFF Explorer надо открыть exe файл и выбрав слева Import Adder добавить нашу библиотеку и указать какую-н функцию для импорта (придется создать одну пустую функцию для этого, на практике ее никто никогда не вызовет) и нажав Rebuild Import Table сохранить файл.
Подключение файлов к проекту
Для снижения количества бесполезных файлов и левых build-event-ов применим известную технологию T4. Это мощный инструмент для генерации текста по шаблонам. Нам же в данном случае важна лишь возможность выполнения произвольного C# кода при сохранении файла. Шаблоном будет сам IDL файл. Суть в том что блок который будет распознан T4 помещаем в комментарий IDL файла и он будет проигнорирован midl-ом, а все что вне этого блока будет проигнорировано T4. Чтобы не дублировать код, вынесем в общий подключаемый файл весь запуск процесса и работу с файлами, оставив только директиву с подключаемым файлом. Таким образом где-н в начале каждого IDL файла будет всегда комментарий вроде
/* <#@ include file="..InternalToolsTransformIDL.tt" #> */
А в свойствах IDL файла указываем TextTemplatingFileGenerator в качестве Custom Tool.
В подключаемом файле ничего интересного – просто запускается наша утилита с нужными параметрами. C# код сгенерированный нашей утилитой во временный файл считывается в скрипте T4-шаблона и возвращается в качестве результата. Если скрипт в T4 возвращает конкретную строку, то результирующий файл будет содержать только ее, и таким образом содержимое шаблона никогда не попадет в выходной файл и может быть произвольным.
Благодаря этому, сохранение любых изменений в .idl файле автоматически запустит генерацию и обновит код.
Следует отметить что в T4 есть ограничения на размеры непрерывных блоков текста (по слухам ~64кб), поэтому при попытке сохранить очень большой файл можно поймать ошибку “Compiling transformation: An expression is too long or complex to compile ”. В этом случае в файл надо периодически добавлять такие строки:
// <# #>
Настройки
Наши промежуточные библиотеки типов будут тащить за собой кучу типов, которые возможно будут повторяться если у нас несколько IDL файлов. К примеру объявление интерфейса IUnknown. Кроме того неплохо бы где то указывать пространство имен в котором лежат сгенерированные классы, а также перечислить используемые пространства имен. Список типов которые надо проигнорировать при кодогенерации и перечень пространств имен можно разместить в виде комментариев в начале IDL файла и считывать перед конвертацией.
Тестирование
Тестировать будем следующие вещи:
- Наличие функций в указанных DLL
- Размеры структур
- Смещения всех полей в структурах
Причем поскольку описываемое решение позиционируется как не привязанное к разрядности ОС, то тестироваться все будет и в 32 разрядном и в 64 разрядном режимах.
Можно также тестировать размеры перечислений. Но в 99% случаев они занимают 4 байта. Поэтому возможность генерации перечислений с базовым типом отличным от int не рассматривается.
Информацию о размерах и смещениях надо получать из native кода. Для этого создадим две сборки на CLI (32 и 64). По сгенерированным утилитой managed-типам сгенерируем файл с кодом для получения необходимых данных. Генерировать будем макросы с инициализаторами:
#define STRUCT_SIZES
{
{ L"ARRAYDESC", sizeof(::ARRAYDESC) },
{ L"BLOB", sizeof(::BLOB) },
{ NULL, 0 }
}
#define STRUCT_OFFSETS
{
{ L"ARRAYDESC.tdescElem", FIELD_OFFSET(::ARRAYDESC, tdescElem) },
{ L"ARRAYDESC.tdescElem.lptdesc", FIELD_OFFSET(::ARRAYDESC, tdescElem.lptdesc) },
{ NULL, 0 }
}
для массивов структур:
STRUCT_SIZE structSizes[] = STRUCT_SIZES;
STRUCT_OFFSET structOffsets[] = STRUCT_OFFSETS;
Без патча компилятора этот шаг автоматизировать бы не удалось!
Пробегаясь по массивам в цикле преобразуем содержимое в Dictionary<string, int>. В первом случае ключом будет являться имя структуры а значением ее размер. Во втором – ключ это нечто вроде ‘полного пути’ к полю, а значение – смещение этого поля в структуре.
Данные будут различаться для 32 и 64 разрядных версий, именно поэтому нам необходимы две сборки. Эти данные подцепим из тестовых классов на C#. Далее тест будет сравнивать эти размеры и смещения с аналогами для managed структур, полученными с помощью Marshal.SizeOf и Marshal.OffsetOf.
Наличие методов в dll будем проверять вызывая LoadLibrary и GetProcAddress. Если они отработали, то все в порядке, если нет то или такой функции нет или накосячено в атрибутах в IDL.
Таким образом при добавлении новых объявлений тесты менять не придется. Ну точнее иногда надо будет только добавить директивы #include с файлами где объявлены исходные структуры, чтобы тест скомпилировался.
Но тут поджидает очередная проблема – VisualStudio не умеет одновременно искать и 32-разрядные и 64-разрядные тесты. Либо одно либо другое. По этой причине тесты будут запускать отдельные процессы которые и будут выполнять всю тестирующую логику, а сами тестовые классы лишь покажут результат выполнения.
Тесты иногда будут выявлять несоответствие выравнивания структур для какой-либо платформы. Поскольку для сохранения совместимости мы не можем указывать явные ненулевые смещения полей атрибутами FieldOffset или размеры структур (и то и другое будет отличаться для разных платформ), то придется химичить. Вот пример:
typedef struct SOCKET_ADDRESS_LIST
{
INT iAddressCount;
SOCKET_ADDRESS Address[1];
} SOCKET_ADDRESS_LIST;
В x64 у массива Address будет смещение 8, т.е. после поля iAddressCount необходим padding из 4 байт. На х86 его быть не должно. Аналог в .NET будет выровнен по 4 байтам на обоих платформах. Хитрый ход конем заключается в следующем:
typedef struct SOCKET_ADDRESS_LIST
{
union
{
INT iAddressCount;
[hidden]
void* ___padding000;
};
SOCKET_ADDRESS Address[1];
} SOCKET_ADDRESS_LIST;
С точки зрения использования в коде, структура остается эквивалентной, но в генерируемых .NET структурах это даст необходимый эффект – дополнительное поле будет занимать 4 байта в 32-разрядном режиме и 8 байт в 64-разрядном, тем самым “смещая” массив на 4 байта только в 64-разрядном режиме.
Маргинальные настройки выравнивания через условную компиляцию (а ля #pragma pack(2) для x86 и #pragma pack(16) для х64) здесь не рассматриваются — 99% структур выровнены либо по умолчанию либо по 1 байту, все остальное не нужно.
Изредка попадаются структуры кардинально отличающиеся на x86 и x64, например WSADATA. Для таких случаев у меня решения нет. С ними придется иметь дело вручную, но такие структуры попадаются крайне редко.
На этом все. Весь исходный код с примером использования в прилагаемом архиве.
Чтобы не нарушать никаких лицензионных соглашений, компилятор midl не прилагается. Его можно взять установив VisualStudio и пропатчить самостоятельно (была использована 64-разрядная версия).
Код к статье: http://niflheimr.is/public_files/download.php?file=Win32.zip
Автор: Einherjar