简单把rust/c/c++编译为wasm并在浏览器中使用

789 阅读3分钟

简介

WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 Rust/ C / C ++ / Go等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。

本篇文章,会讲述如何创建一个webAssembly的npm包和如何在项目中使用*.wasm文件。

配置rust镜像

要是没翻墙,容易下载包之类的失败。所以要配置镜像

将下面的内容复制保存成config(无后缀) 放在.cargo文件夹下面,或者在项目文件夹与.toml文件同级目录里新建一个.cargo文件夹,放在里面

config文件

[source.crates-io]
registry="https://github.com/rust-lang/crates.io-index"
#指定镜像
#如:tuna、sjtu、ustc,或者 rustcc
replace-with='tuna'

#注:以下源配置一个即可,无需全部

#中国科学技术大学
[source.ustc]
registry="https://mirrors.ustc.edu.cn/crates.io-index"

#上海交通大学
[source.sjtu]
registry="https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index/"

#清华大学
[source.tuna]
registry="https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

#rustcc社区
[source.rustcc]
registry="https://code.aliyun.com/rustcc/crates.io-index.git"

我的是在

qaq@MacBook-Pro tree -L 1
.
├── bin
├── config    <----- 创建这么一个文件
├── env
├── git
└── registry
qaq@MacBook-Pro .cargo % pwd
/Users/qaq/.cargo

创建项目

目标是实现一个wasm版本的,斐波那契数列的第n项。(递归方式实现!)

function foo(i) {
  if (i === 0) return 0;
  if (i === 1) return 1;
  return foo(i - 1) + foo(i - 2)
}
foo(43) // 返回第43个斐波那契数列的值

运行rust的cargo

cargo install wasm-pack

cargo new --lib web-fib

创建完成后的项目结构

.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── target

在Cargo.toml里添加如下代码块。

[package]
name = "web-fib"
version = "0.1.0"
edition = "2021"

[lib] #<----------加
crate-type = ["cdylib", "rlib"] #<----------加

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
wasm-bindgen = "0.2.63" #<----------加

修改src下的lib.rs为:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fib(v: u32) -> u32 { // rust递归实现斐波那契数列第n项
    match v {
        1 => 1,
        2 => 1,
        _ => fib(v - 1) + fib(v - 2)
    }
}

然后运行

warm-pack build

这时候就会多一个目录pkg,这个pkg就是我们的npm包

.
├── Cargo.lock
├── Cargo.toml
├── pkg
│   ├── package.json
│   ├── web_fib.d.ts
│   ├── web_fib.js
│   ├── web_fib_bg.js
│   ├── web_fib_bg.wasm
│   └── web_fib_bg.wasm.d.ts
├── src
│   └── lib.rs
└── target
    ├── CACHEDIR.TAG
    ├── debug
    ├── release
    └── wasm32-unknown-unknown

wasm文件在浏览器的preview

(module
  (memory $memory (;0;) (export "memory") 17)
  (func $func0 (param $var0 i32) (result i32)
    (local $var1 i32)
    i32.const 1
    local.set $var1
    local.get $var0
    i32.const -1
    i32.add
    local.tee $var0
    i32.const 2
    i32.ge_u
    if (result i32)
      i32.const 0
      local.set $var1
      loop $label0
        local.get $var0
        call $func0
        local.get $var1
        i32.add
        local.set $var1
        local.get $var0
        i32.const -2
        i32.add
        local.tee $var0
        i32.const 1
        i32.gt_u
        br_if $label0
      end $label0
      local.get $var1
      i32.const 1
      i32.add
    else
      local.get $var1
    end
  )
  (func $fib (;1;) (export "fib") (param $var0 i32) (result i32)
    local.get $var0
    call $func0
  )
  (data (i32.const 1048576) "\04")
)

使用wasm

以npm包的方式使用

我们在package.json里添加npm包。(这里是npm引入未发布的本地npm的包,可以直接用路径)。

{
  "name": "wasm-test",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "web-fib": "file:/Users/dragon/workspace/rust/work/web-ffmpeg/pkg"
  },
  "devDependencies": {
    "@types/react": "^18.0.17",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.1.0",
    "typescript": "^4.6.4",
    "vite": "^3.1.0"
  }
}

