使用WebAssembly编译C++到JS

实际项目开发中遇到了一些已经使用 C++ 实现的功能,需要在新的 Web 客户端使用。由于主要是数学和算法的计算,没有平台依赖性。所以需要一个成本最低的移植方式,显然 WebAssembly 是一个非常好的方式。现在官方的编译工具是emscripten (opens new window)

# 移植方式

从 C++ 编译到 JS。官方提供了两种编译方式embind (opens new window)WebIDL Binder (opens new window)。这两种方式,一开始我也很困惑。不过现在也是有所了解了。向大家介绍一下如何选择。如果你的 C++ 项目中有很多高级数据结构要使用,例如 vector,map。那推荐使用 embind,如果你的项目主要是简单数据类型,例如数字,字符串,bool,都可以简单的映射到 JS,并且是用 class 封装的,那推荐使用 WebIDL Binder。

# 封装处理

由于 C++ 数据结构比 JS 复杂的多,很多时候没有直接暴露成 JS 函数。我们需要进行封装,例如 C++ 中的引用调用,可以改变传入的参数的值。但是编译成 JS 的话,就不会生效。所以这时候我们需要写 wrap 函数,封装这些引用调用的 C++ 函数,然后再单独写 get 函数,得到修改的值。

# webpack 打包

官方的 demo 是使用 src 的方式来引入,并且暴露成 Module 的全局变量。但是现代化的 Web 项目都是使用 webpack 打包,并且自动化引入的。所以我们也不想为了 WebAssembly 搞特殊。接下来就讲述如何配置来引入。首先,使用 emscripten 输出 mjs 文件,这样才可以作为模块被导入。具体的编译参数可以参照emcc (opens new window)。 首先,wasm 格式并不能被 webpack 正确识别,我们需要添加 loader

{
  test: /\.wasm$/,
  type: 'javascript/auto',
  loader: 'file-loader',
}
1
2
3
4
5

然后在项目中,分别 import mjs 文件和 wasm 文件。

import lib from './wasm.mjs';
import libWasm from './wasm.wasm';

const module = lib({
  locateFile(path) {
    if (path.endsWith('.wasm')) {
      return libWasm;
    }
    return path;
  }
});
1
2
3
4
5
6
7
8
9
10
11

然后在调用的时候替换掉默认的locateFile函数,即可完美导入到我们的项目中。

# 内存泄漏

C++ 中有很多高级的数据结构,我们可以映射成 JS 对象,但是内存管理还是需要自己处理。所有我们声明的 WebAssembly 数据结构并且包括函数返回的数据结构,都需要手动调用 delete 方法来释放内存。

# 总结

编译 C++ 到 JS,不仅需要 JS 的知识,还需要 C++ 知识,我们需要先把所有需要的 C++ 文件,先全部编译到 LLVM bitcode(.o 文件)。这里编译 C++ 可以使用 GCC 的全部编译参数,推荐使用 O3 参数来优化代码。最后编译到 JS 文件的时候,需要按照 emscripten 的规范来书写胶水文件。

参考:

webpack-emscripten-wasm (opens new window)

前端中的e2e测试

对于单页面应用,我们需要由前端来控制每个页面的路由。但是在开发过程中,我们经常要对路由进行调整。每次调整都要手动检查所有页面是否正常显示。这个过程实在是太浪费生命了, 所以我们需要选择一个测试框架来自动完成这个过程,结合 CI 系统,在每次 PR 的时候都自动测试一遍,只有通过测试才会进行下一步的 review。

# Nightwatch

Nightwatch 是基于W3C WebDriver API (opens new window)。WebDrive 主要是为了满足浏览器的自动化测试需求,把最终的接口统一成 HTTP 协议。 所以可以统一不同浏览器的自动化测试接口。我们只要安装每个浏览器的 WebDriver 实现,就可以同一套代码在不同的浏览器中进行测试。老版本的 NightWatch 需要使用 Selenium 来管理各个浏览器,但是从 1.0 版本开始便不需要也不推荐。所以我现在的项目中并没有安装 Selenium。

