Vange-rs: взгляд на реализацию WebAssembly в Rust

Kate

Administrator
Команда форума
Вангеры - это одна из самых почитаемых и технологичных игр своего времени (1998 год), и она продолжает жить и развиваться. Благодаря сплоченному сообществу игра получила множество усовершенствований: HD, 60 FPS, новые сетевые режимы и много другое. Vange-rs один из интереснейших проектов по Вангерам. Это rust версия игры, основной изюминкой которой является 3D рендер основанный на wgpu.
Трассировка лучей (vange-rs)
Трассировка лучей (vange-rs)
wgpu - кроссплатформенное, безопасное графическое API написанное исключительно на rust. Может использовать Vulkan, Metal, D3D12, D3D11, OpenGL ES3 нативно, а также WebGPU и WebGL в WebAssembly.
Можно сказать, что этот проект показывает всю мощь и возможности библиотеки. Vange-rs содержит исчерпывающее описание технологий применимых для рендеринга миров Вангеров. Даже сама возможность покататься на мехосе в полноценном 3D выглядит очень круто. И вдвойне было бы круто сделать это через браузер.
Тем более это должно быть не сложно, ведь Rust позиционируется как современный язык с поддержкой WebAssembly, однако не все так просто.

Мир безграничных возможностей​

Документация Rust обещает нам поддержку аж целых трёх возможных вариантов компиляции в WebAssembly. (Tier 2: rustc гарантированно собирается).
  • wasm32-unknown-emscripten
  • wasm32-unknown-unknown
  • wasm32-unknown-wasi
wasm32 - архитектура, unknown - вендор, emscripten | unknown | wasi - операционная система. Для всех платформ заявлена поддержка стандартной библиотеки (функции работы с файловой системой, временем и пр.).

wasm32-unknown-emscripten​

Emscripten, пожалуй, старейший и самый развитый инструмент для компиляции из C/C++ в WebAssembly. Из коробки поддерживаются практически все функции posix, виртуальная файловая система, OpenGL ES3. Формально используя emscripten можно скомпилировать проект в рабочий WebAssembly без каких-либо изменений. По факту это совсем не так.
Чтобы скомпилировать проект с использованием emscripten достаточно передать флаг --target wasm32-unknown-emscripten к вызову cargo.
Rust выполняет компиляцию кода приложения, а на последнем этапе использует emcc для линковки в WebAssembly. К сожалению, в компилятор зашит флаг, вызывающий ошибку в случае обнаружения не реализованных символов (ERROR_ON_UNDEFINED_SYMBOLS=1). Из-за этого даже hello-world проект на данный момент не может быть собран с помощью emscripten.
Конфигурационные флаги из rustc добавляются всегда в конец, поэтому ситуацию невозможно исправить даже через RUSTFLAGS. И если проблему символов ещё можно обойти, добавив соответствующую реализацию в cpp файл (как указано по ссылке), то ASSERTIONS=1 будет с нами всегда. Т.е. собрать проект без отладочного кода средствами rust невозможно!
Emscripten позволяет изменить это поведение через переменную окружения о которой мало кто знает:
EMMAKEN_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 --no-entry -s ASSERTIONS=0"
Таким образом эту проблему можно решить, но этого будет мало. Большинство библиотек, которые имеют поддержку WebAssembly пытаются взаимодействовать с браузером через библиотеки-обертки, такие как web-sys и stdweb. Они предоставляют API браузера, например, WebGL через систему типов Rust. web-sys основан на wasm-bindgen и поэтому он не совместим с emscripten.
The wasm-bindgen project is designed to target the wasm32-unknown-unknown target in Rust. This target is a "bare bones" target for Rust which emits WebAssembly as output.
Если вы попробуете запустить web-sys проект использует --target wasm32-unknown-emscripten, то произойдет ошибка:
cannot call wasm-bindgen imported functions on non-wasm targets
wasm-bindgen для работы требует изменения полученного wasm файла, а конкретно "функции заглушки" заменяются на вызовы импортированных функций из js, вот что пишет один из авторов о возможной поддержке emscripten:
AFAIK emscripten wants its own JS shims and all that, and having two systems of managing shims won't mix well.
...
wasm-bindgen doesn't support the emscripten wasm target at this time. I haven't ever tested it myself so I don't know how far off we would be from getting it to work, but it's expected that emscripten doesn't work at the moment.
Таким образом wasm-bindgen фактически не совместим с emscripten, значит и все библиотеки основанные на web-sys тоже. На данный момент это вообще все библиотеки. Поскольку stdweb фактически мертв:
  • winit (библиотека для работы с окнами) отказывается от поддержки stdweb
  • rust не генерирует правильный wasm для stdweb, начиная с nightly-2019-11-25. Уже 2 года как.
Я пробовал использовать stdweb в связке с winit, и ошибка rust (ссылка выше) не дает сделать это. А вот ещё почему vange-rs не будет работать с emscripten:
  • wgpu пока не поддерживает emscripten
  • glow пока не поддерживает emscripten
  • ron не компилируется для emscripten из-за ошибки
