# 第3章 Emscripten 运行时

本章将简要介绍Emscripten环境下与运行时相关的部分知识,包括消息循环、文件系统、内存管理等内容。

# 3.1 Emscripten 生命周期

生成本地代码时,作为 C/C++程序的入口函数,通常 main()函数意味着程序的整个生命周期,程序随 main()函数返回的返回而退出;而在 Emscripten 下,情况有所不同,来看下面的例子:

//main.cc
#include "em_port_api.h"
#include <stdio.h>

EM_PORT_API(int) show_me_the_answer() {
	return 42;
}

int main() {
	printf("你好,世界!\n");
	return 0;
}
emcc main.cc  -o main.js -s EXPORT_NAME="'Module8'"
<!-- main.html -->
<body>
  <button id="btn_test" onclick="Test()" disabled="true">test</button>
  <script>
    function Test() {
      console.log(Module._show_me_the_answer());
    }
    Module = {};
    Module.onRuntimeInitialized = function () {
      var btn = document.getElementById("btn_test");
      btn.disabled = false;
    };
  </script>
  <script src="main.js"></script>
</body>

页面打开后,main()函数执行,控制台输出了“你好,世界!”
此时,如果点击页面上的“test”按钮,控制台输出:42

你好,世界!
42

TIP

main()函数退出后,Emscripten 运行时核心Module依然可用!
而且在之前的章节,很多例子里面甚至根本都没有main()函数。
由此可见:对 Emscripten 来说,main()函数既不必须,运行时生命周期亦不由其控制。

如果希望在 main()函数返回后注销 Emscripten 运行时,可以在编译时添加-s NO_EXIT_RUNTIME=0 选项,例如: emcc main.cc -s NO_EXIT_RUNTIME=0 -o main.js

# 3.2 Emscripten 消息循环

除了一次性执行立即退出的程序外,大多数 C/C++程序都存在类似下列伪代码的消息循环:

int main() {
    while(1) {
        msg_loop();
    }
    return 0;
}

网页中的 JavaScript 脚本是单线程运行的,一个带有消息循环的 C/C++程序如果不加处理,直接使用 Emscripten 编译后导入网页中运行,消息循环不退出,会阻塞页面程序的运行,导致 DOM 无法更新,整个页面失去响应。为此 Emscripten 提供了一组函数用于消息循环的模拟及调度执行。

# 3.2.1 emscripten_set_main_loop

函数声明:

void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop)

参数:

  • func:消息处理回调函数。
  • fps:消息循环的执行帧率。如果该参数小等于 0,则使用页面的 requestAnimationFrame 机制调用消息处理函数,该机制可以确保页面刷新率与显示器刷新率对齐,对于需要执行图形渲染任务的程序,使用该机制可以得到平滑的渲染速度。
  • simulate_infinite_loop:是否模拟“无限循环”,用法后续介绍。

一个简单例子

//msg_loop.cc
#include <emscripten.h>
#include <stdio.h>

void msg_loop() {
	static int count = 0;
	if (count % 60 == 0) {
		printf("count:%d\n", count);
	}
	count++;
}

int main() {
	printf("main() start\n");
    // emscripten_set_main_loop(msg_loop, 0, 0);
	emscripten_set_main_loop(msg_loop, 0, 1);
	printf("main() end\n");
	return 0;
}
main() start
count: 0
count: 60
count: 120

无论 simulate_infinite_loop 参数是否为 1,消息处理函数都会按照设定的帧率无限执行。

区别仅在于,当其为 1 时:

  • emscripten_set_main_loop 后续代码不执行。
  • main()函数栈未销毁。

# 3.3 Emscipten 文件系统

跨平台的C/C程序常使用fopen()/fread()/fwrite()等libc/libcxx提供的同步文件访问函数。在文件系统这一问题上,通常的JavaScript程序与C/C本地程序有巨大的差异,主要体现在:

运行在浏览器中的JavaScript程序无法访问本地文件系统; 在JavaScript中,无论AJAX还是fetch(),都是异步操作。 Emscripten提供了一套虚拟文件系统,以兼容libc/libcxx的同步文件访问函数。

# 3.3.1 Emscripten虚拟文件系统架构

# 3.4 Emscripten 内存管理

# 内存容量

在使用 emcc 编译时,可以使用 TOTAL_MEMORY 参数控制内存容量,例如:

emcc mem.cc -s TOTAL_MEMORY=67108864 -o mem.js

相应的,栈容量可以通过 TOTAL_STACK 参数控制,例如下列命令将栈容量设为 3MB:

emcc mem.cc -s TOTAL_STACK=3145728 -o mem.js

WARNING

由于 WebAssembly 内存单位为页,1 页=64KB,因此当编译目标为 wasm 时,TOTAL_MEMORY 必须为 64KB 的整数倍。

# 可变内存

在默认设置下,Emscripten 堆一经初始化,容量就固定了,无法再扩容。而某些程序在运行时需要的内存容量在不同工况下可能有很大的波动。为了满足某些极端工况的需求而将 TOTAL_MEMORY 设置得非常高无疑是非常浪费的,为此,Emscripten 提供了可在运行时扩大内存容量的模式,欲开启该模式,需要在编译时增加-s ALLOW_MEMORY_GROWTH=1 参数,例如:

emcc mem.cc -s ALLOW_MEMORY_GROWTH=1 -o mem.js

当编译目标为 wasm 时,使用可变内存模式非常高效,不会影响运行性能,因此在编译为 WebAssembly 时,可变内存是推荐用法

# 内存分配器

Emscripten提供了两种内存分配器:

dlmalloc 默认值。由 Doug Lea 创建的内存分配器,其变种广泛使用于 Linux 等。 emmalloc 专为 Emscripten 设计的内存分配器。 emmalloc 的代码体积小于 dlmalloc,但是如果程序中频繁申请大量的小片内存,使用 dlmalloc 性能较好。

编译时设置 MALLOC 参数可以指定内存分配器,比如下列命令指定使用 emmalloc:

emcc mem.cc -s MALLOC="emmalloc" -o mem.js

TIP

除非对于代码体积极度敏感的场合,使用默认的 dlmalloc 内存分配器无疑是更优的选择。