Skip to content

WASM 编译管线 — 代码走读

Makefile WASM 构建目标

项目根目录的 Makefile 中定义了 WASM 构建的相关目标。下面逐段解读。

构建流程总览

编译器与标志定义

makefile
# WASM 专用编译器设置
WASM_CC := clang
WASM_TARGET := --target=wasm32-unknown-unknown
WASM_CFLAGS := -O2 -fno-builtin -ffreestanding \
               -DMODERN=1 -DWASM=1 \
               $(INCLUDES)

逐行说明

  • WASM_CC := clang — 使用 LLVM Clang 编译器,因为它原生支持 WebAssembly 目标
  • WASM_TARGET := --target=wasm32-unknown-unknown — 指定 WASM 32位目标平台
  • WASM_CFLAGS 中的各个标志:
    • -O2:二级优化,在编译速度和运行性能间取得平衡
    • -fno-builtin:防止编译器用内置实现替换 GBA 自定义的内存操作函数
    • -ffreestanding:告诉编译器不依赖标准库运行时
    • -DMODERN=1 -DWASM=1:启用 WASM 兼容代码路径

源文件收集与编译规则

makefile
# 收集所有需要编译的 C 源文件
WASM_SRCS := $(filter-out $(EXCLUDE_SRCS), $(ALL_SRCS))
WASM_OBJS := $(WASM_SRCS:.c=.wasm.o)

# 模式规则:将 .c 编译为 .wasm.o
%.wasm.o: %.c
	$(WASM_CC) $(WASM_TARGET) $(WASM_CFLAGS) -c $< -o $@

关键细节

  • EXCLUDE_SRCS 排除不需要编译到 WASM 的文件(如 ARM 汇编文件)
  • 每个源文件独立编译,最终链接时合并
  • -c 标志告诉编译器只编译不链接

链接规则

makefile
# WASM 链接
WASM_LDFLAGS := --no-entry \
                --allow-undefined \
                --initial-memory=268435456 \
                --export=AgbMain \
                --export=WasmRunFrame \
                --export-all

output.wasm: $(WASM_OBJS)
	wasm-ld $(WASM_LDFLAGS) -o $@ $^

链接过程

  1. 所有 .wasm.o 文件作为输入传给 wasm-ld
  2. --no-entry 表明没有标准入口点
  3. --allow-undefined 允许未解析的符号(GBA BIOS 函数)
  4. --initial-memory=268435456 分配 256MB 初始内存
  5. --export=AgbMain--export=WasmRunFrame 显式导出两个关键函数
  6. --export-all 额外导出所有全局符号

源码目录结构

关键函数索引

函数名源文件位置说明
AgbMainsrc/main.c游戏主入口,初始化所有子系统
WasmRunFramesrc/main.c逐帧执行函数,由 JS requestAnimationFrame 回调
RunTaskssrc/task.c执行所有活跃任务(协程调度器)
VBlankIntrWaitsrc/gba/bg.c等待垂直空白中断
LZ77UnCompVramsrc/util.cLZ77 解压到 VRAM
LoadPalettesrc/palette.c加载调色板到 Palette RAM
CopyToBgTilemapBuffersrc/bg.c复制图块映射到背景缓冲区
CreateSpritesrc/sprite.c创建精灵,写入 OAM 数据

构建产物分析

产物大小分析

  • 单个 .wasm.o 文件通常几 KB 到几十 KB
  • 最终 .wasm 模块约 10-20MB,包含:
    • 编译后的游戏逻辑代码
    • 通过 资源转换管线 嵌入的图形数据
    • 字符串常量和查找表
    • 函数索引和类型信息等 WASM 元数据

与 JS 运行时的接口

WASM 模块导出的函数通过以下方式与 JavaScript 交互:

javascript
// 简化的加载和调用流程
const wasmBytes = await fetch('output.wasm');
const { instance } = await WebAssembly.instantiate(wasmBytes, {
  env: {
    // JS 提供给 WASM 的导入函数(用于模拟 GBA BIOS)
    // 这些符号因为 --allow-undefined 在链接时未解析
  }
});

// 调用游戏入口
instance.exports.AgbMain();

// 帧循环(渲染在 WASM 内,宿主只搬运 RGBA)
function gameLoop() {
    instance.exports.WasmRunFrame();
    instance.exports.WasmRenderFrame();   // WASM 内部合成 RGBA 帧
    ctx.putImageData(image, 0, 0);         // 把 RGBA 搬到 Canvas
    requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

相关阅读