Appearance
源码兼容与适配 — 练习
练习 1:分析 #if WASM 条件编译守卫
在 src/main.c 中搜索所有 #if WASM 块,回答以下问题:
AgbMain()中,WASM 构建跳过了哪些硬件初始化步骤?为什么这些步骤在浏览器中不需要?- GBA 构建中,
AgbMain()的主循环是for (;;) WasmRunFrame();。WASM 构建中为什么不能使用无限循环?AgbMain()是如何提前退出的? WasmRunFrame()函数末尾,WASM 构建调用VBlankIntr()而 GBA 构建调用WaitForVBlank()。这两者有什么本质区别?
参考答案
WASM 跳过的硬件初始化:
m4aSoundInit():GBA 音频硬件(Sound DMA/FIFO)初始化。浏览器中暂未实现音频模拟,且 WASM 无法直接访问音频硬件EnableVCountIntrAtLine150():设置 GBA LCD 的扫描线中断,用于 VBlank 同步。浏览器中通过requestAnimationFrame实现帧同步,不需要硬件中断InitRFU():无线通信适配器初始化。浏览器环境中不支持 GBA 无线链接RtcInit():实时时钟硬件初始化。WASM 版本不需要 RTC 功能CheckForFlashMemory():检测 Flash 存储芯片类型。WASM 中直接设置gFlashMemoryPresent = TRUE,因为 Flash 存储由 JS 运行时用localStorage模拟
无限循环问题:
- WebAssembly 在浏览器中运行在主线程上,无限循环会导致浏览器冻结(无法处理事件/渲染)
- WASM 构建
AgbMain()在初始化完成后直接return,退出 WASM 执行,将控制权交回 JavaScript - JavaScript 的
requestAnimationFrame回调每帧调用WasmRunFrame(),将原来的无限循环拆分为逐帧执行
VBlank 处理差异:
WaitForVBlank()(GBA):通过 BIOS 调用VBlankIntrWait()让 CPU 进入低功耗等待状态,直到硬件 VBlank 中断触发。这是真正的阻塞等待VBlankIntr()(WASM):直接调用 VBlank 中断处理函数(处理 DMA 传输、回调等),然后立即返回。因为在 WASM 中不需要等待硬件信号——帧执行完毕后直接由 JS 调用渲染
练习 2:编写一个新的垫片声明
假设游戏代码中新增了一个使用 qsort() 函数的模块,编译 WASM 时 Clang 报出了内建声明冲突警告。
- 分析为什么
qsort需要include/wasm/中的垫片声明 - 编写
include/wasm/stdlib.h中需要添加的声明 - 解释
-Wno-incompatible-library-redeclaration编译标志的作用,以及为什么项目选择使用垫片而不是依赖这个标志来抑制所有警告
参考答案
垫片需求分析:
- Clang 为
qsort自动生成内建声明:void qsort(void *, size_t, size_t, int (*)(const void *, const void *)) - 如果游戏代码中有自己的
qsort声明(或在 GBA 头文件中有不同写法的声明),可能导致类型不匹配的编译警告 - 通过
include/wasm/stdlib.h提供一致的声明,让 Clang 使用我们指定的签名,避免内建声明干扰
- Clang 为
需要添加的声明:
c// 在 include/wasm/stdlib.h 中添加 void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));注意保持参数类型与 Clang 内建声明一致,使用
size_t类型。编译标志与垫片的关系:
-Wno-incompatible-library-redeclaration抑制"库函数重声明不兼容"警告- 这是对现有代码的兜底措施——某些 GBA 时代的函数声明可能无法完全匹配 Clang 的内建声明
- 项目选择同时使用垫片和警告抑制:垫片解决已知冲突(如
memcpy、sprintf),警告抑制处理边缘情况 - 如果仅依赖警告抑制,代码中的类型不匹配可能在将来导致实际的运行时错误(如参数传递约定不同)
- 垫片方案更安全:它确保了编译器使用正确的函数签名
练习 3:设计新的 WASM 条件编译守卫
假设你需要在 WASM 构建中添加游戏暂停功能:当浏览器标签页不可见时(document.hidden),暂停游戏执行。
- 在 C 源码层面,你需要在哪里添加
#if WASM守卫?给出具体的文件和位置建议 - 在 JavaScript 层面,你如何将浏览器的
visibilitychange事件传递给 WASM 模块?给出实现方案 - 如何确保这个新功能不影响原始 GBA 构建?列出需要验证的测试点
参考答案
C 源码层面的守卫位置:
- 方案 A(推荐):在
src/main.c的WasmRunFrame()中添加检查cvoid WasmRunFrame(void) { #if WASM if (gWasmPaused) return; // 新增:检查暂停标志 #endif ReadKeys(); // ... 正常帧逻辑 ... } - 需要在全局头文件中声明
gWasmPaused变量:c#if WASM extern bool8 gWasmPaused; #endif - 方案 B(更底层):在
ReadKeys()中处理,将暂停视为一种特殊的输入状态
- 方案 A(推荐):在
JavaScript 层面的实现:
javascript// 在 app.js 中添加 const PAUSE_FLAG_ADDR = /* 导出的 gWasmPaused 地址 */; window.addEventListener('visibilitychange', () => { if (!u8) return; // 设置暂停标志 u8[instance.exports.gWasmPaused] = document.hidden ? 1 : 0; // 页面隐藏时保存存档 if (document.hidden) saveFlashIfChanged(true); });由于
--export-all导出了所有全局符号,gWasmPaused可以直接通过instance.exports.gWasmPaused.value访问。确保 GBA 构建不受影响:
gWasmPaused变量声明在#if WASM守卫内,GBA 构建不会看到它WasmRunFrame()中的暂停检查在#if WASM守卫内- 验证测试点:
make构建 GBA ROM 成功(无未定义符号错误)make compare校验 ROM 完整性不变make wasm构建 WASM 成功- 暂停功能在浏览器中正常工作(标签页切换时游戏暂停)
- 恢复可见后游戏正确继续(不会跳帧或卡死)
- 暂停期间存档正确保存
拓展挑战
- 统计所有
#if WASM/#ifdef WASM守卫的位置和用途,绘制一张完整的适配地图 - 分析
wasm_field_effect_scripts.c中的__attribute__((packed))结构体,验证其内存布局与原始 ARM 汇编字节码一致 - 研究 Clang 的
-Wno-builtin-requires-header标志:它解决了什么具体问题?如果去掉这个标志,哪些文件会报错? - 思考如何添加音频支持:需要在哪些文件中修改
#if WASM守卫?JavaScript 端需要实现哪些 GBA 音频硬件的模拟?