1. JavaScript与C/C++混合编程的技术
JavaScript与C++混合编程可以实现两种语言的优势结合,C++的程序性能很高且支持强大的系统调用能力,JavaScript则生态丰富且开发效率高。
JavaScript与C++混合编程常见的技术手段主要有以下几种:
- Node.js的C++扩展: 常用于Node.js实现的后端服务代码。在WebAssembly出现之前,Node.js的服务器代码通常以这种方式调用C/C++的模块。
- JsAPI和Native API: 常应用于包含WebView的客户端。将Native端的代码封装成Web接口(JsAPI)供前端调用,将Web端的代码封装成Native接口供Native调用。像Electron、CEF技术栈的客户端均采用了此种方式。
- WebAssembly: 主要用于浏览器上运行的前端页面,Node.js从8.0开始也支持WebAssembly,因此也可用于服务端开发。
本章所讲的内容是基于WebAssembly的混合编程技术。
2. 什么是WebAssembly?
WebAssembly是一种新的编码方式,是一种为web设计的高效、低级字节码格式。我们可以将C/C++、Rust等低级语言编写的代码编译成WebAssembly字节码,现代的Web浏览器可以加载WebAssembly,并与JavaScript协同运行。从而使得WebAssembly成为JavaScript与C/C++混合编程并在Web上运行的最有效机制。C/C++编译成的WebAssembly能够以接近原生语言的效率在浏览器上运行。
2.1. 支持WebAssembly的浏览器
支持WebAssembly的常用浏览器及版本:
- Chrome 57及以上版本。
- Firefox 52及以上版本。
- Edge 16(基于Chromium的版本)及以上。
- Safari 11及以上版本。
- Opera 44及以上版本。
参考信息: https://caniuse.com/wasm
此外,Node.js从8.0版本也开始支持WebAssembly,WebAssembly目前已经成了W3C的Web标准之一。
2.2. WebAssembly的编译器
除了C/C++外,WebAssembly还支持多种其他计算机语言编译成.wasm
,常见的语言和编译器如下:
- C/C++: emscripten编译工具链可以将 C/C++ 编译成 WebAssembly。
- Rust: wasm-pack:工具可以将 Rust 编译成 WebAssembly。
- Go: Go语言的官方工具就链支持将 Go 编译成 WebAssembly。
3. 开发环境搭建
- emscripten官方文档: https://emscripten.org/docs/getting_started/downloads.html
依赖的环境准备
- git
- Python3.6或更新版本(Windows)
安装步骤
# 1. 从Github上克隆emsdk仓库 # emsdk即Emscripten SDK,是将C/C++编译成WebAssembly的工具 git clone https://github.com/emscripten-core/emsdk.git # 2. 进入emsdk目录 cd emsdk # 3. 下载和安装最新的SDK tools(包括node.js、emscripten等) # Linux/macOS: ./emsdk install latest # Windows: ./emsdk.bat install latest # (安装大概需要十几分钟的时间,可以去喝杯茶休息一下了) # 会将相关的工具安装在以下三个目录 # emsdk/node # emsdk/upstream # emsdk/python (Windows才有,会安装nuget) # 4. 为当前用户设置latest版本为当前激活的工具 # Linux/macOS: ./emsdk activate latest # Windows: ./emsdk.bat activate latest # 5. 为当前命令终端设置环境变量 # Linux/macOS: source ./emsdk_env.sh # Windows: ./emsdk_env.bat # 6. 验证是否安装成功 emcc -v # (如果有显示正常的版本信息,则说明安装成功)
以上示例基于3.1.72版本的emscripten。
4. Hello World
程序
我们从一个Hello World
程序开始,了解WebAssembly程序的开发、编译、运行的大致流程。
新建一个测试目录
hello_world
和源码文件hello.cpp
。// hello_world/hello.cpp #include <iostream> int main() { std::cout << "Hello World from C++" << std::endl; return 0; }
执行以下命令编译为WebAssembly
emcc hello.cpp -o hello.html
编译后会生成如下三个文件:
./hello_world ├── hello.html # emscripten的测试页面,用来展示输出内容的HTML页面。 ├── hello.js # 是Emscripten生成的胶水代码,其中包含了Emscripten的运行环境和.wasm文件的封装。 └── hello.wasm # 二进制的字节码文件
在当前Demo目录下启动一个http-server服务,可以用python或node.js工具。
# 进入目录 cd hello_world # 在当前目录启动http-server服务 # Python3的用法 python -m http.server # Python2的用法 python -m SimpleHTTPServer
在支持WebAssembly的浏览器中打开
http://localhost:8000/hello.html
页面,正常情况下就可以看到输出内容(Hello World from C++
)了。
5. emscripten常见用法
5.1. emscripten编译流程
Emscripten的诞生早于WebAssembly。WebAssembly出现之前,Emscripten的编译目标时asm.js,即Emscripten的主要功能是将C/C++代码编译成JavaScript代码。Emscripten在1.37.3开始正式支持WebAssembly,可以根据编译选项设置编译目标为asm.js或WebAssembly。
Emscripten的编译流程如下:
- C/C++代码先通过Clang编译为LLVM的字节码,然后再根据设置的不同目标编译为asm.js或WebAssembly(
.wasm
)。 - 可以通过
-s WASM=1
或-s WASM=0
来设置编译目标,Emscripten自v1.38.1开始,默认的缺省编译选项为WASM=1
,之前的版本默认为WASM=0
。 - 相比于asm.js,.wasm具有体积小、执行效率高的优势,因此一般会优先选择.wasm作为编译目标。当然,在实际的项目中,为了增加程序的兼容性,可能会同时构建两个编译目标,在支持WebAssembly的浏览器中加载.wasm,不支持WebAssembly的老版本浏览器中降级为asm.js。
5.2. emsdk常见用法
emsdk是emscripten工具链最核心的部分,emsdk是将C/C++编译成WebAssembly的编译工具,其用法与Clang/GCC有点相似。
1. 最简单用法。
编译指令
:emcc ./hello.cpp
结果文件
:a.out.wasm
: 为C/C++源文件编译后形成的WebAssembly汇编文件,是一个二进制的字节码文件。a.out.js
: 是Emscripten生成的胶水代码,其中包含了Emscripten的运行环境和.wasm文件的封装,导入a.out.js
即可自动完成.wasm
文件的载入、编译、实例化、运行时初始化等繁杂的工作。
2. -o
选项
-o
选项可以指定输出的文件名和文件类型。
【demo1】
编译指令
:emcc ./hello.cpp -o hello.js
结果文件
:hello.wasm
: 与a.out.wasm
文件相同。hello.js
: 与a.out.js
文件相同。
【demo2】
编译指令
:emcc ./hello.cpp -o hello2.html
结果文件
:hello2.wasm
: 与a.out.wasm
文件相同。hello2.js
: 与a.out.js
文件相同。hello2.html
: emscripten的测试页面,用来展示输出内容的HTML页面。
3. -s
选项。
-s
选项是一个用于设置编译目标和编译属性的重要选项:
-s WASM=1
: 指定输出为WebAssembly
式,这能提升执行性能,WASM=1
是默认的缺省参数,此选项会生成XXX.wasm
和XXX.js
文件。-s WASM=0
: 指定输出为asm.js
格式,此选项只会生成XXX.js
文件,不会生成XXX.wasm
文件。-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']
: 指定导出运行时方法ccall
和cwrap
,ccall
/cwrap
辅助函数默认没有导出,在编译时需要通过此选项显示导出。-s MODULARIZE=1
:使输出的 JavaScript 代码成为一个模块化的形式,便于在不同的环境下使用。-s EXPORT_NAME='myModule'
:自定义导出的模块名称。-s ALLOW_MEMORY_GROWTH=1
:允许动态扩展内存,适用于需要可变内存的应用场景。
4. --bind
选项
--bind
选项表示使用embind模块。embind模块可以将C++类和函数绑定到JavaScript环境中,后文将讲解此部分内容。
5. --js-library
选项
--js-library
选项可以指定一个JavaScript文件作为JS库,参与C/C++的编译过程。后文将进一步讲解此相关内容。
5.3. 导出一个函数
Hello World
程序中,我们在HTML页面中加载并调用了C++的main函数。main函数是C/C++程序的入口函数,实际项目中,底层的C/C++模块通常希望通过接口来提供特定功能,而不是直接调用main函数作为单一入口。
emscripten中C/C++要导出一个接口,有两个关键的点:
- 用
extern "C"
以C的方式导出接口,避免C++的函数在编译后会对函数名称进行重整,在《导出接口的定义》一章中已介绍过相关内容。 - 用
EMSCRIPTEN_KEEPALIVE
宏告知编译器后续函数在优化时必须保留,并且该函数将被导出至JavaScript环境。EMSCRIPTEN_KEEPALIVE
是emscripten编译器内置的预编译宏,在<emscripten.h>
头文件中定义了该宏。
函数定义:
extern "C" EMSCRIPTEN_KEEPALIVE int32_t add(int32_t a, int32_t b)
{
return a + b;
}
C++代码:
为了代码编写方便,我们可以定义一个宏来简化代码,如下代码(export_function.cpp
)。
#include <cstdint>
#include <emscripten.h>
#define DECL_API(rettype) extern "C" EMSCRIPTEN_KEEPALIVE rettype
DECL_API(int32_t) add(int32_t a, int32_t b)
{
return a + b;
}
DECL_API(int32_t) sub(int32_t a, int32_t b)
{
return a - b;
}
编译指令:
通过以下指令编译代码。
emcc ./export_function.cpp -o ./export_function.js
HTML代码:
编写html测试页面(test.html
)如下。
<html>
<head>
<meta charset="utf-8" />
<title>Emscripten</title>
</head>
<body>
<h2>Emscripten:你好,世界!</h2>
<script>
Module = {};
Module.onRuntimeInitialized = function () {
let r1 = Module._add(3, 2);
console.log("add(3, 2) = " + r1);
let r2 = Module._sub(3, 2);
console.log("sub(3, 2) = " + r2);
};
</script>
<script src="export_function.js"></script>
</body>
</html>
运行结果:
浏览器打开该页面,可以看到控制台输出了add(3, 2) = 5
和sub(3, 2) = 1
:
代码说明:
WebAssembly模块是异步加载的,这意味着JS加载完成后emscripten的运行时环境可能并未准备好,我们要等待emscripten的运行时环境准备就绪后再调用WebAssembly模块的代码。而onRuntimeInitialized()
就是emscripten的运行时环境准备就绪后的一个回调函数,因此可在该函数内安全的调用WebAssembly模块相关的代码。在无特殊说明(不产生歧义)的情况下,后续文章的测试代码将不再列出该回调函数的完整代码。
Module.onRuntimeInitialized = function () {
<!-- TODO -->
};
历史文章推荐:
大家好,我是陌尘。
IT从业10年+, 北漂过也深漂过,目前暂定居于杭州,未来不知还会飘向何方。
搞了8年C++,也干过2年前端;用Python写过书,也玩过一点PHP,未来还会折腾更多东西,不死不休。
感谢大家的关注,期待与你一起成长。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。