Использование P/Invoke в WinRT

177

Как ни печально, в мире программирования Windows 8 не все языки равны. Теоретически любой язык программирования может использовать любой класс или функцию, доступные для приложений Windows Store, но это возможно только потому, что весь интерфейс прикладного программирования (API) построен на базе модели COM (Component Object Model). В реальном программировании сложность работы с некоторыми областями Windows 8 API зависит от того, какой язык программирования вы используете.

Например, .NET API для приложений Windows 8 — пространства имен, начинающиеся со слова System — доступен напрямую только для программистов, работающих на управляемых языках C# и Visual Basic. Предполагается, что программисты C++ для достижения эквивалентной функциональности будут использовать библиотеки времени выполнения C++ и классы пространства имен Platform. С другой стороны, приложения Windows 8 могут использовать подмножества Win32 и COM API, но с этими функциями и классами удобно работать только на C++. Чтобы добраться до них, программистам C# приходится идти на разные ухищрения.

В этой и последующих статьях я покажу, как это делается. Мы рассмотрим два основных механизма. Первый — Platform Invoke (также называемый PInvoke или P/Invoke) — использовался с самого начала эпохи программирования .NET для обращения к функциям Win32 и функциям из других библиотек DLL. Механизм P/Invoke особенно хорошо подходит для работы с «неструктурированными» API, в которых функции существуют независимо друг от друга (или, возможно, используют дескрипторы, предоставляемые другими функциями), без разбивки на классы.

Второй механизм основан на написании «оберток» — специальных библиотек DLL на C++, и последующем обращении к этим библиотекам DLL из программ на C#. Он больше подходит для объектно-ориентированных API и особенно для обширного набора высокопроизводительных классов для работы с графикой и звуком, объединенных под общим названием DirectX.

В приложениях Windows 8 для обращения к библиотеке DLL, написанной на другом языке, необходимо, чтобы она имела специальный формат Windows Runtime Component. Visual Studio позволяет создавать компоненты Windows Runtime Component, но при этом для таких библиотек устанавливаются различные правила и ограничения. Помните, что эти механизмы не могут использоваться для получения доступа к функциям, запрещенным для приложений Windows Store. В частности, вы не можете использовать их для вызова произвольных функций Win32. Доступны только функции из подмножества, разрешенного для приложений Windows 8. Также запрещается вызывать функции DLL, которые вызывают функции Win32, не входящие в указанное подмножество.

Знакомство с P/Invoke

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

void WINAPI GetNativeSystemInfo(
  _Out_ LPSYSTEM_INFO lpSystemInfo
);

Тот, кто абсолютно не знаком с Wi32 API, ничего не поймет в этой записи. Идентификаторы, записанные символами верхнего регистра, обычно определяются с использованием директив C #define и typedef в различных заголовочных файлах Windows. Эти заголовочные файлы находятся в подкаталогах каталога C:/Program Files (x86)/Windows Kits/8.0 машины, на которой вы установили Visual Studio. Самые важные из них файлы Windows.h, WinDef.h, WinBase.h и winnt.h. Идентификатор WINAPI эквивалентен stdcall, определению стандартной конвенции вызова функций Win32 из кода C. LPSYSTEM_INFO — «длинный» указатель (в отличие от «коротких» 16-разрядных указателей, использовавшихся в первых версиях Windows) на структуру _SYSTEM_INFO. Структура _SYSTEM_INFO определяется следующим образом:

typedef struct _SYSTEM_INFO {
  union {
    DWORD  dwOemId;
    struct {
      WORD wProcessorArchitecture;
      WORD wReserved;
    };
  };
  DWORD     dwPageSize;
  LPVOID    lpMinimumApplicationAddress;
  LPVOID    lpMaximumApplicationAddress;
  DWORD_PTR dwActiveProcessorMask;
  DWORD     dwNumberOfProcessors;
  DWORD     dwProcessorType;
  DWORD     dwAllocationGranularity;
  WORD      wProcessorLevel;
  WORD      wProcessorRevision;
} SYSTEM_INFO;

Суффиксы перед именами полей представляют собой сокращенные обозначения типов данных в простой венгерской записи (которая была изобретена уроженцем Венгрии Чарльзом Симони). Венгерская запись использовалась в Windows API и в некоторых старых книгах по программированию для Windows, но сейчас она почти не встречается в прикладном программировании.

