Как починить Windows-разработку: путь от DLL-ада к статической сборке

Windows-разработка починена: как один разработчик победил DLL-ад

Представьте: вы разработали приложение на C++, протестировали на своей машине — всё работает идеально. Отправляете бинарник коллеге, и он присылает скриншот: «MSVCR140.dll not found». Вы вздыхаете и начинаете объяснять про Visual Studio Redistributable. Это не разработка, это археология зависимостей. Один инженер решил, что с Windows native development пора покончить с этим цирком, и переписал всю цепочку сборки с нуля.

DLL-ад: почему Windows-разработка сломана

На Linux вы компилируете программу через GCC, получаете один бинарник и запускаете его где угодно. На macOS — то же самое с Clang. На Windows? Добро пожаловать в зависимостный лабиринт.

Проблема начинается с того, что Microsoft C Runtime (MSVCRT) распространяется как набор DLL-файлов. Когда вы компилируете программу через MSVC, компилятор генерирует объектные файлы, которые линкуются с этими библиотеками. Результат — ваш EXE требует MSVCR140.dll, VCRUNTIME140.dll, ucrtbase.dll и ещё десяток системных компонентов.

Формально Microsoft предлагает решение: Visual Studio Redistributable — установочный пакет, который копирует все нужные DLL в систему. Но это порождает новые проблемы:

  • Версионный конфликт: разные приложения требуют разные версии runtime
  • Права администратора: для установки redistributable нужны elevated permissions
  • Размер дистрибутива: простое приложение в 500 КБ требует скачивания 30 МБ runtime-пакета
  • Отладка: когда что-то ломается, непонятно, это проблема вашего кода или несовместимой версии DLL

Вторая проблема — сам MSVC. Это не просто компилятор, это экосистема размером десятки гигабайт: Visual Studio IDE, Windows SDK, platform toolsets, специфичные расширения языка. Если вы хотите собирать проекты через CI/CD, вам нужно разворачивать виртуальную машину с полной установкой Visual Studio. Это медленно, дорого и сложно воспроизвести.

Третья проблема — backwards compatibility. Universal C Runtime (UCRT) появился только в Windows 10. Если вы хотите поддерживать Windows 7 или 8, нужно явно таргетироваться на старые версии runtime, патчить манифесты, возиться с side-by-side assemblies. Один бинарник для всех версий Windows? Забудьте.

Zigstrap: философия статической сборки

Решение пришло из неожиданного места — из экосистемы языка Zig. Zig — это современный системный язык, который позиционируется как «better C». Но главная его фича — это компилятор, который умеет кросс-компилировать под любую платформу без дополнительных зависимостей.

Марлер — автор проекта Zigstrap — использовал эту возможность, чтобы создать полноценный C/C++ toolchain для Windows, который работает по принципу «один бинарник, zero dependencies».

Архитектура выглядит так:

  1. Zig CC — обёртка над Zig-компилятором, эмулирующая интерфейс GCC/Clang
  2. Собственная реализация стандартной библиотеки C, написанная на Zig и компилируемая статически
  3. Прямая линковка с ntdll.dll — единственной системной библиотекой, гарантированно присутствующей на любой Windows

Ключевое отличие от MSVC: весь код стандартной библиотеки (malloc, printf, fopen и т.д.) не подтягивается как DLL, а вшивается в итоговый EXE-файл. Никаких внешних зависимостей — только обращения к ntdll.dll, которая есть даже на Windows XP.

Если вам интересны технические детали bootstrapping-процесса и примеры реальных проектов, собранных через Zigstrap (Git, Nginx, SQLite), смотрите видеоразбор — там мы показываем полный цикл сборки с нуля.

Zig CC: как компилировать C без Microsoft

Работа с Zig CC выглядит обманчиво просто. Вместо:

cl.exe /EHsc /O2 myapp.cpp /link /OUT:myapp.exe

Вы пишете:

zig cc myapp.c -o myapp.exe

Под капотом происходит следующее:

Этап 1: парсинг и lowering
Zig-компилятор парсит C-код, строит AST и переводит его в промежуточное представление Zig IR. Это не LLVM IR — Zig использует собственный бэкенд для кодогенерации.

Этап 2: разрешение стандартной библиотеки
Когда код вызывает printf, компилятор не линкуется с MSVCRT. Вместо этого он подтягивает реализацию из zig/lib/libc/mingw — это минималистичная реализация стандартной библиотеки, которая использует напрямую Win32 API через ntdll.

Этап 3: кодогенерация
Zig генерирует машинный код для x86-64 или ARM, включая все зависимости. Итоговый бинарник содержит весь код — и ваш, и стандартной библиотеки.

