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 并无法直接加载,有些许差异,待解决。

参考文档

相关推荐
科技苑5 小时前
C++ CAS与ABA问题详解,无锁编程的经典陷阱怎么解决?
c++·人工智能
卸任6 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance7 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M7 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写7 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李7 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易7 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马7 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员