WebAssembly 小试牛刀

前言

了解和学习一下什么是 WebAssembly,用 WebAssembly 将 C++ 的实现转换为 JavaScript 可以调用的二进制依赖,简单入门。

WebAssembly

WebAssembly 是什么?

WebAssembly 的出现源于对 Web 应用程序性能的需求。随着 Web 应用程序变得越来越复杂,JavaScript 的性能已经无法满足需求。WebAssembly 通过提供一种低级、高效的编译目标来解决这个问题,使开发人员能够使用其他语言(如 C/C++、Rust 和 C#)编写高性能的 Web 应用程序。

简而言之,就是可以把用其他语言编写的代码进过转换之后,可以让 JavaScript 调用,以此来提升应用的性能。

WebAssembly Hello World

下面就以 C/C++ 代码为例,来学习一下如何使用 WebAssembly 。

面对不同的语言 C/C++、Rust、Java,采用 WebAssembly 进行转换需要依赖不同的转换工具,具体可以参考 WebAssembly 官网教程 的示例。

环境配置

Emscripten

Emscripten 是一个 开源的编译器 ,该编译器可以将 C/C++ 的代码编译成 JavaScript 胶水代码。 Emscripten 可以将 C/C++ 代码编译为 WebAssembly 编程语言的代码。

emcc 安装
shell 复制代码
# 1、下载 emsdk

git clone https://github.com/juj/emsdk.git

# 2、进入 emsdk 目录

cd emsdk

# 3、开始安装

# Fetch the latest version of the emsdk (not needed the first time you clone)

git pull 

# Download and install the latest SDK tools.

./emsdk install latest 

# Make the "latest" SDK "active" for the current user. (writes ~/.emscripten file) 

./emsdk activate latest 

# Activate PATH and other environment variables in the current terminal 

source ./emsdk_env.sh

也可以将 emsdk 配置到环境变量中,这样就可以随意调用了。

执行 emcc --version 可以看到相关信息的话,环境就 ready 了。

C++ 代码编译

c++ 复制代码
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

用 emcc 进行编译

shell 复制代码
emcc main.c -s WASM=1 -o index.html

执行命令之后会生成三个文件

shell 复制代码
 index.html        index.js          index.wasm
  • index.wasm 二进制的 wasm 模块代码
  • index.js 胶水代码,包含了原生 C++ 函数和 JavaScript/wasm 之间转换的 JS 文件
  • index.html 用来加载、编译和实例化 wasm 代码并且将其输出在浏览器显示上的 HTML 文件

最后执行 emrun index.html 就可以在浏览器上看到效果了。

这里由于浏览器跨域的问题,直接打开 index.html 是无法正常运行的

这里 index.html 中最核心的代码就是 <script async type="text/javascript" src="index.js"></script> 。 来执行 index.js 这个里面的逻辑。

Node 使用 WebAssembly

以上通过编译自动生成了 wasm 和 JavaScript 的胶水代码,并通过 html 进行加载,下面通过一个 NodeJs 的示例看看如何手动编写胶水层的代码。

  • sum.cpp
c++ 复制代码
int add(int a, int b) {
    return a + b;
}
  • 进行编译
shell 复制代码
emcc src/sum.cpp -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -o out/sum.wasm
  • 读取 wasm 并执行对应的方法
javascript 复制代码
const fs = require('fs');
let src = new Uint8Array(fs.readFileSync('sum.wasm'));
const env = {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({
        initial: 256
    }),
    table: new WebAssembly.Table({
        initial: 2,
        element: 'anyfunc'
    }),
    abort: () => {throw 'abort';}
}
WebAssembly.instantiate(src, {env: env})
    .then(result => {
        console.log(result.instance.exports.add(20, 89));
    })
    .catch(e => console.log(e));

通过以上命令转换生成 wasm 文件之后,就可以通过 node.js 按照读文件的方式读入这个二进制文件,通过其暴露的特定接口 instance.exports 调用相应的方法了。而方法名就是在 C/C++ 代码中声明的方法名。

字符串的处理

WebAssembly 并不支持字符串,而在实际开发中会大量用到字符串。

shell 复制代码
int call_with_string(int a, int b, const char *host, int times) {
    printf("call_with_string Called \n");
    printf("a = %d, b = %d, host = %s,times=%d\n", a, b, host, times);
    return a * b + times;
}

比如这个方法里,参数是 host 是 char 类型的指针,也就是字符串。而通过以上命令直接转换,调用时即便传递了字符串,但是无法正常接受。因此,对于字符串需要特殊处理。

  • 修改函数声明
c++ 复制代码
#include <stdio.h>
#include <emscripten/emscripten.h>

#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN EMSCRIPTEN_KEEPALIVE int add(int a, int b) {
    return a + b;
}

EXTERN EMSCRIPTEN_KEEPALIVE void call_with_string(int a, int b, const char *host, int times) {
    printf("call_with_string Called \n");
    printf("a = %d, b = %d, host = %s,times=%d\n", a, b, host, times);
}

通过头文件 <emscripten/emscripten.h> 导入 EMSCRIPTEN_KEEPALIVE 这个宏定义,并添加 EXTERN 声明。

  • 导出时需要保留某些一些原生方法
shell 复制代码
emcc src/sum.cpp -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall','allocate','intArrayFromString']" -s "EXPORTED_FUNCTIONS=['_free']" -o out/sum.html

除了 ccall 之外,还需要保留 allocate,inArrayFromString,_fres 几个原生方法。

  • 调用端需要使用 string 的指针
javascript 复制代码
    function call_string_input() {
        times++
        var strPtr = allocate(intArrayFromString("I am from Web"), ALLOC_NORMAL)
        Module.ccall(
            "call_with_string", // name of C function
            null, // return type
            [Number, Number, String, Number], // argument types
            [2, 2, strPtr, times], // arguments
        );
        _free(strPtr)
    }

通过 allocate 和 intArrayFromString 获取字符串的指针,进行方法调用,调用结束后通过 _free 方法释放指针。

这样才可以进行正常的调用。

遗留问题

这里字符串传参时,在 c/c++ 中添加了很多额外的参数,字符串的处理这一小节的实现,最终是依赖 emcc 生成的胶水层语言加载 wasm ,通过 nodejs 并无法直接加载,有些许差异,待解决。

参考文档

相关推荐
Scott9999HH37 分钟前
告别流量波动玄学!从法拉第电磁定律到底层 C/C++ 流量累积算法,深度解密工业级电磁流量计选型与开发
c语言·c++·算法
脱胎换骨-军哥1 小时前
C++/Rust无缝互操作:混合系统新常态
开发语言·c++·rust
瑞瑞小同学2 小时前
处理echarts x轴内容过多,重叠问题
前端·javascript·echarts
HexCIer2 小时前
面向未来的原子化 CSS:UnoCSS 核心架构分析与 Tailwind CSS 现状
前端·css·vite
程序员黑豆2 小时前
鸿蒙应用开发中的单位详解:px、vp、fp、lpx
前端·harmonyos
像我这样帅的人丶你还2 小时前
MCP + npm:给五年前的老系统接上AI
前端·javascript·agent
南一Nanyi3 小时前
依赖注入和控制反转
前端·设计模式·nestjs
小村儿3 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
愚公移码3 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
promiseThen3 小时前
5 分钟上手 Markdown:标题到表格、代码块与简历实战
前端