Экспертное мнение о поддержке emscripten в rust
Экспертное мнение о поддержке emscripten в rust
Я понял одну вещь, rust сообщество пытается использовать emscripten неправильно. Сильное заявление, но это так. Вся соль emscripten в том, что он предоставляет идентичное окружение что и обычная *nix система, и поэтому пытаться использовать его совместно с обертками вроде web-sys или stdweb не нужно.
Например, в библиотеке wgpu для OpenGL ES3 бэкенда на линуксе используются библиотеки khronos-egl и glow. Когда происходит компиляция в WebAssembly khronos-egl отключается и вместо него используются обертки web-sys и glow. Но, для emscripten это делать не нужно, поскольку он эмулирует полноценную систему с OpenGL ES3. Таким образом нужно просто использовать тот же код что и для нативной платформы. Если думать таким образом, то можно легко добавить поддержку emscripten и в wgpu и в glow.
Сделав это, мы наконец-то сможем скомпилировать vange-rs. Пришлось полностью отказаться от библиотеки winit, поскольку мы используем emscripten, то и создавать окно нам нет необходимости. В случае браузера это всего лишь canvas который мы создадим в index.html. Остается передать WebGL контекст в wgpu. Библиотека имеет достаточно высокую степень абстракции чтобы принять в качестве хэндла окна заглушку.
struct WebWindow {
}

unsafe impl HasRawWindowHandle for WebWindow {
fn raw_window_handle(&self) -> RawWindowHandle {
RawWindowHandle::Web(WebHandle::empty())
}
}

// ...

let window = WebWindow {};
let surface = unsafe { instance.create_surface(&window) };
wgpu будет считать, что окно создано, хотя по факту его нет, есть только контекст WebGL (OpenGL ES3), который вернется через нативный krhonos-egl. Удалив winit мы лишились управления с клавиатуры, поскольку библиотека делала это за нас, но добавить пару обработчиков мы легко сможем вернуть его. Таким образом это действительно работает, хоть и с большими оговорками.
К сожалению, во многих обсуждениях я видел мнение что rust отказывается от wasm32-unknown-emscripten в пользу wasm32-unknown-unknown. Я считаю это мнение не обоснованным, так как у emscripten достаточно много своих плюсов, которые возможно никогда не появятся в unknown, например поддержка asyncify, 3 вида файловых систем на выбор, сетевой стэк и пр.

wasm32-unknown-unknown​

Все внимание сообщества сосредоточено на обожаемых web-sys и wasm-bindgen, ну они-то должны работать из коробки? Ммм, нет... Собирается все действительно без особых проблем, передаем флаг --target wasm32-unknown-unknown и запускаем в браузере:
panic: operation not supported on this platform
Функции работы с файловой системы не поддерживаются.
???
???
Очевидно, что игра не сможет запуститься без файлов. Напомню, что в emscripten это решено довольно просто: по умолчанию используется файловая система в памяти, она реализует стандартные функции работы с файлами, в неё можно загрузить образ диска перед запуском. Это вполне логичное решение и, как кажется, в Rust можно было бы применить схожий подход (использование виртуальной файловой системы), тогда такой код работал бы:
File::create(file).expect(&format!("Unable to create {}", file))
.write(include_bytes!("../file"))
.expect(&format!("Unable to write in {}", file));
У нас нет файлов в файловой системе, но мы могли бы их создать: загрузить через wasm-bidngen или просто вставив в дата секцию. Однако, это не работает, потому что реализация файловой системы в случае с unknown просто отсутствует. И не только файловой системы, так же отсутствует какая-либо реализация для потоков или времени. Даже env_logger не будет работать из коробки, потому что он пытается получить текущее время, а эта функция не реализована.
Весь драматизм в том, что unknown используется не только на архитектуре wasm32, и нельзя сделать исправление только для wasm32. Т.е. варианта два, либо переписать vange-rs полностью отказавшись от библиотек и кода работающих с файловой системой, либо исправить rustc добавив в него поддержку файловой системы в памяти.
Очевидно что мы выбираем второе. Было не просто, особенно если учесть, что я прочитал только избранные главы из rust book. Но в итоге получилось ведь!
Комичный случай, я несколько дней безуспешно пытался собрать компилятор rust, используя команду:
./x.py build library/std --target wasm32-unknown-unknown
Команда отрабатывала, но использовать это компилятор я не мог, из-за ошибки:
error[E0463]: can't find crate for `core`
Оказалось, что нужно передавать одновременно 2 архитектуры (--target x86_64-unknown-linux-gnu --target wasm32-unknown-unknown):
You need to use --target x86_64-unknown-linux-gnu --target wasm32-unknown-unknown". If you don't include the host in --target you can't build build scripts and proc macros that need to be compiled for the host.
Сделав это, я наконец-то получил рабочий компилятор с виртуальной файловой системой. В моем репозитории можно скачать собранный вариант для x86_64-unknown-linux-gnu. Собрав или скачав компилятор, просто регистрируем его в rustup и применяем для проекта:
rustup toolchain link memfs memfs/stage1
cd <project>
rustup override set memfs
А дальше собираем как обычно через сargo build. Вы удивитесь, но winit тоже не способен ловить события клавиатуры из-за ошибки, так что, как и в случае emscripten приходится реализовывать этот функционал самостоятельно, до тех пор, пока не поправят winit.
Решив эти проблемы, мы наконец-то получаем рабочую версию vange-rs для браузера!

wasm32-unknown-wasi​

Поскольку wasm-bindgen не поддерживает ничего кроме unknown, то мы имеем все те же проблемы что и у emscripten. Кроме того, wasi предназначен для node.js, а не для браузера. Поэтому экспериментировать с ним я не стал.

Вместо выводов​

Вероятно, rust еще слишком молод, и поддержка WebAssembly находится на экспериментальном уровне. Не смотря на большое количество потребовавших усилий оба варианты компиляции (emscripten, unknown) работают! Какой из них проще - решать вам, для unknown придется собрать компилятор, а в случае с emscripten придется отказаться от большинства библиотек. Но результат в обоих случаях вас порадует, ведь нет ничего невозможного.
Огромное спасибо сообществу Вангеров и Дмитрию (@kvark) - автору проекта vange-rs и wgpu, за саму возможность покататься на мехосах в 3D и помощь в исправлении ошибок WebGL.
Ссылки

 
Сверху