Skip to content

源码兼容与适配 — 练习

练习 1:分析 #if WASM 条件编译守卫

src/main.c 中搜索所有 #if WASM 块,回答以下问题:

  1. AgbMain() 中,WASM 构建跳过了哪些硬件初始化步骤?为什么这些步骤在浏览器中不需要?
  2. GBA 构建中,AgbMain() 的主循环是 for (;;) WasmRunFrame();。WASM 构建中为什么不能使用无限循环?AgbMain() 是如何提前退出的?
  3. WasmRunFrame() 函数末尾,WASM 构建调用 VBlankIntr() 而 GBA 构建调用 WaitForVBlank()。这两者有什么本质区别?
参考答案
  1. 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 模拟
  2. 无限循环问题:

    • WebAssembly 在浏览器中运行在主线程上,无限循环会导致浏览器冻结(无法处理事件/渲染)
    • WASM 构建 AgbMain() 在初始化完成后直接 return,退出 WASM 执行,将控制权交回 JavaScript
    • JavaScript 的 requestAnimationFrame 回调每帧调用 WasmRunFrame(),将原来的无限循环拆分为逐帧执行
  3. VBlank 处理差异:

    • WaitForVBlank()(GBA):通过 BIOS 调用 VBlankIntrWait() 让 CPU 进入低功耗等待状态,直到硬件 VBlank 中断触发。这是真正的阻塞等待
    • VBlankIntr()(WASM):直接调用 VBlank 中断处理函数(处理 DMA 传输、回调等),然后立即返回。因为在 WASM 中不需要等待硬件信号——帧执行完毕后直接由 JS 调用渲染

练习 2:编写一个新的垫片声明

假设游戏代码中新增了一个使用 qsort() 函数的模块,编译 WASM 时 Clang 报出了内建声明冲突警告。

  1. 分析为什么 qsort 需要 include/wasm/ 中的垫片声明
  2. 编写 include/wasm/stdlib.h 中需要添加的声明
  3. 解释 -Wno-incompatible-library-redeclaration 编译标志的作用,以及为什么项目选择使用垫片而不是依赖这个标志来抑制所有警告
参考答案
  1. 垫片需求分析:

    • Clang 为 qsort 自动生成内建声明:void qsort(void *, size_t, size_t, int (*)(const void *, const void *))
    • 如果游戏代码中有自己的 qsort 声明(或在 GBA 头文件中有不同写法的声明),可能导致类型不匹配的编译警告
    • 通过 include/wasm/stdlib.h 提供一致的声明,让 Clang 使用我们指定的签名,避免内建声明干扰
  2. 需要添加的声明:

    c
    // 在 include/wasm/stdlib.h 中添加
    void qsort(void *base, size_t nmemb, size_t size,
               int (*compar)(const void *, const void *));

    注意保持参数类型与 Clang 内建声明一致,使用 size_t 类型。

  3. 编译标志与垫片的关系:

    • -Wno-incompatible-library-redeclaration 抑制"库函数重声明不兼容"警告
    • 这是对现有代码的兜底措施——某些 GBA 时代的函数声明可能无法完全匹配 Clang 的内建声明
    • 项目选择同时使用垫片和警告抑制:垫片解决已知冲突(如 memcpysprintf),警告抑制处理边缘情况
    • 如果仅依赖警告抑制,代码中的类型不匹配可能在将来导致实际的运行时错误(如参数传递约定不同)
    • 垫片方案更安全:它确保了编译器使用正确的函数签名

练习 3:设计新的 WASM 条件编译守卫

假设你需要在 WASM 构建中添加游戏暂停功能:当浏览器标签页不可见时(document.hidden),暂停游戏执行。

  1. 在 C 源码层面,你需要在哪里添加 #if WASM 守卫?给出具体的文件和位置建议
  2. 在 JavaScript 层面,你如何将浏览器的 visibilitychange 事件传递给 WASM 模块?给出实现方案
  3. 如何确保这个新功能不影响原始 GBA 构建?列出需要验证的测试点
参考答案
  1. C 源码层面的守卫位置:

    • 方案 A(推荐):在 src/main.cWasmRunFrame() 中添加检查
      c
      void WasmRunFrame(void) {
      #if WASM
          if (gWasmPaused) return; // 新增:检查暂停标志
      #endif
          ReadKeys();
          // ... 正常帧逻辑 ...
      }
    • 需要在全局头文件中声明 gWasmPaused 变量:
      c
      #if WASM
      extern bool8 gWasmPaused;
      #endif
    • 方案 B(更底层):在 ReadKeys() 中处理,将暂停视为一种特殊的输入状态
  2. 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 访问。

  3. 确保 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 音频硬件的模拟?