.NET Runtime надає величезну бібліотеку класів (BCL), що покриває більшість повсякденних завдань: файли, мережа, потоки, колекції. Проте існують сценарії, коли managed API або не існує, або недостатньо для вирішення задачі. Саме тоді розробники звертаються до Platform Invocation Services (P/Invoke) — механізму виклику нативних функцій з C/C++ бібліотек безпосередньо з C# коду.
Розглянемо три типові сценарії, що вимагають P/Invoke:
Сценарій перший: Низькорівневий доступ до ОС. Потрібно отримати детальну інформацію про систему, яку BCL не надає. Наприклад, визначити точну архітектуру процесора (x86, x64, ARM64), отримати серійний номер BIOS, або дізнатися версію firmware материнської плати. Win32 API функція GetSystemInfo() з kernel32.dll надає структуру з 20+ полями, тоді як Environment.ProcessorCount дає лише кількість ядер.
Сценарій другий: Інтеграція зі сторонніми native бібліотеками. Компанія має legacy C++ бібліотеку для обробки зображень, що працює швидше за managed альтернативи завдяки SIMD-інструкціям та ручній оптимізації. Переписувати 100,000 рядків коду на C# — економічно недоцільно. P/Invoke дозволяє викликати функції з DLL безпосередньо, передаючи вказівники на пам'ять та отримуючи результати.
Сценарій третій: Автоматизація UI та системні хуки. Потрібно створити глобальний hotkey (Ctrl+Shift+F12), що працює навіть коли застосунок не у фокусі, або перехопити всі натискання клавіш у системі для keylogger-функціональності (легітимної, наприклад, для accessibility tools). RegisterHotKey() та SetWindowsHookEx() з user32.dll — єдиний спосіб реалізувати це на Windows.
.NET не ізолює вас від платформи — навпаки, надає контрольований міст до нативного коду. P/Invoke — це той міст. Але як і будь-який міст між двома світами (managed та unmanaged), він вимагає розуміння обох сторін: як працює CLR, як працює Win32, і як правильно перекладати дані між ними.
Windows API (також відомий як Win32 API) — це набір C-функцій та структур даних, що становлять фундаментальний інтерфейс операційної системи Windows. Історія його розвитку допомагає зрозуміти деякі дивні на перший погляд рішення.
Windows 1.0–3.11 (1985–1994): Win16 Era. Перші версії Windows працювали у 16-бітному режимі процесорів Intel 8086/80286. API складався з ~450 функцій, розподілених між трьома основними DLL: KERNEL (управління пам'яттю, процеси), USER (вікна, повідомлення), GDI (графіка). Функції мали обмеження: максимум 64 КБ на сегмент пам'яті, кооперативна багатозадачність (програма сама мала віддавати управління).
Windows 95/NT (1995): Win32 Revolution. Перехід на 32-бітну архітектуру (Intel 80386+) дав плоску модель пам'яті (4 ГБ адресного простору), preemptive multitasking, захищену пам'ять. API розширився до ~2000 функцій. З'явилися суфікси A (ANSI) та W (Wide/Unicode) для функцій, що працюють з рядками: CreateFileA() приймає char*, CreateFileW() — wchar_t*. Windows NT використовував Unicode всередині, тому CreateFileA() насправді викликає CreateFileW() після конвертації.
Windows XP–11 (2001–2024): Win32 Maturity. API стабілізувався на ~4000 функціях. Додалися нові DLL: advapi32.dll (безпека, реєстр), ws2_32.dll (сокети), ole32.dll (COM). З'явилася WinRT (Windows Runtime) для UWP застосунків, але класичний Win32 залишається основою для desktop програм.
Win32 API не є монолітною бібліотекою — це колекція DLL, кожна з яких відповідає за певну підсистему:
CreateProcess, TerminateProcess), потоками (CreateThread), пам'яттю (VirtualAlloc, HeapAlloc), файлами (CreateFile, ReadFile, WriteFile), синхронізацією (CreateMutex, WaitForSingleObject). Це найнижчий рівень, що доступний з user mode.CreateWindow), обробка повідомлень (GetMessage, DispatchMessage), діалоги (MessageBox), меню, іконки. Кожен GUI застосунок на Windows використовує user32.dll.LineTo, Rectangle, TextOut), шрифти, кольори, bitmap-и. До Windows Vista це був єдиний спосіб малювати на екрані. Зараз частково замінений на Direct2D/Direct3D, але залишається для сумісності.RegOpenKeyEx, RegQueryValueEx), безпека (OpenProcessToken, AdjustTokenPrivileges), служби (CreateService, StartService), Event Log. Багато функцій вимагають підвищених прав.SHFileOperation), ярлики, іконки файлів, діалоги вибору папок (SHBrowseForFolder). Інтеграція з Explorer.socket(), connect(), send(), recv(). .NET System.Net.Sockets використовує це під капотом.Центральна концепція Win32 API — Handle (дескриптор). Це непрозоре 32/64-бітне значення (тип HANDLE у C, IntPtr у C#), що ідентифікує системний ресурс: файл, процес, потік, mutex, вікно, bitmap, шрифт.
Handle — це не вказівник на об'єкт (хоча на ранніх версіях Windows це було саме так). Це індекс у таблиці дескрипторів процесу, що зберігається у kernel memory. Kernel зберігає реальний вказівник на _OBJECT структуру, а процесу повертає лише індекс. Це забезпечує безпеку: процес не може безпосередньо читати kernel memory, навіть якщо знає handle.
// Приклад: Handle на файл
IntPtr fileHandle = CreateFile(
"data.txt",
GENERIC_READ,
FILE_SHARE_READ,
IntPtr.Zero,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
IntPtr.Zero
);
if (fileHandle == INVALID_HANDLE_VALUE)
{
// Помилка відкриття файлу
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
// Використання handle...
// ОБОВ'ЯЗКОВО закрити handle після використання
CloseHandle(fileHandle);
Найпростіший спосіб зрозуміти P/Invoke — викликати функцію, що не вимагає складних параметрів. MessageBox() з user32.dll — ідеальний кандидат.
Сигнатура у C (з MSDN):
int MessageBoxW(
HWND hWnd, // Handle на батьківське вікно (NULL = немає)
LPCWSTR lpText, // Текст повідомлення (Unicode string)
LPCWSTR lpCaption, // Заголовок вікна
UINT uType // Кнопки та іконка (MB_OK, MB_YESNO, тощо)
);
Переклад на C#:
using System.Runtime.InteropServices;
class Program
{
// Атрибут [DllImport] вказує CLR, де знайти функцію
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(
IntPtr hWnd, // HWND → IntPtr (handle)
string lpText, // LPCWSTR → string (CLR автоматично конвертує)
string lpCaption, // LPCWSTR → string
uint uType // UINT → uint
);
// Константи для uType (з winuser.h)
const uint MB_OK = 0x00000000;
const uint MB_ICONINFORMATION = 0x00000040;
const uint MB_YESNO = 0x00000004;
const uint MB_ICONQUESTION = 0x00000020;
static void Main()
{
// Виклик виглядає як звичайний C# метод
int result = MessageBox(
IntPtr.Zero, // Немає батьківського вікна
"Це виклик нативної Win32 функції!", // Текст
"P/Invoke Demo", // Заголовок
MB_OK | MB_ICONINFORMATION // Кнопка OK + іконка інформації
);
Console.WriteLine($"Користувач натиснув кнопку з кодом: {result}");
}
}
[DllImport] — це спеціальний атрибут, що інструктує CLR виконати наступні дії під час виклику методу:
Крок 1: Завантаження DLL. При першому виклику MessageBox() CLR шукає user32.dll у стандартних шляхах Windows: C:\Windows\System32, C:\Windows\System, поточна директорія, PATH. Якщо DLL не знайдено — викидається DllNotFoundException.
Крок 2: Пошук функції. CLR шукає експортовану функцію з іменем MessageBox у таблиці експорту DLL. Якщо не знайдено — EntryPointNotFoundException. Важливо: ім'я має точно збігатися, включаючи регістр (хоча Windows API зазвичай case-insensitive).
Крок 3: Marshalling параметрів. CLR конвертує managed типи (C#) у unmanaged (C):
string → wchar_t* (UTF-16 Unicode) — CLR виділяє unmanaged пам'ять, копіює рядок, передає вказівникIntPtr → void* — передається "як є"uint → unsigned int — передається "як є"Крок 4: Виклик функції. CLR переключається з managed code у unmanaged, викликає функцію через function pointer, чекає повернення.
Крок 5: Marshalling результату. Повернене значення int копіюється назад у managed heap. CLR звільняє тимчасову unmanaged пам'ять, виділену для рядків.
Крок 6: Обробка помилок. Якщо функція повертає код помилки (зазвичай 0 або -1), CLR може автоматично викинути Win32Exception якщо вказано SetLastError = true.
A/W або для уникнення конфліктів імен.[DllImport("user32.dll", EntryPoint = "MessageBoxW")]
static extern int ShowMessage(IntPtr hWnd, string text, string caption, uint type);
CharSet.Ansi (ANSI/UTF-8, додає суфікс A), CharSet.Unicode (UTF-16, додає суфікс W), CharSet.Auto (Unicode на NT-based Windows, Ansi на Win9x — застаріло). Рекомендація: завжди CharSet.Unicode для сучасних Windows.true — CLR зберігає значення GetLastError() після виклику функції. Доступ через Marshal.GetLastWin32Error(). Обов'язково для функцій, що повідомляють про помилки через GetLastError() (більшість Win32 API).[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, ...);
IntPtr handle = CreateFile("test.txt", GENERIC_READ, ...);
if (handle == INVALID_HANDLE_VALUE)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error); // Автоматично форматує повідомлення помилки
}
CallingConvention.Winapi (за замовчуванням, StdCall на x86, Cdecl на x64), StdCall (параметри справа наліво, callee очищає стек), Cdecl (caller очищує стек). Для Win32 API завжди StdCall або Winapi.true — CLR не додає суфікси A/W автоматично. Корисно для функцій без Unicode/ANSI варіантів або коли ви явно вказали EntryPoint.true. Якщо false — CLR перетворює HRESULT (COM error code) у exception. Використовується для COM Interop, не для Win32 API.Windows внутрішньо використовує UTF-16 Unicode для всіх рядків. Функції з суфіксом A (ANSI) — це обгортки, що конвертують char* → wchar_t*, викликають W-версію, конвертують назад. Це додає overhead та може призвести до втрати даних (не всі Unicode символи можна представити в ANSI).
// ❌ ПОГАНО: використання ANSI
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
MessageBox(IntPtr.Zero, "Привіт 🚀", "Test", MB_OK);
// Результат: "Привіт ?" — emoji втрачено при конвертації в ANSI
// ✅ ДОБРЕ: використання Unicode
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
MessageBox(IntPtr.Zero, "Привіт 🚀", "Test", MB_OK);
// Результат: "Привіт 🚀" — все коректно
CharSet = CharSet.Unicode для Win32 API. Це уникає зайвих конвертацій, зберігає всі символи та відповідає внутрішній реалізації Windows.Marshalling — це процес конвертації даних між managed (CLR) та unmanaged (native) пам'яттю. Не всі типи вимагають конвертації. Типи, що мають однакове представлення в обох світах, називаються blittable (від "blit" — block transfer).
Blittable типи (можна копіювати побайтово без змін):
byte, sbyte, short, ushort, int, uint, long, ulong
float, double
IntPtr, UIntPtr
Ці типи передаються "як є" — CLR просто копіює байти. Швидко та безпечно.
Non-blittable типи (вимагають конвертації):
bool // C#: 1 байт, C: 4 байти (int) або 1 байт (BOOL)
char // C#: 2 байти UTF-16, C: 1 байт ANSI або 2 байти wchar_t
string // C#: managed object, C: char* або wchar_t*
arrays // C#: managed array, C: pointer
Для цих типів CLR виконує складну роботу: виділяє unmanaged пам'ять, копіює дані, конвертує формат, передає вказівник, після виклику звільняє пам'ять.
Рядки — найскладніший випадок marshalling. Існує кілька стратегій:
Стратегія 1: Автоматичний marshalling (рекомендовано для більшості випадків):
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
// CLR автоматично:
// 1. Виділяє unmanaged пам'ять
// 2. Копіює рядок у UTF-16
// 3. Передає wchar_t* у функцію
// 4. Після виклику звільняє пам'ять
Стратегія 2: StringBuilder для out-параметрів:
Коли функція записує дані у буфер, що ви надаєте:
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern uint GetCurrentDirectory(uint nBufferLength, StringBuilder lpBuffer);
var buffer = new StringBuilder(260); // MAX_PATH
uint length = GetCurrentDirectory((uint)buffer.Capacity, buffer);
if (length == 0)
throw new Win32Exception(Marshal.GetLastWin32Error());
string currentDir = buffer.ToString();
Console.WriteLine($"Поточна директорія: {currentDir}");
StringBuilder — це managed клас, але CLR має спеціальну логіку marshalling для нього: передає вказівник на внутрішній буфер, дозволяє native коду писати туди, після виклику синхронізує довжину рядка.Стратегія 3: Ручний marshalling через IntPtr:
Для максимального контролю або коли автоматичний marshalling не підходить:
string managedString = "Hello, Win32!";
// Виділяємо unmanaged пам'ять та копіюємо рядок
IntPtr unmanagedString = Marshal.StringToHGlobalUni(managedString);
try
{
// Передаємо IntPtr у функцію
SomeNativeFunction(unmanagedString);
}
finally
{
// ОБОВ'ЯЗКОВО звільняємо пам'ять
Marshal.FreeHGlobal(unmanagedString);
}
Marshal.StringToHGlobalUni(), НЕ управляється GC. Якщо забути FreeHGlobal() — це memory leak. Завжди використовуйте try/finally або using з custom wrapper.У C# структури за замовчуванням мають автоматичний layout — CLR може переставляти поля для оптимізації (вирівнювання, щільність). У C структури мають послідовний layout — поля розміщені у порядку оголошення з вирівнюванням за правилами платформи.
Коли ви передаєте C# структуру у Win32 API, CLR має знати, як розмістити поля у пам'яті, щоб native код правильно їх прочитав. Для цього використовується атрибут [StructLayout].
Win32 API функція GetSystemTime() заповнює структуру SYSTEMTIME поточним часом UTC:
Визначення у C (з winbase.h):
typedef struct _SYSTEMTIME {
WORD wYear; // 2 байти
WORD wMonth; // 2 байти
WORD wDayOfWeek; // 2 байти
WORD wDay; // 2 байти
WORD wHour; // 2 байти
WORD wMinute; // 2 байти
WORD wSecond; // 2 байти
WORD wMilliseconds; // 2 байти
} SYSTEMTIME; // Загалом: 16 байт
Переклад на C#:
using System.Runtime.InteropServices;
// StructLayout.Sequential — поля у порядку оголошення
// CharSet.Unicode — для сумісності з іншими структурами, що містять рядки
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct SYSTEMTIME
{
public ushort wYear; // WORD → ushort (2 байти)
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
class Program
{
[DllImport("kernel32.dll")]
static extern void GetSystemTime(out SYSTEMTIME lpSystemTime);
static void Main()
{
SYSTEMTIME st;
GetSystemTime(out st);
Console.WriteLine($"UTC Time: {st.wYear:D4}-{st.wMonth:D2}-{st.wDay:D2} " +
$"{st.wHour:D2}:{st.wMinute:D2}:{st.wSecond:D2}.{st.wMilliseconds:D3}");
Console.WriteLine($"Day of week: {(DayOfWeek)st.wDayOfWeek}");
}
}
[FieldOffset(N)]. Дозволяє створювати union-и (кілька полів за однією адресою) або пропускати байти. Складніший, але дає повний контроль.[StructLayout(LayoutKind.Explicit)]
struct Union
{
[FieldOffset(0)] public int IntValue;
[FieldOffset(0)] public float FloatValue; // Той самий offset — union!
[FieldOffset(0)] public byte Byte0;
[FieldOffset(1)] public byte Byte1;
[FieldOffset(2)] public byte Byte2;
[FieldOffset(3)] public byte Byte3;
}
Процесори ефективніше читають дані, вирівняні за певними адресами. Наприклад, 4-байтове int краще читати з адреси, кратної 4. Компілятор C та CLR автоматично додають padding (порожні байти) для вирівнювання.
// Приклад: структура з padding
[StructLayout(LayoutKind.Sequential)]
struct Example
{
public byte b1; // Offset 0, розмір 1
// [padding 3 байти]
public int i; // Offset 4, розмір 4 (вирівняно за 4)
public byte b2; // Offset 8, розмір 1
// [padding 3 байти]
}
// Загальний розмір: 12 байт (не 6!)
Console.WriteLine(Marshal.SizeOf<Example>()); // Виведе: 12
Якщо C структура має інший padding (наприклад, через #pragma pack), ви можете вказати Pack у StructLayout:
[StructLayout(LayoutKind.Sequential, Pack = 1)] // Без padding
struct Packed
{
public byte b1; // Offset 0
public int i; // Offset 1 (не 4!)
public byte b2; // Offset 5
}
// Розмір: 6 байт
Pack тільки якщо C код також використовує #pragma pack.Коли структура містить масив фіксованого розміру, використовуйте [MarshalAs]:
// C структура:
// typedef struct {
// char name[256];
// int id;
// } Person;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct Person
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string name; // Фіксований масив 256 байт
public int id;
}
Для масивів чисел:
[StructLayout(LayoutKind.Sequential)]
struct Data
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public int[] values; // Масив з 10 int-ів (40 байт)
}
Традиційний [DllImport] виконує marshalling у runtime — CLR генерує IL-код для конвертації типів під час виконання програми. Це має кілька недоліків:
PublishTrimmed=true важко визначити, які типи використовуються для marshalling.NET 7 додав [LibraryImport] — атрибут, що використовує source generator для генерації marshalling коду на етапі компіляції. Це дає:
Порівняння синтаксису:
// Старий підхід (DllImport)
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
// Новий підхід (LibraryImport) — .NET 7+
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
static partial int MessageBox(IntPtr hWnd, string text, string caption, uint type);
// ^^^^^^^ Обов'язково partial — source generator додасть реалізацію
Source generator створює приблизно такий код (спрощено):
static partial int MessageBox(IntPtr hWnd, string text, string caption, uint type)
{
IntPtr textPtr = Marshal.StringToCoTaskMemUni(text);
IntPtr captionPtr = Marshal.StringToCoTaskMemUni(caption);
try
{
return MessageBoxW(hWnd, textPtr, captionPtr, type);
}
finally
{
Marshal.FreeCoTaskMem(textPtr);
Marshal.FreeCoTaskMem(captionPtr);
}
}
[DllImport("user32.dll", EntryPoint = "MessageBoxW", ExactSpelling = true)]
static extern int MessageBoxW(IntPtr hWnd, IntPtr text, IntPtr caption, uint type);
✅ Використовуйте LibraryImport
⚠️ Залишайтеся на DllImport
Коли ви отримуєте handle з Win32 API як IntPtr, виникає проблема: GC не знає, що це системний ресурс, який потрібно звільнити. Якщо об'єкт, що тримає IntPtr, збирається GC до виклику CloseHandle() — handle leak.
Наївний підхід (небезпечний):
class FileWrapper
{
private IntPtr _handle;
public FileWrapper(string path)
{
_handle = CreateFile(path, GENERIC_READ, ...);
if (_handle == INVALID_HANDLE_VALUE)
throw new Win32Exception();
}
// ❌ ПРОБЛЕМА: якщо забути викликати Dispose — handle leak
public void Dispose()
{
if (_handle != IntPtr.Zero && _handle != INVALID_HANDLE_VALUE)
{
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
}
// ❌ ПРОБЛЕМА: finalizer може викликатися у будь-якому потоці,
// після того як об'єкт частково зруйновано
~FileWrapper()
{
Dispose();
}
}
SafeHandle — абстрактний клас з BCL, що вирішує всі проблеми:
AppDomain unload або Thread.Abort()Реалізація для файлових handles:
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
// SafeHandleZeroOrMinusOneIsInvalid — базовий клас для handles,
// де 0 або -1 означають невалідний handle
class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// Конструктор для створення "порожнього" handle
public SafeFileHandle() : base(ownsHandle: true) { }
// Конструктор для обгортання існуючого handle
public SafeFileHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle)
{
SetHandle(handle);
}
// Метод звільнення ресурсу — викликається автоматично GC або при Dispose
protected override bool ReleaseHandle()
{
return CloseHandle(handle);
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr handle);
}
class Program
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile
);
const uint GENERIC_READ = 0x80000000;
const uint OPEN_EXISTING = 3;
static void Main()
{
// using гарантує виклик Dispose, навіть при exception
using SafeFileHandle handle = CreateFile(
"test.txt",
GENERIC_READ,
0,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero
);
if (handle.IsInvalid)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Console.WriteLine("Файл відкрито успішно!");
// handle автоматично закриється при виході з using
}
}
SafeFileHandle (для файлів), SafeWaitHandle (для синхронізації), SafeProcessHandle (для процесів). Використовуйте їх замість власних реалізацій.Багато Win32 API функцій приймають callback — вказівник на функцію, яку вони викликатимуть для повідомлення про події. Класичний приклад — EnumWindows(), що перебирає всі вікна у системі та викликає ваш callback для кожного.
Сигнатура у C:
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc, // Callback функція
LPARAM lParam // Користувацькі дані
);
// Тип callback-функції
typedef BOOL (CALLBACK *WNDENUMPROC)(
HWND hwnd, // Handle вікна
LPARAM lParam // Користувацькі дані
);
У C# делегат може бути перетворений на function pointer для передачі у native код. Використовується атрибут [UnmanagedFunctionPointer]:
using System.Runtime.InteropServices;
using System.Text;
class Program
{
// Делегат, що відповідає сигнатурі WNDENUMPROC
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
static extern bool IsWindowVisible(IntPtr hWnd);
static void Main()
{
var windows = new List<(IntPtr Handle, string Title)>();
// Callback функція — буде викликана для кожного вікна
bool EnumWindowCallback(IntPtr hWnd, IntPtr lParam)
{
// Фільтруємо тільки видимі вікна
if (!IsWindowVisible(hWnd))
return true; // true = продовжити перебір
var title = new StringBuilder(256);
int length = GetWindowText(hWnd, title, title.Capacity);
if (length > 0)
{
windows.Add((hWnd, title.ToString()));
}
return true; // Продовжити перебір
}
// Передаємо делегат у native функцію
EnumWindows(EnumWindowCallback, IntPtr.Zero);
Console.WriteLine($"Знайдено {windows.Count} видимих вікон:\n");
foreach (var (handle, title) in windows.Take(10))
{
Console.WriteLine($"[0x{handle:X8}] {title}");
}
}
}
Коли ви передаєте делегат у native код, CLR створює function pointer (thunk), що перенаправляє виклик з unmanaged у managed. Проблема: GC не знає, що native код тримає посилання на делегат.
Сценарій помилки:
// ❌ НЕБЕЗПЕЧНО!
void RegisterCallback()
{
EnumWindows((hWnd, lParam) =>
{
Console.WriteLine($"Window: {hWnd}");
return true;
}, IntPtr.Zero);
}
// Делегат (lambda) — локальна змінна, може бути зібрана GC
// Native код може викликати callback після того, як GC зібрав делегат
// Результат: AccessViolationException або corrupted state
Правильний підхід:
class WindowEnumerator
{
// Зберігаємо делегат як поле класу — GC не збере його
private readonly EnumWindowsProc _callback;
public WindowEnumerator()
{
_callback = EnumWindowCallback;
}
private bool EnumWindowCallback(IntPtr hWnd, IntPtr lParam)
{
// Обробка...
return true;
}
public void Enumerate()
{
EnumWindows(_callback, IntPtr.Zero);
}
}
Для callbacks, що живуть довго (наприклад, Windows Hooks), використовуйте GCHandle.Alloc з GCHandleType.Normal:
class HookManager
{
private EnumWindowsProc _callback;
private GCHandle _callbackHandle;
public void Install()
{
_callback = MyCallback;
// "Закріплюємо" делегат — GC не зможе його зібрати
_callbackHandle = GCHandle.Alloc(_callback, GCHandleType.Normal);
// Передаємо у native код...
SetWindowsHookEx(..., _callback, ...);
}
public void Uninstall()
{
UnhookWindowsHookEx(...);
// Звільняємо GCHandle — тепер GC може зібрати делегат
if (_callbackHandle.IsAllocated)
_callbackHandle.Free();
}
private bool MyCallback(IntPtr hWnd, IntPtr lParam)
{
// ...
return true;
}
}
Побудуємо повноцінну утиліту для отримання детальної інформації про систему через Win32 API.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct SYSTEM_INFO
{
public ushort wProcessorArchitecture;
public ushort wReserved;
public uint dwPageSize;
public IntPtr lpMinimumApplicationAddress;
public IntPtr lpMaximumApplicationAddress;
public IntPtr dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort wProcessorLevel;
public ushort wProcessorRevision;
}
// Константи для wProcessorArchitecture
enum ProcessorArchitecture : ushort
{
Intel = 0,
MIPS = 1,
Alpha = 2,
PowerPC = 3,
ARM = 5,
IA64 = 6,
AMD64 = 9,
ARM64 = 12,
Unknown = 0xFFFF
}
static class NativeMethods
{
[DllImport("kernel32.dll")]
public static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern void GetSystemDirectory(
StringBuilder lpBuffer,
uint uSize
);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern void GetWindowsDirectory(
StringBuilder lpBuffer,
uint uSize
);
}
using System.Text;
class SystemInfoHelper
{
public static void PrintSystemInfo()
{
SYSTEM_INFO sysInfo;
NativeMethods.GetSystemInfo(out sysInfo);
Console.WriteLine("═══════════════════════════════════════════════");
Console.WriteLine(" SYSTEM INFORMATION");
Console.WriteLine("═══════════════════════════════════════════════\n");
// Архітектура процесора
var arch = (ProcessorArchitecture)sysInfo.wProcessorArchitecture;
Console.WriteLine($"Processor Architecture: {arch}");
string archDescription = arch switch
{
ProcessorArchitecture.Intel => "x86 (32-bit)",
ProcessorArchitecture.AMD64 => "x64 (64-bit)",
ProcessorArchitecture.ARM64 => "ARM64 (64-bit)",
ProcessorArchitecture.ARM => "ARM (32-bit)",
_ => "Unknown"
};
Console.WriteLine($" → {archDescription}\n");
// Кількість процесорів
Console.WriteLine($"Number of Processors: {sysInfo.dwNumberOfProcessors}");
Console.WriteLine($" → Logical cores available to OS\n");
// Розмір сторінки пам'яті
Console.WriteLine($"Page Size: {sysInfo.dwPageSize:N0} bytes");
Console.WriteLine($" → {sysInfo.dwPageSize / 1024} KB (memory allocation unit)\n");
// Діапазон адресного простору
Console.WriteLine($"Application Address Space:");
Console.WriteLine($" Min: 0x{sysInfo.lpMinimumApplicationAddress:X16}");
Console.WriteLine($" Max: 0x{sysInfo.lpMaximumApplicationAddress:X16}");
long addressSpaceSize = sysInfo.lpMaximumApplicationAddress.ToInt64() -
sysInfo.lpMinimumApplicationAddress.ToInt64();
Console.WriteLine($" → {addressSpaceSize / (1024.0 * 1024 * 1024):F1} GB virtual address space\n");
// Granularity для VirtualAlloc
Console.WriteLine($"Allocation Granularity: {sysInfo.dwAllocationGranularity:N0} bytes");
Console.WriteLine($" → {sysInfo.dwAllocationGranularity / 1024} KB (VirtualAlloc alignment)\n");
// Системні директорії
var sysDir = new StringBuilder(260);
var winDir = new StringBuilder(260);
NativeMethods.GetSystemDirectory(sysDir, (uint)sysDir.Capacity);
NativeMethods.GetWindowsDirectory(winDir, (uint)winDir.Capacity);
Console.WriteLine($"System Directory: {sysDir}");
Console.WriteLine($"Windows Directory: {winDir}\n");
// Маска активних процесорів (bitmap)
Console.WriteLine($"Active Processor Mask: 0x{sysInfo.dwActiveProcessorMask:X}");
Console.WriteLine($" → Binary: {Convert.ToString(sysInfo.dwActiveProcessorMask.ToInt64(), 2).PadLeft(64, '0')}");
int activeCores = CountBits(sysInfo.dwActiveProcessorMask.ToInt64());
Console.WriteLine($" → {activeCores} cores enabled\n");
Console.WriteLine("═══════════════════════════════════════════════");
}
private static int CountBits(long value)
{
int count = 0;
while (value != 0)
{
count += (int)(value & 1);
value >>= 1;
}
return count;
}
}
class Program
{
static void Main()
{
try
{
SystemInfoHelper.PrintSystemInfo();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
}
}
dotnet run
Клас System.Runtime.InteropServices.Marshal надає статичні методи для ручного управління unmanaged пам'яттю та конвертації типів. Це низькорівневий API, що дає повний контроль.
GlobalAlloc Win32 API). Повертає вказівник. Пам'ять не ініціалізована (містить сміття).IntPtr ptr = Marshal.AllocHGlobal(1024); // 1 KB
try
{
// Використання пам'яті...
}
finally
{
Marshal.FreeHGlobal(ptr); // ОБОВ'ЯЗКОВО звільнити
}
CoTaskMemAlloc). Використовується для COM Interop та деяких Win32 API, що очікують саме цей allocator.AllocHGlobal. Виклик з невалідним вказівником — undefined behavior (crash).AllocCoTaskMem.byte[] data = { 1, 2, 3, 4, 5 };
IntPtr ptr = Marshal.AllocHGlobal(data.Length);
Marshal.Copy(data, 0, ptr, data.Length);
// Тепер unmanaged пам'ять містить {1, 2, 3, 4, 5}
IntPtr ptr = GetSomeNativeStructure();
SYSTEMTIME st = Marshal.PtrToStructure<SYSTEMTIME>(ptr);
AllocHGlobal.string.int size = Marshal.SizeOf<SYSTEMTIME>(); // 16 байт
IntPtr offset = Marshal.OffsetOf<SYSTEMTIME>("wYear"); // 0
IntPtr offset2 = Marshal.OffsetOf<SYSTEMTIME>("wMonth"); // 2
Побудуємо інструмент для автоматизації роботи з вікнами Windows: пошук вікон за заголовком, зміна позиції/розміру, відправка повідомлень.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width => Right - Left;
public int Height => Bottom - Top;
public override string ToString() =>
$"({Left}, {Top}) - ({Right}, {Bottom}) [{Width}x{Height}]";
}
[StructLayout(LayoutKind.Sequential)]
struct POINT
{
public int X;
public int Y;
public override string ToString() => $"({X}, {Y})";
}
static class WindowConstants
{
// ShowWindow commands
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_RESTORE = 9;
// SetWindowPos flags
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOMOVE = 0x0002;
public const uint SWP_NOZORDER = 0x0004;
public const uint SWP_SHOWWINDOW = 0x0040;
// Window messages
public const uint WM_CLOSE = 0x0010;
public const uint WM_SETTEXT = 0x000C;
}
using System.Runtime.InteropServices;
using System.Text;
static class WindowNativeMethods
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int X,
int Y,
int cx,
int cy,
uint uFlags
);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsIconic(IntPtr hWnd); // Мінімізоване
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsZoomed(IntPtr hWnd); // Максимізоване
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(
IntPtr hWnd,
uint Msg,
IntPtr wParam,
IntPtr lParam
);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
}
using System.Text;
class WindowWrapper
{
public IntPtr Handle { get; }
public WindowWrapper(IntPtr handle)
{
if (handle == IntPtr.Zero)
throw new ArgumentException("Invalid window handle", nameof(handle));
Handle = handle;
}
public string GetTitle()
{
int length = WindowNativeMethods.GetWindowTextLength(Handle);
if (length == 0)
return string.Empty;
var sb = new StringBuilder(length + 1);
WindowNativeMethods.GetWindowText(Handle, sb, sb.Capacity);
return sb.ToString();
}
public void SetTitle(string title)
{
IntPtr titlePtr = Marshal.StringToHGlobalUni(title);
try
{
WindowNativeMethods.SendMessage(
Handle,
WindowConstants.WM_SETTEXT,
IntPtr.Zero,
titlePtr
);
}
finally
{
Marshal.FreeHGlobal(titlePtr);
}
}
public RECT GetBounds()
{
if (!WindowNativeMethods.GetWindowRect(Handle, out RECT rect))
throw new InvalidOperationException("Failed to get window bounds");
return rect;
}
public void SetBounds(int x, int y, int width, int height)
{
WindowNativeMethods.SetWindowPos(
Handle,
IntPtr.Zero,
x, y, width, height,
WindowConstants.SWP_NOZORDER | WindowConstants.SWP_SHOWWINDOW
);
}
public void MoveTo(int x, int y)
{
WindowNativeMethods.SetWindowPos(
Handle,
IntPtr.Zero,
x, y, 0, 0,
WindowConstants.SWP_NOSIZE | WindowConstants.SWP_NOZORDER
);
}
public void Resize(int width, int height)
{
WindowNativeMethods.SetWindowPos(
Handle,
IntPtr.Zero,
0, 0, width, height,
WindowConstants.SWP_NOMOVE | WindowConstants.SWP_NOZORDER
);
}
public void Show() =>
WindowNativeMethods.ShowWindow(Handle, WindowConstants.SW_SHOWNORMAL);
public void Hide() =>
WindowNativeMethods.ShowWindow(Handle, WindowConstants.SW_HIDE);
public void Minimize() =>
WindowNativeMethods.ShowWindow(Handle, WindowConstants.SW_SHOWMINIMIZED);
public void Maximize() =>
WindowNativeMethods.ShowWindow(Handle, WindowConstants.SW_SHOWMAXIMIZED);
public void Restore() =>
WindowNativeMethods.ShowWindow(Handle, WindowConstants.SW_RESTORE);
public void BringToFront() =>
WindowNativeMethods.SetForegroundWindow(Handle);
public void Close() =>
WindowNativeMethods.SendMessage(Handle, WindowConstants.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
public bool IsVisible =>
WindowNativeMethods.IsWindowVisible(Handle);
public bool IsMinimized =>
WindowNativeMethods.IsIconic(Handle);
public bool IsMaximized =>
WindowNativeMethods.IsZoomed(Handle);
public string GetState()
{
if (!IsVisible) return "Hidden";
if (IsMinimized) return "Minimized";
if (IsMaximized) return "Maximized";
return "Normal";
}
}
class WindowManager
{
public static WindowWrapper? FindByTitle(string title)
{
IntPtr handle = WindowNativeMethods.FindWindow(null, title);
return handle != IntPtr.Zero ? new WindowWrapper(handle) : null;
}
public static WindowWrapper? FindByTitleContains(string partialTitle)
{
WindowWrapper? found = null;
WindowNativeMethods.EnumWindows((hWnd, lParam) =>
{
if (!WindowNativeMethods.IsWindowVisible(hWnd))
return true;
var window = new WindowWrapper(hWnd);
string title = window.GetTitle();
if (title.Contains(partialTitle, StringComparison.OrdinalIgnoreCase))
{
found = window;
return false; // Зупинити перебір
}
return true;
}, IntPtr.Zero);
return found;
}
public static List<WindowWrapper> GetAllVisibleWindows()
{
var windows = new List<WindowWrapper>();
WindowNativeMethods.EnumWindows((hWnd, lParam) =>
{
if (WindowNativeMethods.IsWindowVisible(hWnd))
{
var window = new WindowWrapper(hWnd);
string title = window.GetTitle();
if (!string.IsNullOrWhiteSpace(title))
windows.Add(window);
}
return true;
}, IntPtr.Zero);
return windows;
}
}
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
if (args.Length == 0)
{
ShowHelp();
return;
}
try
{
string command = args[0].ToLower();
switch (command)
{
case "list":
ListWindows();
break;
case "find":
if (args.Length < 2)
{
Console.WriteLine("Usage: find <title>");
return;
}
FindWindow(args[1]);
break;
case "move":
if (args.Length < 4)
{
Console.WriteLine("Usage: move <title> <x> <y>");
return;
}
MoveWindow(args[1], int.Parse(args[2]), int.Parse(args[3]));
break;
case "resize":
if (args.Length < 4)
{
Console.WriteLine("Usage: resize <title> <width> <height>");
return;
}
ResizeWindow(args[1], int.Parse(args[2]), int.Parse(args[3]));
break;
default:
Console.WriteLine($"Unknown command: {command}");
ShowHelp();
break;
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
}
static void ShowHelp()
{
Console.WriteLine("Window Automation Tool");
Console.WriteLine("======================\n");
Console.WriteLine("Commands:");
Console.WriteLine(" list - List all visible windows");
Console.WriteLine(" find <title> - Find window by title");
Console.WriteLine(" move <title> <x> <y> - Move window to position");
Console.WriteLine(" resize <title> <w> <h> - Resize window");
}
static void ListWindows()
{
var windows = WindowManager.GetAllVisibleWindows();
Console.WriteLine($"Found {windows.Count} visible windows:\n");
Console.WriteLine($"{"Handle",-12} {"State",-12} {"Bounds",-30} {"Title"}");
Console.WriteLine(new string('─', 100));
foreach (var window in windows.Take(20))
{
try
{
var bounds = window.GetBounds();
Console.WriteLine(
$"0x{window.Handle:X8} " +
$"{window.GetState(),-12} " +
$"{bounds.Width}x{bounds.Height} at ({bounds.Left},{bounds.Top})".PadRight(30) +
$"{window.GetTitle()}"
);
}
catch { }
}
}
static void FindWindow(string title)
{
var window = WindowManager.FindByTitleContains(title);
if (window == null)
{
Console.WriteLine($"Window not found: {title}");
return;
}
Console.WriteLine($"Found window:");
Console.WriteLine($" Handle: 0x{window.Handle:X8}");
Console.WriteLine($" Title: {window.GetTitle()}");
Console.WriteLine($" State: {window.GetState()}");
Console.WriteLine($" Bounds: {window.GetBounds()}");
}
static void MoveWindow(string title, int x, int y)
{
var window = WindowManager.FindByTitleContains(title);
if (window == null)
{
Console.WriteLine($"Window not found: {title}");
return;
}
window.MoveTo(x, y);
Console.WriteLine($"✓ Moved '{window.GetTitle()}' to ({x}, {y})");
}
static void ResizeWindow(string title, int width, int height)
{
var window = WindowManager.FindByTitleContains(title);
if (window == null)
{
Console.WriteLine($"Window not found: {title}");
return;
}
window.Resize(width, height);
Console.WriteLine($"✓ Resized '{window.GetTitle()}' to {width}x{height}");
}
}
# Список всіх вікон
dotnet run list
# Знайти вікно
dotnet run find "Chrome"
# Перемістити вікно
dotnet run move "Chrome" 100 100
# Змінити розмір
dotnet run resize "Chrome" 1280 720
P/Invoke Основи
[DllImport] — атрибут для виклику native функційCharSet.Unicode — завжди для Win32 APISetLastError = true — для обробки помилок через Marshal.GetLastWin32Error()[LibraryImport] — сучасна альтернатива (.NET 7+) з source generationMarshalling
[StructLayout(LayoutKind.Sequential)] — для структур[MarshalAs] — для складних типів (масиви, рядки фіксованого розміру)SafeHandle
SafeFileHandle, SafeWaitHandle, SafeProcessHandleCallbacks
[UnmanagedFunctionPointer] — вказати calling conventionGCHandle.Alloc — для довгоживучих callbacksРозширте приклад GetSystemInfo:
GetComputerName() для отримання імені комп'ютераGetUserName() для поточного користувачаGetTickCount64() для uptime системи (мілісекунди з останнього завантаження)Створіть CLI утиліту для роботи з атрибутами файлів через Win32 API:
GetFileAttributes() та SetFileAttributes() з kernel32.dllReadOnly, Hidden, System, Archiveget <file>, set <file> <attributes>, clear <file> <attributes>GetLastError()Реалізуйте систему глобальних hotkey-ів:
RegisterHotKey() та UnregisterHotKey() з user32.dllGetMessage() та DispatchMessage()Unsafe Code та Вказівники
Повний розбір unsafe коду в C# — вказівники, pointer arithmetic, fixed statement для pinning, stackalloc та Span<T>, sizeof, function pointers (C# 9+), та практичні сценарії використання для high-performance коду.
Реєстр Windows — Центральна База Конфігурації Системи
Повний розбір Windows Registry — від архітектури Hives та Keys до практичних прикладів автозапуску програм, файлових асоціацій, персоналізації системи та моніторингу змін. Теорія, API та вау-ефекти з детальними прикладами.