...

пятница, 15 ноября 2013 г.

[Из песочницы] Генерация 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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий