Системное программирование,
первое занятие
Начал вести курс «Системное программирование» в нашей академии для старшеклассников. Почитал в интернетах, под этим понимают разное, поэтому наполнение такого курса может быть разным.
Как я понял, под «системой» в данном случае подразумевается операционная система. В нашем курсе это «Windows 10». Под «программированием» имеется в виду написание программ, взаимодействующих с железом и операционной системой на «низком уровне». Для первого занятия в наших методических материалах имеются только заголовки тем. Оказалось, что в нашем случае программировать предлагается на языке C#, а под «низким уровнем» имеется в виду вызов функций Windows API из кода на языке C#.
У Windows API есть разные версии. Самая популярная на сегодня — Win32. Функции Windows API реализованы в динамических библиотеках (DLL). Для вызова определенной функции нужно знать название файла динамической библиотеки, в которой находится реализация этой функции. На первом занятии мы использовали функции из библиотеки user32.dll.
Некоторые понятия
На первом занятии я запланировал (ориентируясь на заголовки в наших методических материалах) затронуть следующие понятия:
- унаследованный (legacy) код в системе Windows 10;
- управляемый (managed) и неуправляемый (unmanaged) код в C#;
- нативный (native) код в системе Windows 10;
- маршалинг.
Как я понял, автор наших методических материалов подразумевает под унаследованным кодом в системе «Windows 10» набор функций Windows API. В общем под «унаследованным (legacy) кодом» обычно понимают устаревший по разным причинам, но до сих пор работающий и приносящий большую пользу код. В целом, на мой взгляд, набор функций Win32 подходит под такое определение.
Почему набор функций Win32 можно посчитать устаревшим? Например, он не объектно-ориентированный. Во-вторых, в этих функциях используется множество типов со странными названиями, например: HWND, LPCSTR, LPCTSTR и тому подобных. Разобраться, конечно, можно, но поначалу использовать эти типы неудобно.
Как я понял, одной из причин создания платформы .NET и стала необходимость как-то рефакторить набор функций Windows API. По этому поводу в одной из статей на «Хабре» пишут, что платформа .NET — это высокоуровневая обертка для функций Windows API. Полезные статьи по теме:
- «WinAPI из C#» от 2021 года;
- «Интероперабельность с нативным кодом через платформу .NET» от 2024 года.
Механизм Platform Invoke
Механизм «Platform Invoke» еще часто называют «P/Invoke» или «PInvoke». Это способ вызывать функции Windows API из кода на языке C#.
Для иллюстрации работы этого механизма на занятии мы написали две маленькие консольные программы на языке C#, использующие этот механизм. В этих программах требуется подключение пространства имен System.Runtime.InteropServices. Писали двумя способами: 1) для компиляции из командной строки разработчика с помощью компилятора csc.exe, который подразумевает платформу «.NET Framework»; 2) из среды Visual Studio Community 2022 с использованием шаблона «Консольное приложение (Майкрософт)» (по-английски «Console App»), который подразумевает платформу «.NET».
Код первой консольной программы на языке C#, которая просто открывает окошко с сообщением:
using System;
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("user32.dll")]
public static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type
);
}
public class HelloWorld
{
public static void Main()
{
const uint MB_OKCANCEL = 0x00000001; // две кнопки: ОК и Отмена
const uint MB_ICONINFORMATION = 0x00000040; // синий круг с воскл.знаком
const int IDOK = 1;
const int IDCANCEL = 2;
int res = Win32.MessageBox(
IntPtr.Zero,
"Привет мир!",
"Пример использования механизма Platform Invoke",
MB_OKCANCEL | MB_ICONINFORMATION
);
if (res == IDOK)
Console.WriteLine("Вы нажали OK");
else if (res == IDCANCEL)
Console.WriteLine("Вы нажали Отмена или закрыли окно");
}
}
Подключение функций из набора Windows API не обязательно выполнять в отдельном классе Win32 (название этого класса тоже можно придумать другое), но мне так показалось удобнее, потому что при вызове подключенной функции нужно добавлять приставку Win32., что дает понять читателю кода, что это функция из набора Windows API.
Использование служебного слова extern в определении метода подразумевает, что код реализации данного метода размещен не здесь, а где-то вовне, в данном случае — в библиотеке user32.dll.
Синтаксическая конструкция с использованием квадратных скобок перед определением метода — это атрибут. Атрибуты бывают разных классов, в данном случае класс этого атрибута — DllImportAttribute. По этой ссылке в статье документации описаны возможные поля атрибута данного класса, например: EntryPoint, CharSet, SetLastError и другие. С помощью этих полей можно настраивать подключение функции из набора Windows API. Скажем, использование поля EntryPoint позволяет подключить функцию, сделав ее доступной в коде на языке C# с другим именем.
Самое сложное при подключении функции из набора Windows API — определение соответствия между типами в коде Windows API и типами в языке C#. Еще определение такого соответствия называют «маршалингом».
На занятии мы находили нужную функцию в документации Windows API и пытались самостоятельно определить соответствующие типы в языке C#. Например, в заголовке функции MessageBox в документации Windows API используются типы int, HWND, LPCTSTR и UINT. В программном коде выше им соответствуют типы int, IntPtr, string и uint.
В сложных случаях, когда непонятно, как найти соответствие, можно воспользоваться справочными сайтами в интернете, например, pinvoke.net. Однако, сейчас проще использовать ИИ поисковых систем (например, «Алиса» у Яндекса или «AI Mode» у Google): при правильном запросе вы легко найдете нужные соответствия и сразу получите код для вставки в программу на языке C#.
Код второй консольной программы на языке C#, которая находит открытое окно другой программы и закрывает его (в данном случае закрываем окно программы «Калькулятор», входящей в состав системы «Windows 10», если эта программа запущена):
using System;
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(
IntPtr hWnd, uint msg, int wParam, int lParam);
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string className, string windowName);
}
public class MyProgram
{
public static void Main()
{
IntPtr hWnd = Win32.FindWindow(null, "Калькулятор");
if (hWnd == IntPtr.Zero)
{
Console.WriteLine("Окно калькулятора не найдено!");
}
else
{
const uint WM_CLOSE = 0x0010; // приказ закрыть окно
Win32.SendMessage(hWnd, WM_CLOSE, 0, 0);
Console.WriteLine("Окно калькулятора найдено и закрыто!");
}
}
}
В этой программе используются две функции Windows API: FindWindow, чтобы найти окно программы «Калькулятор», и SendMessage, чтобы послать найденному окну сообщение с требованием закрыться. В системах Windows у каждого окна есть уникальный идентификатор, который еще называют «дескриптором окна», по-английски «window handle», в программах часто сокращают до hWnd.
В наборе функций Windows API у функций бывает несколько версий с похожими именами, различающимися только в последней букве имени. Обычно это буквы A и W (тут про это подробнее). Например, в наборе функций Win32 на самом деле нет функции с названием FindWindow, но есть две функции с названиями FindWindowA и FindWindowW. Однако, вышеприведенный код работает корректно, так как при попытке подключить функцию FindWindow нужный вариант выбирается автоматически (можно указать название конкретной функции в точности, если это необходимо). Как я понял, на автоматический выбор можно влиять с помощью указания определенных полей в атрибуте DllImport соответствующего метода.
При тестировании программы, закрывающей «Калькулятор», бывает, что нет открытых окон с программой «Калькулятор», а в диспетчере задач видно, что приложение «Калькулятор» запущено. В этом случае у меня программа таки находит окно с названием «Калькулятор» и рапортует о его закрытии, но на самом деле закрытия не происходит. Для решения этой проблемы просто закройте приложение «Калькулятор» из диспетчера задач. После этого наша программа будет работать так, как мы рассчитываем.
На следующих занятиях планирую разбирать понятия «процесса», «потока» и «многопоточности», а также заняться параллельным программированием.