Skip to content

WASM 编译管线 — 练习

练习 1:追踪单个 .c 文件的编译过程

目标:理解一个 C 源文件如何从源代码变为 WASM 目标文件。

任务

  1. 在项目根目录找到 Makefile,定位 .c.wasm.o 的编译规则
  2. 选择一个简单的源文件(如 src/main.c),写出完整的编译命令
  3. 说明 -DMODERN=1-DWASM=1 在该文件中触发了哪些条件编译分支
参考答案

步骤分析

  1. 在 Makefile 中查找 .wasm.o 的模式规则,通常形如:
makefile
%.wasm.o: %.c
	$(CC) --target=wasm32-unknown-unknown $(CFLAGS) -DMODERN=1 -DWASM=1 -c $< -o $@
  1. src/main.c 为例,完整编译命令:
bash
clang --target=wasm32-unknown-unknown -O2 -fno-builtin -ffreestanding \
      -Iinclude -DMODERN=1 -DWASM=1 \
      -c src/main.c -o src/main.wasm.o
  1. 条件编译分支示例:
c
#ifdef WASM
    // WASM 特定实现:通过 WASM 线性内存地址访问
    *(volatile u32*)(0x04000000) = value;
#else
    // 原生 GBA:直接访问硬件寄存器
    REG_DISPCNT = value;
#endif

#ifdef MODERN
    // 现代实现:使用标准 C 函数
    memcpy(dst, src, size);
#else
    // 原始实现:使用 GBA BIOS 调用
    CpuFastCopy(src, dst, size);
#endif

关键理解:-DMODERN=1 使代码使用标准 C 库函数替代 GBA BIOS 调用;-DWASM=1 将硬件访问重定向到 WASM 线性内存中的模拟地址。


练习 2:分析 wasm-ld 链接选项

目标:深入理解每个链接选项对最终 WASM 模块的影响。

任务

  1. 列出 wasm-ld 的所有链接选项,逐个分析其必要性
  2. 如果去掉 --allow-undefined,会发生什么?
  3. 为什么 --initial-memory 设置为 268435456(256MB)?
参考答案

选项逐一分析

选项必要性分析
--no-entry必要。WASM 模块由 JS 侧调用导出函数驱动,不需要标准入口点
--allow-undefined必要。游戏代码调用 GBA BIOS 函数(如 LZ77UnCompVramBgAffineSet),这些符号在编译时不存在
--initial-memory=268435456必要。256MB 提供足够空间存放解压后的图形资源、地图数据等
--export=AgbMain必要。JS 需要调用此函数初始化游戏
--export=WasmRunFrame必要。JS 的渲染循环每帧调用此函数
--export-all调试用。导出所有全局符号,便于调试和直接访问内部函数

去掉 --allow-undefined 的后果

链接会失败,报出大量 "undefined symbol" 错误。游戏代码中引用了 GBA BIOS 函数,这些函数的实现在 JS 侧而非 WASM 模块中。--allow-undefined 允许这些符号在链接时保持未解析状态,运行时由 JS 通过 WASM 表导入提供。

256MB 初始内存的原因

  • GBA 原始内存:32KB WRAM + 256KB 外部 RAM + 96KB VRAM = 约 384KB
  • WASM 版本需要更多空间因为:
    • 图形资源解压后占用大量空间(原始 ROM 约 16MB,解压后更大)
    • 所有 VRAMOAMPalette 数据都在线性内存中模拟
    • 运行时数据结构(战斗状态、地图数据等)需要额外空间

练习 3:对比 GBA ARM 构建与 WASM 构建

目标:理解同一份源代码如何通过不同编译配置生成不同目标。

任务

  1. 在 Makefile 中找到 GBA 原生构建规则(ARM 目标)
  2. 对比 ARM 构建与 WASM 构建的编译标志差异
  3. 分析为什么某些文件只能用特定的构建配置编译
参考答案

编译标志对比

维度GBA ARM 构建WASM 构建
目标三元组arm-none-eabiwasm32-unknown-unknown
处理器ARM7TDMI (32-bit RISC)WASM 虚拟机
宏定义-DGBA=1-DMODERN=1 -DWASM=1
优化级别-O2-O2
标准库无(裸机)无(-ffreestanding
链接器GNU ld (ARM)wasm-ld
输出格式ELF → ROM (.gba)WASM (.wasm)
内联汇编ARM 汇编 (__asm__)不可用,需纯 C 替代

只能用特定配置编译的文件

  • src/gba/*.c 中的 BIOS 函数实现 — ARM 构建需要这些,WASM 构建跳过(由 JS 模拟)
  • src/agb_flash.c(Flash 存储访问)— WASM 构建使用 JS 模拟的存储接口
  • 包含 ARM 内联汇编的文件 — WASM=1 宏会启用纯 C 替代实现

关键差异的理解

核心洞察:MODERN=1WASM=1 这两个宏是使同一份源代码能在完全不同的硬件/虚拟机上运行的关键抽象层。


拓展挑战

  1. 自定义编译标志:尝试在 Makefile 中添加 -g 调试信息标志,观察 WASM 模块体积的变化,并思考如何在浏览器 DevTools 中调试 WASM。

  2. 符号导出分析:使用 wasm-objdump -x output.wasm 列出所有导出符号,统计导出函数数量,并找出除了 AgbMainWasmRunFrame 之外最常用的导出函数。

  3. 内存布局优化:当前 --initial-memory 设为 256MB。分析实际内存使用情况,尝试找到更小的合理值,并测量启动时间的变化。

  4. 编译速度优化:研究如何使用 ccache 或并行编译(make -j)加速 WASM 构建,记录构建时间对比。

  5. 交叉对比验证:对比同一帧中 ARM 构建和 WASM 构建的游戏状态,验证两者在逻辑上的一致性。这需要理解 GBA 硬件模拟 中的内存映射。