Генерация P/Invoke сигнатур в C#. Нецелевое использование Interface Definition Language и OLE Automation Type Libraries

от автора

Это НЕ очередная статья о том что такое P/Invoke.

Итак, допустим в сферическом C# проекте необходимо использовать какую-либо технологию, отсутствующую в .NET, и все что у нас есть это Windows SDK 8.1 в котором имеется лишь набор заголовочных файлов для C/С++. Придется объявлять кучу типов, проверять корректность выравнивания структур и писать различные обертки. Это большое количество рутинной работы, и риск допустить ошибку. Можно конечно написать парсер заголовочных файлов… Тут просто и понятно все кроме количества требуемых на это человекочасов. Поэтому этот вариант отбрасываем и постараемся как либо иначе свести к минимуму количество необходимых действий для взаимодействия с unmanaged кодом.

Кроме того, полученный в результате код не будет зависеть от разрядности процесса, будет сохранена строгая типизация, будет применено автоматическое тестирование.

Взаимодействие Managed и Unmanaged кода.

Как известно, в .NET существует 2 основных способа взаимодействия с unmanaged кодом:

  1. С++/CLI: Можно написать враппер – обернуть unmanaged вызовы в managed методы, вручную преобразовывать native структуры, строки и массивы в managed объекты. Бесспорно это максимально гибко, но недостатков больше.
    Во-первых это куча кода, в том числе unmanaged, соответственно потенциальный риск допустить ошибку (без багов пишут только боги и лжецы).
    Во-вторых полученные сборки гвоздями приколочены к архитектуре – x64, x86 и.т.п., соответственно если у нас весь проект AnyCPU то придется собирать врапперы под несколько платформ и тащить их все с собой, распаковывая при установке или загружая при запуске сборку соответствующую конфигурации.
    В-третьих это C++, а он не нужен.
  2. 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.0\VC\bin\amd64\1033\clui.dll
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\c1.dll
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\cl.exe
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64\mspdb120.dll
C:\Program Files (x86)\Windows Kits\8.1\bin\x64\midl.exe
C:\Program Files (x86)\Windows Kits\8.1\bin\x64\midlc.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="..\InternalTools\TransformIDL.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

ссылка на оригинал статьи http://habrahabr.ru/post/202282/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *