Первые программы на Windows API: как попросить систему о ресурсе и как запустить другую программу
DWORD, BOOL, HANDLE, LPSTRGetLastErrorg++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe; заголовок <windows.h> и нужные библиотеки MinGW-w64 подключает сам. Правила курса (решение Д1): функции Windows API — в ANSI-версиях с буквой A на конце, строки — массивы char (раздел 10).На компьютере одновременно работают десятки программ, а процессор, память, диск и сеть — одни на всех. Кто решает, какая программа сейчас выполняется, где лежат её данные в памяти, можно ли ей открыть этот файл? Операционная система. Её главная работа — распределять ресурсы между программами и не давать одной программе испортить другую или саму систему.
| Ресурс | Что делает ОС | Раздел курса |
|---|---|---|
| процессорное время | делит его между потоками, переключая их много раз в секунду | 14, 16 |
| оперативная память | выделяет каждому процессу своё адресное пространство, защищает чужое | 18 |
| файлы и устройства | проверяет права, упорядочивает доступ | 11, 17 |
| каналы связи, сеть | передаёт данные между процессами и компьютерами | 17, 19 |
Защита держится на двух режимах работы процессора. В режиме ядра выполняется код самой ОС и драйверов: ему доступны вся память и всё оборудование. В режиме пользователя выполняются обычные программы — и ваши тоже: они не могут напрямую обратиться к диску или к памяти другой программы. Когда программе нужен ресурс, она просит систему: вызывает функцию, которая переключает процессор в режим ядра, выполняет запрос с проверками и возвращает результат. Набор таких функций для программ Windows — Windows API.
Вы уже пользовались им не зная того: std::cout в итоге вызывает функцию Windows WriteFile, а std::ifstream — CreateFile и ReadFile. Стандартная библиотека C++ — удобная обёртка; в этом блоке мы спускаемся на уровень ниже и обращаемся к системе напрямую.
// Первая программа на Windows API: сведения о своём процессе и о компьютере
#include <windows.h>
#include <iostream>
int main()
{
std::cout << "PID: " << GetCurrentProcessId() << '\n';
std::cout << "Command line: " << GetCommandLineA() << '\n';
char name[MAX_COMPUTERNAME_LENGTH + 1];
DWORD size = sizeof(name); // на входе — размер буфера, на выходе — длина имени
if (GetComputerNameA(name, &size)) {
std::cout << "Computer: " << name << " (" << size << " chars)\n";
}
char dir[MAX_PATH];
const UINT length = GetWindowsDirectoryA(dir, MAX_PATH);
if (length > 0 && length < MAX_PATH) { // 0 — ошибка, >= MAX_PATH — буфер мал
std::cout << "Windows directory: " << dir << '\n';
}
const DWORD priority = GetPriorityClass(GetCurrentProcess());
std::cout << "Priority class: " << (priority == NORMAL_PRIORITY_CLASS ? "normal" : "not normal") << '\n';
std::cout << "Uptime: " << GetTickCount64() / 1000 / 60 << " min\n";
return 0;
}
> info
PID: 7412
Command line: info
Computer: DESKTOP-K3L7 (12 chars)
Windows directory: C:\Windows
Priority class: normal
Uptime: 312 min
Числа и имена у вас будут свои. Типы Windows API — псевдонимы обычных типов C++, объявленные в <windows.h>. Их имена пишут заглавными буквами:
| Тип Windows | Что это в C++ | Где встречается |
|---|---|---|
BOOL | int; TRUE = 1, FALSE = 0 | результат большинства функций: успех или нет |
DWORD | беззнаковое целое 32 бита | идентификаторы, размеры, коды ошибок и завершения |
UINT, ULONGLONG | unsigned int, беззнаковое 64 бита | длины, время в миллисекундах |
HANDLE | void* — адрес «чего угодно» | дескриптор объекта ядра (пункт 03) |
LPSTR, LPCSTR | char*, const char* | строки: изменяемые и только для чтения |
MAX_PATH | константа 260 | вместимость буфера для пути к файлу |
copyString в разделе 10: функция, которая возвращает строку, получает ваш массив и его вместимость. GetComputerNameA даже меняет size через указатель — на выходе там длина имени.GetComputerNameA) — версия для строк char (ANSI). У каждой такой функции есть версия W для «широких» строк wchar_t (Unicode) и имя без буквы, которое превращается в одну из них в зависимости от настроек сборки. В курсе всегда пишем букву A явно.Процессы, потоки, открытые файлы, каналы, события — всё это объекты ядра: структуры данных, которые хранит и которыми управляет ОС в своей памяти. Программа не может до них дотянуться — её память отделена. Вместо адреса объекта она получает дескриптор (HANDLE) — номер в таблице объектов своего процесса. По этому номеру она потом просит систему что-то сделать с объектом: прочитать, подождать, завершить.
| Действие | Функция | Результат |
|---|---|---|
| создать или открыть объект | CreateFileA, CreateProcessA, OpenProcess, CreateToolhelp32Snapshot… | дескриптор |
| работать с ним | ReadFile, WaitForSingleObject, TerminateProcess… | дескриптор — первый параметр |
| закрыть дескриптор | CloseHandle(h) | запись в таблице процесса освобождается |
CloseHandle, когда он больше не нужен. Незакрытые дескрипторы — утечка: таблица процесса растёт, объекты ядра не освобождаются. Для программы, которая работает минуту, это незаметно; для сервера, который работает месяц, — отказ.CloseHandle процесса не завершает процесс — он лишь говорит: «мне этот процесс больше не интересен».GetCurrentProcess() возвращает псевдодескриптор — особое значение «этот процесс». Закрывать его не нужно.GetLastErrorФункция Windows API сообщает о неудаче своим результатом, а причину записывает в «код последней ошибки» потока. Его получают GetLastError() — сразу после неудачного вызова: следующая функция Windows может его перезаписать.
| Функция возвращает | Признак неудачи | Примеры |
|---|---|---|
BOOL | FALSE (0) | CreateProcessA, CloseHandle, GetExitCodeProcess |
HANDLE от функций создания файлов и снимков | INVALID_HANDLE_VALUE (−1) | CreateFileA, CreateToolhelp32Snapshot |
HANDLE от остальных | NULL (0) | OpenProcess, CreateThread, CreateEventA |
| длину строки | 0 (а длина ≥ размера буфера — буфер мал) | GetWindowsDirectoryA |
Разное значение ошибки у HANDLE — историческая особенность; какое именно, написано в документации каждой функции (Microsoft Learn, раздел «Return value»). Проверка не того значения — частая ошибка: if (file == NULL) после CreateFileA не ловит ничего.
// Ошибки Windows API: о неудаче функция сообщает результатом, о причине — через GetLastError
#include <windows.h>
#include <iostream>
void printError(const char what[]);
int main(int argc, char* argv[])
{
SetConsoleOutputCP(1251); // текст системы — в кодировке 1251 (раздел 2)
const char* name = argc > 1 ? argv[1] : "no-such-file.txt";
HANDLE file = CreateFileA(name, GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) { // у CreateFile ошибка — не NULL, а INVALID_HANDLE_VALUE
printError("CreateFile");
return 2;
}
LARGE_INTEGER size;
if (GetFileSizeEx(file, &size)) {
std::cout << name << ": " << size.QuadPart << " bytes\n";
} else {
printError("GetFileSizeEx");
}
CloseHandle(file); // дескриптор получен — должен быть закрыт
return 0;
}
// Что не удалось, код ошибки и её текст от системы; вызывается сразу после неудачной функции
void printError(const char what[])
{
const DWORD code = GetLastError();
char text[256];
const DWORD length = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, code, 0, text, sizeof(text), nullptr);
std::cout << what << " failed, error " << code;
if (length > 0) {
std::cout << ": " << text; // текст системы уже кончается переводом строки
} else {
std::cout << '\n';
}
}
> lasterror
CreateFile failed, error 2: Не удается найти указанный файл.
> lasterror info.cpp
info.cpp: 1034 bytes
FormatMessageA превращает код в текст на языке системы. Функцию printError вы будете копировать во все программы блока В. Самые частые коды: 2 — файл не найден, 3 — путь не найден, 5 — доступ запрещён, 6 — неверный дескриптор, 87 — неверный параметр, 183 — объект уже существует.
Процесс — запущенная программа вместе со всем, что ей выдала система. Файл notepad.exe на диске — программа; три открытых окна Блокнота — три процесса одной программы.
| Что есть у процесса | Пояснение |
|---|---|
| адресное пространство | своя виртуальная память: код, данные, стек, кучи. Адрес 0x5ffe9c из раздела 9 в двух процессах — две разные ячейки. Один процесс не может прочитать память другого (подробно — раздел 18) |
| идентификатор PID | число, уникальное среди живых процессов системы; после завершения может достаться новому процессу |
| хотя бы один поток | поток выполняет код; процесс без потоков завершается. Первый поток выполняет main (раздел 16) |
| таблица дескрипторов | объекты ядра, которые процесс открыл |
| приоритет, права, текущий каталог, переменные окружения, командная строка | получены от родителя или заданы при создании |
| код завершения | STILL_ACTIVE (259), пока процесс работает; затем — значение return из main |
Состояния. Процесс создаётся, работает (его потоки выполняются или ждут своей очереди на процессор, файла, клавиши), завершается. После завершения объект процесса живёт, пока кто-то держит его дескриптор: так родитель успевает узнать код завершения.
CreateProcessAУ функции десять параметров. В курсе почти всегда нужны три: командная строка, структура STARTUPINFOA и структура PROCESS_INFORMATION, куда система запишет результат (раздел 11, пункт 06: функция получает адрес структуры и заполняет её поля).
| Параметр | В курсе | Смысл |
|---|---|---|
lpApplicationName | nullptr | имя программы берётся из командной строки |
lpCommandLine | свой массив char | «программа аргументы»; функция может временно его менять, поэтому литерал не подходит |
lpProcessAttributes, lpThreadAttributes | nullptr | права доступа по умолчанию |
bInheritHandles | FALSE | наследует ли дочерний процесс дескрипторы (раздел 17 — TRUE) |
dwCreationFlags | 0 | флаги: CREATE_NEW_CONSOLE — своя консоль, CREATE_SUSPENDED — создать приостановленным, класс приоритета |
lpEnvironment, lpCurrentDirectory | nullptr | окружение и текущий каталог — как у родителя |
lpStartupInfo | &si | окно и стандартные потоки дочернего процесса; обязательно si.cb = sizeof(si) |
lpProcessInformation | &pi | на выходе: hProcess, hThread — дескрипторы; dwProcessId, dwThreadId — идентификаторы |
PATH; расширение .exe можно не писать.argv по пробелам и кавычкам (раздел 10, пункт 09). Аргумент с пробелом берут в кавычки.CreateProcessA возвращает управление сразу, как только процесс создан, — не дожидаясь, пока он поработает. Родитель и потомок дальше работают одновременно.Для опытов пригодится маленькая «подопытная» программа child: ждёт заданное время и завершается с заданным кодом.
// Дочерняя программа для опытов: ждёт заданное число миллисекунд и завершается с заданным кодом
#include <windows.h>
#include <iostream>
#include <cstdlib>
int main(int argc, char* argv[])
{
const int code = argc > 1 ? std::atoi(argv[1]) : 0; // atoi — без проверки: для опытов достаточно
const int ms = argc > 2 ? std::atoi(argv[2]) : 0;
std::cout << "child " << GetCurrentProcessId() << ": sleep " << ms << " ms, exit " << code << '\n';
Sleep(ms);
return code;
}
WaitForSingleObject(h, миллисекунды) приостанавливает поток, пока объект не перейдёт в «сигнальное» состояние. Для процесса это значит — «завершился». Время ожидания INFINITE — ждать сколько угодно. Пока поток ждёт, он не расходует процессор: ОС просто не выделяет ему времени.
// Фрагмент run.cpp: запуск, ожидание, код завершения. Полностью — в примерах раздела
STARTUPINFOA si = {}; // все поля — нули
si.cb = sizeof(si); // размер структуры — обязательное поле
PROCESS_INFORMATION pi = {};
const ULONGLONG start = GetTickCount64();
if (!CreateProcessA(nullptr, cmd, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
printError("CreateProcess");
return 2;
}
std::cout << "Started: " << cmd << ", PID " << pi.dwProcessId << '\n';
CloseHandle(pi.hThread); // дескриптор главного потока не нужен
WaitForSingleObject(pi.hProcess, INFINITE); // ждать, пока процесс не завершится
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
std::cout << "Finished in " << GetTickCount64() - start << " ms, exit code " << exitCode << '\n';
CloseHandle(pi.hProcess);
> run child 3 1500
Started: child 3 1500, PID 9120
child 9120: sleep 1500 ms, exit 3
Finished in 1516 ms, exit code 3
> run nosuch
CreateProcess failed, error 2: Не удается найти указанный файл.
cmd программа склеивает из своих argv[1], argv[2]… в свой массив — функцией buildCommandLine с проверкой вместимости (примеры раздела, файл run.cpp).CREATE_NEW_CONSOLE потомок получает консоль родителя.GetExitCodeProcess до завершения вернёт STILL_ACTIVE (259) — поэтому код читают после ожидания. Если программа сама вернёт 259, их не различить — такой код не используют.GetTickCount64(): миллисекунды с запуска системы. Разность двух замеров — длительность.| Способ | Что происходит |
|---|---|
return из main | нормальное завершение: деструкторы объектов, закрытие потоков std::ofstream (данные дописываются на диск), код завершения — возвращённое значение |
ExitProcess(code) | немедленное завершение своего процесса; деструкторы локальных объектов не вызываются |
TerminateProcess(h, code) | принудительное завершение чужого процесса: он не узнаёт об этом и ничего не успевает сохранить. Крайняя мера — как «Снять задачу» в Диспетчере задач |
Если ждать не бесконечно, а с тайм-аутом, WaitForSingleObject возвращает, почему ожидание кончилось:
// Фрагмент timeout.cpp: ожидание не дольше seconds секунд. Полностью — в примерах раздела
DWORD exitCode = 0;
const DWORD result = WaitForSingleObject(pi.hProcess, static_cast<DWORD>(seconds) * 1000);
switch (result) {
case WAIT_OBJECT_0: // завершился сам
GetExitCodeProcess(pi.hProcess, &exitCode);
std::cout << "Finished, exit code " << exitCode << '\n';
break;
case WAIT_TIMEOUT: // время вышло — завершить принудительно
if (TerminateProcess(pi.hProcess, TIMEOUT_EXIT_CODE)) {
WaitForSingleObject(pi.hProcess, INFINITE); // TerminateProcess только начинает завершение
std::cout << "Timeout: terminated after " << seconds << " s\n";
exitCode = TIMEOUT_EXIT_CODE;
} else {
printError("TerminateProcess");
}
break;
default:
printError("WaitForSingleObject");
break;
}
CloseHandle(pi.hProcess);
> timeout 2 "child 0 5000"
child 5208: sleep 5000 ms, exit 0
Timeout: terminated after 2 s
> timeout 5 "child 7 1000"
child 6044: sleep 1000 ms, exit 7
Finished, exit code 7
Результат WaitForSingleObject | Значит |
|---|---|
WAIT_OBJECT_0 | объект в сигнальном состоянии: процесс завершился |
WAIT_TIMEOUT | время вышло, процесс ещё работает |
WAIT_FAILED | ошибка ожидания (например, неверный дескриптор) — причина в GetLastError |
Чужой процесс по PID открывают OpenProcess(PROCESS_TERMINATE, FALSE, pid) — получают дескриптор с правом завершения (или NULL и ошибку 5, если прав нет: процессы других пользователей и системы так не завершить).
Как Диспетчер задач узнаёт, какие процессы есть? Один из способов — снимок: CreateToolhelp32Snapshot копирует список процессов на данный момент, а Process32First и Process32Next выдают его записи по одной в структуру PROCESSENTRY32 (заголовок <tlhelp32.h>):
// Фрагмент ps.cpp: процессы, в имени которых есть строка из аргумента. Полностью — в примерах раздела
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
printError("CreateToolhelp32Snapshot");
return 2;
}
PROCESSENTRY32 entry = {};
entry.dwSize = sizeof(entry);
int count = 0;
std::cout << " PID Threads Name\n";
if (Process32First(snapshot, &entry)) {
do {
if (containsIgnoreCase(entry.szExeFile, filter)) {
std::cout << std::setw(8) << entry.th32ProcessID << std::setw(9) << entry.cntThreads
<< " " << entry.szExeFile << '\n';
++count;
}
} while (Process32Next(snapshot, &entry));
}
CloseHandle(snapshot);
std::cout << count << " processes\n";
> ps note
PID Threads Name
10344 7 notepad.exe
1 processes
do while (раздел 6): первая запись берётся Process32First, остальные — Process32Next, пока она не вернёт FALSE.dwSize заполняется до вызова — как si.cb: по размеру система понимает, какую версию структуры ей передали. С нулевым размером Process32First вернёт FALSE с ошибкой 24 (неверная длина).containsIgnoreCase — поиск подстроки без учёта регистра латинских букв, своими циклами, как в разделе 10.| Ошибка | Признак | Сообщение g++ / ошибка Windows | Как избежать |
|---|---|---|---|
литерал в командной строке CreateProcessA | собирается с предупреждением; при запуске функция может записать в литерал — аварийное завершение | ISO C++ forbids converting a string constant to 'LPSTR' {aka 'char*'} [-Wwrite-strings] | командная строка — свой массив char |
| результат функции не проверен | программа «работает», но ничего не делает | — | каждый вызов — с проверкой и printError |
GetLastError не сразу после ошибки | код 0 или чужая ошибка | — | вызывать первой строкой обработки ошибки |
== NULL вместо == INVALID_HANDLE_VALUE (или наоборот) | ошибка не замечена, дальше — работа с неверным дескриптором | ошибка 6 у следующей функции | значение ошибки — из документации функции |
не заполнены si.cb или entry.dwSize | функция отказывает или ведёт себя непредсказуемо | у Process32First — ошибка 24 | структура — = {}, затем размер |
| дескрипторы не закрыты | растёт счётчик дескрипторов в Диспетчере задач | — | CloseHandle для hProcess, hThread, снимков, файлов |
| код завершения прочитан до завершения | код 259 | — | сначала WaitForSingleObject |
после TerminateProcess сразу читают код или удаляют файлы процесса | процесс ещё не успел завершиться | — | ожидание после TerminateProcess |
| программа не найдена | CreateProcess отказывает | ошибка 2 | путь к программе или запуск из её каталога |
| русский текст ошибки — «кракозябры» | нечитаемый вывод | — | SetConsoleOutputCP(1251) (раздел 2) |
A, строки — массивы char с размером буфераHANDLE; каждый полученный дескриптор закрывается CloseHandleFALSE, NULL или INVALID_HANDLE_VALUE), причина — GetLastError сразу после вызоваCreateProcessA: изменяемая командная строка, si.cb, два дескриптора на выходеWaitForSingleObject — ожидание с тайм-аутом; код завершения — после ожиданияTerminateProcess — крайняя мера; список процессов — снимок Toolhelpdo while, код завершения; раздел 9 — указатели, nullptr, параметр-указатель; раздел 10 — строки char, буфер и размер, argc/argv; раздел 11 — структуры, которые заполняет функция; раздел 13 — ТЗ на утилиту. Нужен для: раздела 15 — дескриптор консоли; раздела 16 — потоки, WaitForSingleObject для потока; раздела 17 — дочерний процесс с перенаправленным вводом-выводом; раздела 18 — адресное пространство процесса.