
Web для Flutter-платформы с одной стороны является очень хорошо изученной платформой (поскольку Dart создавался как альтернатива JavaScript и изначально компилировался в JS и предусматривал возможности взаимодействия с JS-объектами и функциями, а также с DOM браузера), но в действительности и сейчас это Terra Incognita из-за большого потенциала интеграции с веб-платформой (как на уровне API HTML5, так и с использованием технологий WebAssembly). В этой статье мы обсудим некоторые аспекты взаимодействия Dart.Flutter-кода с WebAssembly-модулями, поговорим о компиляции Flutter-приложений в WASM и о том, как можно компилировать C-библиотеку для использования во Flutter-приложениях.
Сначала поговорим о самой технологии WebAssembly. Она создавалась для выполнения произвольного кода в среде браузера (и не только, например есть wasmtime — среда выполнения для автономного выполнения wasm-кода или встраивания в приложения на C/C++, Python, Go, Rust, .Net), при этом технология основана на стековой виртуальной машине и модели памяти, независимой от исходного языка программирования. Согласно исследованию расхода энергии, выполнение WebAssembly-кода на мобильном браузере более энергоэффективно, чем аналогичного JavaScript. Для компиляции исходных текстов (есть мнемонический язык самого wasm, а также скриптовый язык AssemblyScript, но в большинстве случаев приложения разрабатываются на других языках и компилируются в байткод с помощью emscripten или с указанием целевой платформы для компилятора языка, если она поддерживается). Для доступа к возможностям среды выполнения (например, браузера) используется интерфейс WASI (Web Assembly System Interface), который предоставляет доступ к абстракциям, таким как объектная модель документа, файлы и информация о системе, объекты WebGL, доступ к информации от сенсоров, камере и т.п. Поддержку функций WebAssembly в браузерах можно посмотреть здесь. Поскольку wasm может выполняться не только на веб-платформе, то он может стать альтернативным способом создания переносимых приложений.
Из важных ограничений WebAssembly можно обозначить отсутствие изначальной поддержки автоматической сборки мусора (которая может быть реализована только непосредственно в байт-коде), но сейчас проводится работа над реализацией wasm-gc в Chrome и Firefox и есть экспериментальные флаги в V8 для включения поддержки сборки мусора.
Перед тем, как обсуждать использование WASM в Flutter приложениях, посмотрим какие возможности есть по взаимодействию Dart и WebAssembly. Для этого создадим простое приложение на C и скомпилируем его в байт-код wasm.
#include "stdio.h" int sum(int a,int b) { return a+b; } int main() { printf("Hello, Web Assembly, 2+2=%d\n", sum(2,2)); return 0; }
Установим emscripten SDK по инструкции с этой страницы.
git clone https://github.com/emscripten-core/emsdk.git cd emsdk ./emsdk install latest export PATH=`pwd`:$PATH emcc test.c -o test.html
Для тестирования нужно помнить, что в браузере по умолчанию запрещено обращаться к file URI и нужно либо открыть браузер с отключением CORS (в Chrome —disable-web-security), либо запустить локальный nginx (например, через docker run -d -p 8000:80 -v `pwd`:/usr/share/nginx/html nginx). После перехода по адресу http://localhost:8000/test.html будет запущена страница emscripten по умолчанию, которая показывает вывод консоли Hello, Web Assembly, 2+2=4.
Для консольных приложений Dart можно использовать пакет wasm (аналог dart:ffi) для загрузки wasm-модулей, поиска и вызова экспортированных функций. Пакет использует wasmer runtime и предоставляет возможность вызова системных функций и взаимодействия с файловой системой. Для простого консольного приложения добавим wasm в dependencies (pubspec.yaml), выполним установку и настройку пакета и его зависимостей (на примере Ubuntu):
apt install rustc cargo dart pub get dart run wasm:setup
Теперь мы сможем получить доступ к wasm-модулю:
final brotliPath = Platform.script.resolve('test.wasm'); final moduleData = File(brotliPath.path).readAsBytesSync(); final module = WasmModule(moduleData); final instance = module.instantiate().enableWasi().build();
После получения объекта instance можно через него получать Function-объекты вокруг wasm-функций instance.lookupFunction('sum'),а также работать с динамической памятью instance.memory:
-
grow— увеличить резервирование памяти до заданного значения (должно быть кратноWasmMemory.kPageSizeInBytes) -
view— получить объект для манипуляции памятью (например, заполнение байтовыми данными черезsetRange)
Указатель на свободное место в памяти может быть получен через memory.lengthInBytes, относительно него будут рассчитываться указатели для передаваемых и получаемых данных из функции. Для передачи дополнительных данных в вызываемую функцию память необходимо расширить через grow, и затем заполнить с помощью memory.view.setRange. Для вызова внешних функций можно использовать полученное значение из lookupFunction. Например, для нашего примера:
final sum = module.lookupFunction('sum'); println(sum(2,2));
Теперь попробуем сделать Dart for Web и сделаем вызов wasm-функции. Для этого будем использовать JS-связывания Dart. Для начала создадим новый проект Dart for Web:
dart create -t web wasmtest cd wasmtest dart pub global activate webdev webdev serve
Загрузка wasm-модуля выполняется через вызов статического метода instantiate от объекта WebAssembly, создадим обертку для его вызова из dart (используется package:js/js.dart), либо можем использовать JS-метод для загрузки wasm-объекта и в дальнейшем обращаться к нему через context (в dart:js).
@JS('WebAssembly.instantiate') external Object instantiate(Object bytesOrBuffer, Object import);
Также можно использовать пакет wasm_interop, который внутри себя выполняет все необходимые преобразования. Создадим класс-обертку для загрузки wasm из файлов, расположенных на том же сервере (в случае с Flutter также можно будет использовать возможности загрузки wasm-файлов из assets через rootBundle):
import 'dart:html'; import 'package:wasm_interop/wasm_interop.dart'; class WasmLoader { WasmLoader({required this.path}); late Instance? _wasmInstance; final String path; Future<bool> initialized() async { try { final data = await HttpRequest.request(path, method: 'GET', responseType: 'arraybuffer'); _wasmInstance = await Instance.fromBufferAsync(data.response, importMap: {}); return isLoaded; } catch (exc) { print('Error on wasm initialization ${exc}'); } return false; } bool get isLoaded => _wasmInstance != null; Object? callfunction(String name, {List<Object>? arguments}) { if (isLoaded) { final func = _wasmInstance?.functions[name]; final arg = arguments ?? []; switch (arg.length) { case 0: return func?.call(); case 1: return func?.call(arg[0]); case 2: return func?.call(arg[0], arg[1]); case 3: return func?.call(arg[0], arg[1], arg[2]); case 4: return func?.call(arg[0], arg[1], arg[2], arg[3]); } } return null; } } void main() { Future(() async { final wasm = WasmLoader(path: 'test.wasm'); await wasm.initialized(); print(wasm.callfunction('sum', arguments: const [2, 4])); }); }
Обратите внимание, что при компиляции C в wasm нужно явно перечислить экспортируемые символы (и добавить перед ними символ подчеркивания) и собрать standalone WASM (включается по умолчанию при выводе результата в файл с расширением wasm). Также, если в исходном файле нет функции main, нужно добавить опцию —no-entry. Оставим только функцию sum в исходном тексте на C и выполним компиляцию:
emcc test.c -o test.wasm --no-entry -s EXPORTED_FUNCTIONS=_sum
Теперь при обновлении страницы (http://localhost:8080) мы увидим в консоли значение 6 (результат обращения к wasm-функции sum).
Аналогичный подход может использоваться также для Flutter-приложений, в этом случае инициализация может выполняться в методе didChangeDependencies() async, а результат сохраняться как значение Future или отправляться в Stream (обратите внимание, что вызов внешней функции является синхронным). Например, мы можем сделать следующий виджет счетчика для использования wasm-функции, для загрузки .wasm будет использоваться (await rootBundle.load('assets/test.wasm')).buffer.
class WasmWidget extends StatefulWidget { const WasmWidget({Key? key}) : super(key: key); @override State<WasmWidget> createState() => _WasmWidgetState(); } class _WasmWidgetState extends State<WasmWidget> { late final WasmLoader _loader; StreamController<int> data = StreamController(); int currentValue = 0; @override void didChangeDependencies() async { super.didChangeDependencies(); _loader = WasmLoader(path: "test.wasm"); await _loader.initialized(); data.add(0); } void increment() { currentValue = _loader.callfunction("sum", arguments: [currentValue, 1]) as int; data.add(currentValue); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: StreamBuilder( stream: data.stream, builder: (_, value) => Column( children: [ Text("Current Value is ${value.data}"), ElevatedButton( onPressed: increment, child: const Text('Increment'), ), ], ), ), ), ); } }
Альтернативно для Android может использовать плагин flutter_wasm (по информации с pub.dev больше не поддерживается, но можно подсмотреть основные идеи). Он также основан на wasmer runtime и после установки требует настройки через flutter pub run flutter_wasm:setup), пример приложения можно посмотреть здесь. Для инициализации wasm-файлов из набора байтов также можно использовать assets.
Для компиляции библиотек, основанных на autoconf необходимо выполнить последовательность команд:
emconfigure ./configure emmake make emcc project.o -o project.wasm
При использовании cmake необходимо выполнить подготовку сборочных файлов (mkdir build && cmake -S . -B build) и затем в каталоге build выполнить emmake make и emcc project.c -o project.wasm
Важно отметить, что emscripten не только компилирует алгоритмическую часть кода, но и подменяет некоторые API через механизм портов, например поддерживается библиотека SDL с подменой запросов (для браузера) в соответствующие события и вызовы методов для canvas. При этом для рисования используется объект canvas, который был сохранен в WASM-модуль (в свойство canvas), хороший пример можно посмотреть здесь (также там выполняется интеграции main_loop для emscripten-кода с обновлением кадра). Сам объект Canvas во Flutter Web-приложение может быть добавлен через интеграцию платформенного view:
import 'dart:ui' as ui; import 'dart:html' as html; //... ui.platformViewRegistry.registerFactory('canvas', (id) => html.CanvasElement()..width=512..height=512);
Также можно подключить wasm-код, который использует библиотеку SDL (в этом случае в модуль будет необходимо записать свойство canvas и связать его с объектом для отрисовки графических изображений), при этом при компиляции emcc нужно дополнительно указать опции:
-
-s USE_SDL=2— транслировать вызовы SDL на HTML Canvas -
-s USE_SDL_IMAGE=2— добавить поддержку вывода изображений -
-s SDL2_IMAGE_FORMATS='["png"]'— список поддерживаемых типов изображений -
-s USE_SDL_TTF=2— поддержка шрифтов TrueType (и отображения текста)
Полный список поддерживаемых портов можно найти здесь.
Аналогично может быть добавлена библиотека для сложных вычислений, в ней преобразований не потребуется, но нужно внимательно смотреть за экспортируемыми символами и при вызовах правильно использовать указатели (передаются смещениями в выделяемой памяти).
А что насчет компиляции Dart в WebAssembly вместо JS? Пока все не очень хорошо, поддержка еще очень далеко от использование в production, но тем не менее, существует проект dart2wasm, который создает вариант компилятора для этой целевой архитектуры. Для компиляции Dart-исходного текста (для консольных приложений на Dart) можно использовать следующий сценарий:
git clone https://github.com/dart-lang/sdk cd sdk dart --enable-asserts pkg/dart2wasm/bin/dart2wasm.dart ../test.dart ../test.wasm
Для запуска скомпилированного файла должна быть включена экспериментальная поддержка GC, Stack Switching и возможности рефлексии в d8:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH=`pwd`/depot_tools:$PATH fetch v8 cd v8 make native cd .. v8/out/native/d8 --experimental-wasm-gc --wasm-gc-js-interop \ --experimental-wasm-stack-switching \ --experimental-wasm-type-reflection \ pkg/dart2wasm/bin/run_wasm.js -- ../test.wasm
Во второй части статьи мы подключим к Flutter-приложению эмулятор GameBoy на SDL и обсудим, как обеспечить обработку жестов в canvas-виджете (с SDL) совместно с другими Flutter-виджетами, реализовать работу со звуком и видео во встраиваемом wasm-приложении, а также поговорим про использование других портов для emscripten.
И в заключение приглашаю всех желающих на бесплатный урок по теме: «Сферический Flutter в вакууме. Создаем свою систему координат для RenderObject».
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/693572/
Добавить комментарий