Готовим окружение для программирования на WinAPI

7 ноября 2013

Что-то я решил молодость вспомнить. Перед тем, как меня понесло в сторону Perl, юниксов и всякой функциональщины, я долгое время игрался с сями, ассемблером и WinAPI. Сейчас все это начинает потихоньку забываться, а жаль, потому что, как мы уже выясняли, знать всякие низкоуровневые вещи полезно.

Предвижу возмущение некоторых читателей сего блога, дескать, что за дела, я не хочу читать про WinAPI, я хочу ФП и юниксы! Если вы оказались одним из них, пожалуйста, пройдите по приведенной выше ссылке и внимательно ознакомьтесь с тамошней заметкой, она вам все объяснит. Хорошая новость для новичков — при помощи серии постов о WinAPI, которую я планирую написать, вы можете начать изучение программирования практически с нуля. Впрочем, прочитать сначала книжку Кернигана и Ритчи, а также иметь представление о том, как язык Си хранит в памяти целые числа и строки, не повредит.

Для написания кода я буду использовать Windows XP, запущенный под Ubuntu Linux в VirtualBox, и среду разработки Visual Studio 2005. Если вы тоже не сидите под Windows, советую выбрать такую же конфигурацию, поскольку более поздние версии Visual Studio и Windows требуют крайне много ресурсов. Обязательно установите гостевые дополнения, это позволит использовать в Windows такое же разрешение экрана, что и в хост-системе. Если даже WinXP для вас тяжеловата, подойдут Windows 2000 и Visual C++ 6.0. Несмотря на то, что я буду использовать не самые свежие Windows и Visual Studio, по идее все должно прекрасно работать и в более поздних их версиях. Писать будем на Си, возможно, лишь изредка прибегая к некоторым фишкам C++.

Дополнение: Надо отдать должное Microsoft. Самые актуальные на середину 2017-го года Windows 10 и Visual Studio Community довольно сносно работают в условиях ограниченных ресурсов под VirtualBox. Более того, Visual Studio Community доступна для скачивания совершенно бесплатно.

Установить Visual Studio несложно. Будем считать, что с этим вы справились. Давайте создадим новый проект. Запустите Visual Studio и выберите в меню File → New → Project. В появившимся диалоге выберите Visual C++ → Win32 → Win32 Project. В поле Name введите какое-нибудь название проекта, например, First. Жмите OK, Next, Finish. В результате будет создан каркас проекта. Обратите внимание на выпадающий список с конфигурациями проекта по центру экрана. Выберите в нем конфигурацию Release:

Выбираем Release-конфигурацию проекта в Visual Studio

Для компиляции программы нажмите F7. Для ее запуска нажмите Ctr+F5. Если вы используете VirtualBox, обратите внимание, что правая клавиша Ctr используется в хоткеях, управляющих виртуальной машиной. Например, сочетание Ctr+F служит для переключения между оконным и полноэкранным режимами. Поэтому, если вы решили использовать VirtualBox, используйте в хоткеях Visual Studio клавишу Ctr, находящуюся слева.

Если теперь перейти в каталог с проектом и найти собранную программу (она находится в подкаталоге с именем release), можно обнаружить, что ее размер составляет 64 Кб. По нынешним меркам это не так уж много, но все-таки что-то многовато для программы, которая почти ничего не делает. Мы же собираемся писать на Си и WinAPI, а одним из преимуществ этого подхода является возможность писать очень компактные программки.

Давайте разберемся, как это делается:

  • Закройте текущий проект: File → Close Solution;
  • Создайте новый проект с именем Project, в визарде поставьте галочку Additional Options → Empty Project;
  • Выберите конфигурацию Release;
  • Создайте новый файл исходного кода, сказав Project → Add New Item… → Visual C++ → Code → C++ File (.cpp), с именем Main;
  • Откройте свойства проекта: Project → Project Properies…;
  • Отключите проверку безопасности буфера: C/C++ → Code Generation → Buffer Security Check → No;
  • Сообщите, что мы пишем код на Си: С/C++ → Advanced → Compile As → Compile As C Code;
  • Игнорируем библиотеки по умолчанию: Linker → Input → Ignore All Default Libraries → Yes;
  • В качестве точки входа указываем процедуру main: Linker → Advanced → Entry Point → main;
  • Отключаем отладочную информацию: Linker → Debugger → Generate Debug Info → No;
  • Отключаем генерацию манифеста: Liner → Manifest File → Generate Manifest → No;
  • Жмем Apply, затем OK;

