Типы данных в Win32 API
В WINAPI определено множество типов данных, так же, как и в C/С++ ( int , char , float и т.д.). Учить их определения не обязательно. Достаточно помнить, что они существуют, а когда они появятся или потребуются где-нибудь в программе, посмотреть их определения. В дальнейшем мы будем использовать их все. Условно их можно разделить на несколько видов: основные, дескрипторные, строковые и вспомогательные.
Основные типы
С основными типами данных трудностей возникнуть не должно. Если всё же возникнут, то нужно сюда.
BOOL – этот тип данных аналогичен bool . Он также имеет два значения – 0 или 1. Только при использовании WINAPI принято использовать вместо 0 спецификатор NULL . О нём ниже.
BYTE – байт, ну или восьмибитное беззнаковое целое число. Аналог – unsigned char .
DWORD — 32-битное беззнаковое целое. Аналоги: unsigned long int , UINT .
INT – 32-битное целое. Аналог – long int .
LONG – 32-битное целое – аналог всё также long int .
NULL – нулевой указатель. Вот его объявление:
UINT – 32-битное беззнаковое целое. Аналоги: unsigned long int , DWORD .
Дескрипторные типы данных
Про дескрипторные типы немного рассказывалось на вводном уроке в WINAPI. Дескриптор, как говорилось ранее, — это идентификатор какого-либо объекта. Для разных типов объектов существуют разные дескрипторы. Дескриптор объекта можно описать так:
Есть также дескрипторы кисти, курсора мыши, шрифта и т.д. С их помощью мы можем при инициализации или в процессе работы приложения поменять какие-нибудь настройки, чего, например, мы не могли сделать в консольном приложении. Используются они в описательных функциях, управляющих типа: CreateProcess(), ShowWindow() и т.д. или как возвращаемое значение некоторых функций :
В этой функции мы получили дескриптор считывания потоков std_in и std_out. И можем, например, его использовать в каком-нибудь условии.
Не будем вдаваться в физику создания дескрипторов. Разве что, при необходимости или для большего понимания процессов.
HANDLE – дескриптор объекта.
HBITMAP – дескриптор растрового изображения. От фразы handle bitmap.
HBRUSH – дескриптор кисти. От фразы handle brush.
HCURSOR – дескриптор курсора. От фразы handle cursor.
HDC – дескриптор контекста устройства. От фразы handle device context.
HFONT – дескриптор шрифта. От фразы handle font.
HICONS – дескриптор криптограммы. От фразы handle icons.
HINSTANCE – дескриптор экземпляра приложения. От фразы handle instance.
HMENU – дескриптор меню. От фразы handle menu.
HPEN – дескриптор пера. От фразы handle pen.
HWND – дескриптор окна. От фразы handle window.
Строковые типы данных
Для начала начнём, с того, какие кодировки существуют в Windows ОС.
Есть два вида кодировок символов: ANSI и UNICODE. Однобайтные символы относятся к ANSI, двухбайтные — к кодировке UNICODE. Мы можем с лёгкостью подключить UNICODE кодировку в свойствах проекта. И тогда в коде создать переменную типа char можно будет так:
Если же мы хотим использовать кодировку ANSI, то мы традиционно напишем:
В WINAPI, в зависимости от того, подключён юникод или нет, используются два вида строк UNICODE-ные или TCHAR-ные. Ниже описаны строковые типы данных.
LPCSTR – указатель на константную строку, заканчивающуюся нуль-терминатором. От фразы long pointer constant string.
LPCTSTR – указатель на константную строку, без UNICODE. От фразы long pointer constant TCHAR string. Это надстройка функции LPCSTR.
LPCWSTR – указатель на константную UNICODE строку. От фразы фразы long pointer constant wide character string. Это надстройка функции LPCSTR.
LPSTR – указатель на строку, заканчивающуюся нуль-терминатором. От фразы long pointer string.
LPTSTR – указатель на строку, без UNICODE. От фразы long pointer TCHAR string. Это надстройка функции LPSTR.
LPWSTR – указатель на UNICODE строку. От фразы long pointer wide character string. Это надстройка функции LPSTR.
TCHAR – символьный тип — аналог char и wchar_t.
Вспомогательные типы
Вспомогательные типы данных используются в некоторых функциях. В частности, параметры, описанные ниже, используются при работе с функцией обратного вызова оконной процедуры такого вида:
Работа с данной функцией будет в следующих разделах.
LPARAM – тип для описания lParam (long parameter). Используются вместе с wparam в некоторых функциях.
LRESULT – значение, возвращаемое оконной процедурой имеет тип long .
WPARAM – тип для описания wParam (word parameter). Используются вместе с lParam в некоторых функциях.
На этом типы данных не закончены. В дальнейшем мы обязательно будем обращаться к данной статье.
WinAPI: Не запутайтесь в типах
Очень часто вами будет использоваться тип HANDLE — дескриптор, предназначенный для описания различных объектов. На самом деле этот тип представляет собой ни что иное, как указатель на void, т.е. как бы на любой тип.
Объекты Windows обычно представлены своими дескрипторами. Например, HWND — дескриптор окна. Что он из себя представляет? Давайте посмотрим:
В файле windef.h можно обнаружить такую строчку:
Эта строка при определенной опции STRICT разворачивается в
То есть HWND есть указатель на структуру HWND__. Если же опция STRICT не определена, то HWND везде заменяется на HANDLE.
Идентификатор STRICT указывает на необходимость проводить более строгую проверку типов. Как вы уже убедились, без этой опции все HWND, а также описатели других объектов Windows — HPEN, HBITMAP, HFONT, HMENU, HDC и др. будут фактически представлять собой один тип — HANDLE. Если же вы включите определение STRICT, тогда они будут трактоваться как разные типы (благодаря макросу DECLARE_HANDLE), и при их несоответствии компилятор будет выдавать сообщение об ошибке. Использование STRICT рекомендуется для того, чтобы было легче находить возможные ошибки в программе.
В заключение давайте рассмотрим очень часто используемый тип COLORREF. По сути это unsigned long. Этот тип представляет возможность задать цвет набором его RED, GREEN и BLUE составляющих, для этого используйте макрос RGB:
Результат этого выражения — длинное целое число, самый младший байт которого содержит интенсивность красного, второй — зеленого и третий байт — синего. В этом случае color будет содержать голубой цвет. Сам макрос RGB(r,g,b) при обработке препроцессором расширяется до ((COLORREF)((BYTE)(r) | ((WORD)(g) <<8)) | (((DWORD)(BYTE)(b))<<16))).
Вопрос — ответ
eFi: Как делать окна нестандартной формы? Например, круг (как у диска Компьютерры — там окно обычное, но с помощью прозрачности виден только круг, так?)
На этот вопрос пришло довольно много фактически одинаковых ответов, и я сейчас постараюсь объяснить самую суть.
В Windows существует понятие регионов — областей. Каждое окно имеет свою область, которая по умолчанию создается прямоугольной. В WinAPI (и в классе CWnd) существует функция SetWindowRgn( ), которая позволяет задать форму этой области. То есть сначала вы создаете область, потом устанавливаете его как форму для окна (это можно сделать, например, в OnInitDialog( ) ). Создать область можно с помощью функций Create. Rgn( ). Например, чтобы сделать круглое окно, можно воспользоваться CreateEllipticRgn( ). Подробно параметры я описывать не буду — смотрите пример. Замечу только, что регионы можно создавать сложные, составленные из нескольких примитивов. Они образуются путем комбинирования областей ( CombineRgn( ) ).
Пример (прислал Sergey Melnikov):
А если добавить такой код, получим окно с «прорезью» в виде эллипса:
Ответ на этот вопрос прислали (в порядке получения): Андрей Колчанов, Ренат Васиков, Ilgar Mashayev, Sergey Skornyakov, LiMar, Sergey Melnikov, Igor Kurilov, Michael Stepanenkov.
George V. Samodumov: Как загружать 256-цветный курсор в приложении? Т.е. проблема в том что в редакторе ресурсов можно сделать либо только черно-белый курсор, либо еще и цветной, но при этом LoadCursor загружет только ч.б.
Ответ на этот вопрос часто сводится к рекомендации воспользоваться LoadImage( ) вместо LoadCursor( ). Вот самый полный и интересный ответ из присланных:
Дело в том, что файл курсора имеет схожий формат с файлом иконки, т.е. в одном файле могут находиться несколько изображений разных форматов, например: 16х16х16, 32х32х256 и т.д. При добавлении нового курсора редактор ресурсов VC автоматически создает курсор формата 32х32х2, который вероятно и грузится первым даже если добавлены еще несколько изображений. Поэтому нужно сделать так, чтобы курсор содержал только одно изображение. В редакторе ресурсов выполняем Insert|Cursor, потом открываем его для редактирования и в появившемся меню Image выбираем «New Device Image», а там «Custom» и задаем параметры изображения, например 48x48x256. Редактируем курсор, а потом переключаемся на монохромное изображение и удаляем его: «Image|Open Device Image -> Monochrome32x32», «Image|Delete Device Image». Теперь мы избавились от монохромного изображения и можем грузить курсор функциями: LoadCursor(), LoadCursorFromFile(), LoadImage():
Что такое HANDLE в программировании под windows?
Что такое HANDLE? Я начал путать его с контекстом устройства. Но что-то мне подсказывает, что это разные вещи.
HANDLE — дескриптор, т.е. число, с помощью которого можно идентифицировать ресурс. С помощью дескприторов можно ссылаться на окна, объекты ядра, графические объекты и т.п.
Можно провести аналогию с массивом: у нас имеется набор ресурсов, а HANDLE — это индекс, который указывает на конкретный ресурс. Это все, конечно, абстрактно, но думаю идея понятна.
![]()
Вы не работаете с контекстом устройства напрямую. Сам по себе контекст устройства — «черный ящик», что у него внутри, мы не знаем. У контекста устройства есть хэндл (идентификатор), который, например, возвращают функции CreateDC и GetDC и который можно передать в другие WinAPI функции.
Получается, контекст устройства (DC) — некий сложный объект, а хэндл контекста устройства (hDC) — число. Хэндл контекста устройства — это один из видов (частный случай) хэндлов. Кроме хэндлов DC, есть хэндлы окон, файлов, битмэпов, всяких кистей/карандашей и т.д.
Использование handle и intrusive reference counter-ов в многопоточных средах в языке C
Доступ к одим и тем же данным в нескольких потоках считается плохой практикой, но во многих случаях это неизбежно, и это не тот вопрос, который обсуждается здесь. Вопрос который здесь обсуждается, это как организовать такой доступ наиболее безопасным способом. Также тут не обсуждаются атомарные операции, которые тут упоминаются: разные компиляторы предлагают различные средства для таких операций.
В многопоточной среде при использовании объекта или структуры данных, один из главных вопросов, помимо прочего, это гарантия того, что объект к которому производится доступ все еще жив и память, выделенная для структуры не освобождена.
Это может быть сделано несколькими способами, но мы будем говорить только о двух из них: хэндлы (handles) и встроенные счётчики ссылок (intrusive reference counters).
Хэндлы — это небольшие структуры, которые содержат указатель на объект данных и вспомогательные данные, чтобы гарантировать, что объект еще жив. Как правило для работы с хэндлами пишутся две функции: lock_handle и unlock_handle (имена выбраны произвольно, чтобы показать функциональность). Lock_handle проверяет «живость» объекта, увеличивает атомарный счетчик ссылок и возвращает указатель на объект данных, если он ещё доступен. Если нет, то функция возвращает NULL, или с помощью другого способа даёт знать, что объект больше не доступен. Соответственно своему названию, unlock_handle атомарно уменьшает счетчик ссылок и как только он достигает значения 0, удаляет объект.
Встроенные счетчики ссылок — это атомарные числовые переменные внутри объекта данных, которые считают количество ссылок в программе на указанный объект данных. Как только счетчик ссылок достигает значения 0, объект удаляется.
Теперь давайте взглянем на преимущества и распространенные ошибки обеих стратегий и определим, какую из них лучше использовать в конкретных случаях. Сперва давайте разберемся с встроенными счетчиками ссылок.
Встроенные счетчики ссылок
Встроенные счетчики ссылок, как понятно из названия, должны быть встроены в структуру данных: это атомарное целое число. В простой структуре, назовем ее data_packet_s, подобная модификация может выглядеть так:
Именно это является главным недостатком этого подхода: он нуждается в модификации структуры данных, поэтому использовать его можно только для таких структур, которые мы можем изменить. Мы не можем использовать его с произвольным типом данных или структурами, например, библиотечными.
Любопытно, но это же факт является и преимуществом. Преимущество состоит в том, что нам не нужны никакие дополнительные структуры и дополнительное выделение памяти для подобных структур.
Еще один недостаток, или, скорее, специфика этого подхода заключается в следующем: доступность объекта должна быть гарантирована, когда происходит приращение счетчика ссылок. Другими словами, мы не можем просто хранить указатель на объект и дожидаться момента, когда мы хотим получить к нему доступ, и затем просто увеличить счетчик ссылок и далее работать с объектом, поскольку между моментом когда мы сохранили указатель и моментом когда мы начинаем использовать объект он может быть уничтожен, при этом уничтожается и счётчик ссылок. Давайте продемонстрируем простой случай такого события, используя простой условный язык.

