C# для системного программирования: низкий старт без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

C# для системного программирования: когда высокий уровень встречается с низким железом

Когда слышишь словосочетание «системное программирование», в голове обычно всплывают образы бородатых гуру, пишущих на C или ассемблере, с терминалами, залитыми зелёным текстом. А C# — это же «для бизнес-приложений», «под Windows Forms» и «для тех, кто не хочет париться с указателями». Спойлер: это миф.

На самом деле C# — это швейцарский нож для системного программиста, который хочет писать быстро, безопасно, но при необходимости — лезть в самое пекло неуправляемой памяти. В этой статье мы разберем, как C# помогает решать реальные системные задачи: от работы с процессами до прямого доступа к памяти через WinAPI. И да, примеры кода будут — без них никуда.

Почему C# — не просто «бизнес-игрушка»?

Системное программирование на C# — это не оксюморон. Платформа .NET (особенно .NET 5+) даёт разработчику уникальную комбинацию: производительность, близкую к нативному коду, и удобство высокоуровневого языка. Вы можете писать драйверы, работать с процессами, выделять память вручную и при этом не сойти с ума от управления ресурсами.

Ключевые возможности, которые делают C# пригодным для системного программирования:

  • Unsafe-код и указатели — да, в C# можно работать с адресами напрямую, как в C++.
  • P/Invoke — вызов любых функций из нативных библиотек (WinAPI, libc, драйверы).
  • Span и Memory — безопасная работа с непрерывными блоками памяти без лишних аллокаций.
  • Native AOT — компиляция в нативный код без JIT и CLR.
  • Interop с C/C++ — встраивание C# в существующие системные проекты.

Если вы думали, что C# — это только про веб и базы данных, приготовьтесь удивляться.

Unsafe-код: игра с огнём, но под контролем

Ключевое слово unsafe в C# — это как красная кнопка на ядерной ракете. Нажал — и ты можешь делать всё: разыменовывать указатели, работать с фиксированными объектами, выделять стековую память. Но и ответственность — абсолютная.

Допустим, нам нужно быстро скопировать массив байт, минуя проверки границ. В безопасном коде мы бы использовали Array.Copy, но если мы пишем системную утилиту для работы с драйвером, каждая микросекунда на счету.

using System;

unsafe class MemoryCopy
{
    public static void FastCopy(byte[] source, byte[] dest, int length)
    {
        fixed (byte* pSrc = source, pDst = dest)
        {
            byte* ps = pSrc;
            byte* pd = pDst;
            for (int i = 0; i < length; i++)
                *pd++ = *ps++;
        }
    }
}

Здесь мы используем fixed для «закрепления» управляемых массивов в памяти, чтобы сборщик мусора не сдвинул их во время копирования. Результат — копирование на скорости, близкой к memcpy на C. Конечно, для повседневных задач это избыточно, но для системного программирования на C# — must-have.

Когда unsafe оправдан?

  • Работа с сетевыми пакетами на уровне сырых байт.
  • Взаимодействие с аппаратным обеспечением через DMA.
  • Реализация собственных аллокаторов памяти.
  • Оптимизация критических участков кода (например, обработка видео с камеры).

P/Invoke и WinAPI: C# стучится в ядро

Настоящее системное программирование невозможно без вызова нативных функций. C# предоставляет для этого механизм P/Invoke (Platform Invocation Services). Хотите получить список процессов, управлять окнами или читать память другого процесса? Пожалуйста.

Рассмотрим классический пример — получение списка запущенных процессов через WinAPI-функцию CreateToolhelp32Snapshot.

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

public class ProcessInfo
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

    [DllImport("kernel32.dll")]
    private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

    [DllImport("kernel32.dll")]
    private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESSENTRY32
    {
        public uint dwSize;
        public uint cntUsage;
        public uint th32ProcessID;
        public IntPtr th32DefaultHeapID;
        public uint th32ModuleID;
        public uint cntThreads;
        public uint th32ParentProcessID;
        public int pcPriClassBase;
        public uint dwFlags;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string szExeFile;
    }

    public static List GetProcessList()
    {
        var processes = new List();
        uint TH32CS_SNAPPROCESS = 0x00000002;
        IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

        if (snapshot == IntPtr.Zero) return processes;

        PROCESSENTRY32 procEntry = new PROCESSENTRY32();
        procEntry.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));

        if (Process32First(snapshot, ref procEntry))
        {
            do
            {
                processes.Add(procEntry.szExeFile);
            } while (Process32Next(snapshot, ref procEntry));
        }
        return processes;
    }
}

Этот код — типичный пример C# системного программирования с WinAPI. Мы объявляем структуру PROCESSENTRY32 с точным размещением полей (StructLayout), импортируем функции и работаем с неуправляемым дескриптором. Красота в том, что C# берёт на себя маршалинг строк и структур, а мы получаем чистый системный функционал.

Советы по P/Invoke:

  • Всегда проверяйте SetLastError = true и вызывайте Marshal.GetLastWin32Error().
  • Используйте SafeHandle вместо сырых IntPtr — это предотвратит утечки.
  • Для сложных структур используйте Marshal.SizeOf и Marshal.PtrToStructure.

Работа с памятью: Span, Memory и аллокаторы

В системном программировании на C# нельзя игнорировать современные фичи .NET — Span и Memory. Они позволяют работать с любыми буферами (массивы, строки, неуправляемая память) без копирования и с минимальными накладными расходами.

Например, вы пишете

Блоги

Рекомендаций по книгам