时间:2025-04-02 20:47
人气:
作者:admin
JavaScript与C++混合编程可以实现两种语言的优势结合,C++的程序性能很高且支持强大的系统调用能力,JavaScript则生态丰富且开发效率高。
JavaScript与C++混合编程常见的技术手段主要有以下几种:
本章所讲的内容是基于WebAssembly的混合编程技术。
WebAssembly是一种新的编码方式,是一种为web设计的高效、低级字节码格式。我们可以将C/C++、Rust等低级语言编写的代码编译成WebAssembly字节码,现代的Web浏览器可以加载WebAssembly,并与JavaScript协同运行。从而使得WebAssembly成为JavaScript与C/C++混合编程并在Web上运行的最有效机制。C/C++编译成的WebAssembly能够以接近原生语言的效率在浏览器上运行。
支持WebAssembly的常用浏览器及版本:
参考信息: https://caniuse.com/wasm
此外,Node.js从8.0版本也开始支持WebAssembly,WebAssembly目前已经成了W3C的Web标准之一。
除了C/C++外,WebAssembly还支持多种其他计算机语言编译成.wasm,常见的语言和编译器如下:
emscripten官方文档: https://emscripten.org/docs/getting_started/downloads.html
依赖的环境准备
安装步骤
# 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。
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++)了。

Emscripten的诞生早于WebAssembly。WebAssembly出现之前,Emscripten的编译目标时asm.js,即Emscripten的主要功能是将C/C++代码编译成JavaScript代码。Emscripten在1.37.3开始正式支持WebAssembly,可以根据编译选项设置编译目标为asm.js或WebAssembly。
Emscripten的编译流程如下:

.wasm)。-s WASM=1或-s WASM=0来设置编译目标,Emscripten自v1.38.1开始,默认的缺省编译选项为WASM=1,之前的版本默认为WASM=0。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++的编译过程。后文将进一步讲解此相关内容。
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,未来还会折腾更多东西,不死不休。
感谢大家的关注,期待与你一起成长。
上一篇:C++多线程初步
扫码二维码,关注微信公众号,阅读更多精彩内容