В указанном случае, чтобы обеспечить целостность объекта, придется увеличить количество ссылок в потоке 1 и передать полученную ссылку в собственность потоку 2 (уменьшать reference_count будет поток 2 после того как объект больше не нужен). Другими словами, эта схема может работать очень хорошо, если необходимо обработать объект в другом потоке, и забыть о нем. Это может быть продемонстрировано с помощью следующей иллюстрации:

Такая схема может быть использована любым количеством потоков при условии, что каждый поток, который увеличивает счетчик ссылок, уже владеет по крайней мере одним экземпляром-ссылкой (либо увеличил количество ссылок и не передал собственность, либо получил экземпляр-ссылку в собственность от другого потока). Таким образом, в последнем случае поток 1 может увеличить счетчик ссылок несколько раз (по одному для каждого потока) и лишь после этого он может запустить несколько потоков (столько, на сколько был увеличен счетчик ссылок.
Тут становится виден и второй недостаток: мы увеличивать и уменьшаем счетчик ссылок в разных потоках, что часто может приводить к различным ошибкам, начиная от утечек памяти когда ссылка не освобождается, заканчивая двойным освобождением ссылки и соответственно ложным освобождением объекта, что ведёт к его удалению во время того как он ещё используется в другом потоке или потоках.
Эта схема реализует так называемый «shared ownership», когда потоки совместно владеют объектом, и нить, уменьшающая счетчик ссылок до нуля, будет удалять объект.
И если вы хотите сохранить указатель на объект в одном из потоков, и использовать его при необходимости вы увидите, что память выделенная вашей программой будет расти с каждым хранящимся объектом, потому что поток никогда не освободит ссылку на объект, «живость» которого он хочет гарантировать.
Другая возможная ошибка в таком подходе это обращение к косвенно переданным объектам, то есть объектам, которые передаются по указателю, содержащемуся в одном из других, прямо или косвенно переданных объектов, которые ссылаются, но не владеют объектом. Все такие объекты должны быть известны, при этом должен быть обеспечен безопасный доступ ко всем подобным объектам, если они используются наряду с основным.
Итак, давайте просуммируем недостатки и преимущества встроенных счетчиков ссылок:
- Встроенные счётчики требуют изменения структур данных
- Ссылка должна оставаться залоченной пока есть вероятность что объект будет использован
- Собственность ссылок передаётся между потоками
- Необходимо внимательное отношение к косвенно переданным объектам
- Не требуются дополнительные структуры и операции с памятью
Хэндлы
Хэндлы — это легкие структуры, которые передаются по значению, они ссылаются на объект, управляют ссылками и обеспечивают целостность объекта.
Очевидно, что хэндлы, которые ссылаются на один и тот же объект, будут иметь указатель на один счетчик ссылок. Как правило, это означает, что счетчик ссылок должен быть выделен на куче динамически. Простейшая структура хэндла, которая первой приходит на ум, может выглядеть так:
Где reference_count выделяется при создании первого хэндла на данный объект.
Первый недостаток такого подхода уже очевиден. Мы должны управлять еще одной дополнительной структурой, выделять еще одну область памяти для счетчика ссылок. Но это окупается, если вы хотите использовать подсчет ссылок со структурами, к которым вы не имеете доступа.
Типичное применение такого хэндла будет следующим:
Давайте посмотрим, что происходит, когда последняя ссылка на объект удалена. Прежде всего, мы, очевидно, хотим, чтобы удалился управляемый объект. Если объект является простым куском выделенной памяти, мы просто хотим, чтобы память, используемая объектом была освобождена. Но в большинстве случаев это не так. Как и в вышеупомянутом примере data_packet_s, где мы хотели бы освободить также и память data_buffer. Если мы используем хэндл для только одного типа объектов, это не создаёт большой проблемы. Но если мы хотим, чтобы хэндл мог обрабатывать разыличные типы, это привносит ещё один вопрос: как правильно удалить управляемый объект?
Мы можем добавить в хэндл ещё одно поле: указатель на функцию, которая будет использоваться для уничтожения/освобождения управляемого объекта. Теперь хэндл выглядит следующим образом:
Теперь release_handle не обязательно знать специфику объекта, чтобы удалить его, он просто будет использовать функцию, которую мы сохранили в хэндле, так что давайте вернёмся к тому, что происходит, когда последняя ссылка будет освобождена.
После этого мы хотели бы освободить память, которая содержит reference_count. Но не тут то было: сделать это будет ужасной ошибкой. Если другие хэндлы на этот же объект до сих пор хранятся в других потоках, после освобождения памяти они будут ссылаться на reference_count, который уже удален. А на следующей попытке получить доступ к объекту, мы будем иметь попытку обращения к освобожденной памяти.
Есть ли решение, которое позволило бы не допустить утечки памяти но при этом избежать обращений к освобождённой памяти счетчиков ссылок? Такой способ есть. Это пул объектов, который будут управлять освобожденными счетчиками. И вот тут-то и появляется проблема, найти которую может быть довольно непросто, и которая известна как «проблема ABA». Представьте себе, ситуацию, когда у вас есть хэндл на объект. Один из потоков удаляет объект. Затем конструируется другой объект, и для него создается управляющих хэндл. Что при этом произойдет?
Когда объект уничтожен, reference_count связанный с данным объектом (назовем его object1) освобождается обратно в пул объектов со значением 0. Пока что всё идёт по плану. Но когда выделяется другой хэндл для нового объекта (назовем его object2), то reference_count, который будет связан с этим объектом берется из пула объектов, при этом данный reference_count устанавливается в 1. Теперь представьте, что поток хранящий хэндл на object1 пытается получить указатель. Это удастся, потому что reference_count на который указывает данный хэндл уже не 0, хотя он и принадлежит теперь object2. Функция блокировки вернет неверный указатель, программа (если повезёт) потерпит крушение, или (если не повезёт) повредит содержимое памяти обращением к освобожденному участку.
Решение, разумеется, существует, иначе я бы не писал всё это.
Мы хотим сделать структуру handle_s настолько легкой, насколько это возможно, чтобы иметь возможность передать ее по значению, а не по указателю, так что сделаем следующее: создадим две структуры, одна из которых будет «слабым» хэндлом, то есть не ограничивать и не проверять объект, которым она управляет, а другой будет «сильным» хэндлом, т.е. таким, который будет иметь жесткую связь с конкретным управляемым объектом и вернет NULL, если «слабый» хэндл связанный с ним больше не ссылается на тот же объект.
Давайте определим их так:
Итак, как вы видите, теперь обе ручки имеют поле «version», и strong_handle_s уже не имеет указателя на объект, так как он теперь хранится в общем для объекта weak_handle_s.
Давайте посмотрим, как он защищает нас от проблемы ABA, показанной выше.
В strong_handle_s и weak_handle_s, которые ссылаются на один и тот же объект имеются поля «version», которые равны друг другу.
Всякий раз, когда ручка освобождается и weak_handle_s помещается обратно в пул объектов, мы будем увеличивать номер версии в weak_handle_s.
В следующий раз, если освобожденный в пул weak_handle_s переиспользуется для обработки другого объекта, он будет иметь номер версии, отличный от номера версии который был у объекта, который был освобожден. Теперь в функции lock_handle, сравнивая поля версии в обоих, «слабом» и «сильном» хэндле, мы можем сказать, ссылается ли weak_handle_s по-прежнему на тот объект, указатель которого мы пытаемся получить из strong_handle_s, и вернуть NULL, если это не так.
Так что, как мы видим, хэндлы приносят некоторые довольно сложные проблемы, но он также имеет и свои плюсы: хэндл может быть сохранен и забыт, пока нам не понадобится управляемый им объект; хэндл не является встроенным, что означает, что мы можем использовать его практически с любыми типами данных.
Кроме того, передача хэндла не обязательно означает передачу права собственности на объект, так что он может быть использован вместо указателя в качестве безопасной ссылки на объекты везде, где они не являются собственностью ссылающегося объекта (в структурах данных и т.д.).
Таким образом хэндлами сложнее управлять. Но и возможности их гораздо мощнее.
- Нуждается в дополнительной структуре и памяти.
- Занимает дополнительное время процессора
- Добавляет некоторые неочевидные проблемы
- Позволяет сохранять хэндлы и иметь к ним доступ по мере необходимости.
- Не требует того, чтобы объект постоянно находился в собственности.
Выводы
Встроенный счетчик ссылок реализует общие сильные ссылки (strong reference), которые должны удерживать счетчик ссылок, пока объект не будет гарантированно невостребован. Пока все ссылки не будут разлочены, объект не будет удален. Если ссылка разблокирована в потоке, который имел в собственности лишь одну ссылку, этот поток уже больше не сможет гарантированно безопасно получить еще одну ссылку.
Хэндл реализует общую слабую ссылку (weak reference). Если объект жив, функция lock_handle вернет указатель на запрашиваемый объект, и объект гарантированно не будет удален, пока хэндл не будет разлочен. Это безопасно для блокировки и разблокировки сколько угодно раз, так как хэндл является отдельным объектом, и гарантированно является валидным участком памяти. Соответствующие меры должны быть приняты, чтобы гарантировать, что разделяемая память счетчика ссылок не освобождается, когда объект достигает счетчика ссылок 0, и что повторно используемый счетчик ссылок не используется, чтобы проверить количество ссылок на уже освобожденные объекты.