然后执行

npm install

wasm的npm包使用

vite中使用

import init from "../node_modules/web-fib/web_fib_bg.wasm?init";

init({}).then((instance) => {
  console.time();
  console.log('通过调用wasm计算fib(43)', instance.exports.fib(43))
  console.timeEnd();
})

webpack5中使用

import * as wasm from "web-fib";

console.time();
console.log('通过调用wasm计算fib(43)', wasm.fib(43))
console.timeEnd();

这样使用的话,需要配置webpack5,这是个实验性功能。或者可以使用wasm-loader,或者参考以http的方式直接引入的js

// webpack.config.js
module.exports = {
  experiments: {
    asyncWebAssembly: true,
  }
}

结果:

截屏2022-11-01 10.00.17.png

以http的方式直接引入

先把pkg/web_fib_bg.wasm放到服务器上,给到一个xxx.xxx.xxx/web_fib_bg.… 有node npx环境的话

npx serve ./ --cors -p 3000

test.html的代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebAssembly</title>
</head>
<body>
<script>
function loadWebAssembly(fileName) {
  return fetch(fileName, { mode: 'cors' })
    .then(response => response.arrayBuffer()) //加载字节码
    .then(bits => WebAssembly.compile(bits)) //编译字节码
    .then(module => {
    return new WebAssembly.Instance(module)
  });//实例化
};
loadWebAssembly('http://localhost:3000/fib.wasm') // 你的web_fib_bg.wasm的文件地址
  .then(instance => {
  console.log(instance);
  fib = instance.exports.fib;
  console.time();
  console.log('通过调用wasm计算fib(43)', fib(43))
  console.timeEnd();
});
</script>
</body>
</html>
function loadWebAssembly(fileName) {
  return fetch(fileName, { mode: 'cors' })
    .then(response => response.arrayBuffer())
    .then(buffer => {
    return WebAssembly.instantiate(buffer)// <--------------使用WebAssembly.instantiate
  });//实例化
};
loadWebAssembly('http://localhost:3000/fib.wasm')
  .then(({instance}) => {
  console.log(instance);
  fib = instance.exports.fib;
  console.time();
  console.log('通过调用wasm计算fib(39)', fib(39))
  console.timeEnd();
});

把c编译为wasm

创建如下内容的文件

int fib (int n) {
  if (n <= 0) return 0;
  if (n <= 2) return 1;
  return fib(n - 2) + fib(n - 1);
}

安装emscripten然后运行

emcc --no-entry fib.c -s EXPORTED_FUNCTIONS='["_fib"]' -o fib.wasm

得到fib.wasm就可以如上一样使用

把c++编译为wasm

#ifndef EM_PORT_API
#	if defined(__EMSCRIPTEN__)
#		include <emscripten.h>
#		if defined(__cplusplus)
#			define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
#		else
#			define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
#		endif
#	else
#		if defined(__cplusplus)
#			define EM_PORT_API(rettype) extern "C" rettype
#		else
#			define EM_PORT_API(rettype) rettype
#		endif
#	endif
#endif


EM_PORT_API(int) fib (int n) {
  if (n <= 0) return 0;
  if (n <= 2) return 1;
  return fib(n - 2) + fib(n - 1);
}

c++导出fib函数需要定义函数导出宏,不然会报错

emcc --no-entry ./fib.cc -s EXPORTED_FUNCTIONS=_fib -o fib.wasm

emscripten.h写绝对路径的话在,emscripten的安装路径里,改为这样:

</emsdk/upstream/emscripten/cache/sysroot/include/emscripten.h>

如果不定义的话需要这样,但是函数全部导出,不写就莫得fib函数,emcc --help可以看见一些优化项

emcc ./fib.cc -Wl,--export-all -o fib.wasm

得到fib.wasm就可以如上一样使用

注意:c/c++要和js直接通讯的话,就只能以数字的方式交流,意思你传个字符串都得变数字。rust是因为有wasm-bindgen帮我们干这个事。

参考