WebAssembly

编译 zbar 到 WebAssembly

工作上需要前端实现扫描二维码的功能,这部分属于图像识别的工作,图像识别相关的库基本都是 C 语言库,所以需要将这些库编译到 WebAssembly,然后在前端调用。

# 胶水函数

我们需要将 C 语言的函数暴露出来,这样前端才能调用这些函数,这部分代码称为胶水函数。下面列举了一个简单的例子。

#include <emscripten.h>
#include <zbar.h>
#include <stdio.h>
#include <stdlib.h>

#define EXPORT EMSCRIPTEN_KEEPALIVE
typedef int int32_t;
typedef unsigned int uint32_t;

EXPORT zbar_image_scanner_t* ImageScanner_create() {
  return zbar_image_scanner_create();
}
1
2
3
4
5
6
7
8
9
10
11
12

# 编写 Makefile

ZBAR_VERSION = 0.23.90
ZBAR_SRC = zbar-$(ZBAR_VERSION)

SRC = src
BUILD = build
DIST = dist

EM_VERSION = 3.1.44

# See https://emscripten.org/docs/tools_reference/emcc.html
EMCC = emcc
EMMAKE = emmake
EMCONFIG = emconfigure

ZBAR_DEPS = $(ZBAR_SRC)/make.done
ZBAR_OBJS = $(ZBAR_SRC)/zbar/*.o $(ZBAR_SRC)/zbar/*/*.o
ZBAR_INC = -I $(ZBAR_SRC)/include/ -I $(ZBAR_SRC)/

# See https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
EMCC_FLAGS = -Oz -Wall -Werror -s ALLOW_MEMORY_GROWTH=1 \
	-s EXPORTED_FUNCTIONS="['_malloc','_free']" \
	-s MODULARIZE=1 -s EXPORT_NAME=zbarWasm

.PHONY: all build clean-build clean

all: build

build: $(BUILD)/zbar.js $(BUILD)/zbar.wasm

clean-build:
	-rm -rf $(DIST) $(BUILD)

clean: clean-build
	-rm $(ZBAR_SRC).tar.gz
	-rm -rf $(ZBAR_SRC)

$(BUILD)/zbar.wasm $(BUILD)/zbar.js: $(ZBAR_DEPS) $(SRC)/export.c
	mkdir -p $(BUILD)/
	$(EMCC) $(EMCC_FLAGS) -o $(BUILD)/zbar.js -sEXPORT_ES6 $(SRC)/export.c $(ZBAR_INC) $(ZBAR_OBJS)

$(ZBAR_DEPS): $(ZBAR_SRC)/Makefile
	cd $(ZBAR_SRC) && $(EMMAKE) make CFLAGS=-Os CXXFLAGS=-Os \
		DEFS="-DZNO_MESSAGES -DHAVE_CONFIG_H"
	touch -m $(ZBAR_DEPS)

$(ZBAR_SRC)/Makefile: $(ZBAR_SRC)/configure
	cd $(ZBAR_SRC) && $(EMCONFIG) ./configure --without-x --without-xshm \
		--without-xv --without-jpeg --without-libiconv-prefix \
		--without-imagemagick --without-npapi --without-gtk \
		--without-python --without-qt --without-xshm --disable-video \
		--disable-pthread --disable-assert --host=x86_64-linux-gnu

$(ZBAR_SRC)/configure: $(ZBAR_SRC).tar.gz
	tar zxvf $(ZBAR_SRC).tar.gz
	touch -m $(ZBAR_SRC)/configure

$(ZBAR_SRC).tar.gz:
	curl -L -o $(ZBAR_SRC).tar.gz https://linuxtv.org/downloads/zbar/zbar-$(ZBAR_VERSION).tar.gz
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

Makefile 中定义了编译 zbar 的流程,我们首先使用 curl 下载 zbar 的源码,然后解压,然后配置编译参数,最后编译生成 zbar 的调用 JS 和 WASM 文件。注意在配置参数中我们禁用了一些功能,因为这些功能在 WebAssembly 中不需要,并且在指定 host 的时候需要根据运行环境设置正确的参数,如果是运行在 Github Actions 中,需要设置为 x86_64-linux-gnu。如果是 M1 芯片的 Mac,需要设置为 arm

# 函数封装

虽然我们从 zbar 暴露出来关键的图像处理相关的函数,但是用起来还不是那么方便,所以我们需要写一点 Typescript 的代码来封装这些函数。然后使用 rollup 打包成 JS 文件,并生成对应的 TS 类型文件。这部分实现细节就不赘述了。

# 总结

编译 zbar 到 WebAssembly 的过程并不复杂,但是需要注意一些细节,比如编译参数的设置,胶水函数的编写等。这部分工作需要一定的 C 语言基础,如果没有的话,可以参考一些现成的例子,然后根据自己的需求进行修改。我也是参考网上的例子,然后根据自己的需求进行修改的。

# 参考

undecaf/zbar-wasm (opens new window)

acgotaku/zbar-wasm (opens new window)

使用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)