WebAssembly (Emscripten) 移植¶
EmbeddedGUI 支持通过 Emscripten 编译为 WebAssembly,在浏览器中运行 GUI 演示。这对于在线展示、文档演示和跨平台测试非常有用。
构建环境¶
前提条件¶
Emscripten SDK (emsdk) 已安装并激活
Python 3.x
Make 工具
环境变量¶
# 激活 emsdk
source /path/to/emsdk/emsdk_env.sh
# 或设置 EMSDK_PATH
export EMSDK_PATH=/path/to/emsdk
Emscripten 编译配置¶
构建命令¶
# 构建单个示例
make all APP=HelloSimple PORT=emscripten
# 构建后在 output/ 目录生成:
# - HelloSimple.html (入口页面)
# - HelloSimple.js (JS 胶水代码)
# - HelloSimple.wasm (WebAssembly 二进制)
# - HelloSimple.data (预加载资源,如有)
build.mk 配置¶
porting/emscripten/build.mk 定义了 Emscripten 特有的编译选项:
# 使用 Emscripten 的 SDL2 端口
COMMON_FLAGS += -s USE_SDL=2
LFLAGS += -s USE_SDL=2
# 内存配置
LFLAGS += -s ALLOW_MEMORY_GROWTH=1
LFLAGS += -s INITIAL_MEMORY=33554432 # 32MB 初始内存
LFLAGS += -s STACK_SIZE=5242880 # 5MB 栈
# 导出函数
LFLAGS += -s EXPORTED_FUNCTIONS='["_main"]'
LFLAGS += -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
# 自定义 HTML 模板
LFLAGS += --shell-file $(EGUI_PORT_PATH)/shell.html
# 预加载资源文件到虚拟文件系统
LFLAGS += --preload-file $(OUTPUT_PATH)/app_egui_resource_merge.bin@app_egui_resource_merge.bin
Makefile.emscripten¶
porting/emscripten/Makefile.emscripten 是 Emscripten 专用的构建规则文件,替代 PC 平台的 Makefile.base。主要区别:
编译器使用
emcc而非gcc输出目标为
.html而非可执行文件排除 PC 平台的
main.c和egui_port_pc.c,使用 Emscripten 专用版本使用 per-app OBJDIR 避免重复编译共享核心库
浏览器事件映射¶
主循环适配¶
浏览器环境不允许阻塞式主循环。Emscripten 使用 emscripten_set_main_loop 替代 while(1):
static void main_loop_iteration(void)
{
egui_polling_work(&core);
VT_sdl_refresh_task();
if (VT_is_request_quit())
{
emscripten_cancel_main_loop();
}
}
int main(int argc, const char *argv[])
{
printf("Hello, egui! (WebAssembly)\n");
strcpy(input_file_path, "app_egui_resource_merge.bin");
VT_init();
egui_init(&core, egui_pfb);
egui_port_register_core(&core);
egui_port_init();
#if EGUI_CONFIG_FUNCTION_SUPPORT_TOUCH
egui_port_register_touch_driver(&core);
#endif
egui_platform_register(&core, egui_port_get_platform());
egui_display_driver_register(&core, egui_port_get_display_driver());
uicode_disp0_init(&core);
egui_screen_on(&core);
// 0 = requestAnimationFrame (~60fps), 1 = simulate infinite loop
emscripten_set_main_loop(main_loop_iteration, 0, 1);
VT_deinit();
return 0;
}
当前 emscripten 入口仍采用单屏低层初始化路径:egui_init() 后手动注册 platform / display / touch。新的多屏推荐流程见 多屏方案,其核心入口是 egui_setup_display()。
与 PC 版本的关键区别:
单线程运行(浏览器主线程)
使用
requestAnimationFrame驱动帧循环delay函数为空操作(不能阻塞浏览器)
时间戳¶
使用 emscripten_get_now() 获取高精度时间戳:
static uint32_t em_get_tick_ms(void)
{
return (uint32_t)emscripten_get_now();
}
断言处理¶
浏览器环境下不能使用 while(1) 死循环,改为取消主循环并退出:
static void em_assert_handler(const char *file, int line)
{
printf("Assert@ file = %s, line = %d\n", file, line);
emscripten_cancel_main_loop();
emscripten_force_exit(1);
}
触摸/鼠标事件¶
Emscripten 的 SDL2 端口自动将浏览器鼠标和触摸事件映射为 SDL 事件,与 PC 模拟器共享同一套 SDL 事件处理代码。
资源加载¶
外部资源通过 Emscripten 的 --preload-file 预加载到虚拟文件系统,代码中使用标准 fopen/fread 访问:
strcpy(input_file_path, "app_egui_resource_merge.bin");
// 后续通过 fopen(input_file_path, "rb") 访问
HTML 模板¶
porting/emscripten/shell.html 提供了简洁的 HTML 模板:
全屏黑色背景,居中显示 Canvas
加载状态指示器(spinner + 进度)
WebGL 上下文丢失自动恢复
响应式布局,适配不同屏幕
模板中的 {{{ SCRIPT }}} 占位符会被 Emscripten 替换为生成的 JS 代码。
wasm_build_demos.py 批量构建¶
scripts/web/wasm_build_demos.py 用于批量构建所有示例的 WASM 版本。
用法¶
# 构建所有示例
python scripts/web/wasm_build_demos.py
# 指定 emsdk 路径
python scripts/web/wasm_build_demos.py --emsdk-path /path/to/emsdk
# 指定输出目录
python scripts/web/wasm_build_demos.py --output-dir web/demos
# 只构建指定示例
python scripts/web/wasm_build_demos.py --app HelloSimple
`HelloCustomWidgets` 的 WASM 构建与发布已迁移到独立仓库 `EmbeddedGUI_Widgets`。
工作流程¶
扫描
example/目录获取示例列表;默认整站构建会跳过HelloUnitTest对每个示例:
生成资源文件(
make resource)使用 Emscripten 编译(
make all PORT=emscripten)复制输出文件到部署目录
使用 per-app OBJDIR 优化构建产物复用
HelloBasic、HelloVirtual这类多子应用示例会按家族顺序构建,共享中间产物或避免共享输出互相覆盖在输出目录生成
demos.json,供web/index.html、basic.html、examples.html读取
并行构建¶
脚本使用 ProcessPoolExecutor 并行构建独立示例;多子应用家族则保持顺序构建,以兼顾速度和输出隔离。
GitHub Pages 部署¶
目录结构¶
批量构建后的部署目录结构:
web/
├── index.html # 首页
├── basic.html # HelloBasic 聚合页
├── examples.html # 独立示例聚合页
├── widgets repo # HelloCustomWidgets 已迁移到独立仓库 `EmbeddedGUI_Widgets`
├── doc-render.js # README 渲染
├── i18n.js # 多语言切换
├── style.css # 页面样式
├── lib/
└── demos/
├── demos.json
├── HelloSimple/
│ ├── HelloSimple.html
│ ├── HelloSimple.js
│ ├── HelloSimple.wasm
│ └── HelloSimple.data
├── HelloActivity/
│ └── ...
└── HelloBasic_button/
└── ...
CI 配置¶
在 GitHub Actions 中自动构建和部署:
- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
- name: Build WASM demos
run: python scripts/web/wasm_build_demos.py
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: web
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
本地预览¶
# 构建单个示例后,预览 output/ 下的 HTML
cd output
python3 -m http.server 8000
# 浏览器打开
# http://localhost:8000/HelloSimple.html
# 构建整站后,直接启动 web/ 下的本地服务器
python web/start_server.py
Windows 本地 emsdk 工作流¶
推荐先在仓库根目录准备本地 emsdk:
python scripts\setup_env.py --python-mode none --install-emsdk
Windows 下 porting/emscripten/build.mk 会通过 python scripts/web/emcc_wrapper.py 优先使用仓库内的 tools\emsdk。因此直接执行以下命令即可,不要求先手动激活当前 shell:
make all APP=HelloSimple PORT=emscripten
scripts/web/wasm_build_demos.py 也会优先复用本地 emsdk。只有在当前终端手动执行 emcc -v、em++ -v 等命令时,才需要额外运行:
call tools\emsdk\emsdk_env.bat
如需跳过 Emscripten 检查,可在环境脚本中使用:
python scripts\setup_env.py --python-mode none --skip-emsdk
SDL2 端口缓存排障¶
当前 WASM 端口使用 -s USE_SDL=2。首次构建时,Emscripten 会在 tools/emsdk/upstream/emscripten/cache/ports/ 下载 SDL2 端口,因此第一次构建可能明显更慢。
如果下载过程中网络中断,或者历史代理残留导致 SDL2 端口缓存变成半成品,常见现象是构建长时间停在 SDL2 port 阶段。可删除以下缓存后重试:
tools/emsdk/upstream/emscripten/cache/ports/sdl2/
tools/emsdk/upstream/emscripten/cache/ports/sdl2.zip
清理后重新执行:
make all APP=HelloSimple PORT=emscripten
注意事项¶
Emscripten 构建禁用了录制测试(
EGUI_CONFIG_FUNCTION_RECORDING_TEST=0)浏览器中
delay为空操作,不会阻塞初始内存设为 32MB,启用了
ALLOW_MEMORY_GROWTH允许动态增长栈大小设为 5MB,足够大多数 GUI 应用使用
WASM 文件通常比原生二进制大,但经过 gzip 压缩后传输量可接受