返回「计算机、信息技术与工程」

底层深入-WebGL应用逻辑运行时(JavaScript与WebAssembly)

底层深入 WebGL应用逻辑运行时(JavaScript与WebAssembly)

更多
Markdown 结构化数据
本文目录 9 个章节

底层深入-WebGL应用逻辑运行时(JavaScript与WebAssembly)

# 底层深入-WebGL应用逻辑运行时(JavaScript与WebAssembly)

什么是 “JavaScript 运行时”?

运行时(runtime)= JS 引擎 + 事件循环 + 宿主 API 引擎执行脚本;事件循环调度任务;宿主提供 “外界能力”(网络、文件、WebGL 等)。

在 Web / 小程序里的两层结构

┌─────────────  宿主层  ──────────────┐
│  setTimeout / requestAnimationFrame│
│  wx.request / canvas / WebGL       │
└────────────────────────────────────┘
┌───────────  JS 引擎层  ────────────┐
│  解析(Parser) → 字节码(Interpreter)│
│  → JIT 编译(Hot code) → 机器码     │
│  GC / 隐式类型系统 / PromiseJobs   │
└────────────────────────────────────┘
  • Android 微信小游戏:引擎是 V8
  • iOS 微信小游戏:引擎是 JavaScriptCore (JSC);二者都 没 DOM、只暴露 wx.* API。
  • 小游戏容器在 C++/Java 层维护事件循环,把宿主回调排进 宏任务队列,V8 / JSC 执行完一轮微任务后再取宏任务。

JS 代码到机器码的流水线(以 V8 为例)

  1. 解析:生成 AST。
  2. Ignition 把 AST 编译成 字节码 并立即解释执行。
  3. Profiling:热点函数被标记为 warm/hot
  4. Sparkplug / TurboFan JIT 把热点字节码优化为本机指令;还能随时 反优化

最终机器码直接跑在 ARM64 / x86-64 CPU 上;GC 维护堆对象与类型反馈。


WebAssembly 与 JavaScript 的关系

  • 它们“共用同一个(物理)运行时”,因为引擎、事件循环、宿主线程都完全重合;
  • 它们各自拥有“独立的语言 runtime”,因为字节码格式、内存模型和首段编译器彼此不同。

差异部分

物理上共用同一 runtime,但逻辑上独立。

JavaScript WebAssembly
模块格式 .js 源文本/ESM .wasm 二进制、.wat 文本
类型系统 动态、可变对象 静态, 固定位宽的 i32/f64
内存模型 GC 堆(对象可移动) 线性内存 WebAssembly.Memory(连续字节)
首段编译器 Ignition 解释器 / Sparkplug baseline Liftoff baseline 编译器
互调桥 WebAssembly.Instance.exports.fn() import "js" "log" 直接调 JS 函数
加载 API <script>eval WebAssembly.instantiateStreaming()

示例:同一个 add 函数的两种形态

// JavaScript
export function add(a, b) { return a + b }
;; WebAssembly Text Format
(module
  (func $add (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

加载并互调:

const { add } = await WebAssembly
      .instantiateStreaming(fetch('add.wasm'))
      .then(m => m.instance.exports);

console.log(add(40, 2));  // 42

编译流水线(以 V8 为例)

JavaScript             WebAssembly
--------               ------------
source .js             binary .wasm
   │ parse               │ decode
   ▼                     ▼
 Ignition bytecode      Liftoff machine code   ← 首帧快速启动
   │ (解释执行 & 收集热度)  │
   ├───────┐             ├───────┐
   ▼       │             ▼       │
TurboFan JIT ───────────┴─► TurboFan JIT       ← 热点函数重编译
  (优化、内联、SIMD…)             (相同优化管线)

两条支流最后都汇入 同一个机器码调度器,在同一线程栈上运行。

共用的部分 = “引擎 + 事件循环 + 宿主 API”

两者 共用同一个引擎内核与事件循环:Wasm 模块只是 JS 引擎中的另一类 “代码对象”。当 Wasm 函数被调用时,执行栈可跨语言跳转。

层级 共享? 说明
宿主事件循环 浏览器/小游戏的宏任务队列只维护一份;JS 代码、Wasm 代码都要“让出栈”才能让 UI 与 I/O 事件继续跑。
引擎核心 (V8/JSC) V8 官方定义自己是 “JavaScript and WebAssembly engine”;同一个堆、同一套线程模型。
优化 JIT (TurboFan / DFG) 优化器既能接收 JS 字节码也能接收 Wasm IR,再生成本机指令。
Host APIs (wx.request, WebGL…) 这部分暴露给 JS,全靠 JS 调用把参数塞进 Wasm 内存后再供 Wasm 代码使用。

调用栈与内存的实际交错

JS (app.js)
  └─ calls wasmAdd() ─► Wasm (add.wasm)
                         └─ calls console.log          (回到 JS)
  • “JS→Wasm” 时,引擎生成 TRAMPOLINE:把 JS 数值装箱/拆箱成 i32 写进线性内存;
  • “Wasm→JS” 时走 import 表,返回点仍在同一执行栈帧;
  • GC 线程若移动 JS 对象,只需更新托管句柄;Wasm 线性内存地址永远不变,因而无需移动补丁。

在微信小游戏 / WebGL 场景中的落地

  • 逻辑线程只有一个:JS 执行、Wasm 执行、WebGL API 调用全在同一 V8/JSC 线程完成。

  • WebGL 本质是宿主(GameRuntime)暴露的 C++ API:

    const id = gl.createShader(gl.VERTEX_SHADER); // JS 调 => C++ => GPU
    
  • Unity WebGL:IL2CPP 先把 C# 编译成 Wasm;运行时模板在 Module.onRuntimeInitialized 里用 JS 把渲染循环排进宿主事件循环。

  • 同源崩溃上报wx.onError(JS)+ Module.onAbort(Wasm)监听到异常后,共用 sendCrash() API 把数据发往服务器——JS 和 Wasm 层面虽然触发点不同,但最终走的是同一个 JS 发送函数。