В терминологии Windows WORD интерпретируется как 16-разрядное значение без знака, известное программистам C# как ushort. DWORD — двойное слово, то есть 32-разрядное значение без знака (uint). Будьте внимательны со ссылками на long; речь идет не о 64-разрядных значениях long языка C#, а о значениях long языка C++, размер которого совпадает с размером int (32 бита). LPVOID означает «длинный указатель на void» (в записи C — void *), a DWORD_PTR — 32- или 64 -разрядное число без знака в зависимости от того, работает ли Windows на 32- или 64-разрядном процессоре. Эти обозначения эквивалентны IntPtr в C#.

Почему нужно знать, каким типам данных C# соответствуют эти типы данных Windows API? Потому что для использования этой структуры в программе C# необходимо переопределить ее в C#. К счастью, в документации SYSTEM_INFO указано, что поле dwOemId считается устаревшим, то есть вы можете проигнорировать ключевое слово union и просто создать структуру C# с открытыми полями — вероятно, заодно присвоив структуре имя, более привычное для языка C#:

public struct SystemInfo
{
        public ushort wProcessorArchitecture;
        public ushort wReserved;
        public ushort wProcessorLevel;
        public ushort wProcessorRevision;
        public uint dwPageSize;
        public IntPtr lpMinimumApplicationAddress;
        public IntPtr lpMaximumApplicationAddress;
        public IntPtr dwActiveProcessorMask;
        public uint dwNumberOfProcessors;
        public uint dwProcessorType;
        public uint dwAllocationGranularity;
}

В C# поля должны определяться как открытые (public), если вы хотите обращаться к ним за пределами структуры. При желании полям также можно присвоить новые имена (например, ProcessorArchitecture и PageSize). Также можно использовать другие типы данных того же размера (например, short вместо ushort, или int вместо uint), если вы уверены в том, что фактические значения не приведут к переполнению знаковых типов. С точки зрения Windows API передается обычный блок памяти. Структура занимает 36 байт в 32-разрядной версии Windows и 48 байт в 64-разрядной версии.

Очень часто в коде P/Invoke таким структурам предшествует следующий атрибут:

[StructLayout(LayoutKind.Sequential)]
public struct SystemInfo
{
    // ...
}

Класс StructLayoutAttribute и перечисление LayoutKind определяются в пространстве имен System.Runtime.InteropServices, которое содержит множество других классов, связанных с P/Invoke. Этот атрибут явно указывает, что поля должны рассматриваться как смежные и выравниваемые по границам байтов.<,/

Теперь, когда у вас появилась структура, которую можно передать функции GetNativeSystemInfo, вы должны объявить саму функцию. При этом используется атрибут DllImportAttribute, также определяемый в пространстве имен System.Runtime.InteropServices. Как минимум необходимо указать библиотеку DLL, в которой она находится. В документации указано, что функция GetNativeSystemInfo определяется в kernel32.dll. Объявление функции выглядит так:

[DllImport("kernel32.dll")]
static extern void GetNativeSystemInfo(out SystemInfo systemInfo);

Это объявление должно располагаться в определении класса C# на одном уровне с другими методами. Функция должна объявляться с ключевым словом static, что типично для обычных классов C#, а также с ключевым словом extern, означающим, что фактическая реализация функции находится вне класса. Если вы хотите, чтобы функция была видимой за пределами класса, ее также следует объявить с ключевым словом public.

Если не считать extern, объявление функции в остальном выглядит как метод C#. Метод возвращает void, а его единственным аргументом является ссылка на объект SystemInfo. Многие вызовы Windows API получают или возвращают информацию в структурах, передаваемых в аргументах с использованием указателей; эти аргументы определяются с использованием ключевых слов out или ref. Они функционально идентичны, но с ref компилятор C# перед вызовом функции убедится в том, что тип значения был инициализирован.

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

SystemInfo info;
GetNativeSystemInfo(out info);

Давайте посмотрим, как это все работает в контексте полноценного приложения. Файл XAML приложения SystemInfoPInvoke использует панель Grid табличного форматирования информации, полученной от GetNativeSystemInfo:

