Программирование PC Маршалинг в C#. Составные типы Thu, November 21 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Маршалинг в C#. Составные типы Печать
Добавил(а) microsin   

В этой части рассматривается, как маршалировать составные типы (compound types). В первой части [2] (Маршалинг C#. Простые типы) рассматривались основные понятия маршалинга и маршалирование простых типов. Здесь приведен перевод статьи [1], посвященной маршалингу в C# (часть 3), автор Mohammad Elsheimy. Многие непонятные термины разъяснены во врезках в первой части статьи, см. также раздел "Словарик" в первой части статьи.

Составные типы собираются из других типов - простых и/или сложных. Например, структуры и классы являются составными типами. При рассмотрении необслуживаемые составные типы (unmanaged compound types) разбиты на 2 категории - структуры (struct) и объединения (union).

Вы можете спросить, почему такое разделение на структуры и объединения, ведь есть еще и классы? Почему они не рассматриваются? Ответ прост. Для упрощения эта статья фокусируется в основном на Windows API. Таким образом, здесь будут обсуждаться функции и структуры Win32. Однако те же правила можно применить к классам, и к другим unmanaged-типам.

[Составные типы]

Составной тип инкапсулирует в себе связанные друг с другом данные; это позволяет организованный, понятный контейнер для передачи группы переменных между клиентским приложением и unmanaged-сервером (имеется в виду API операционной системы). Составной тип обычно состоит из простых типов и может также состоять и из других составных типов. Кроме того, внутри составного типа могут быть определены и другие составные типы.

Составные типы бывают 2 видов:

• Необслуживаемые структуры (unmanaged struct)
• Необслуживаемые объединения (unmanaged union)

Например, в структуре OSVERSIONINFOEX инкапсулирована информация о версии операционной системы. Те, кто знаком с интерфейсом программирования DirectX, наверняка знают, что DirectX API очень сильно использует структуры. 

Как Вы знаете, нет совместимости между .NET и unmanaged-кодом в контексте размещения переменных в памяти, так что должны быть предусмотрены какие-то подпрограммы для обмена данными между managed-кодом (это к примеру Ваше приложение на C#) и unmanaged-сервером (операционная система Windows и её API). Конечно же, составные типы не исключение. 

[Маршалирование unmanaged-структур

Неуправляемые (unmanaged) структуры могут маршалироваться как managed-структуры или даже классы. Вы сами можете выбрать между managed-структурой или managed-классом, здесь нет каких-то правил. Однако когда маршалируются managed-классы, есть некоторые ограничения при в механизме передачи параметров, что мы рассмотрим позже. 

Когда маршалируемые структуры находятся managed-окружении, нужно иметь в виду, что доступ к переменным Вы осуществляете по её имени, а Windows получает доступ к переменной по её адресу (т. е. позиции) в памяти. Windows нисколько не волнуют имена полей структуры, однако для неё важно положение полей и их размер. Таким образом, распределение памяти и размер полей/переменных очень важны. 

Вы можете маршалировать unmanaged-структуру за несколько шагов: 

1. Создайте маршалируемый тип либо managed-структуры, либо класса.
2. Добавьте только типы полей (переменных). Повторю снова, что порядок следования полей и их размер критически важны. Таким образом, поля должны быть расположены именно в том порядке, как были определены изначально в unmanaged-окружении, так чтобы Windows могла корректно осуществить к ним доступ.
3. Декорируйте Ваш тип атрибутом StructLayoutAttribute, который определяет вид расположения в памяти. 

[Решение проблемы расположения в памяти

Когда маршалируется unmanaged-структура, Вы должны позаботиться о том, как тип будет расположен в памяти.

Фактически память приложения делится на блоки (по базе в 4 байта), и у каждого блока есть свой адрес. Когда Вы декларируете переменную или тип в своей программе, то он сохраняется в памяти и получает свой адрес в памяти. Следовательно, у каждой переменной (поля) внутри структуры есть собственный адрес относительно адреса начала структуры.

Рассмотрим следующие структуры: 

typedef struct SMALL_RECT
{
  SHORT Left;
  SHORT Top;
  SHORT Right;
  SHORT Bottom;
};

typedef struct COORD { SHORT X; SHORT Y; };

Когда Вы декларируете эти структуры в коде, переменные этих структур будут размещены в памяти вот так:

Marshalling Memory Laid Out fig3 1

Очевидно, что адрес каждого поля структуры зависит от размера поля предыдущей структуры. Таким образом, нужно иметь в виду, что размер каждого поля и его расположение относительно других полей очень критичны, и Вы должны позаботиться о том, как этот тип будет размещен в памяти.

Чтобы решить проблему размещения в памяти, Вы должны применить атрибут StructLayoutAttribute к Вашему маршалируемому типу, указав способ размещения в свойстве LayoutKind. Это свойство может принимать следующие значения:

LayoutKind.Auto (по умолчанию): дает для CLR возможность выбрать, как тип будет размещен в памяти. Если установить это значение, то теряется взаимодействие для этого типа, что означает - нельзя будет осуществить маршалинг unmanaged-структуры с этим типом, и если Вы попытаетесь это сделать, то будет выброшено исключение (exception thrown).

LayoutKind.Sequential: переменные будут размещены в памяти последовательно, друг за другом. Когда устанавливаете этот вариант, то убедитесь, что все переменные расположены в правильном порядке, как они определены в unmanaged-структуре, и конечно же что они имеют такой же размер.

LayoutKind.Explicit: позволяет точно управлять размещением каждого поля внутри типа. Когда установите это значение, то Вы должны обеспечить атрибут FieldOffsetAttribute для каждого поля типа структуры, который указывает относительную позицию поля в байтах относительно начала типа структуры. Обратите внимание, что когда Вы установите этот вариант, то порядок следования полей в managed типе структуры уже не имеет значения.

Ради простоты выполняйте размещение всех Ваших типов последовательно. Однако, когда Вы работаете с объединениями (union), то понадобится полный контроль за размещением каждой переменной.

Было сказано, то Вы должны только добавить только типы полей в маршалируемый тип, однако это не всегда верно. В структурах, где значение какого-то её поля определяет размер структуры (наподобие структуры OPENFILENAME), Вы можете добавить свои собственные поля в конец структуры. Однако Вы должны установить поле размера в значение размера всей структуры, не включая размеры добавленных Вами членов.

Давайте попробуем на примере все вышесказанное. Следующий пример показывает, как маршалировать структуры SMALL_RECT и COORD. Обе использовались в функции ScrollConsoleScreenBuffer(), которая встречалась еще в первой части (см. [2]).

Вот managed-сигнатуры для этих структур. Обратите внимание, что Вы можете их маршалировать также и как managed-классы (закомментированные строчки public class ..).

// Структура будет размещена в памяти последовательно.
[StructLayout(LayoutKind.Sequential)]
//public class SMALL_RECT
public struct SMALL_RECT
{
    // Поскольку мы выбрали последовательный вариант размещения,
    //  то сохраним старый порядок следования полей.
    public UInt16 Left;
    public UInt16 Top;
    public UInt16 Right;
    public UInt16 Bottom;
}
 
[StructLayout(LayoutKind.Sequential)]
//public class COORD
public struct COORD
{
    public UInt16 X;
    public UInt16 Y;
}

[Маршалинг объединения (union)]

Объединение это область памяти, которая является общей для двух или большего количества типов переменных. Это способ интерпретировать по-разному одни и те же данные.

Фактически объединения маршалируются (как впрочем, и определяются) во многом так же, как и структуры. Полезно знать, что как и структуры, объединения также могут быть определены как отдельно, так и внутри структуры. Кроме того, внутри объединения, как и внутри структуры, могут содержаться составные типы.

Чтобы лучше понять смысл объединений, рассмотрим простой пример:

typedef union SOME_CHARACTER
{
  int i;
  char c;
};

Это простое объединение, которое определяет символ. В нем два поля i и c, размещенные в одной и той же области памяти. Таким образом, к символу, определенному через это объединение, есть 2 способа доступа: по его коду (int) и по его символьному значению (char). Чтобы это работало, достаточно выделить такое место в памяти, которое достаточно для хранения наибольшего по размеру поля в объединении (в данном примере это int), и это поле иногда называют контейнером. В нашем случае контейнер это i, потому что на Win32 тип int занимает 4 байта, а на Win16 2 байта, в то время как c занимает только 1 байт. На рис. 3.2 показано, как назначается память для объединения SOME_CHARACTER.

Marshalling SOME CHARACTER union fig3 2

Поскольку оба члена (поля) объединения используют одну и ту же область памяти, то при изменении одного из членов другой также изменится автоматически. Рассмотрим следующий пример на языке C:

int main()
{
   union SOME_CHARACTER ch;
 
   ch.i = 65;                 // 65 для A
   printf("c = %c", ch.c);    // выведется 'A'
   printf("\n");
ch.c += 32; // 97 для a printf("i = %d", ch.i); // выведется '97' printf("\n"); return 0; }

Когда Вы меняете любое поле в объединении, то и все другие поля в объединении также поменяются, потому что они находятся по одному и тому же общему адресу.

Теперь рассмотрим тот же пример, но со значениями, которые не поместятся в поле char:

int main()
{
   union SOME_CHARACTER ch;
ch.i = 330; // == 0x0000014A printf("c = %c", ch.c); // выведется 'J', потому что её код 0x4A printf("\n"); // ... ого!
ch.c += 32; printf("i = %d", ch.i); // выведется '362' printf("\n"); return 0; }

Что произошло? Поскольку char размером в 1 байт, то он интерпретируется только как первый байт в объединении, которое размером в 32 бита (4 байта).

То же самое правило сработает, если Вы добавите в объединение еще один член (поле), см. следующий пример. Обратите внимание, что порядок определения полей в объединении не имеет никакого значения.

int main()
{
   union
   {
      int i;
      char c;
      short n;
   } ch;
ch.i = 2774186; // == 0x002A54AA
// выведется '2774186': printf("i = %d\n", ch.i); // выведется '170', потому что 0xAA==170: printf("c = %i\n", (unsigned char)ch.c); // выведется '21674', потому что 0x54AA==21674: printf("n = %d\n", ch.n);
return 0; }

Здесь член i, контейнер, интерпретируется как 32 бита. Член c, интерпретируется как первые 8 бит (обратите внимание, что он в коде преобразовывается в unsigned char, чтобы не показывать отрицательное значение). Поле n интерпретируется как первое слово (16 бит).

Вы можете спросить: зачем мне может понадобиться объединение? Ведь я могу просто использовать оператор приведения типа (cast operator), чтобы делать конвертацию между типами данных!

Ответ очень прост. Объединения дают очень эффективный способ преобразования типов, не связанный с дополнительной тратой ресурсов. Рассмотрим следующий пример: Вам нужно записать целое число в файл. К сожалению, в стандартной библиотеке языка C нет функций, которые позволяют записать тип int в файл, и использование fwrite() требует некоторых дополнительных затрат (примечание переводчика: это не совсем так, если использовать указатель на int и оператор sizeof(int)). Лучшее решение - определить union, который содержит массив символов, который также может быть интерпретирован как int, и передать этот массив функции fwrite(). Пример:

typedef union myval
{
   int i;
   char str[4];
};

Теперь массив str можно передать функции fwrite, и в результате в файл запишется число i. Дополнительно объединение дает эффективность лучше, чем приведение типов (type cast). Кроме того, при использовании объединений Ваш код становится более читаемым и эффективным. 

Вы можете маршалировать объединение таким же способом, как и структуры. Отличие только в том, что нужно явно указать позицию полей (членов) внутри типа. Т. е. нужно обязательно использовать LayoutKind.Explicit в качестве значения свойства LayoutKind атрибута StructLayoutAttribute, и нужно для каждого поля задать атрибут FieldOffsetAttribute. 

Выполняйте маршалирование union по следующим шагам: 

1. Создайте свой маршалируемый тип, не заботясь о том, что это - managed структура или класс. Однако напомню, что классы требуют специальной обработки, когда передаются в качестве аргумента функции. Механизм передачи аргументов мы скоро рассмотрим.

2. Декорируйте тип атрибутом StructLayoutAttribute, указав для него LayoutKind.Explicit.

3. Добавьте типы полей. Не добавляйте полей сверх тех, что определены в исходной unmanaged-сигнатуре. Поскольку мы явно управляем размещением типов, то порядок определения полей не имеет значения.

4. Декорируйте каждое поле атрибутом FieldOffsetAttribute, задавая для поля абсолютную позицию в байтах относительно начала типа. 

Следующий пример показывает, как маршалировать наше объединение SOME_CHARACTER. 

// Union-ы требуют явного (explicit) размещения в памяти:
[StructLayout(LayoutKind.Explicit)]
//public class SOME_CHARACTER
public struct SOME_CHARACTER
{
    // Оба поля размещены в одной и той же позиции
    //  относительно начала union.
    // Это поле содержит контейнер размером в 4 байта.
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.U4)]
    public int i;
    // Здесь только один байт. Следовательно, он
    //  уместится в контейнере.
    [FieldOffset(0)]
    public char c;
}
 
public static void Main()
{
    SOME_CHARACTER character = new SOME_CHARACTER();
 
    // Код для символа 'A':
    character.i = 65;
    // Будет напечатано 'A':
    Console.WriteLine("c = {0}", character.c);
 
    character.c = 'B';
    // Будет напечатано '66':
    Console.WriteLine("i = {0}", character.i);
}

Из последнего кода можно научиться следующему:

• Union-ы маршалируются так же, как и структуры, причем можно маршалировать либо как managed-структуру, либо как managed-класс.
• Установка StructLayoutAttribute.LayoutKind в LayoutKind.Explicit позволяет нам точно управлять положением полей объединения.
• Мы используем FieldOffsetAttribute, чтобы указать начальное положение поля (в байтах).
• Чтобы создать объединение между полями, нужно указать одно и то же место в памяти (обычно для этого FieldOffsetAttribute устанавливается в 0 для обоих полей).
• В этом примере поле i занимает байты от 0 до 3, и поле c занимает байт 0.
• Если нам не нужно использовать полезные свойства объединения, то мы можем опустить поле c, потому что оно содержится внутри области поля i. Однако мы не можем опустить поле i, поскольку это наш контейнер.
• Когда мы меняем одно из полей объединения, другая переменная также будет изменена, потому что находится в памяти по тому же самому адресу. 

[Объединения с массивами]

Другой пример:

typedef union UNION_WITH_ARRAY
{
    SHORT number;
    CHAR charArray[128];
};

Этот union должен маршалироваться по-особенному, потому что managed-код не разрешает наложения друг на друга типов значений (value type, здесь number это SHORT) и ссылочных типов (reference type, здесь charArray это указатель на CHAR). 

Напомню, что value-type является типом, который хранится в стеке (memory stack); он наследуется из System.ValueType. Все примитивные типы данных, структуры и перечисления (enum) считаются как value-type. С другой стороны, reference-type хранится в куче (memory heap); он наследуется из System.Object. Большинство типов в .NET являются reference-type (за исключением System.ValueType и конечно его наследников).

Таким образом все value-типы прямо или косвенно наследуются от System.ValueType.

В результате для нашего примера мы не можем иметь в union два таких поля, поскольку несмотря на то, что вторая переменная-массив charArray маршалируется как System.String или как System.Text.StringBuilder, она все еще reference-type. Таким образом, мы должны забыть про преимущества union-ов, и маршалировать только одно поле. Для нашего примера создадим 2 маршалируемых типа для нашего объединения, где в одном будет первое маршалируемое поле, и в другом будет второе поле.

Как мы знаем, размещение и размер типов в памяти имеет критически важное значение. Таким образом, нужно сохранить для нашего объединения распределение памяти и размер. Это объединение имеет как контейнер массив из 128 байт, и содержит только одно поле, и это поле состоит только из 2 байт. Так что есть два варианта для выбора: маршалировать union с полем контейнера, или маршалировать его с содержащимся полем, но расширить его до самого большого поля. В этом примере будем использовать оба варианта.

В первом куске кода показано, как маршалировать только второе поле-контейнер, а второй кусок показывает как маршалировать на основе первого поля.

// Установка StructLayoutAttribute.CharSet
// гарантирует корректную кодировку для всех строковых
// полей union нашего примера.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
//public struct UNION_WITH_ARRAY_1
public struct UNION_WITH_ARRAY_1
{
    // Как мы знаем, массивы символов могут маршалироваться
    // либо как массив, либо как строка
 
    // Для массива и строки требуется атрибут MarshalAsAttribute
    //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)]
    //public char[] charArray;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string charArray;
}
 
// StructLayoutAttribute.Size определяет размер типа в байтах.
// Если указан размер больше, чем размер поля, то под
//  последнее поле будет предоставлен расширенный размер.
// Поскольку здесь только одно поле, то за ним будет
//  последовательно выделена память.
[StructLayout(LayoutKind.Sequential, Size = 128)]
//public class UNION_WITH_ARRAY_2
public struct UNION_WITH_ARRAY_2
{    [MarshalAs(UnmanagedType.I2)]
    public short number;
}

[Value-типы и Reference-типы]

В мире .NET типы разбиты на 2 категории:

Value-типы: это такие типы, которые хранятся в стеке (memory stack). Они будут уничтожены, когда область кода (scope) завершается, так что они имеют короткое время жизни. Все типы этой категории наследуются от System.ValueType (это типы наподобие примитивных, структур, перечислений enum).

Reference-типы: эти типы хранят данные в куче (memory heap). Они управляются сборщиком мусора (Garbage Collector, GC), так что они могут оставаться в памяти так долго, как это нужно. Все Reference-типы прямо или косвенно наследуются из System.Object (за исключением System.ValueType и конечно его наследников). Все классы .NET попадают в категорию Reference-типов. 

[Стек]

Стек (stack) это аппаратно организованная система хранения данных в оперативной памяти (т. е. она встроена в процессор, выполняющий код), которая предназначена для временного сохранения каких-то величин. Это могут быть разные значения - состояние флагов процессора в момент прерывания, адреса возврата из прерывания или подпрограммы, значений регистров процессора, значений переменных, выделения памяти под локальные переменные подпрограммы.

В контексте программирования на C++/C# и .NET следует знать, что стек используется для локальных переменных (т. е. тех, которые существуют в теле функции или блока кода, ограниченного скобками), и для передачи значений в функцию (т. е. для аргументов, аргументы функции это те же локальные переменные, просто несущие дополнительное назначение).

Слово stack переводится с английского как "палочка, стопка", и это название точно отражает организацию стека. Доступ в стек осуществляется строго последовательно, по принципу FIFO - First Input First Output, т. е. первым вошел первым вышел.

[Куча]

Куча (heap) - это тоже система для хранения данных в оперативной памяти, но устроена она по-другому - чисто программно. Если кратко - это организованное в памяти хранилище плюс специальная библиотека (стандартная в контексте какой-то системы программирования), которая предназначена для временного (или постоянного) создания объектов в памяти. Когда нужно создать какую-то переменную в памяти (например массив, или экземпляр объекта), то от хранилища кучи откусывается часть под этот объект, и размер кучи уменьшается. Когда объект не нужен, то он удаляется, и размер кучи увеличивается. Название "куча" отражает асинхронность работы хранилища объектов - в куче объекты создаются и удаляются независимо друг от друга, т. е. асинхронно по отношению друг к другу.

В контексте программирования на C++/C# и .NET куча используется для создания экземпляров объектов классов и для динамических массивов.

Интересно поговорить про value-типы и reference-типы в контексте механизма передачи параметров. Этому посвящена следующая секция.

[Механизм передачи параметров]

В [2] мы обсуждали механизм передачи параметров для простых типов, и как это влияет на вызов функции. Пришло время разобраться, как это также работает с составными типами.

Вспомним, что когда тип передается по значению (by value), то функции передается копия переменной указанного типа, но не сама переменная со своим значением. Таким образом, любые изменения переменной внутри функции не попадут наружу, т. е. не повлияют на оригинальную переменную, когда работа функции будет завершена. С другой стороны, передача переменной по ссылке (by reference) передает в функцию указатель на эту переменную. Другими словами, в функцию передается сама переменная вместе со своим значением. Так что любые изменения переменной внутри кода функции сохранятся, когда работа функции завершится и управление перейдет обратно к вызывающему коду.

Функции требуют, чтобы переменная указанного типа была передана в параметре либо по значению, либо по ссылке. Также передавать аргумент по ссылке нужно только тогда, когда нужно изменять переменную аргумента внутри тела функции.

Кроме того, есть специальный синтаксис, указывающий способ передачи переменной по ссылке - либо как  Input/Output (In/Out), либо только как Output (Out). In/Out аргументы используются функцией для получения входного значения от вызывающего кода, и одновременно для выдачи результата вычислений обратно. Следовательно, In/Out аргументы по смыслу должны быть проинициализированы должным образом перед передачей их в вызов функции. С другой стороны, аргументы output (Out) нужны только лишь для возврата выходного значения из вызывающей функции, поэтому для них не нужна предварительная инициализация - функция сама будет заполнять значением эти переменные.

Все это мы уже обсуждали в [2], где такой же принцип передачи параметров применялся простых типов, и как видите, то же самое верно и для составных типов.

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

Если Вы маршалируете структуру, то можете добавить к параметру модификатор ref. Однако классы по умолчанию передаются как ссылочные типы ссылке. Они не могут передаваться по значению, так что не нужно добавлять модификатор ref (все это нужно помнить, и увеличивает путаницу, если не знать основные принципы).

С другой стороны, если Вы передаете тип как output, выходной (Out), то Вам нужно все равно добавлять модификатор, независимо от того что передаете - структуру или класс.

Как Вы знаете, можно декорировать аргументы In/Out атрибутами как InAttribute, так и OutAttribute. Для аргументов Out указывайте только атрибут OutAttribute.

Обратите внимание на большое различие на между managed и unmanaged классами. Unmanaged-классы по умолчанию являются типами по значению (value-types). Managed-классы являются типами по ссылке (reference-types).

Следующий пример демонстрирует метод PInvoke для функции GetVersionEx(). Эта функция требует один аргумент In/Out. Тип этого аргумента OSVERSIONINFO.

Функция использует в структуре OSVERSIONINFO поле dwOSVersionInfoSize в качестве входного значения, определяющего размер типа, использует его остальные поля для отправки обратно информации о версии операционной системы. Таким образом, функция требует передачи аргумента по ссылке как In/Out.

Вот определение функции и структуры:

BOOL GetVersionEx(OSVERSIONINFO lpVersionInfo);
 
typedef struct OSVERSIONINFO
{
  DWORD dwOSVersionInfoSize;
  DWORD dwMajorVersion;
  DWORD dwMinorVersion;
  DWORD dwBuildNumber;
  DWORD dwPlatformId;
  TCHAR szCSDVersion[128];
};

В вот managed-версия и тестовый код:

[DllImport("Kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetVersionEx
    ([param: In, Out]
    // если класс, то удалите ключевое слово "ref"
    ref OSVERSIONINFO lpVersionInfo);
 
[StructLayout(LayoutKind.Sequential)]
//public class OSVERSIONINFO
public struct OSVERSIONINFO
{
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dwOSVersionInfoSize;
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dwMajorVersion;
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dwMinorVersion;
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dwBuildNumber;
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dwPlatformId;
 
    // Может быть также быть маршалировано как массив
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string szCSDVersion;
}
 
static void Main()
{
    OSVERSIONINFO info = new OSVERSIONINFO();
    info.dwOSVersionInfoSize = (uint)Marshal.SizeOf(info);
 
    //GetVersionEx(info);
    GetVersionEx(ref info);
 
    Console.WriteLine("System Version: {0}.{1}",
        info.dwMajorVersion, info.dwMinorVersion);
}

[Составные типы и кодировка символов]

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

Вы уже знаете из [2], что кодировка символов может быть либо ANSI, либо Unicode.

Когда строка закодирована в ANSI, то каждый символ резервирует в памяти приложения только 1 байт. С другой стороны, каждый символ строки в кодировке Unicode занимает 2 байта памяти. Таким образом, строка наподобие "Hello World" из 11 символов займет в памяти 11 байт в кодировке и 22 байта в кодировке Unicode (иногда также добавляется в конце строки нулевой символ, так что получится соответственно 12 и 24 байта).

Вы можете определить кодировку символов составного типа, указав свойство CharSet атрибута StructLayoutAttribute. Это свойство может принимать следующие значения (все то же самое, как и для простых типов в [2]):

CharSet.Auto (умолчание CLR): строки кодируются на базе операционной системы; это Unicode на Windows NT и ANSI для других версий Windows.
CharSet.Ansi (умолчание C#): строки всегда кодируются как 8-bit ANSI.
CharSet.Unicode: строки кодируются всегда как 16-bit Unicode.
CharSet.None: устаревший вариант. Работает так же, как и CharSet.Ansi.

Примечание: имейте в виду, что если Вы не установили свойство CharSet, CLR автоматически установит свойство в CharSet.Auto. Однако некоторые языки переназначают поведение по умолчанию. Например умолчание C# это CharSet.Ansi.

Дополнительно Вы можете определить кодировку символов на гранулярном уровне (т. е. для каждого поля по отдельности), задав свойство CharSet атрибута MarshalAsAttribute для каждого члена составного типа (если, конечно, это символьный тип).

[Примеры из реальной жизни]

Структура DEVMODE. В первом примере мы сделаем маршалирование одну из самых сложных составных структур Windows API, это структура DEVMODE.

Если Вы работали с GDI, то наверно знакомы с этой структурой. Она инкапсулирует в себе инициализацию и рабочее окружение принтера или устройства отображения. Она требуется многим функциям наподобие EnumDisplaySettings(), ChangeDisplaySettings() и OpenPrinter().

Сложна эта структура по нескольким причинам. Во-первых, есть определения объединений (union) внутри структуры. Кроме того, определение этой структуры отличается на разных платформах. Как мы увидим, структура определяет некоторые свои поля на базе операционной системы.

Вот определение структуры DEVMODE вместе со структурой POINTL, на которую ссылается DEVMODE.

typedef struct DEVMODE
{
  BCHAR  dmDeviceName[CCHDEVICENAME];
  WORD   dmSpecVersion;
  WORD   dmDriverVersion;
  WORD   dmSize;
  WORD   dmDriverExtra;
  DWORD  dmFields;
  union
  {
    struct
    {
      short dmOrientation;
      short dmPaperSize;
      short dmPaperLength;
      short dmPaperWidth;
      short dmScale;
      short dmCopies;
      short dmDefaultSource;
      short dmPrintQuality;
    };
    POINTL dmPosition;
    DWORD  dmDisplayOrientation;
    DWORD  dmDisplayFixedOutput;
  };
  short  dmColor;
  short  dmDuplex;
  short  dmYResolution;
  short  dmTTOption;
  short  dmCollate;
  BYTE  dmFormName[CCHFORMNAME];
  WORD  dmLogPixels;
  DWORD  dmBitsPerPel;
  DWORD  dmPelsWidth;
  DWORD  dmPelsHeight;
  union
  {
    DWORD  dmDisplayFlags;
    DWORD  dmNup;
  }
  DWORD  dmDisplayFrequency;
#if(WINVER >= 0x0400)
  DWORD  dmICMMethod;
  DWORD  dmICMIntent;
  DWORD  dmMediaType;
  DWORD  dmDitherType;
  DWORD  dmReserved1;
  DWORD  dmReserved2;
  #if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400)
  DWORD  dmPanningWidth;
  DWORD  dmPanningHeight;
  #endif
#endif
};
 
typedef struct POINTL
{
  LONG x;
  LONG y;
};

Как Вы возможно заметили, есть два объединения (union), определенных прямо внутри структуры. Дополнительно в первом union определена структура! Кроме того, последние 8 полей не поддерживаются в Windows NT. Плюс последние два поля dmPanningWidth и dmPanningHeight не поддерживаются в Windows 9x (95/98/ME). Абзац...

При работе с Windows API, нужно позаботиться о совместимости с операционной системой. Некоторые функции, например, не поддерживаются определенными системами (к примеру, многие Unicode-версии не поддерживаются в Win9x). Другие функции получают аргументы по-разному, в зависимости от операционной системы (например функция EnumPrinters()). Если Ваше приложение попытается вызывать функцию, к примеру, не поддерживаемую в текущей операционной системе, то произойдет ошибка.

Чтобы обеспечить работоспособность приложения на каждой платформе, Вам нужно создать три версии структуры, одну для Windows и её предшественниц, другую для Windows NT, и последнюю для Windows 2000 и более новых версий. Дополнительно нужно создать 3 перезагрузки (overloads) каждой функции, которая требует структуру DEVMODE; три перезагрузки для трех структур. Ради простоты предположим, что нужно работать на Windows 2000 или более свежей версии. Следовательно, мы будем маршалировать все поля структуры.

Вот managed-версия структур DEVMODE и POINTL:

// Установка StructLayout.LayoutKind в значение LeyoutKind.Explicit
//  позволяет точно выбрать позицию поля в памяти. Это нужно для
//  использования объединений (union).
// Эта структура занимает 156-байт
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi)]
//public class DEVMODE
public struct DEVMODE
{
    // Вы можете определить следующую константу, но ТОЛЬКО
    //  ВНЕ СТРУКТУРЫ, потому что, как Вы знаете, сохранение
    //  размера и карты памяти структуры критически важны.
    // CCHDEVICENAME = 32 = 0x50
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public Char[] dmDeviceName;
    // Дополнительно можно задать последний массив символов
    //  следующим образом:
    //MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    //public string dmDeviceName;
 
    // Затем 32-байтный массив
    [FieldOffset(32)]
    [MarshalAs(UnmanagedType.U2)]
    public UInt16 dmSpecVersion;
 
    [FieldOffset(34)]
    [MarshalAs(UnmanagedType.U2)]
    public UInt16 dmDriverVersion;
 
    [FieldOffset(36)]
    [MarshalAs(UnmanagedType.U2)]
    public UInt16 dmSize;
 
    [FieldOffset(38)]
    [MarshalAs(UnmanagedType.U2)]
    public UInt16 dmDriverExtra;
 
    [FieldOffset(40)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmFields;
 
    // ************ Начало объединения (union) ************
    // Поскольку DEVMODE_PRINT_SETTINGS самое большое поле,
    //  и его размер 16 байт, то оно будет служить контейнером.
    // Помните, что Вы не можете пропустить контейнер.
    [FieldOffset(44)]
    public DEVMODE_PRINT_SETTINGS dmSettings;
 
    // Спозиционировано внутри DEVMODE_PRINT_SETTINGS,
    //  здесь только 8 байт.
    [FieldOffset(44)]
    public POINTL dmPosition;
 
    // Также внутри DEVMODE_PRINT_SETTINGS
    [FieldOffset(44)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmDisplayOrientation;
 
    // Позиционировано внутри DEVMODE_PRINT_SETTINGS
    [FieldOffset(44)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmDisplayFixedOutput;
    // ************* Конец объединения *************
 
    // Поскольку размер DEVMODE_PRINT_SETTINGS
    //  16 байт, dmColor позиционируется на байте 60
    [FieldOffset(60)]
    [MarshalAs(UnmanagedType.I2)]
    public Int16 dmColor;
 
    [FieldOffset(62)]
    [MarshalAs(UnmanagedType.I2)]
    public Int16 dmDuplex;
 
    [FieldOffset(64)]
    [MarshalAs(UnmanagedType.I2)]
    public Int16 dmYResolution;
 
    [FieldOffset(66)]
    [MarshalAs(UnmanagedType.I2)]
    public Int16 dmTTOption;
 
    [FieldOffset(70)]
    [MarshalAs(UnmanagedType.I2)]
    public Int16 dmCollate;
 
    // CCHDEVICENAME = 32 = 0x50
    [FieldOffset(72)]
    [MarshalAs(UnmanagedType.ByValArray,
        SizeConst = 32,
        ArraySubType = UnmanagedType.U1)]
    public Byte[] dmFormName;
 
    // Затем 32-байтный массив
    [FieldOffset(102)]
    [MarshalAs(UnmanagedType.U2)]
    public UInt16 dmLogPixels;
 
    [FieldOffset(104)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmBitsPerPel;
 
    [FieldOffset(108)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmPelsWidth;
 
    [FieldOffset(112)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmPelsHeight;
 
    // ************ Начало объединения (union) ************
    // Поскольку размер обоих полей 4 байта, то объединение
    //  займет 4 байта, и его поля перекроют друг друга.
    // Снова напомню, что контейнер пропускать нельзя.
    // В нашем случае они равны, так что контейнером может
    //  быть любое поле, и любое поле можно опустить,
    //  оставивив одно.
    [FieldOffset(116)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmDisplayFlags;
 
    [FieldOffset(116)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmNup;
    // ************* Конец объединения *************
 
    [FieldOffset(120)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmDisplayFrequency;
 
    [FieldOffset(124)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmICMMethod;
 
    [FieldOffset(128)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmICMIntent;
 
    [FieldOffset(132)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmMediaType;
 
    [FieldOffset(136)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmDitherType;
 
    [FieldOffset(140)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmReserved1;

[FieldOffset(144)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmReserved2;
 
    [FieldOffset(148)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmPanningWidth;
 
    [FieldOffset(152)]
    [MarshalAs(UnmanagedType.U4)]
    public UInt32 dmPanningHeight;
}
 
// Структура из 16 байт
[StructLayout(LayoutKind.Sequential)]
//public class DEVMODE_PRINT_SETTINGS
public struct DEVMODE_PRINT_SETTINGS
{
    public short dmOrientation;
    public short dmPaperSize;
    public short dmPaperLength;
    public short dmPaperWidth;
    public short dmScale;
    public short dmCopies;
    public short dmDefaultSource;
    public short dmPrintQuality;
}
 
// Структура из 8 байт
[StructLayout(LayoutKind.Sequential)]
//public class POINTL
public struct POINTL
{
    public Int32 x;
    public Int32 y;
}

Как мы уже писали в предыдущей части [2], все написанное здесь подразумевает 32-разрядные версии Windows. Например, в случае с DEVMODE было предположение, что DWORD занимает 4 байта. Если хотите портировать Ваше приложение на 64-битную систему, то нужно принять, что DWORD будет занимать 8 байт.

Получилось длинно, не так ли? DEVMODE одна из самых больших составных структур GDI. Из последнего кода мы научились следующему:

• Как бы ни было определено объединение (union), как одиночная сущность или внутри структуры, Вам нужно явно разместить тип в памяти, чтобы обеспечить положение двух или большего количества переменных в одной и той же области памяти.
• Когда размещение в памяти установлено явно, мы применяем атрибут FieldOffsetAttribute к переменной, указывая её положение - по её смещению в байтах относительно начала типа.
• В объединении (union), которое определено внутри структуры, мы маршалировали структуру вне объединения, и ссылались на неё как контейнер других полей.

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

Для этого примера мы будем использовать структуры DEVMODE и POINTL, которые мы маршалировали ранее. Дополнительно создадим managed-прототипы 2 новых функций Windows API, EnumDisplaySettings и ChangeDisplaySettings. Вот unmanaged-сигнатура этих функций (дополнительную информацию по этим функциям см. в MSDN):

BOOL EnumDisplaySettings(
  LPCTSTR lpszDeviceName,            // устройство дисплея
  DWORD iModeNum,                    // графический режим
  [In, Out] LPDEVMODE lpDevMode      // настройки графического режима
);
 
LONG ChangeDisplaySettings(
  LPDEVMODE lpDevMode,               // графический режим
  DWORD dwflags                      // опции графического режима
);

Вот managed-версии этих функций:

[DllImport("User32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern Boolean EnumDisplaySettings(
    [param: MarshalAs(UnmanagedType.LPTStr)]
    string lpszDeviceName,
    [param: MarshalAs(UnmanagedType.U4)]
    int iModeNum,
    [In, Out]
    ref DEVMODE lpDevMode);
 
[DllImport("User32.dll")]
[return: MarshalAs(UnmanagedType.I4)]
public static extern int ChangeDisplaySettings(
    [In, Out]
    ref DEVMODE lpDevMode,
    [param: MarshalAs(UnmanagedType.U4)]
    uint dwflags);

И наконец, вот эти 4 функции, которые обращаются к native-функциям:

public static void GetCurrentSettings()
{
    DEVMODE mode = new DEVMODE();
    mode.dmSize = (ushort)Marshal.SizeOf(mode);
 
    if (EnumDisplaySettings(null,
        ENUM_CURRENT_SETTINGS, ref mode) == true) // успешно
    {
        Console.WriteLine("Текущий режим:\n\t" +
            "{0}x{1}, {2} бит, {3} градусов, {4} Гц",
            mode.dmPelsWidth, mode.dmPelsHeight,
            mode.dmBitsPerPel, mode.dmDisplayOrientation * 90,
            mode.dmDisplayFrequency);
    }
}
 
public static void EnumerateSupportedModes()
{
    DEVMODE mode = new DEVMODE();
    mode.dmSize = (ushort)Marshal.SizeOf(mode);
 
    int modeIndex = 0; // 0 = первый режим
 
    Console.WriteLine("Поддерживаемые режимы:");
    while (EnumDisplaySettings(null,
        modeIndex, ref mode) == true) // найден режим
    {
        Console.WriteLine("\t{0}x{1}, {2} бит, " +
            "{3} градусов, " +
            "{4} Гц",
            mode.dmPelsWidth, mode.dmPelsHeight,
            mode.dmBitsPerPel, mode.dmDisplayOrientation * 90,
            mode.dmDisplayFrequency);
 
        modeIndex++; // переход к следующему режиму
    }
}
 
public static void ChangeDisplaySettings
    (int width, int height, int bitCount)
{
    DEVMODE originalMode = new DEVMODE();
    originalMode.dmSize = (ushort)Marshal.SizeOf(originalMode);
 
    // Получение текущих настроек и их редактирование
    EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref originalMode);
 
    // Здесь делается копия текущих настроек, чтобы можно было
    //  вернуться в старый режим
    DEVMODE newMode = originalMode;
 
    // Изменение настроек
    newMode.dmPelsWidth = (uint)width;
    newMode.dmPelsHeight = (uint)height;
    newMode.dmBitsPerPel = (uint)bitCount;
 
    // Захват результата операции
    int result = ChangeDisplaySettings(ref newMode, 0);
    if (result == DISP_CHANGE_SUCCESSFUL)
    {
        Console.WriteLine("Успешно.\n");
 
        // Проверка нового режима
        GetCurrentSettings();
        Console.WriteLine();
        // Ожидание подтверждения, что результаты видны
        Console.ReadKey(true);
 
        ChangeDisplaySettings(ref originalMode, 0);
    }
    else if (result == DISP_CHANGE_BADMODE)
        Console.WriteLine("Режим не поддерживается.");
    else if (result == DISP_CHANGE_RESTART)
        Console.WriteLine("Нужна перезагрузка.");
    else
        Console.WriteLine("Неудача. Код ошибки = {0}", result);
}
 
public static void RotateScreen(bool clockwise)
{
    // Получение текущих настроек
    // ...
    // Поворот экрана
    if (clockwise)
        if (newMode.dmDisplayOrientation < DMDO_270)
            newMode.dmDisplayOrientation++;
        else
            newMode.dmDisplayOrientation = DMDO_DEFAULT;
     else
        if (newMode.dmDisplayOrientation > DMDO_DEFAULT)
            newMode.dmDisplayOrientation--;
        else
            newMode.dmDisplayOrientation = DMDO_270;
 
    // Тут экран переставляется вверх ногами
    uint temp = newMode.dmPelsWidth;
    newMode.dmPelsWidth = newMode.dmPelsHeight;
    newMode.dmPelsHeight = temp;
 
    // Захват результата операции
    // ...
}

Консольная библиотека. Демонстрируется функционал консольных приложений, который недоступен из .NET Framework - наподобие очистки экрана консоли и перемещения по нему текста.

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

SafeNativeMethods.cs

using System;
using System.Runtime.InteropServices;
using System.Text;
 
///
/// Безопасные native-функции
///
internal static class SafeNativeMethods
{
   /// Стандартное устройство ввода
   public const int STD_INPUT_HANDLE = -10;
   /// Стандартное устройство вывода
   public const int STD_OUTPUT_HANDLE = -11;
   /// Стандартное устройство для сообщения об ошибках
   public const int STD_ERROR_HANDLE = -12;
   /// Пробел для очистки дисплея
   public const char WHITE_SPACE = ' ';
           
   /// 
   /// Запрашивает handle для стандартных устройств ввода, вывода или
   ///  сообщения об ошибке.
   /// 
   /// nStdHandle: Стандартное устройство, для которого
   /// запрашивается handle.
   /// Возвращает: Выбранный handle для стандартного устройства, или
   ///  недопустимый хендл, если функция завершилась с ошибкой.
   [DllImport("Kernel32.dll")]
   public static extern IntPtr GetStdHandle(
      [param: MarshalAs(UnmanagedType.I4)] int nStdHandle);
                  
   /// 
   /// Записывает строку символов в буфер консоли, начиная с текущей
   /// позиции курсора.
   /// 
   /// hConsoleOutput: Хендл для открытого устройства вывода.
   /// lpBuffer: Строка, которая будет записана.
   /// nNumberOfCharsToWrite: Количество записываемых символов.
   /// lpNumberOfCharsWritten: Выходное количество записанных
   /// символов. 
   /// lpReserved: Зарезервировано.
   /// Возвращает: True если успех, иначе False.
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool WriteConsole(
      IntPtr hConsoleOutput,
      string lpBuffer,
      [param: MarshalAs(UnmanagedType.U4)]
      uint nNumberOfCharsToWrite,
      [param: MarshalAs(UnmanagedType.U4)]
      [Out] out uint lpNumberOfCharsWritten,
      [param: MarshalAs(UnmanagedType.U4)]
      uint lpReserved);
                   
   /// 
   /// Читает строку символов из буфера консоли с текущей позиции курсора.
   /// 
   /// hConsoleInput: Хендл открытого устройства ввода.
   /// lpBuffer: Строка, прочитанная из буфера.
   /// nNumberOfCharsToRead: Количество символов для чтения.
   /// lpNumberOfCharsRead: Выходное количество прочитанных
   /// символов.
   /// lpReserved: Зарезервировано.
   /// Возвращает: True если успех, иначе False.
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool ReadConsole(
      IntPtr hConsoleInput,
      StringBuilder lpBuffer,
      [param: MarshalAs(UnmanagedType.U4)]
      uint nNumberOfCharsToRead,
      [param: MarshalAs(UnmanagedType.U4)]
      [Out] out uint lpNumberOfCharsRead,
      [param: MarshalAs(UnmanagedType.U4)]
      uint lpReserved);
      
   /// 
   /// Запрашивает информацию курсора консоли, такую как его размер и видимость.
   /// 
   /// hConsoleOutput: Хендл открытого устройства вывода.
   /// lpConsoleCursorInfo: Информация курсора.
   /// Возвращает: True если успех, иначе False.
   [DllImport("kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool GetConsoleCursorInfo(
      IntPtr hConsoleOutput,
      [Out] out CONSOLE_CURSOR_INFO lpConsoleCursorInfo);
 
   /// 
   /// Устанавливает свойства курсора консоли, такие как размер и видимость.
   /// 
   /// hConsoleOutput: Хендл для открытого устройства вывода.
   /// lpConsoleCursorInfo: Информация о курсоре.
   /// Возвращает: True если успех, иначе False.
   [DllImport("kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool SetConsoleCursorInfo(
      IntPtr hConsoleOutput,
      ref CONSOLE_CURSOR_INFO lpConsoleCursorInfo);
 
   /// 
   /// Перемещает блок данных в буфере экрана.
   /// 
   /// hConsoleOutput: Хендл для открытого устройства вывода.
   /// lpScrollRectangle: Координаты блока для перемещения.
   /// lpClipRectangle: Координаты, на которые влияет скроллинг.
   /// dwDestinationOrigin: Координаты нового места блока.
   /// lpFill: Указывает символ и цвет для ячеек, оставшихся пустыми
   /// после перемещения.
   /// Возвращает: True если успех, иначе False.
   /// 
   /// Поскольку мы устанавливаем параметр lpClipRectangle в NULL, то маршалируем
   /// его как IntPtr, так что можем использовать IntPtr.Zero.
   /// Если Вам не нужно устанавливать это значение, то можете маршалировать
   /// его как SMALL_RECT.
   [DllImport("kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool ScrollConsoleScreenBuffer(
      IntPtr hConsoleOutput,
      ref SMALL_RECT lpScrollRectangle,
      IntPtr lpClipRectangle,
      COORD dwDestinationOrigin,
      ref CHAR_INFO lpFill);
 
   /// 
   /// Получает информацию по буферу экрана указанной консоли.
   /// 
   /// hConsoleOutput: Хендл устройства, у которого запрашивается
   /// информация.
   /// lpConsoleScreenBufferInfo: Выходная информация указанного
   /// буфера экрана.
   /// Возвращает: True если успех, иначе False.
   [DllImport("Kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool GetConsoleScreenBufferInfo (
      IntPtr hConsoleOutput,
      [Out] out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
 
   /// 
   /// Заполняет буфер консоли указанным символом.
   /// 
   /// hConsoleOutput: Хендл для открытого устройства вывода.
   /// cCharacter: Символ, который заполнит буфер. Установка символа
   /// в пробел означает очистку ячеек экрана.
   /// nLength: Количество заполняемых ячеек.
   /// dwWriteCoord: Место, откуда начинается заполнение.
   /// lpNumberOfCharsWritten: Выводится количество записанных
   /// символов.
   /// Возвращает: True если успех, иначе False.
   [DllImport("Kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool FillConsoleOutputCharacter (
      IntPtr hConsoleOutput,
      char cCharacter,
      [param: MarshalAs(UnmanagedType.U4)]
      uint nLength, COORD dwWriteCoord,
      [param: MarshalAs(UnmanagedType.U4)]
      [Out] out uint lpNumberOfCharsWritten);
 
   /// 
   /// Устанавливает курсор консоли в указанную позицию.
   /// 
   /// hConsoleOutput: Хендл для открытого устройства вывода.
   /// dwCursorPosition: Новая позиция курсора внутри буфера
   /// консоли.
   /// Возвращает: True если успех, иначе False.
   [DllImport("Kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   public static extern bool SetConsoleCursorPosition (
      IntPtr hConsoleOutput,
      COORD dwCursorPosition);
}
 
///
/// Информация о буфере экрана.
/// [StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_SCREEN_BUFFER_INFO
{
   /// Размер буфера.
   public COORD dwSize;
 
   /// Положение курсора в буфере.
   public COORD dwCursorPosition;
 
   /// Дополнительные атрибуты буфера для записи переднего цвета
   /// и заднего цвета.
   [MarshalAs(UnmanagedType.U2)]
   public ushort wAttributes;
 
   /// Место границ экрана.
   public SMALL_RECT srWindow;
 
   /// Максимальный размер окна.
   public COORD dwMaximumWindowSize;
}
 
/// 
/// Координаты (X, Y).
///
[StructLayout(LayoutKind.Sequential)]
public struct COORD
{
   /// Позиция слева (X).
   [MarshalAs(UnmanagedType.I2)]
   public short X;
 
   /// Позиция сверху (Y).
   [MarshalAs(UnmanagedType.I2)]
   public short Y;
}
 
/// 
/// Определяет координат левого верхнего и правого нижнего
/// углов прямоугольника.
///
[StructLayout(LayoutKind.Sequential)]
public struct SMALL_RECT
{
   /// X-координата верхнего левого угла прямоугольника.
   [MarshalAs(UnmanagedType.I2)]
   public short Left;
   
   /// Y-координата верхнего левого угла прямоугольника.
   [MarshalAs(UnmanagedType.I2)]
   public short Top;
   
   /// X-координата нижнего правого угла прямоугольника.
   [MarshalAs(UnmanagedType.I2)]
   public short Right;
   
   /// The Y-координата нижнего правого угла прямоугольника.
   [MarshalAs(UnmanagedType.I2)]
   public short Bottom;
}
 
///
/// Задает информацию курсора консоли.
///
[StructLayout(LayoutKind.Sequential)]
public struct CONSOLE_CURSOR_INFO
{
   /// Размер курсора. Обычно 0.25 от ячейки.
   [MarshalAs(UnmanagedType.U4)]
   public uint dwSize;
   
   /// Виден курсор или нет.
   [MarshalAs(UnmanagedType.Bool)]
   public bool bVisible;
}
 
///
/// Задает информацию символа.
/// 
[StructLayout(LayoutKind.Sequential)]
public struct CHAR_INFO
{
   /// Символ.
   public char Char;
   
   /// Добавочные атрибуты символа наподобие переднего
   /// и заднего цветов.
   [MarshalAs(UnmanagedType.U2)]
   public ushort Attributes;
}

ConsoleLib.cs

using System;
using System.Runtime.InteropServices;
using System.Text;
 
// Выравнивание текста консоли по горизонтали.
public enum ConsoleTextAlignment
{
   /// Текст выровнен по левому краю.
   Left,
   /// Текст выровнен по правому краю.
   Right,
   /// Текст центрирован.
   Center
}
 
///
/// Стандартные устройства консоли.
///
public enum ConsoleStandardDevice
{
   /// Устройство ввода.
   Input = SafeNativeMethods.STD_INPUT_HANDLE,
   /// Устройство вывода.
   Output = SafeNativeMethods.STD_OUTPUT_HANDLE,
   /// Устройства ошибок (обычно это устройство вывода.)
   Error = SafeNativeMethods.STD_ERROR_HANDLE
}
 
///
/// Расширенные методы консоли.
///
public static class ConsoleExtensions
{
   /// 
   /// Очищает буфер экрана.
   /// 
   public static void ClearScreen()
   {
      // Очистка экрана, начиная с первой ячейки.
      COORD location = new COORD();
      location.X = 0;
      location.Y = 0;
      ClearScreen(location);
   }
   
   /// 
   /// Очищает буфер экрана, начиная с указанного места.
   /// 
   /// location: Место, с которого начитается очистка буфера
   /// экрана.
   public static void ClearScreen(COORD location)
   {
      // Очистка экрана, начиная с указанного места
      // Установка символа в пробел означает очистку экрана
      // Установка счетчика в 0 означает очистку до конца, а не на 
      // определенную длину.
      FillConsoleBuffer(location, 0, SafeNativeMethods.WHITE_SPACE);
   }
 
   /// 
   /// Заполняет определенные ячейки указанным символом, начиная с нужного
   /// места.
   /// 
   /// location: Место, откуда начать заполнение.
   /// count: Количество ячеек для очистки.
   /// character: Символ, которым будут заполняться ячейки.
   public static void FillConsoleBuffer(COORD location,
                                        uint count,
                                        char character)
   {
      // Получение хендла выходного устройства консоли
      IntPtr handle = GetStandardDevice(ConsoleStandardDevice.Output);
      uint length;
      
      // Если счетчик равен 0, то нужно очистить весь экран
      if (count == 0)
      {
         // Получение информации о буфере экрана
         CONSOLE_SCREEN_BUFFER_INFO info = GetBufferInfo(ConsoleStandardDevice.Output);
         // Весь экрана
         length = (uint)(info.dwSize.X * info.dwSize.Y);
      }
      else
         length = count;
         
      // Количество записываемых символов
      uint numChars;
      
      // Вызов функции Win32 API
      SafeNativeMethods.FillConsoleOutputCharacter(handle,
                                                   character,
                                                   length,
                                                   location,
                                                   out numChars);
      // Установка позиции курсора консоли
      SetCursorPosition(location);
   }
 
   /// 
   /// Получает хендл для нужного устройства.
   /// 
   /// device: Устройство, для которого запрашивается хендл.
   /// Возвращает: Хендл указанного устройства.
   public static IntPtr GetStandardDevice(ConsoleStandardDevice device)
   {
      // Вызов функции Win32 API
      return SafeNativeMethods.GetStdHandle((int)device);
   }
 
   /// 
   /// Записывает пустую строку в буфер консоли в текущей позиции курсора.
   /// 
   public static void WriteLine()
   {
      WriteLine(string.Empty);
   }
 
   /// 
   /// Записывает текст и терминатор строки в консоль,
   /// в текущую позицию курсора.
   /// 
   /// txt: Текст для записи.
   public static void WriteLine(string txt)
   {
      WriteLine(txt, ConsoleTextAlignment.Left);
   }
 
   /// 
   /// Записывает текст и в его конец терминатор строки в буфер консоли,
   /// в текущую позицию курсора с указанным выравниванием строки.
   /// 
   /// txt: Текст для записи.
   /// alignment: Горизонтальное выравнивание текста.
   public static void WriteLine(string txt, ConsoleTextAlignment alignment)
   {
      Write(txt + Environment.NewLine, alignment);
   }
 
   /// 
   /// Записывает текст в буфер консоли, в текущую позицию курсора.
   /// 
   /// txt: Текст для записи.
   public static void Write(string txt)
   {
      Write(txt, ConsoleTextAlignment.Left);
   }
 
   /// 
   /// Записывает текст в буфер консоли, в текущую позицию курсора,
   /// с указанным выравниванием строки.
   /// 
   /// txt: Текст для записи.
   /// alignment: Горизонтальное выравнивание текста.
   public static void Write(string txt, ConsoleTextAlignment alignment)
   {
      if (alignment == ConsoleTextAlignment.Left)
         InternalWrite(txt);
      else
      {
         // Определение места, откуда начитается запись
         CONSOLE_SCREEN_BUFFER_INFO info = GetBufferInfo(ConsoleStandardDevice.Output);
         COORD pos = new COORD();
         if (alignment == ConsoleTextAlignment.Right)
            pos.X = (short)(info.dwSize.X - txt.Length);
         else // Центр
            pos.X = (short)((info.dwSize.X - txt.Length) / 2);
         pos.Y = info.dwCursorPosition.Y;
         // Изменение позиции курсора
         SetCursorPosition(pos);
         // Теперь запись в текущую позицию
         InternalWrite(txt);
      }
   }
 
   /// 
   /// Запись текста в выходной буфер консоли, начиная с текущей позиции курсора.
   /// 
   /// txt: Текст для записи.
   private static void InternalWrite(string txt)
   {
      // Для функции WriteConsole() требуется количество записываемых символов
      uint count;
      // Получение хендла для вывода
      IntPtr handle = GetStandardDevice(ConsoleStandardDevice.Output);
      // Вызов функции Win32 API
      SafeNativeMethods.WriteConsole(handle, txt, (uint)txt.Length, out count, 0);
   }
 
   /// 
   /// Показывает или прячет курсор.
   /// 
   /// show: Указывает, спрятать или показать курсор.
   public static void ShowCursor(bool show)
   {
      CONSOLE_CURSOR_INFO info;
      
      // Получение устройства вывода
      IntPtr handle = GetStandardDevice(ConsoleStandardDevice.Output);
      
      // Получение информации о курсоре
      SafeNativeMethods.GetConsoleCursorInfo(handle, out info);
      
      // Определение видимости курсора
      info.bVisible = show;
      
      // Установка информации курсора
      SafeNativeMethods.SetConsoleCursorInfo(handle, ref info);
   }
 
   /// 
   /// Читает следующую строку из устройства ввода.
   /// 
   public static string ReadText()
   {
      // Буфер. Максимальное количество символов 256.
      StringBuilder buffer = new StringBuilder(256);
      // Требуется для вызова функции
      uint count;
      // Получение устройства ввода, которое используется для приема
      // ввода пользователя.
      SafeNativeMethods.ReadConsole(
               GetStandardDevice(ConsoleStandardDevice.Input),
               buffer,
               (uint)buffer.Capacity,
               out count,
               0);
  
      // Возвращает ввод пользователя до терминатора строки
      return buffer.ToString().Substring(
               0,
               (int)(count - Environment.NewLine.Length));
   }
 
   /// 
   /// Получает информацию о буфере указанного устройства.
   /// 
   /// device: Устройство, о котором запрашивается
   /// информация.
   /// Возвращает: Информация о буфере указанного устройства.
   public static CONSOLE_SCREEN_BUFFER_INFO GetBufferInfo
      (ConsoleStandardDevice device)
   {
      // Получение хендла выбранного устройства
      IntPtr handle = GetStandardDevice(device);
      
      // Получение информации о буфере экрана консоли
      CONSOLE_SCREEN_BUFFER_INFO info;
      SafeNativeMethods.GetConsoleScreenBufferInfo(handle, out info);
      return info;
   }
 
   /// 
   /// Установка позиции курсора в буфере.
   /// 
   /// pos: Координаты, куда перемещается курсор.
   public static void SetCursorPosition(COORD pos)
   {
      // Получение хендла устройства ввода
      IntPtr handle = SafeNativeMethods.GetStdHandle
               (SafeNativeMethods.STD_OUTPUT_HANDLE);
      
      // Перемещение курсора в новую позицию
      SafeNativeMethods.SetConsoleCursorPosition(handle, pos);
   }
 
   /// 
   /// Записывает информацию буфера в экран.
   /// 
   /// info: Информация для записи.
   public static void WriteBufferInfo(CONSOLE_SCREEN_BUFFER_INFO info)
   {
      // Определение информации буфера экрана консоли
      WriteLine("Информация буфера консоли:");
      WriteLine("--------------------------");
      WriteLine("Позиция курсора:");
      WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                              "\t{0}, {1}",
                              info.dwCursorPosition.X,
                              info.dwCursorPosition.Y));
      // Эта информация верна?
      WriteLine("Максимальный размер окна:");
      WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                              "\t{0}, {1}",
                              info.dwMaximumWindowSize.X,
                              info.dwMaximumWindowSize.Y));
      
      // Эта информация верна?
      WriteLine("Размер буфера экрана:");
      WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                              "\t{0}, {1}",
                              info.dwSize.X,
                              info.dwSize.Y));
      WriteLine("Границы буфера экрана:");
      WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                              "\t{0}, {1}, {2}, {3}",
                              info.srWindow.Left,
                              info.srWindow.Top,
                              info.srWindow.Right,
                              info.srWindow.Bottom));
      WriteLine("--------------------------");
   }
   
   /// 
   /// Записывает текст с терминатором строки слева, и перемещает его вправо.
   /// 
   /// txt: Текст для записи.
   public static void MoveText(string txt)
   {
      // Сначала запишем текст
      WriteLine(txt);
      // Получение хендла устройства вывода
      IntPtr handle = GetStandardDevice(ConsoleStandardDevice.Output);
      // Получение информации буфера экрана для устройства вывода
      CONSOLE_SCREEN_BUFFER_INFO screenInfo =
               GetBufferInfo(ConsoleStandardDevice.Output);
      // Выбор текста для перемещения
      SMALL_RECT rect = new SMALL_RECT();
      rect.Left = 0;
      // 1-я ячейка
      rect.Top = (short)(screenInfo.dwCursorPosition.Y - 1);
      // Строка текста
      rect.Bottom = (short)(rect.Top);
      // Только одна строка
      while (true)
      {
         // Перемещение вправо
         rect.Right = (short)(rect.Left + (txt.Length - 1));
         // Не перемещать текст, если дошли до правого края буфера
         if (rect.Right == (screenInfo.dwSize.X - 1))
            break;
         
         // Символ для заполнения пустых ячеек, получающихся после
         // перемещения текста
         CHAR_INFO charInfo = new CHAR_INFO();
         charInfo.Char = SafeNativeMethods.WHITE_SPACE;
         // Для очистки ячеек
         // Вызов функции API
         SafeNativeMethods.ScrollConsoleScreenBuffer(
                  handle,
                  ref rect,
                  IntPtr.Zero,
                  new COORD(){X = (short)(rect.Left + 1), Y = rect.Top},
                  ref charInfo);
         // Блокирование потока, чтобы пользователь увидел
         // эффект перемещения
         System.Threading.Thread.Sleep(100);
         // Перемещение прямоугольника
         rect.Left++;
      }
   }
}

[Выводы]

После всего Вы узнали, что такое составные типы и unmanaged-структуры и объединения, и что они называются составными потому что состоят из других типов.

Вы узнали, что составные типы можно маршалировать либо в виде managed-структуры, либо класса. Дополнительно Вы узнали как типы размещаются в памяти.

Снова и снова нужно повторить, что карта памяти размещения полей и размер типов полей в структурах имеют критически важное значение.

Вы научились работать с объединениями, и узнали, что это просто группа из нескольких переменных, которые используют одну и ту же общую память.

Как маршалировать массивы см. [5].

[Ссылки]

1. Marshaling with C# site:codeproject.com.
2. Маршалинг C#. Простые типы.
3. Binary Data Marshaling site:codeproject.com.
4. Marshaling Structures site:codeproject.com.
5. Marshaling Arrays site:msdn.microsoft.com.

 

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


Защитный код
Обновить

Top of Page