Una guía práctica para desarrollar componentes wasm utilizando Rust para ingenieros de front-end web

¿Qué son los componentes wasm?

El nombre completo de wasm es WebAssembly, es un programa binario que se puede ejecutar en entornos como servidores y clientes como navegadores a través de máquinas virtuales. Es rápido, eficiente y portátil.

El mayor beneficio de nuestro proyecto front-end web es que podemos usar programas binarios en el lado del navegador para manejar algunos procesos computacionales intensivos y usar sus características más rápidas que JavaScript para optimizar el rendimiento.

La compatibilidad actual del navegador con wasm es la siguiente:

https://img10.360buyimg.com/imagetools/jfs/t1/180904/35/36038/170761/64ded9bdF6f54c383/e85e037cdd4fa1fd.jpg

En el lado móvil, excepto Android 4.4 e iOS 10, que no son compatibles, otras versiones pueden brindar soporte. También debe tenerse en cuenta que wasm puede ocupar una gran cantidad de memoria. Cuando utilice componentes de terceros que incluyan llamadas wasm, debe prestar atención al uso de la memoria para evitar fallas.

¿Por qué utilizar Rust?

El módulo wasm se puede compilar en varios lenguajes, incluidos C/C++/C#, Rust, JAVA y Go. Rust se utiliza aquí porque tiene un mecanismo estricto de administración de memoria y trata sintácticamente de evitar el desbordamiento de la memoria, lo que permite a los ingenieros escribir programas más seguros.

También hay una herramienta de soporte, wasm-pack, que permite compilar y empaquetar códigos escritos en Rust en paquetes npm, de modo que otros códigos que usan este programa puedan llamarse como otras bibliotecas públicas sin costos de aprendizaje adicionales.

Instalación de herramientas

  1. Instale Rustup, que es un instalador de Rust y una herramienta de administración de versiones. Para el front-end web, es equivalente a herramientas como nvm. Instale de acuerdo con el método en el sitio web oficial de Rust: https://www.rust-lang.org/zh-CN/tools/install. También se instalará Cargo, que es la herramienta de compilación y el administrador de paquetes de Rust. Para interfaces web, es equivalente a herramientas como npm.

  2. Instale wasm-pack, que es la herramienta mencionada anteriormente que compila y empaqueta programas Rust en componentes wasm. Instálelo también de acuerdo con el sitio web oficial de wasm-pack: https://rustwasm.github.io/wasm-pack/installer/

  3. 使用 wasm 模板 使用 wasm-pack 提供的模板可以快速生成 Rust 的 wasm 项目。

cargo generate --git https://github.com/rustwasm/wasm-pack-template

输入希望的项目目录名称,将新建目录并在其中生成项目。

在目录下我们可以看到几个文件,其中一个是 Cargo.toml ,这个是 Rust 项目的描述文件,对于 web 前端来说相当于 package.json 文件。

项目目录下还有一个 src 目录,里面有 lib.rs 和 utils.rs 两个文件,其中 lib.rs 这个文件就是我们主要的逻辑入口,他引用了 wasm-bindgen 库来输出暴露给外部调用的接口,在函数之前加上#[wasm_bindgen]可以让外部调用这个方法。

编译项目

本来 Rust 的项目编译用的是 cargo build 的命令,但是我们这里是希望编译 wasm 组件,所以用的是 wasm-pack build 命令。

执行后会在项目目录下的 pkg 目录下生成编译后的产品,是一个 npm 包的结构。需要调用这个组件的逻辑只需要像其他公共包一样 import 就可以使用了。

实战

以上的就是 wasm-pack 官方的教程,还有其他组件测试、发布等的流程先不在这里介绍了。以下用一个实际开发中的模块来说一下开发 wasm 组件过程中遇到的问题和解决方法。

背景

需要使用的 wasm 组件是一个优化3D模型的方法,传入一个模型的顶点信息和距离阈值,比较每个顶点位置之间的距离,如果没达到阈值距离就合并这两个顶点,以达到减少顶点的优化目的。

原逻辑是使用 javascript 编写的,在模型顶点数量比较多的时候执行的时间比较长。这种大量计算的情况就很适合使用 wasm 来处理。

数据传递