<Page ...>

    <Page.Resources>
        <Style x:Key="rightText" TargetType="TextBlock">
            <Setter Property="TextAlignment" Value="Right" />
            <Setter Property="Margin" Value="12 0 0 0" />
            <Setter Property="FontSize" Value="26" />
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="26" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Архитектура процессора: " Grid.Row="0" Grid.Column="0" />
            <TextBlock Name="processorArchitecture" Grid.Row="0" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Размер страницы: " Grid.Row="1" Grid.Column="0" />
            <TextBlock Name="pageSize" Grid.Row="1" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Минимальный адрес приложения: " Grid.Row="2" Grid.Column="0" />
            <TextBlock Name="minAppAddr" Grid.Row="2" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Максимальный адрес приложения: " Grid.Row="3" Grid.Column="0" />
            <TextBlock Name="maxAppAddr" Grid.Row="3" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Потоков: " Grid.Row="4" Grid.Column="0" />
            <TextBlock Name="activeProcessorMask" Grid.Row="4" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Кол-во ядер процессора: " Grid.Row="5" Grid.Column="0" />
            <TextBlock Name="numberProcessors" Grid.Row="5" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Минимальный размер выделяемой памяти: " Grid.Row="6" Grid.Column="0" />
            <TextBlock Name="allocationGranularity" Grid.Row="6" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Физических ядер: " Grid.Row="7" Grid.Column="0" />
            <TextBlock Name="processorLevel" Grid.Row="7" Grid.Column="1"
                       Style="{StaticResource rightText}" />

            <TextBlock Text="Марка процессора: " Grid.Row="8" Grid.Column="0" />
            <TextBlock Name="processorRevision" Grid.Row="8" Grid.Column="1"
                       Style="{StaticResource rightText}" />
        </Grid>
    </Grid>
</Page>

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

using System;
using Windows.UI.Xaml.Controls;
using System.Runtime.InteropServices;

namespace WinRTTestApp
{
    [StructLayout(LayoutKind.Sequential)]
    public struct SystemInfo
    {
        public ushort wProcessorArchitecture;
        public byte wReserved;
        public uint dwPageSize;
        public IntPtr lpMinimumApplicationAddress;
        public IntPtr lpMaximumApplicationAddress;
        public IntPtr dwActiveProcessorMask;
        public uint dwNumberOfProcessors;
        public uint dwProcessorType;
        public uint dwAllocationGranularity;
        public ushort wProcessorLevel;
        public ushort wProcessorRevision;
    }

    public enum ProcessorType
        {
            x86 = 0,
            ARM = 5,
            ia64 = 6,
            x64 = 9,
            Unknown = 65535
        };

    public sealed partial class MainPage : Page
    {
        [DllImport("kernel32.dll")]
        static extern void GetNativeSystemInfo(out SystemInfo info);

        public MainPage()
        {
            this.InitializeComponent();

            SystemInfo info = new SystemInfo();
            GetNativeSystemInfo(out info);

            processorArchitecture.Text =
                            ((ProcessorType)info.wProcessorArchitecture).ToString();
            pageSize.Text = info.dwPageSize.ToString();
            minAppAddr.Text = ((ulong)info.lpMinimumApplicationAddress).ToString("X");
            maxAppAddr.Text = ((ulong)info.lpMaximumApplicationAddress).ToString("X");
            activeProcessorMask.Text = ((ulong)info.dwActiveProcessorMask).ToString("X");
            numberProcessors.Text = info.dwNumberOfProcessors.ToString("X");
            allocationGranularity.Text = info.dwAllocationGranularity.ToString();
            processorLevel.Text = info.wProcessorLevel.ToString();
            processorRevision.Text = info.wProcessorRevision.ToString("X");
        }
    }
}

В документации указано, что поле wProcessorArchitecture может принимать значения 0 (для архитектур x86), 6 (для Intel Itanium), 9 (для x64) и 0xFFFF (неизвестная архитектура). Значение для процессора ARM (как в первой версии Microsoft Surface) в моей документации не указано, но все возможные значения соответствуют константам, начинающимся с префикса PROCESSOR_ARCHITECTURE и определяемым в winnt.h, а константа PROCESSOR_ARCHITECTURE_ARM определяется со значением 5.

Чтобы упростить форматирование значения wProcessorArchitecture, я определил перечисление ProcessorType и преобразовал wProcessorArchitecture к типу этого перечисления. Поля IntPtr преобразуются в ulong и отображаются в шестнадцатеричном формате. Экран приложения на планшете выглядит так:

Получение данных о системе в приложении Windows Runtime

При использовании P/Invoke для определения структур и объявления функций вы несете полную ответственность за правильность своих действий. Например, вы должны предоставить правильное имя файла DLL, в котором находится функция. Если вы обращаетесь к библиотеке DLL, которая не является системной, приложение должно содержать ссылку на эту библиотеку. Также необходимо правильно записать имя функции и объявить все аргументы. Поддержка IntelliSense для P/Invoke недоступна!

Эти структуры и объявления функций часто бывают сложными. На вики-сайте www.pinvoke.net многие программисты размещают определения структур и объявления функций, которые вы можете копировать в свой код. Более того, вы даже сможете опубликовать на сайте собственную информацию!

Пройди тесты
Лучший чат для C# программистов