Теперь, если вы напишите в Main.cpp код:

void main() {

}

… и скомпилируете его, то получите абсолютно ничего не делающий исполняемый файл Project.exe, размером ровно 1 Кб. Программа получилась такой маленькой, потому что мы выкинули из нее систему времени исполнения языка C++, а также отладочную информацию и прочий «мусор». Фактически, это минимальный размер исполняемого файла в Windows, поскольку он должен содержать некоторую служебную информацию (DOS-заглушку, PE-заголовок, таблицу секций) и иметь некоторое выравнивание. Можно ужать его еще чуть-чуть, но этим мы рискуем сломать совместимость с некоторыми версиями Windows.

Чтобы при создании каждого нового проекта не повторять описанные выше шаги, закройте проект и сохраните его в архиве с названием типа project_template.zip. Теперь попробуем на основе этого проекта создать программу, которая что-то делает. Разархивируйте проект, переименуйте его каталог из Project в MsgBox, откройте проект в Visual Studio. Введите следующий код:

#include <windows.h>

void main() {
  MessageBox(0, L"Hello!", L"MsgBox", 0);
  ExitProcess(0);
}

Заметьте, что VisualStudio может дополнять имена процедур, если при их вводе нажать Ctr+пробел. Для сохранения кода используйте сочетание Ctr+S. При вводе аргументов процедур отображается подсказка.

Если теперь вы запустите программу, то увидите примерно следующее:

Пример использования MessageBox

Размер exe-шника при этом составил 2 Кб. Можно уменьшить его до 1 Кб, добавив в самом начале кода строку:

#pragma comment(linker, "/MERGE:.rdata=.text")

Этим мы говорим компилятору объединить несколько секций исполняемого файла в одну. Но мне кажется, что это уже перебор, файл и так получился крохотный. Есть и другие приемы, но нам вполне хватит описанных выше.

Приведенный выше код довольно прост. Заголовочный файл windows.h содержит объявление процедур, структур и констант, необходимых для работы с WinAPI. Процедура ExitProcess завершает текущий процесс с кодом возврата, переданным в качестве аргумента. Процедура MessageBox отображает окно с сообщением. Первый и последний ее аргументы сейчас не важны. Второй по счету аргумент представляет собой строку с сообщением, а третий — заголовок окна.

Префикс L перед открывающей кавычкой означает, что строка хранится в unicode. В действительности, есть две версии процедуры MessageBox — MessageBoxA, работающая с ascii-строками, и MessageBoxW, работающая со строками в unicode. Аналогичная ситуация характерна и для большинства других процедур WinAPI. По умолчанию в свойствах проекта сказано использовать unicode-версии процедур, поэтому во время сборки проекта компилятор подменяет вызов MessageBox на MessageBoxW. На низком уровне ascii-процедуры представляют собой всего лишь обертки над unicode-версиями (для преобразования строк из unicode в ascii и обратно в Windows используются процедуры MultiByteToWideChar и WideCharToMultiByte). Работая с процедурами, имена которых заканчиваются на «W», мы получаем более быстрые программы.

В заключение хочется отметить, что приведенный код успешно компилируется MinGW и запускается под Wine:

$ i586-mingw32msvc-gcc -s -DUNICODE msgbox.c -o msgbox.exe
$ wine msgbox.exe

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

Собственно, это все, о чем я хотел сегодня рассказать. В качестве домашнего задания можете почитать документацию к MessageBox на MSDN. Попробуйте отобразить окна с различными иконками и кнопками, а также по коду возврата определить, какая из кнопок была нажата.

Дополнение: Пишем простое консольное приложение на чистом WinAPI

Метки: , .


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