顶点信息是存储在一个 Float32Array 的数组中的,而 wasm 设计上除了 int 和 float 类型(对应 javascript 就是 number 类型)可以直接传递外,其他的类型都通过地址来传递。这对我们的程序来说是好消息,因为顶点信息的数据非常多,如果以值传递,就需要做数据复制,这个过程消耗的时间可能比我们换成 wasm 处理减 少的时间还要多。得益这个特点,我们的入参可以直接传入。

/*---  rust ----*/

// rust 获取 javasctipt 数据
pub fn add_attribute(&mut self, attribute: &Float32Array, item_size: u32) {
    self.attributes.push(BufferAttribute {
        array: attribute.to_vec(),
        item_size,
    });
}
/*---  javascript ----*/

// javascript 传递数据到 rust
for (const name of attributeNames) {
  const attr = attrArrays[name]
  bg.add_attribute(attr.array, attr.itemSize)
}

而计算后的结果,wasm 也提供了返回数组的指针和数组长度的方法,javascript 可以读取 wasm 的内存空间,根据这两个值构造新的顶点信息Float32Array。

/*---  rust ----*/

// 返回指定数据的内存指针位置
pub fn get_attribute_ptr(&self, index: usize) -> *const f32 {
  self.attributes[index].array.as_ptr()
}

// 返回指定数据的长度
pub fn get_attribute_length(&self, index: usize) -> usize {
  self.attributes[index].array.len()
}
/*---  javascript ----*/

// javascript 或取 rust 内存空间中的指定部分,构建Float32Array
const ptr = bg.get_attribute_ptr(i)
const length = bg.get_attribute_length(i)

const buffer = new attr.array.constructor(wasm.getMemory().buffer, ptr, length)

数据类型

合并顶点计算的逻辑中,有一段是这样的:每个顶点的位置、UV等信息,经过给定的精度计算后,生成一个特征值,之后比较每个顶点的特征值,如果是相同的话就表示这两个顶点可以合并。

原 javascript 版本的代码是逐个信息按顺序,加上分隔号,拼成一个字符串。

Rust 版本的代码如果也按同样的方法处理,因为顶点的信息量是不定的,有可能只有位置信息,也有可能有UV、法线、颜色等信息,所以生成的特征值字符串长度也不确定。

Rust 对於可变长度的字符串使用 String 类型,每次对字符串使用push_str方法增加内容。得到的结果 wasm 版本的执行速度跟 javascript 版本相差不大,甚至在某些情况下耗时还更多,经过逐个过程作排查,发现是在生成特征值和在表中查询特征值这个过程中花费的时间比较多。

根据程序的意图,特征值并不一定要是字符串,只需要在不同输入值的时候能够输出相关的值就可以,这跟生成 hash 值的需求是一样的,于是考虑将特征值生成替换成 hash 值计算。

因为在存储特征值的表使用了std::collections::hash_map类型,于是 hash 值也使用了其下的std::collections::hash_map::DefaultHasher类来计算

use std::collections::hash_map::DefaultHasher;

...

let mut hasher = DefaultHasher::new();

for j in 0..self.attributes.len() {
  ...

  let value = (attr.array[i * attr.item_size as usize + index as usize]
    * self.shift_multiplier)
    .trunc() as i32;
    
  hasher.write_i32(value);
    
  ...
}

let hash = hasher.finish();

需要注意的是对写入不同类型的内容,需要调用不同的方法,顶点信息中的值是正负值都用,经过精度计算后取整得到的值类型是i32,所以用write_i32来写入内容。

生成的 hash 值为u64,作为hash_mapkey记录对应顶点的序号。

替换特征值的类型之后,wasm 版本的耗时达到了 javascript 版本的 1/2,基本符合 wasm 设计的性能范围。

适配打包工具

wasm-pack 工具打包出来的 npm 包,可以直接在webpack下加载并调用运行。

我们原本的项目使用 vite 构建,vite 对import wasm 组件策略和 webpack 的不一样,vite 加载会返回一个加载方法,调用加载方法会返回一个 Promise,resolve 后才会返回跟 webpack 加载一样的 wasm 组件。

