Компилируем приложения для электронной книги PocketBook в WebAssembly и запускаем их прямо в браузере, не меняя исходный код
Что такое PocketBook
PocketBook — это электронная книга на e-ink экране. У неё есть SDK под названием InkView, которое позволяет писать приложения на C/C++. SDK поставляется в виде заголовочного файла inkview.h и библиотеки. Приложения компилируются под ARM и запускаются на устройстве.
Разработка под реальное устройство - ну такое себе. Есть эмулятор, но работает под виндой. Собрать версию для устройства можно на линуксе (или WSL).
Идея: взять Emscripten, скомпилировать C/C++ код в WebAssembly, и реализовать InkView API на JavaScript с Canvas 2D. Тогда любое приложение, написанное под PocketBook, можно будет запустить в браузере без изменений в исходном коде.
Архитектура
Симулятор состоит из трёх слоёв:
1 | приложение на C/C++ |
Приложение не знает, что работает в браузере. Оно вызывает FillArea, DrawString, OpenFont — как обычно, только вместо реального устройства под капотом оказывается браузерный холст.
inkview.h — API для рисования
inkview.h — это большой заголовочный файл, описывающий всё, что умеет InkView. Вот что там есть.
Экран и очистка
1 | int ScreenWidth(void); |
Стандартный экран PocketBook 6” — это 600×800 пикселей в портретной ориентации.
Рисование примитивов
1 | void DrawPixel(int x, int y, int color); |
Цвет передаётся как int в формате 0xRRGGBB. Поскольку экран e-ink — монохромный с несколькими уровнями серого, настоящие приложения используют оттенки серого: BLACK = 0x000000, DGRAY = 0x555555, LGRAY = 0xaaaaaa, WHITE = 0xffffff.
Шрифты и текст
1 | ifont *OpenFont(const char *name, int size, int aa); |
OpenFont возвращает указатель на шрифт. SetFont устанавливает активный шрифт и цвет текста. DrawTextRect умеет выравнивать текст по горизонтали и вертикали — флаги ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, VALIGN_TOP, VALIGN_MIDDLE, VALIGN_BOTTOM.
Обновление экрана
1 | void FullUpdate(void); |
На реальном устройстве e-ink экран не обновляется сам по себе — нужно явно вызвать FullUpdate или PartialUpdate. FullUpdate делает полную перерисовку с характерным миганием. PartialUpdate быстрее, но оставляет артефакты.
Таймеры
1 | void SetHardTimer(const char *name, iv_timerproc proc, int ms); |
Устанавливает таймер с именем. Если таймер с таким именем уже есть — он перезаписывается. Это удобно для отмены: SetHardTimer("my_timer", NULL, 0) отменяет таймер.
События
Приложение получает события через функцию-обработчик с сигнатурой int handler(int type, int par1, int par2). Она регистрируется через InkViewMain:
1 | int main() { |
Основные события:
| Константа | Описание |
|---|---|
EVT_INIT |
Приложение запущено |
EVT_SHOW |
Нужно нарисовать экран |
EVT_EXIT |
Приложение закрывается |
EVT_KEYPRESS |
Нажата кнопка, par1 — код клавиши |
EVT_POINTERDOWN |
Касание экрана, par1/par2 — координаты |
EVT_POINTERMOVE |
Движение пальца |
EVT_POINTERUP |
Отпустили палец |
EVT_ORIENTATION |
Изменилась ориентация |
Коды кнопок: KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_OK, KEY_BACK, KEY_PREV, KEY_NEXT и другие.
Emscripten
Emscripten — это компилятор, который превращает C/C++ код в WebAssembly. Он поставляется как замена gcc/clang: вместо gcc main.c -o main пишешь emcc main.c -o main.mjs и получаешь .mjs + .wasm, которые можно загрузить в браузере.
1 | emcc -Iinclude -s WASM=1 app.c inkview.o \ |
Несколько ключевых флагов:
-s MODULARIZE=1— вместо глобального модуля генерируется фабричная функцияcreatePocketBookModule(). Это позволяет запускать несколько модулей на одной странице.-s EXPORT_ES6=1— выходной файл в формате ES-модуля, удобно импортировать черезimport.-s INVOKE_RUN=0—main()не вызывается автоматически при загрузке. Мы вызываем его сами, когда всё готово.-s USE_FREETYPE=1— подключает Freetype, скомпилированный под WASM. InkView использует Freetype для рендеринга шрифтов.-O3— оптимизация. Без неё.wasmбудет раза в два больше.
EM_JS — мост между C и JavaScript
Самая интересная часть — макрос EM_JS. Он позволяет прямо в C-файле написать тело JavaScript-функции, которая будет вызываться из C-кода:
1 | EM_JS(void, FillArea, (int x, int y, int w, int h, int color), { |
Это объявляет C-функцию FillArea, тело которой — JavaScript. Когда C-код вызывает FillArea(10, 20, 100, 50, 0), управление уходит в JS, где Module.api — это наш JavaScript-объект с реализацией симулятора.
Строки из C в JS передаются как указатели. Чтобы их прочитать, Emscripten предоставляет UTF8ToString:
1 | EM_JS(ifont*, jsOpenFont, (const char *name, int size, int aa), { |
Обратно — stringToUTF8 для записи строки в память WASM.
Структуры читаются напрямую из памяти WASM через HEAP16, HEAP32:
1 | // imenu: short type(2B) + short index(2B) + char* text(4B) + imenu* sub(4B) |
Вызов C из JavaScript
В обратную сторону — вызов C-функций из JS — используется таблица функций WASM. Когда C-код передаёт указатель на функцию (например, обработчик событий или колбэк таймера), это индекс в таблице:
1 | // Вызов C-обработчика событий |
Именно так работает SetHardTimer: в JS создаётся setTimeout, который при срабатывании вызывает wasmTable.get(tproc)().
JavaScript-симулятор
simulator/inkview-emu.mjs — это ES-модуль, который реализует InkView API через Canvas 2D. Объект api содержит методы: FillArea, DrawLine, DrawTextRect, OpenFont, SetFont, FullUpdate и другие.
1 | function colorToCSS(color) { |
Шрифты загружаются через CSS @font-face, файлы .ttf лежат в simulator/fonts/. Для рендеринга используется LiberationSans — свободный шрифт, совместимый с Arial.
Makefile
Структура Makefile простая: одна цель на приложение.
1 | CC = emcc |
lib/inkview.o компилируется один раз и линкуется с каждым приложением. Важный момент: inkview.o нельзя упаковать в архив (.a) и линковать через -l. EM_JS-символы в .o-файле имеют тип U (undefined) и не разрешаются из архива — нужно передавать .o напрямую.
Для C++ проектов с несколькими файлами перечисляем их все:
1 | HELLOWORLD_SRC = projects/helloworld/src/main.cpp \ |
Загрузка в браузере
В браузере модуль загружается и инициализируется через стандартный ES-импорт:
1 | import createPocketBookModule from './projects/calc/index.mjs'; |
После _main() приложение вызывает InkViewMain(handler), который сохраняет указатель на обработчик. Затем симулятор отправляет EVT_INIT и EVT_SHOW, и приложение начинает рисовать.
Результат
В симуляторе работают несколько приложений:
- demo01 — простая демка с рисованием
- calc — калькулятор с шрифтами и клавиатурным вводом
- touch — демонстрация pointer-событий
- helloworld — многофайловый C++ проект с заголовком, рисованием касаниями и выпадающим меню
Поворот экрана (0°/90°/180°/270°) работает через CSS-трансформацию канваса и передачу EVT_ORIENTATION в приложение. Навигация между приложениями — через URL-хэш: #calc, #helloworld и т.д.
Проект лежит на GitHub: esix/pocketbook-simulator