Этап 4: линковка
Линкер Zig создаёт PE-файл (Portable Executable — формат Windows EXE), прописывает импорты только для ntdll.dll, и генерирует финальный бинарник.

Результат: EXE-файл размером 150-300 КБ (в зависимости от сложности), который работает на любой Windows без дополнительных установок.

Bootstrapping: компилятор, который собирает сам себя

Классическая проблема компиляторов — для их сборки нужен другой компилятор. Обычно на Windows это означает обязательную установку MSVC или MinGW. Zigstrap решает это через multi-stage bootstrap:

Stage 0: Скачивается minimal Zig binary (около 50 МБ), собранный на CI-серверах проекта. Это самодостаточный бинарник, который умеет компилировать Zig-код.

Stage 1: Этот бинарник компилирует полноценный Zig-компилятор из исходников, но без оптимизаций (debug mode). Сборка занимает 3-5 минут.

Stage 2: Полноценный компилятор пересобирает сам себя с оптимизациями (release mode). Это финальная версия, которая используется для реальной работы.

Весь процесс автоматизирован одним PowerShell-скриптом. Никаких прав администратора, никаких зависимостей — только интернет-соединение для скачивания stage 0.

Практика: реальные проекты на Zigstrap

Автор статьи протестировал сборку нескольких production-проектов:

Git for Windows: собрался без патчей, статический бинарник работает на Windows 7/10/11. Размер увеличился на 20% из-за статической линковки, но зато zero dependencies.

SQLite: чистая C-кодовая база, собралась тривиально. Benchmark показал идентичную производительность с MSVC-версией.

Nginx: потребовались минорные патчи для совместимости с Zig libc, но после этого собрался статически. Один EXE-файл вместо связки бинарников и DLL.

ImageMagick: сложный проект с десятками зависимостей. Все библиотеки (libpng, libjpeg, zlib) собрались статически через Zig CC. Итоговый бинарник — 15 МБ, но полностью переносимый.

Ограничения и trade-offs

Несмотря на впечатляющие результаты, есть нюансы:

C++ поддержка: Zig CC корректно компилирует C++11/14, но с C++17/20 могут быть проблемы. Concepts, modules, ranges — всё это ещё не полностью поддерживается.

Отладка: Visual Studio Debugger не понимает Zig debug symbols out of the box. Нужно использовать GDB или LLDB, что требует привыкания.

Экосистема: Некоторые библиотеки используют MSVC-специфичные расширения (__declspec, #pragma intrinsic). Для их сборки нужны патчи.

Размер бинарника: Статическая линковка увеличивает размер на 10-30%. Для embedded-систем это может быть критично.

Но даже с этими ограничениями преимущества перевешивают: переносимость, простота развёртывания, отсутствие версионных конфликтов.

Почему Microsoft не делает это из коробки

Вопрос резонный: если статическая линковка решает столько проблем, почему Microsoft продолжает навязывать DLL-модель?

Причина — бизнес-модель. DLL-зависимости создают vendor lock-in: разработчики вынуждены использовать Visual Studio, обновлять SDK, платить за подписки. Это контроль над экосистемой.

Вторая причина — безопасность (в теории). Если в MSVCRT находят уязвимость, Microsoft может пропатчить DLL через Windows Update, и все приложения автоматически получат фикс. Со статической линковкой нужно пересобирать каждый бинарник.

Но на практике это не работает: у большинства приложений есть специфичные версии runtime, которые не обновляются автоматически. Плюс security-критичные приложения всё равно пересобираются при каждом патче.

Будущее native development

Zigstrap и Zig CC — это не просто инструменты, это proof of concept: Windows native development может быть таким же простым, как на Linux. Один компилятор, один бинарник, zero configuration.

Сообщество уже использует эту технологию для embedded-проектов, кросс-компиляции под Windows ARM, CI/CD-пайплайнов без Docker. GitHub Actions с Zig CC собирают релизы для всех платформ за один проход — никаких VM, никаких контейнеров.

Ключевой вывод: проблемы Windows native development — это не техническое ограничение, а архитектурное решение Microsoft. И его можно обойти.

Заключение

Если вы разработчик на C/C++ и устали от DLL-археологии, MSVC-хаоса и redistributable-квестов — попробуйте Zigstrap. Это займёт 10 минут установки и даст toolchain, который просто работает.

Статическая сборка — это не серебряная пуля, но для большинства приложений это оптимальный выбор. Переносимость, предсказуемость, отсутствие версионных конфликтов. Как native development должен был работать с самого начала.

Можно ещё почитать:
Loading...
Пожалуйста ждите...