我们要对 wasm-pack 生成的产物作一些修改,假设我们的 wasm 组件命名为 merge_vertice_wasm,生成的主 js 文件应该会命名为merge_vertice_wasm.js,内容如下:

import * as wasm from './merge_vertice_wasm_bg.wasm'
import { __wbg_set_wasm } as wasm_bg from './merge_vertice_wasm_bg.js'
__wbg_set_wasm(wasm);
export * from './merge_vertice_wasm_bg.js'

为兼容 vite 的加载策略,修改成下面的内容

import * as wasm from './merge_vertice_wasm_bg.wasm'
import * as wasm_bg from './merge_vertice_wasm_bg.js'

let memory
if (wasm.default) {
  wasm.default({
    './merge_vertice_wasm_bg.js': wasm_bg,
  }).then(_wasm => {
    memory = _wasm.memory
    wasm_bg.__wbg_set_wasm(_wasm)
  })
else {
  memory = _wasm.memory
  wasm_bg.__wbg_set_wasm(wasm)
}
export * from './merge_vertice_wasm_bg.js'

export function getMemory({
  return memory
}

就可以在 webpack 和 vite 下都可以顺利加载并运行了。

其中增加了getMemory的方法供外部获取 wasm 组件的内存空间。

wasm 调用 javascript 方法

当我们在调试和测试性能表现时,需要打印日志,由于我们的 wasm 跑在浏览器环境中,我们需要调用 javascript 的方法,比如console.logconsole.time

wasm-bindgen 库提供了 web-sys 的组件,让 Rust 可以调用这些方法。

首先需要在cargo.toml中添加 web-sys 的依赖,并声明需要用到的特性:

[dependencies]
wasm-bindgen = "0.2.84"

[dependencies.web-sys]
version = "0.3.64"
features = ["console"]

这样在下次编译的时候,cargo 就会自动处理这些依赖,将会下载并构建。

然后在我们的 Rust 文件中,加入对 web-sys 的引用:

extern crate web_sys;

就可以调用 javascript 的 console 下的方法了:

// 调用console.log
web_sys::console::log_1(&JsValue::from(logContent));

// 调用console.time(label)
web_sys::console::time_with_label(label);

// 调用console.timeEnd(label)
web_sys::console::time_end_with_label(label);

原 javascript 版本优化模型耗时:

https://img14.360buyimg.com/imagetools/jfs/t1/109410/21/37527/8537/64dedd1cFe4c8c5c4/596fc2d36cc9fe5c.jpg

wasm 版本优化模型耗时:

https://img12.360buyimg.com/imagetools/jfs/t1/188745/32/36809/10529/64dedd1cF49a8b5cc/8dea820d278ad577.jpg

总结

以上为根据官网文档把模型合并顶点优化方法迁移为 wasm 版本的开发经历,从安装工具到发布、调试的整个过程。

中间因为对 Rust 数据类型的不熟悉和对不同前端构建工具对 wasm 组件处理的不同不够清晰,在开发过程中遇到的问题和解决方法。

Rust 版本的代码逻辑基本上是从 javascript 版本翻译过来的,其中应该还有在 Rust 环境下的优化手段,将在之后的学习中继续迭代。

引用

[1]

Rust 官方文档: https://doc.rust-lang.org/book/

[2]

Rust Wasm 官方介绍文档: https://rustwasm.github.io/docs/book/

[3]

wasm-pack 官方文档: https://rustwasm.github.io/docs/wasm-pack/


本文分享自微信公众号 - 凹凸实验室(AOTULabs)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

Bun 发布 1.0 正式版本,Zig 编写的 JavaScript 运行时 Windows 文件资源管理器的神奇 bug,一秒提升性能 JetBrains 发布 Rust IDE:RustRover PHP 最新统计数据:市场份额超 7 成、CMS 中的王者 将 Python 程序移植到 Mojo,性能提升 250 倍、速度比 C 还快 .NET 8 性能大幅提升,比 .NET 7 遥遥领先 JS 三大运行时对比:Deno、Bun 和 Node.js Visual Studio Code 1.82 网易伏羲回应员工“因 BUG 被 HR 威胁”离世 Unity 引擎明年起根据游戏安装量收费 (runtime fee)
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/o2team/blog/10110614
Recomendado
Clasificación