# 依赖

由于不需要安装 Selenium,所以并不需要安装 Java 依赖。我使用 Chrome 浏览器来进行自动化测试,所以需要测试环境安装 Chrome,注意安装的必须是 Chrome,Chromium 不可以。 在 Travis CI 中可以很方便的添加Chrome (opens new window)。还需要安装 ChromeDriver 依赖来驱动 Chrome 完成自动化测试。

# 配置

在 Linux 测试环境下,基本上都是没有图形化界面的,所以我们在配置 Chrome 的启动参数中需要加入--headless选项,这样就可以不启动 UI。当我们以 root 权限启动的时候还需要添加--no-sandbox选项,这两个选项基本上是必须的。在我的实际情况下,需要测试不同 UA 下的展示效果,所以还需要添加--user-agent=Mozilla/5.0 (Macintosh; Test自定义的 UA。 下面给出一个配置的例子供大家参考:

    // http://nightwatchjs.org/gettingstarted#settings-file

    module.exports = {
      output_folder: "tests/e2e/reports",
      custom_assertions_path: ["tests/e2e/custom-assertions"],
      globals_path: "globalsModule.js",
      src_folders: ["tests/e2e/specs/web"]

      webdriver: {
        start_process: true,
        server_path: require("chromedriver").path,
        cli_args: ["--verbose"],
        port: 9515
      },

      test_settings: {
        default: {
          desiredCapabilities: {
            browserName: "chrome",
            javascriptEnabled: true,
            chromeOptions: {
              args: ["--headless", "--no-sandbox"]
            },
            acceptSslCerts: true
          }
        }
      }
    };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

每个参数的详细含义可以参考官方的说明文档。

# 总结

在集成 Nightwatch 的时候,我是卡在了安装浏览器上。在 Mac 系统上可以正常执行,在 CentOS 的 docker 里面却不行。找了半天发现是自己用了 Chromium,之前对 Chromium 能代替 Chrome 深信不疑。。。 在遇到问题的时候,还是多怀疑,多找找可能性比较好。

localStorage互斥锁的使用

JavaScript 是单线程语言,所以我们在写代码的时候根本不会遇到互斥锁的问题。但是当用户打开多个 Tab 页面的时候,这些页面却是共享同一个 localStorage 的。当我们试图修改 localStorage 的时候就会遇到竞争问题,如果两个页面同时修改了 localStorage,程序的可靠性就无法保证。

# 多 Tab 互斥

这个问题查了不少时间,目前有的解决方案:Shared Web Workers (opens new window)fast mutex (opens new window)。由于想尽可能的支持更多浏览器,所以我们选择了后者。

# 实现 localStorage 互斥

fast mutex 的源码也不是很多,所以读起来也没有很复杂。当执行 lock 函数的时候,会为一个 key 存储 _MUTEX_LOCK_X_KEY_MUTEX_LOCK_Y_KEY。以下简称 X 和 Y。 首先存 X,然后读 Y。如果 Y 存在说明别人已经拿到互斥锁了,所以重新执行函数。直到获取到互斥锁。如果 Y 不存在,说明没有人竞争锁。所以往下继续执行存储 Y。 但是我们不能保证这段时间没有别的 tab 来存储 X 或者 Y。所以继续读取 X,如果 X 没有发生变化,说明没有人来竞争锁。我们就可以 resolve 传入的回调函数了。 如果 X 发生了变化,说明有人来竞争互斥锁,这时候的函数设置了一个 50ms 的延迟执行,就是保证检测的足够晚。竞争的 tab 能够执行完自己的 lock 函数。50ms 之后再去读取 Y,如果发现 Y 没有发生变化,则自己还拥有这个互斥锁,可以顺利执行 resolve。否则自己丢失了互斥锁,重新执行 lock 函数。

# 总结

fast mutex 的实现确实很巧妙,通过添加两个 localStorage 值和 setTimeout 完成互斥锁的实现。看到这个项目之前,一直以为想做到 localStorage 的互斥是不现实的事情。