winweb: Win32 в браузере, где C-приложения компилируются по-настоящему

Рабочий стол в духе Win9x, на котором живут настоящие Win32-приложения на C — и компилируются прямо во вкладке браузера, без сервера и без Emscripten.

  • Демо — рабочий стол с Сапёром, Блокнотом, командной строкой и компилятором; всё на настоящем Win32 C, скомпилированном в WebAssembly
  • Репозиторий

Открываю Сапёр, потом — исходник программы, компилирую hello-world и запускаю. Всё в браузере.

Что это

winweb — маленький рабочий стол в стиле Windows 9x, живущий в браузере. Окна, панель задач, меню «Пуск», Проводник, иконки. Но суть не в оформлении: приложения здесь — настоящие программы на Win32 C, написанные с WinMain, RegisterClass, циклом сообщений и рисованием через GDI. Они скомпилированы в WebAssembly и реально выполняются.

В комплекте — Сапёр (тот самый, на RegisterClass/WM_PAINT/BitBlt), Блокнот, командная строка cmd, демка с иконками, консольный hello-world. И — компиляторы cc и msbuild, потому что C-исходники можно собирать прямо здесь.

Сначала был Emscripten

Идея была очевидная: взять Win32-приложение на C, скомпилировать в wasm и подсунуть ему JS-фасад вместо настоящего user32/gdi32. Первая мысль — Emscripten: он отлично компилирует C в wasm, у него есть всё.

И поначалу так и было. Но мне хотелось большего — чтобы приложения можно было собирать изнутри: открыть .c в Блокноте, поправить, нажать сборку и тут же запустить. Маленькая IDE внутри самого рабочего стола.

Вот тут Emscripten и закончился. Чтобы компилировать C в браузере, нужен компилятор в браузере. А Emscripten — это clang плюс LLVM, сотни мегабайт нативного кода; в браузер его не затащить. Плюс мелочь, но неприятная: модули Emscripten делят общий сегмент данных, и запустить два экземпляра одного приложения независимо (два Сапёра рядом) без плясок не выходит.

Значит, нужен изначально маленький компилятор.

Маленький lcc вместо Emscripten

Компилятор у меня уже был — про него прошлая статья: я взял учебный ретаргетируемый компилятор C LCC (~16 тысяч строк), написал ему бэкенд в WebAssembly, а потом скомпилировал его же самого в wasm. Получился rcc.wasm — компилятор C размером ~280 КБ, целиком работающий как WebAssembly.

С ним всё встало на места:

  • Компиляция в браузере стала реальной. cc demo.c в командной строке — это rcc.wasm плюс крошечный препроцессор на TypeScript. Тот же компилятор, что собирает приложения под node, работает и во вкладке — байт-в-байт.
  • Каждый модуль самодостаточный. lcc-приложение экспортирует свою память и таблицу функций — никакого общего сегмента. Два Сапёра рядом — это два независимых экземпляра, каждый со своей памятью. То, что не получалось с Emscripten, тут вышло само.
  • Размер. hello-world — пара килобайт wasm, а не «рантайм плюс программа». Всё, что приложению нужно снаружи, оно импортирует у хоста.

Полное избавление от Emscripten оказалось не жертвой, а апгрейдом.

GDI, user32, System32

Дальше — самое интересное: чем приложение «думает», что оно общается с Windows.

GDI. Когда приложение зовёт Rectangle, TextOut, CreateFontW, BitBlt — за этим стоит фасад, рисующий на <canvas>. Сапёр рисует поле, цифры, объёмные рамки DrawEdge, мину и улыбку — всё это настоящие GDI-вызовы, просто исполняет их JS поверх canvas.

Окна. Менеджер окон — на DOM: каждое окно это <div> с заголовком, кнопкой закрытия, меню и иконкой. CreateWindowEx создаёт окно, RegisterClass запоминает оконную процедуру, а цикл GetMessage/DispatchMessage гоняет настоящие сообщения — WM_PAINT, WM_COMMAND, WM_TIMER, WM_SIZE.

DLL — по-настоящему. Самое неожиданное: user32, gdi32 и kernel32 — это реальные файлы C:\Windows\System32\user32.wasm и так далее. Приложение импортирует функции оттуда, а уже эти wasm-«библиотеки» тонкими переходниками зовут JS-фасад. То есть цепочка честная: app.wasmuser32.wasm → JS. Не потому что иначе нельзя — а потому что так аккуратнее и красивее.

System32. В C:\Windows\System32 лежат и инструменты: cmd.wasm, cc.wasm, msbuild.wasm, notepad.wasm. Это обычные исполняемые wasm-файлы. cmd ищет их по имени (как по PATH), а Проводник умеет отличить консольную программу от оконной и при двойном клике открыть консоль.

Это работает по-настоящему

Лучшая проверка — собрать приложение изнутри. Открываешь в Блокноте iconsdemo.c, что-то меняешь, в командной строке набираешь msbuild IconsDemo — компилятор в браузере собирает .wasm и кладёт в C:\Program Files\.... Запускаешь иконку на рабочем столе — открывается твой изменённый вариант. Сервера во всём этом нет нигде: компилятор, линковка, файловая система (на IndexedDB), запуск — всё во вкладке.

И мелочи, из которых складывается ощущение настоящего: командная строка с cd/dir/type и регистронезависимыми путями; иконки приложений, зашитые в сам wasm и подхватываемые в заголовке окна и на панели задач; ресайз окна Блокнота; кириллица в UTF-8; консольные программы с вводом и exit.

Получилась штука, которую приятно открыть в браузере и потыкать: не имитация Win9x, а маленький, но настоящий Win32-рантайм — со своим компилятором, своими DLL и реальными C-приложениями. И всё это — меньше мегабайта, грузится мгновенно.