Капстоун: переиспользуемый движок виджетов и крючок для Web3
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Капстоун: переиспользуемый движок виджетов и крючок для Web3
Мы прошли весь путь: что такое WASM, мост в JS, эмулятор в браузере и его цена. Теперь отойдём на шаг и заметим то, что было перед глазами весь блок. Каждый раз, заводя движок в браузер (
bcpu, NES, netcode), мы писали почти одинаковую обвязку: загрузи модуль, положи данные в память, дёрни шаг, прочитай состояние по указателю, покажи. Три разных Rust-проекта, один и тот же силуэт. Сегодня мы этот силуэт назовём, превратим в переиспользуемый контракт, на котором держатся все виджеты курса, и оставим крючок: на этом же движке поедут узел блокчейна и мини-EVM из раздела про Web3. Это финал раздела 23, и заодно мост в следующий.
Три движка, один силуэт
Посмотри на экспортируемые функции трёх наших движков рядом, и узор проступит сам:
// bcpu (examples/our-cpu)
bcpu_clear(); bcpu_load(addr, byte); bcpu_step();
bcpu_reg(i); bcpu_pc(); bcpu_flags(); bcpu_text_ptr();
// NES (examples/our-nes)
nes_boot(); nes_rom_ptr(len); nes_run_frame(); nes_step();
nes_set_button(p, b, v); nes_framebuffer_ptr(); nes_pc(); nes_text_ptr();
// netcode (examples/our-netgame)
netgame_init(seed); netgame_set_input(btn, aim); netgame_step();
netgame_server_fb_ptr(); netgame_peer_rtt(i); netgame_scoreboard_ptr();
Имена разные, глаголы одни и те же. Везде есть: создать свежую машину, загрузить в неё данные снаружи, передать ввод, продвинуть время на шаг или кадр, прочитать состояние (числа напрямую, буферы и текст по указателю). Это не совпадение, это естественная форма любого движка за границей WASM. Назовём её.
Контракт из пяти глаголов
Сведём узор в единый контракт из пяти глаголов. Это не код, который можно расшарить через границу (там только числа), а договорённость о форме, которой следует каждый движок:
- init / clear: свежее состояние машины с нуля.
- load: положить данные снаружи (ROM, образ, сид) в линейную память по указателю.
- input: передать ввод числами (кнопка, ползунок канала, прицел).
- step / frame: продвинуть время на одну единицу.
- read: прочитать состояние: числа напрямую, буферы и текст по указателю.
Узнаёшь? Это ровно дисциплина HAL из проекта про процессор: стабильный интерфейс, за которым свободно меняется реализация. Только теперь интерфейс проходит через границу WASM, а реализаций три (скоро будет больше). На стороне Rust контракт это тонкий модуль wasm.rs в каждом крейте, плоские extern "C" обёртки над уже готовым ядром. Ядро движок не знает про браузер вовсе.
JS-сторона: один загрузчик на всех
Раз форма общая, обвязку на JS можно написать один раз. Сейчас bcpu-engine.ts и nes-engine.ts повторяют друг друга: оба инстанцируют модуль, оба заводят call(name, ...args), оба читают текст из буфера. Вынесем общее в переиспользуемый загрузчик, а движок-специфичное оставим тонкой надстройкой:
// Переиспользуемое ядро загрузчика: общее для bcpu, NES, netcode и будущих движков.
export type WasmEngine = {
call: (name: string, ...args: number[]) => number;
// Окно поверх линейной памяти. Берётся заново перед каждым чтением: после роста
// памяти старый буфер отвязывается (урок 69).
viewBytes: (ptr: number, len: number) => Uint8Array;
readText: (ptrFn: string, lenFn: string) => string;
};
export async function loadWasmEngine(url: string): Promise<WasmEngine> {
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {});
const raw = instance.exports as Record<string, CallableFunction> & {
memory: WebAssembly.Memory;
};
const decoder = new TextDecoder();
const call = (name: string, ...args: number[]): number =>
(raw[name] as (...values: number[]) => number)(...args);
return {
call,
viewBytes: (ptr, len) => new Uint8Array(raw.memory.buffer, ptr, len),
readText: (ptrFn, lenFn) =>
decoder.decode(new Uint8Array(raw.memory.buffer, call(ptrFn), call(lenFn))),
};
}
Теперь nes-engine.ts это просто отображение глаголов NES на этот загрузчик (runFrame: () => call('nes_run_frame') и так далее), а не копия загрузки и декодирования. То же ядро обслуживает любой движок, который следует контракту. Один раз написать правило про окна и рост памяти, и больше не ошибаться в нём в каждом виджете.
Весь третий проект в браузере
Контракт держит и самый сложный наш движок, сетевую игру. netgame.wasm следует той же пятёрке: netgame_init(seed) это init, netgame_set_link и netgame_set_input это input, netgame_step это шаг, а указатели на два кадра арены и таблицу очков это read. Весь netcode (предсказание, реконсиляция, надёжность поверх UDP, симулятор плохого канала) крутится в Rust прямо в браузере, без единого байта бэкенда. Слева правда сервера, справа мир глазами клиента, ползунки дёргают LinkConfig:
Тот же Rust проходит нативные тесты с golden-трейсами, крутится в терминальном TUI и живёт вот этим виджетом. Одна реализация, три поверхности, и виджет здесь ещё один потребитель контракта.
Крючок для Web3
А теперь главное про будущее, ради чего капстоун и существует. Контракту всё равно, что считает движок. Пять глаголов одинаково ложатся на эмулятор, на сетевую игру и на что угодно ещё, что умеет шагать и отдавать состояние. В разделе про Web3 это сыграет дважды.
Узел блокчейна (эталон examples/our-chain) экспортирует тот же силуэт: chain_init это init, chain_add_tx это load, chain_mine это step, а указатели на заголовок блока и состояние это read. Менти будет майнить блок, видеть рост сложности и реорганизацию форка прямо в браузере. Мини-EVM (эталон examples/our-evm) тоже: загрузи байткод, шагай по опкодам, читай стек, память и расход газа. Опкод-степпер EVM это тот же шаговый режим, что у bcpu, просто машина другая. Новая тема приносит лишь новое ядро и новые имена, а обвязка и скелет виджета переиспользуются один в один. Движок виджетов готов, и он переживёт раздел.
Что ты построил за раздел 23
Капстоун это повод оглянуться. Семьдесят один урок назад ты настраивал rustup. Между тем и этим уроком ты прошёл владение как модель ресурсов, трейты и систему типов, биты и машинный уровень, глубокий core с unsafe и вариантностью, макросы, и собрал три системных проекта: эмулятор учебного процессора с HAL, полноценный эмулятор NES и сетевой игровой движок со своим протоколом. Потом разобрал async как машину состояний, атомики на уровне книги Мары Бос и наконец увёл весь этот Rust в браузер через WebAssembly. Один язык, от первой программы до движка, который читает дизассемблер, не боится атомиков и крутится живьём на странице. Это и был план: не выучить синтаксис, а стать системным инженером, которому видно до самого низа.
Дальше раздел про Web3 ставит на эту базу всю криптоэкономику: свой блокчейн, чужие виртуальные машины, DeFi и HFT на Rust. Но фундамент уже под ногами, и движок виджетов поедет с тобой.
Что унести из урока
Все три наших движка (bcpu, NES, netcode) экспортируют один и тот же силуэт, и это не совпадение, а естественная форма движка за границей WASM. Силуэт сводится к контракту из пяти глаголов: init, load, input, step, read, причём это договорённость о форме, а не общий код, потому что через границу ходят только числа. Раз форма общая, JS-обвязку пишут один раз (загрузчик с call, окнами поверх памяти и чтением текста), а движок-специфичное остаётся тонкой надстройкой, отображением глаголов на этот загрузчик. На этом контракте весь netcode крутится в браузере без бэкенда, и на нём же поедут узел блокчейна и мини-EVM из раздела Web3: контракту всё равно, что внутри движка, поэтому новая тема приносит лишь новое ядро, а не новую обвязку. Раздел 23 на этом закрыт: один язык довёл тебя от первой программы до системного инженера. Дальше Web3 на этом фундаменте.