title | lang |
---|---|
СПО. ЛР № 1. C++ и Windows API |
ru |
- Владеть C++ на минимально необходимом уровне для выполнения ЛР.
- Уметь находить и читать официальную документацию Windows API.
- Уметь применять применять отдельные функции и группы связанных функций Windows API для решения задач, связанных с назначением этих функций.
Написать программу, использующую Windows API, которая без запросов к пользователю выводит в терминал сведения о системе, а именно:
- Скорость вычисления квадратного корня при помощи механизма QPC.
- Номер версии Windows.
- Системный каталог, название машины и псевдоним текущего пользователя.
- Список логических томов с их названиями, первой точкой монтирования размером и количеством свободного места.
По результатам работы должен быть составлен отчет (один на бригаду) в электронном виде, к которому должен быть приложен полный код. Все члены бригады должны быть способны пояснить любую часть отчета и кода.
Защита ЛР проводится индивидуально и состоит в демонстрации отчета и кода, ответе на возможные вопросы и замечания к ним, а также в ответе на один контрольный вопрос.
Операционная система — единственная программа с прямым доступом к аппаратным ресурсам компьютера. Пользовательские программы могут получить к ним доступ только через посредничество ОС. С этой целью ОС предоставляет им набор функций, называемый интерфейсом программирования приложений (application programming interface, API). Большая часть функций API совершает системные вызовы, то есть передает управление ядру ОС для совершения операций с аппаратными ресурсами или с самой ОС.
Примеры функций API ОС: запись в файл, выделение области памяти, запуск процесса. Каждая ОС, например, Windows, GNU/Linux или OS X, имеет собственный API; у ОС семейства *nix они в большой мере совместимы.
Работа с API ОС нужна как в драйверах, так и в прикладных программах.
Драйверы — это библиотеки, обеспечивающие взаимодействие ОС с конкретным оборудованием. Они действуют как часть ОС и получают прямой доступ к аппаратным ресурсам. Их задача — принять от ОС стандартную команду, выполнить её специфичным для устройства образом и выдать ОС результат в стандартном виде.
Все прикладные программы, на самом деле, обязательно обращаются к API ОС,
для взаимодействия со своим окружением: для ввода-вывода, для отображения
интерфейса, даже для корректного завершения. Как правило, вызовы API ОС
скрыты от прикладного программиста библиотеками. Так, для работы с файлами
обычно применяют стандартные функции: OpenFile()
в Delphi, fopen()
или std::fstream
в C++, open()
в Python — но все они в конечном итоге
обращаются к функции CreateFile()
из API ОС (в Windows).
Однако в некоторых случаях и в прикладных программах нужно обращаться к API ОС напрямую для реализации сложной или нестандартной функциональности. Примеры: получение подробной информации о системе и объектах в ней, особое отображение GUI (в Windows), эффективное управление ресурсами (памятью, процессорным временем, коммуникациями). Для многих таких задач есть готовые библиотеки, но это лишь значит, что с API ОС приходится работать их авторам.
Microsoft предоставляет официальный и полный справочник по Windows API в составе библиотеки MSDN на английском языке. Перевод на русский известен неполнотой и ошибками, его использования следует избегать. Библиотека MSDN содержит не только описание Windows API, но и примеры использования, полезные в учебе и на практике.
Описания функций в MSDN даны на языке C, причем в специфичном виде:
используются переименованные типы данных, например, LPSTR
вместо char*
.
Поэтому удобнее всего работать с Windows API из C/C++ или Delphi,
где есть указатели и контроль над размещением данных в памяти.
Для знающих Delpi. Урок «Reading C code in Windows API» (англ.) дает сжатое объяснение, как читать их и переводить на Delphi. Подробнее использование Windows API в Delphi освещает статья «Основы работы с Win API в VCL-приложениях».
Работать с Windows API, например, из Python, неудобно, хотя и возможно через
модуль ctypes
. Также есть пакеты, упрощающие работу
с Windows API (например, win32api
), но они неполны.
На C++ можно писать в разных средах, официальной среды, как в Delphi, нет. В лаборатории — CodeBlocks, потому что она работает на Windows XP и выше, Linux и Mac, легковесная, бесплатная и простая. На своих ноутбуках можно пользоваться любой средой и языком.
Установите CodeBlocks по инструкции.
Если возникнут проблемы, как временное решение можно установить CodeBlocks
со встроенным компилятором (вариант codeblocks-17.12mingw-setup.exe
со страницы загрузок.
CodeBlocks позволяет писать код в отдельном файле (File → New → C++ File) или в проекте (File → New… → Project… → Console application). Для задания нужно использовать проект, для экспериментов можно пользоваться отдельными файлами.
Нужен самый примитивный уровень, фактически, C.
Достаточно изучить:
- базовый синтаксис (структуру программы, объявление переменных, вызов функций) — см. материалы РПОСУ;
- ввод данных через
cin
, вывод черезprintf()
— см. РПОСУ;printf()
работает как в MATLAB, но только для скаляров; - битовые операции, указатели, структуры — объясняется в ходе этой ЛР.
Полезные источники, в них же есть ссылки на литературу:
-
Материалы РПОСУ для I курса:
- первая лекция — структура программы, основы синтаксиса;
- раздаточные материалы к ней же, в частности, там есть таблица соответствия конструкций Pascal/Delphi аналогам в C++;
- лекции про низкоуровневые конструкции (1, 2), которые как раз применяются в ЛР по СПО.
-
Конспект лекций по C и C++ доцента кафедры Прикладной математики МЭИ(ТУ) Натальи Владимировны Чибизовой.
-
Повторение C++ для студентов IV курса, также акцентированное на низкоуровневых конструкциях.
* N. B.: если не работает сайт кафедры, нужно заменить в адресе uii.mpei.ru
на uii.bitbucket.io
):
Часто бывает нужно знать, сколько по времени работает участок программы. Для этого нужно замерять текущее время до и после выполнения участка кода и вычислять разницу. Если участок кода мал и выполняется быстро, замер должен быть очень точным.
Windows API предоставляет функции для точных замеров времени:
-
QueryPerformanceCounter()
выдает текущее значение счетчика, который непрерывно и равномерно растет с момента включения компьютера. -
QueryPerformanceFrequency()
выдает частоту этого счетчика, то есть на сколько единиц он возрастает в секунду.
Время выполнения участка кода тогда оценивается по формуле:
интервал [тактов] = счетчик_после [тактов] - счетчик_до [тактов]
время [мкс] = 10⁶ [мкс/с] × интервал [тактов] / частота [тактов/с]
Рассмотрим функцию для замера текущего значения счетчика:
BOOL WINAPI QueryPerformanceCounter(
_Out_ LARGE_INTEGER *lpPerformanceCount
);
Тип возвращаемого значения — BOOL
, то есть логический признак: TRUE
(успех)
или FALSE
(неудача). Встроенный логический тип C++ bool
не используется
в Windows API, потому что в C его не было.
Макрос WINAPI
не важен для прикладного программирования. Он определяет
низкоуровневый способ вызова данной функции, об этом позаботится компилятор.
QueryPerformanceCounter
— имя функции. За ним в скобках следует список
формальных параметров, в данном случае параметр один:
lpPerformanceCount
— его имя;LARGE_INTEGER*
— его тип (звездочка после важна), см. ниже; из названия уже понятно, что он так или иначе представляет большое (large) целое число (integer);- макрос
_Out_
не несет смысла с точки зрения C++, но сообщает читателю кода, что параметр является выходным.
Рассмотрим определение типа LARGE_INTEGER
:
typedef struct _LARGE_INTEGER {
LONGLONG QuadPart;
} LARGE_INTEGER;
Это структура, аналог записи (record
) в Delphi
или объекта с атрибутами-данными в Python.
Его единственное поле называется QuadPart
и имеет тип LONGLONG
(целое число
самого большого размера, доступного на данной аппаратной платформе).
Как и в Delphi или Python, к полю структуры можно обращаться через точку:
LARGE_INTEGER t0;
t0.QuadPart = 100500;
Тип LONGLONG
печатается спецификатором формата %lld
:
printf("t0 = %lld\n", to.QuadPart);
Заметим, что форма typedef struct _X { ... } X;
является устаревшей
(унаследованной из C), а в C++ та же структура объявляется проще:
struct LARGE_INTEGER {
LONGLONG QuadPart;
};
Чем важна звездочка в *lpPerformanceCounter
? Она означает, что тип параметра
не структура LARGE_INTEGER
, а указатель на такую структуру. Указателей нет
в Python, однако они играют большую роль в системных и переносимых API.
Всю память компьютера можно представить как массив байтов. Тогда индекс в этом массиве, то есть номер ячейки памяти, называется адресом, а переменная, содержащая адрес, называется указателем.
При объявлении указателей перед переменной ставится звездочка (хотя часто ее «прижимают» к имени типа). Например, так объявляется указатель на действительное число:
double* r1;
В указатель записывается не значение переменной, а ее адрес. Адрес берется
оператором взятия адреса в виде амперсанда (&
):
double x = 3.14;
double* p = &x;
Вот как расположены при этом данные в памяти:
адреса: 0 1 8 9 10 11 12 42 43 44 45 46
+---+- -+---+---+---+---+- -+---+---+---+---+-
ячейки: | | ..... | 3.14 | ..... | 8 | ...
+---+- -+---+---+---+---+- -+---+-.'+---+---+-
↑\_____________/ \__.'_________/
| x .' p
| .'
&x = 8 = ...............'
Значение указателя (адрес) можно напечатать спецификатором формата %p
:
printf("p = %p\n", p);
Видно, что это целое число, традиционно представляемое шестнадцатеричным.
Операции над указателем — операции с адресом, а не с тем, что находится в памяти по адресу, хранящемуся в указателе:
p = 2.71;
— ошибка, указатель не может сохранить вещественное число.
Чтобы, имея указатель, обратиться к тем данным, адрес которых он хранит, используется оператор разыменования в виде звездочки:
*p = 2.71;
— то же самое, что x = 2.71;
printf("x = %f\n", x);
— выведет 2.71
Важнейшее применение указателей в API — выходные параметры: функция принимает
указатель на результат и при помощи разыменования записывает туда значение.
Вот пример функции такого рода для решения квадратного уравнения.
Она принимает три коэффициента, возвращает false
, если действительных корней
нет, а если есть, записывает их по адресам в r1
и r2
и возвращает true
:
bool
solve(double a, double b, double c, double* r1, double* r2) {
double d = b*b - 4*a*c;
if (d < 0) {
return false;
}
*r1 = (-b + sqrt(d)) / (2*a);
*r2 = (-b - sqrt(d)) / (2*a);
return true;
}
При вызове этой функции в нее передаются адреса переменных-результатов, при помощи которых функция изменит значения этих переменных:
double x1, x2;
solve(1, 3, 2, &x1, &x2);
printf("x1 = %f, x2 = %f\n", x1, x2);
Разыменование позволяет не только изменять, но и считывать значения:
double x = 3.14;
double* p = &x;
printf("*p = %f\n", *p);
Поэтому еще по указателю часто передают крупные данные, чтобы не копировать их в параметр функции.
Есть специальное значение указателя — нулевой, NULL
, 0
или nullptr
.
Указатель, хранящий такой адрес, запрещено разыменовывать.
Важно, что при передаче указателя в функцию нужно передавать адрес каких-либо данных или переменной, а не просто какой-то указатель. Типичная ошибка:
LARGE_INTEGER* t0;
QueryPerformanceCoutner(t0);
Переменная t0
- указатель, но какой адрес в нем хранится? Случайный,
так как t0
ничего не присвоено (в Delphi он нулевой, но это не поможет).
Когда функция QueryPerformanceCounter()
попытается записать результат
по случайному адресу, она либо испортит какие-то данные, либо, что более
вероятно, случайный адрес попадет в область памяти, куда писать запрещено,
и программа аварийно завершится.
Итак, для замера точного времени нужно завести переменную типа LARGE_INTEGER
,
передать ее адрес в функцию, тогда после выполнения функции ее поле QuadPart
будет заполнено текущим значением счетчика:
LARGE_INTEGER t0;
QueryPerformanceCounter(&t0);
Затем следует произвести вычисления, длительность которых нужно замерить. Для примера измерим время вычисления квадратного корня из текущего времени:
double result = sqrt(t0.QuadPart);
Сразу после вычислений нужно снова замерить текущее время в переменную t1
.
Сделайте это самостоятельно по аналогии с t0
.
Вид функции замера частоты счетчика временных интервалов схож в только что рассмотренным и вызывается так же:
BOOL WINAPI QueryPerformanceFrequency(
_Out_ LARGE_INTEGER *lpFrequency
);
Самостоятельно получите с ее помощью частоту в переменную frequency
.
Сначала можно вычислить длительность интервала в тактах:
double ticks = t1.QuadPart - t0.QuadPart;
Для наглядности можно занести частоту и число микросекунд в секунде в отдельные переменные:
double ticks_per_sec = frequency.QuadPart;
double usec_per_sec = 1e6;
Затем можно вычислить время в микросекундах и напечатать результат:
double usec = usec_per_sec * ticks / ticks_per_sec;
printf("result=%g, duration=%f.3 usec\n", result, usec);
Заметим, что важно напечатать результат вычислений, иначе компилятор может заметить, что он в программе не используется, и не вычислять его вовсе.
Внимание. На практике измерение точного времени и производительности имеет много нюансов, которые здесь опущены. Подробнее в MSDN.
Функция GetVersion()
позволяет получить номер версии ОС.
В документации она описана так:
DWORD WINAPI GetVersion(void);
DWORD
— тип возвращаемого значения.WINAPI
— не важно на данном этапе (определяет низкоуровневый способ вызова данной функции, об этом позаботится компилятор).GetVersion
— имя функции.(void)
— список параметров.
Имя типа DWORD
расшифровывается как double word, двойное слово. Исторически
машинным словом называют целое число без знака размером 16 бит (два байта).
Соответственно, двойное слово занимает четыре байта, но это тоже целое число.
Список параметров (void)
означает, что параметров у функции нет.
Он мог бы быть записан иначе как просто ()
. В C++ void
— специальный
тип, у которого нет значений. Например, если нужно объявить функцию f
,
которая ничего не возвращает (процедуру в терминах Delphi), это делается так:
void f();
По аналогии: тип возвращаемого значения — void
, имя f
, параметров нет.
Итого, чтобы получить номер версии Windows, нужно вызвать GetVersion()
без параметров и получить целое число, которое означает номер версии.
Как его интерпретировать? Например, как представить целым числом версию «XP SP3»? Обратимся к разделу Return value.
If the function succeeds, the return value includes the major and minor version numbers of the operating system in the low-order word, and information about the operating system platform in the high-order word.
Перевод:
В случае успешного выполнения возвращаемое значение содержит мажорный и минорный номер версии ОС в младшем слове и информацию о платформе в старшем слова.
Версия состоит из двух чисел, например, версия 5.1 — мажорный номер 5, минорный номер 1 (это Windows XP). Соответствие таких технических номеров версий и коммерческих названий приведено в таблице.
Старшее и младшее слово — это первые 16 бит возвращаемого значения
и последние 16 бит его же. Чтобы извлечь их из значения типа DWORD
нужны
битовые операции. В C++ они такие же, как в Python (вернее, наоборот).
Имеется 32-битное число вида pppppppp pppppppp vvvvvvvv vvvvvvvv
,
где буквами обозначены биты, относящиеся к платформе (p
) и к версии (v
).
-
Чтобы получить младшее слово (правые 16 бит), нужно обратить старшие 16 бит в нули, а младшие 16 бит не изменять.
Вспомним побитовое «И»:
0 & 0 == 0 0 & 1 == 0 1 & 0 == 0 1 & 1 == 1
Если второй операнд 0, результат результат тоже 0 («обращение в 0»).
Если второй операнд 1, результат равен первому операнду («не изменяется»).Можно составить 32-битное значение, у которого биты, подлежищие в исходном числе обнулению, равны нулю, а биты, которые нужно сохранить из исходного числа, равны 1; затем сделать побитовое «И» исходного числа с этим значением. Оно называется маской, а операция — наложением маски, или маски´рованием.
DWORD mask = 0b00000000'00000000'11111111'11111111; DWORD version = info & mask;
Двоичные числа записываются в C++ так же, как в Python, с префиксом
0b
. Для читаемости можно ставить апостроф ('
) между группами разядов. Двочиные числа слишком длинные для чтения, поэтому обычно маски пишут в шестнадцатеричном виде:DWORD mask = 0x0000ffff;
-
Чтобы получить старшее слово, пригодится операция сдвига вправо на 16 бит:
DWORD platform = info >> 16;
Продолжим чтение документации:
For all platforms, the low-order word contains the version number of the operating system. The low-order byte of this word specifies the major version number, in hexadecimal notation. The high-order byte specifies the minor version (revision) number, in hexadecimal notation. The high-order bit is zero, the next 7 bits represent the build number, and the low-order byte is 5.
Перевод:
Для всех платформ (то есть для любых значений старшего слова) младшее слово содержит номер версии ОС: младший байт — мажорный номер, старший байт — минорный. Когда самый старший бит [всего числа] равен 0, следующие 7 бит [старшего байта старшего слова] содержат номер сборки, а младший байт [старшего слова] равен 5.
-
Нужно разделить число
version
видаMMMMMMMM mmmmmmmm
на два байта, которые его составляют.Получите младший и старший байты
version_major
иversion_minor
изversion
можно по аналогии с получением младшего и старшего слова. -
Как проверить, что самый старший бит равен 0? Можно замаски´ровать все биты, кроме него. Тогда, если он нулевой, результат маскирования тоже окажется нулевым. Эти типовой способ проверить, установлен ли бит:
if ((info & 0b10000000'00000000'0000000'00000000) == 0) { ... }
Перепишите эту проверку, записав маску шестнадцатеричной.
-
Если старший бит нулевой, можно получить число, которое составляют семь следующих бит, как просто старший байт, то есть на самый старший бит не обращать особого внимания:
DWORD build = platform >> 8;
Пример в документации вместо сдвигов и маскирования использует макросы
LOBYTE
, LOWORD
, HIBYTE
и HIWORD
. Внутри они делают то же самое.
В последующих ЛР можно пользоваться ими, в данной ЛР стоит цель научиться
использовать битовые операции.
Напечатайте результаты в формате Windows v5.1 (build 1234)
.
Даже если не допущено ошибок, программа не соберется: компилятору неизвестен
тип DWORD
и функции Windows API. Нужно подключить модуль для работы с ними
в самом начале файла:
#include <windows.h>
После этого программа должна собираться и запускаться.
Рассмотрим функцию для получения системного каталога (обычно C:\Windows
)
как самую простую из необходимых:
UINT WINAPI GetSystemDirectory(
_Out_ LPTSTR lpBuffer,
_In_ UINT uSize
);
Тип возвращаемого значения UINT
(unsigned integer — целое без знака).
-
Параметр
lpBuffer
имеет типLPTSTR
(см. ниже). Макрос_Out_
перед ним не несет смысла с точки зрения C++, но подсказывает читателям кода, что параметр является выходным. -
Параметр
uSize
имеет типUINT
, но является входным, судя по_In_
.
Называние типа LPTSTR
состоит из трех частей: LP
, T
и STR
.
STR
означает, что это один из видов строк. Строки в C не являются встроенным
типом, как в Delphi или Python, а представляют собой просто массивы символов.
Буква T
(и вообще часть между LP
и STR
в других случаях) определяет,
чем является символ в строке. В данном случае T
означает, что функция
может работать с кодировкой ANSI или Unicode.
LP
означает long pointer, указатель (long — дальний — не несет в настоящее
время смысла).
Как известно, символы в компьютере представлены числовыми кодами, соответствие между кодами и символами определяется набором символов (character set). Наиболее распространены наборы ANSI и Unicode. Символов ANSI всего 256, поэтому они всегда кодируются как один байт на символ. Символов Unicode более 137 тыс.; и коды не помещаются в байт, и есть разные способы поместить их в памяти: по 4 байта на символ (UCS-4), по 1 или 2 слова на символ (UCS-2), от 1 до 6 байтов на символ (UTF-8). Способ записать код символа называется кодировкой (encoding).
Подробнее: хрестоматийный «Абсолютный минимум, который каждый разработчик ПО обязательно должен знать о Unicode и наборах символов» Джоэля Спольски.
На практике следует пользоваться Unicode, так как она универсальна,
но работать с ANSI проще, поэтому будем пользоваться ею.
Это значит, что строки будем представлять как массивы элементов типа char
(в данном случае — из 256 элементов):
char system_dir[256];
Все функции Windows API, работающие со строками, представлены в двух вариантах:
для ANSI (суффикс A
) и для Unicode UCS-2 (суффикс W
). То есть на самом
деле нет функции GetSystemDirectory()
, а есть GetSystemDirectoryA()
и GetSystemDirectoryW()
. Об этом написано в разделе Requirements
документации, строка таблицы Unicode and ANSI names. Но в программе
используется несуществующая GetSystemDirectory()
. При компиляции ее вызов
автоматически заменяется на ANSI или Unicode вариант в зависимости от опций
компилятора.
Как уже было сказано, строки C представляются как массивы символов.
Указатель на строку, таким образом, — это указатель на массив, то есть указатель на его начальный элемент (с индексом 0):
char buffer[16];
— массив символов;
char* fancy = &buffer[0];
— указатель на массив содержит адрес элемента 0;
char* simple = buffer ;
— то же самое, записанное проще.
Строки C являются не просто массивами символов, а массивами символов,
завершающимися нулем (null-terminated character string). Это означает,
что в после всех символов строки в таком массиве находится символ '\0'
.
Он находится именно в элементе после текста, а не в последней ячейке буфера.
По символу '\0'
определяется длина строки.
В отличие от Delphi и Python, над строками C не определено ни операции
сцепления (+
), ни специальных методов. Для работы со строками C есть
два варианта:
-
Использовать модуль
<cstring>
. Например, функцияstrlen()
позволяет вычислить длину строки (до'\0'
), а функцияstrcat()
сцепляет строки. -
Подключить библиотеку C++
string
, предоставляющую классstd::string
с удобными операторами и методами. Из объекта типаstd::string
можно получить строку C методом.c_str()
.
Второй способ проще и безопаснее, но требует преобразования между строками C
и std::string
.
Разберем еще раз смысл параметров и возвращаемого значения GetSystemInfo()
.
_Out_ LPTSTR lpBuffer
— указатель на массив символов, элементы которого будут заполнены путем к системному каталогу._In_ UINT uSize
— наибольшее количество символов, которое функция может записать в массив, начиная с символа, адрес которого вlpBuffer
(обычно — размер буфера).
Возвращается количество символов, записанных в буфер, UINT
.
Итого вызов функции делается так:
char system_dir[MAX_PATH];
GetSystemDirectory(system_dir, MAX_PATH);
printf("System directory: %s", system_dir);
MAX_PATH
— константа, равная 256, означающая наибольшую возможную длину пути.
Особого внимания заслуживает то, что в качестве параметра-указателя всегда должен передаваться адрес переменной или данных, а не просто указатель. Распространенная ошибка:
LPTSTR buffer;
GetSystemDirectory(buffer, 256);
Переменная buffer
- указатель, но какой адрес в нем хранится? Случайный,
так как buffer
ничего не присвоено (в Delphi он нулевой, но это не поможет).
Когда функция GetSystemDirectory()
попытается записать результат
по случайному адресу, она либо испортит какие-то данные, либо, что более
вероятно, случайный адрес попадет в область памяти, куда писать запрещено,
и программа аварийно завершится.
Самостоятельно используйте функции GetUserNameA()
и GetComputerNameA()
, чтобы получить имя текущего
пользователя и имя машины.
Их вызов похож на вызов GetSystemDirectory()
с одним отличием: вместо
наибольшей допустимой длины результата передается указатель на переменную,
которая эту длину содержит, а после вызова функции в эту переменную будет
Перечисление логических томов (в просторечии — дисков) относится как раз к задачам, которые требуют применения Windows API в прикладных программах. Алгоритм решения:
-
Начать перечисление томов функцией
FindFirstVolume()
. -
Для каждого тома:
- Напечатать техническое имя тома
(вида
\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
). - Функцией
GetVolumePathNamesForVolumeName()
из технического имени тома получить путь к его каталогу (видаC:\
). - Функцией
FindNextVolume()
перейти к следующему тому.
- Напечатать техническое имя тома
(вида
-
Закончить перебор функцией
FindVolumeClose()
.
Функция FindFirstVolume()
в документации приведена
только в Unicode-варианте (суффикс W
и строка типа LP
W
STR
),
но на самом деле существует и для ANSI:
HANDLE FindFirstVolumeW(
LPWSTR lpszVolumeName,
DWORD cchBufferLength
);
Тип ее возвращаемого значения — HANDLE
. Это так называемый дескриптор.
Никаких операций над ним, кроме передачи в функции Windows API, делать нельзя
(можно напечатать как указатель, сравнить с другим дескриптором или с нулем).
Он используется, чтобы идентифицировать процесс перебора томов в программе.
При запуске перебора дескриптор создается функцией FindFirstVolume()
, затем
функция FindNextVolume()
использует его для того, чтобы определить, на каком
томе находится перебор. Наконец, функция FindVolumeClose()
принимает тот же
дескриптор, чтобы освободить ресурсы ОС, связанные с данным перебором томов.
Пара из lpszVolumeName
и cchBufferLength
— уже изученный способ возврата
строки из функции: lpszvolumeName
— непосредственно выходная строка,
cchBufferLength
— размер буфера, куда функция может эту строку записать.
Строка представляет собой техническое имя тома.
Функция FindNextVolume()
принимает первым параметром
дескриптор, а прочие параметры у нее совпадают с FindFirstVolume()
.
Как определить, что перебор томов окончен? Обратимся к документации:
If the function succeeds, the return value is nonzero.
If the function fails, the return value is zero. To get extended error information, call
GetLastError()
. If no matching files can be found, theGetLastError()
function returns theERROR_NO_MORE_FILES
error code. In that case, close the search with theFindVolumeClose()
function.
Перевод:
В случае успеха функция возвращает не-нуль [
TRUE
].В случае неудачи [найти следующий том] возвращается нуль. Для получения расширенной информации об ошибке, нужно вызвать функцию
GetLastError()
. Если дело в том, что больше томов нет,GetLastError()
вернет константуERROR_NO_MORE_FILES
. В этом случае перебор следует завершить функциейFindVolumeClose()
.
Таким образом, основной цикл алгоритма реализуется так:
char buffer[MAX_PATH];
HANDLE search = FindFirstVolume(buffer, sizeof(buffer));
do {
// операции с томом, техническое имя которого в buffer
}
while (FindNextVolume(search, buffer, sizeof(buffer)));
if (GetLastError() != ERROR_NO_MORE_FILES) {
// произошла ошибка, а не штатное оокнчание перебора
}
FindVolumeClose(search);
-
Реализуйте перебор томов и печать их технических имен без дополнительной информации.
-
Изучите подробнее дескрипторы и обработку ошибок Windows API. Обработайте возможные ошибки всех используемых функций (как минимум, печатая код ошибки.
Оператор sizeof
позволяет получить размер переменной
или типа данных:
int x;
printf("sizeof(int) = %zu, sizeof(x) = %zu\n", sizeof(int), sizeof(x));
В частности, его удобно применять для передачи в функцию размера массива,
как показано в примере выше. Но требуется осторожность: если применить
sizeof
к указателю, будет получен именно размер указателя (4 байта
или 8 байтов), а не размер данных, адрес которых в нем хранится.
Устройство внутренних структур ОС весьма сложно. Прикладному программисту же подробности не нужны и не должны быть доступны, а требуется простой способ указать ОС на конкретный ее внутренний объект. С этой целью широко применяются дескрипторы объектов (object handles), называемые также описателями (перевод «descriptor»), жарг. «ручки» (дословный перевод «handle»).
Аналогом дескрипторов можно считать тип File
/TextFile
в Delphi и Pascal.
С точки зрения программирования это простые переменные, которые можно передать
в функции WIndows API или сравнить их значения. Есть специальное значение
INVALID_HANDLE_VALUE
— некорректный описатель, оно часто используется
для индикации ошибок.
Как правило, по возвращаемому функцией Windows API значению можно определить, завершился ли вызов успешно. Об этом сообщается в разделе «Return value» в описании каждой функции.
Установить причину ошибки позволяет
функция GetLastError()
.
Она возвращает один из кодов ошибки.
Почти каждый вызов функций Windows API может изменить последнюю ошибку,
поэтому GetLastError()
нужно вызывать сразу же, как только ошибка обнаружена,
до какого-либо взаимодействия с системой, включая ввод-вывод.
Правильно:
BOOL result = SomeApiCall();
if (!result) {
int code = GetLastError();
printf("SomeApiCall() failed with error %d\n", code);
}
Неверно:
BOOL result = SomeApiCall();
if (!result) {
puts("SomeApiCall() failed!"):
printf("Error code: %d\n", GetLastError());
}
Поскольку puts()
выводит данные на экран, она может сделать вызов функции API
для этого, код ошибки которого (в том числе код 0 при успехе) заменит код
ошибки вызова SomeApiCall()
.
Разумный первый шаг в решении проблем с Windows API — распечатать
и проверить возвращаемое значение функции и результат GetLastError()
.
Диски в «Проводнике» Windows — на самом деле не сами логические тома,
а их точки монтирования. То есть существует том (например, раздел жесткого
диска) \\?\Volumes\{...}
, и его содержимое монтируется в C:
, то есть
делается доступным по пути C:\...
. Возможно смонтировать один и тот же том
в разные точки (например, в D:
и в C:\Backups
) одновременно. Поэтому
GetVolumePathName
s
ForVolumeName()
выдает для тома не один путь, а список путей (ходя обычно в нем один элемент).
BOOL GetVolumePathNamesForVolumeNameW(
LPCWSTR lpszVolumeName,
LPWCH lpszVolumePathNames,
DWORD cchBufferLength,
PDWORD lpcchReturnLength
);
Параметр lpszVolumeName
— техническое имя тома, в lpszVolumePathNames
функция сохраняет список точек монтирования, cchBufferLength
ограничивает
количество сохраняемых в буфер данных, а lpcchReturnLength
принимает
количество сохраненных символов.
Примечательно описание lpszVolumeNames
:
A pointer to a buffer that receives the list of drive letters and mounted folder paths. The list is an array of null-terminated strings terminated by an additional NULL character. <…>
С одной стороны, это строка C с завершающим нулем. С другой стороны,
она имеет необычную структуру — это не одна, а несколько завершающихся '\0'
строк подряд, а на конце — два '\0'
(иначе говоря, пустая строка).
Вот пример содержимого:
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| D | : | \0 | C | : | \\ | B | a | c | k | u | p | \0 | \0 |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
\_______/ \__________________________________________/
первый путь второй путь
D: C:\Backups
Таким образом иногда передаются списки строк в Windows API.
После вызова функции тривиально распечатать первый из путей: указатель на эту строку совпадает с указателем на начало всего буфера:
char buffer[MAX_PATH * 4];
GetVolumePathNamesForVolumeName(…, buffer, …);
char* path = buffer;
printf("Mount point: %s\n", path);
Этого достаточно для продолжения выполнения задания.
Материал повышенной сложности, необязательно к выполнению.
Как обратиться к остальным элементам списка в buffer
, если они есть?
Нужно передвинуть указатель path
за завершающий '\0'
той строки, адрес
которой он содержит. Для этого нужна адресная арифметика. Ее правила:
-
Если к указателю на тип
T
прибавить целое числоN
, получится указатель, отстоящий в памяти наN
эначений типаT
, то есть наN*T
байт. -
Если вычесть указатели на значения типа
T
, получится количество элементов типаT
между адресами в этих указателях. -
Вычитать можно только указатели на части одного и того же массива; адресная арифметика не работает для
void*
.
Для типа char*
, размер которого 1, решение простое: нужно к указателю
на начало строки прибавить длину этой строки (получится указатель на '\0'
),
затем прибавить еще 1:
path += strlen(path) + 1;
После этого path
указывает на очередную строку списка строк в buffer
.
Сдвиг нужно повторять до тех пор, пока длина строки path
не будет нулевой,
то есть пока path[0]
не равен '\0'
.
Как для проверки добавить для диска C точку монтирования C:\lab01-test
(от имени администратора):
cd c:\
mkdir lab01-test
diskpart
select volume C
assign mount=C:\lab01-test
exit
Можно убедиться, что в C:\lab01-test
отображается содержимое диска C:
,
включая каталог lab01-test
, в который можно (почти) бесконечно переходить.
Как убрать точку монтирования:
mountvol C:\lab01-test /D
Функция GetDiskFreeSpaceEx()
принимает путь
к любому каталогу на томе и выдает три значения типа ULARGE_INTEGER
—
структуры, похожей на LARGE_INTEGER
, но с полем-целым без знака.
Самостоятельно напишите ее вызов, проверьте ошибки и выведите результат.
-
Для чего нужен и что представляет собой программирования приложений (API) операционной системы?
-
В каких случаях прикладные программы обращаются к API ОС? Приведите примеры, когда такие обращения явны и неявны (скрыты от прикладного программиста).
-
Где доступна официальная справка по Windows API и какие типовые сведения доступны в ней для каждой функции?
-
Почему часть функций Windows API существует в вариантах с суффиксом
A
иW
? -
Как диагностировать ошибки, возникающие при вызовах функций Windows API?