给 Web 前端工程师看的用 Rust 开发 wasm 组件实战
什么是wasm组件?
wasm 全称 WebAssembly,是通过虚拟机的方式,可以在服务端、客户端如浏览器等环境执行的二进制程序。他有速度快、效率高、可移植的特点。
对我们 Web 前端工程最大的好处就是可以在浏览器端使用二进制程序处理一些计算量大的处理,使用他比 javascript 快的特点优化性能。
目前浏览器对wasm的兼容性如下:

在移动端除了 android 4.4 和 ios 10 下不支持外,其他版本都能提供支持。还需要注意的是 wasm 有可能占用大量内存,使用第三方包含 wasm 调用的组件需要注意内存占用防止闪退。
为什么用Rust?
wasm模块 可以用多种语言来编译,包括 C/C++/C#、Rust、JAVA、Go。在这里使用 Rust 是因为他有严格的内存管理机制,从语法上尽量避免内存溢出,让工程师写出更安全的程序。
而且还有配套的工具 wasm-pack,让使用 Rust 编写的代码,编译包装成 npm 包,让使用这段程序的其他代码可以像使用其他公共库一样调用,不需要额外学习成本。
工具安装
安装 rustup,他是 Rust 安装器和版本管理工具。对于 web 前端来说相当于 nvm 这样的工具。
按照 rust 官网的方法安装:https://www.rust-lang.org/zh-CN/tools/install
同时也会安装 cargo,他是 Rust 的构建工具和包管理器。对于 web 前端来说相当于 npm 这样的工具。安装 wasm-pack,他是上文提到的把 rust 程序编译包装成 wasm 组件的工具。
同样按照 wasm-pack 官网的方法安装:https://rustwasm.github.io/wasm-pack/installer/使用 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_map的key记录对应顶点的序号。
替换特征值的类型之后,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.log和console.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 版本优化模型耗时:

wasm 版本优化模型耗时:

总结
以上为根据官网文档把模型合并顶点优化方法迁移为 wasm 版本的开发经历,从安装工具到发布、调试的整个过程。
中间因为对 rust 数据类型的不熟悉和对不同前端构建工具对 wasm 组件处理的不同不够清晰,在开发过程中遇到的问题和解决方法。
rust 版本的代码逻辑基本上是从 javascript 版本翻译过来的,其中应该还有在 rust 环境下的优化手段,将在之后的学习中继续迭代。
以上的完整代码在http://xingyun.jd.com/codingRoot/LemonTea-lib/merge-vertice-wasm/仓库,欢迎提优化修改建议。
引用
rust 官方文档:https://doc.rust-lang.org/book/
rust wasm 官方介绍文档:https://rustwasm.github.io/docs/book/
wasm-pack 官方文档:https://rustwasm.github.io/docs/wasm-pack/
作者:京东零售 胡俊文
来源:京东云开发者社区 转载请注明来源
给 Web 前端工程师看的用 Rust 开发 wasm 组件实战的更多相关文章
- Web前端工程师成长之路——知识汇总
一.何为Web前端工程师? 前端工程师,也叫Web前端开发工程师.他是随着web发展,细分出来的行业.Web前端开发工程师,主要职责是利用(X)HTML/CSS/JavaScript ...
- 谈谈Web前端工程师的定位
原文地址:http://www.360doc.com/content/10/0708/17/1277406_37692580.shtml 2010-07-08 锋子chans 阅 1116 转 ...
- web前端工程师在移动互联网时代里的地位问题
支付宝十周年推出了一个新产品:支付宝的十年账单,我也赶个时髦查看了一下我的支付宝十年账单,哎,感慨自己真是太屌丝了,不过这只是说明我使用淘宝少了,当我大规模网上购物时候,我很讨厌慢速的快递,所以我大部 ...
- Web前端工程师成长之路
一.何为Web前端工程师? 前端工程师,也叫Web前端开发工程师.他是随着web发展,细分出来的行业.Web前端开发工程师,主要职责是利用(X)HTML/CSS/JavaScript/D ...
- 如何成为一名优秀的web前端工程师(前端攻城师)?
程序设计之道无远弗届,御晨风而返.———— 杰佛瑞 · 詹姆士 我所遇到的前端程序员分两种:第一种一直在问:如何学习前端?第二种总说:前端很简单,就那么一点东西. 我从没有听到有人问:如何做一名优秀. ...
- 如何成为一名优秀的web前端工程师(转给自己,共勉)
来源:王子墨的博客 程序设计之道无远弗届,御晨风而返.———— 杰佛瑞 · 詹姆士 我所遇到的前端程序员分两种: 第一种一直在问:如何学习前端? 第二种总说:前端很简单,就那么一点东西. 我从没有听到 ...
- 如何成为一名优秀的web前端工程师
我所遇到的前端程序员分两种: 第一种一直在问:如何学习前端? 第二种总说:前端很简单,就那么一点东西. 我从没有听到有人问:如何做一名优秀.甚至卓越的WEB前端工程师. 何为:前端工程师? 前端工程师 ...
- web前端工程师在移动互联网时代里的地位问题 为啥C/S系统在PC端没有流行起来,却在移动互联网下流行了起来 为啥移动端的浏览器在很多应用里都是靠边站,人们更加倾向于先麻烦自己一下,下载安装个客户端APP
web前端工程师在移动互联网时代里的地位问题 支付宝十周年推出了一个新产品:支付宝的十年账单,我也赶个时髦查看了一下我的支付宝十年账单,哎,感慨自己真是太屌丝了,不过这只是说明我使用淘宝少了,当我大规 ...
- 【转】如何成为一名优秀的web前端工程师(前端攻城师)?
[转自]http://julying.com/blog/how-to-become-a-good-web-front-end-engineer/ 程序设计之道无远弗届,御晨风而返.———— 杰佛瑞 · ...
- web前端工程师面试技巧 常见问题解答
web前端工程师面试技巧 常见问题解答 每年的春招是各企业需求人才的黄金时期,不少的前端大牛或者前端新手在面试时候不知道怎么来回答面试官的问题,下面来看下我转载的这篇文章吧,希望对从事前端工作的你有所 ...
随机推荐
- 如何爆破js加密后的密码?
如何爆破js加密后的密码? 1.首先burp中安装插件: https://github.com/whwlsfb/BurpCrypto 安装插件完毕后,分析进行js加密的算法. 2.分析加密过程: 找到 ...
- html5 2.0学习
列表定义:是一种特别的对象集合.集合:集中在一起合二为一(聚集). 聚集:多个列(信息资源)排在一起.信息资源:一堆数据,可能是字符,可能是图片. 列表分类:有序列表 无序列表 (自)定义列表 有 ...
- 古早wp合集
0x00 首先非常感谢大家阅读我的第一篇.本文章不仅仅是题解,一些细枝末节的小问题也欢迎大家一起解答. 小问题的形式如Qx:xxxxxxx? 欢迎发现小问题并讨论~~ N1nE是本人另外一个名字,目前 ...
- SpringBoot 启动流程追踪(第二篇)
上一篇文章分析了除 refresh 方法外的流程,并着重分析了 load 方法,这篇文章就主要分析 refresh 方法,可以说 refresh 方法是 springboot 启动流程最重要的一环,没 ...
- 发布策略:蓝绿部署、金丝雀发布(灰度发布)、AB测试、滚动发布、红黑部署的概念与区别
蓝绿发布(Blue-Green Deployment) 蓝绿发布提供了一种零宕机的部署方式.不停老版本,部署新版本进行测试,确认OK,将流量切到新版本,然后老版本同时也升级到新版本.始终有两个版本同时 ...
- 用一个示例来学习DockerFile
在Docker的世界里,我们经常会听到Dockerfile这个词.那么,什么是Dockerfile?它如何工作?本文将简要介绍Dockerfile的基本概念,原理以及一些常用的Dockerfile命令 ...
- 使用Springboot+SpringCloud+Seata1.3.0+Nacos1.2.1进行全局事务管理
一.官方文档网址 http://seata.io/zh-cn/docs/overview/what-is-seata.html Seata1.3.0开发组提供的开发文档 二.常见问题 2.1:网址: ...
- 深信服 9-15笔试 C卷
深信服 9-15笔试 C卷 15题填空,4道编程 填空 填空比较基础,考察了数据结构(队列,二叉树),和简单编程,二进制等. 1.1 2 3 4 5 入队,然后队首弹出,6 7 8 9 10 入队,然 ...
- 图解 LeetCode 算法汇总——二分查找
二分查找(Binary Search)是一种在有序数组中查找特定元素的高效算法.它的基本思想是将目标值与数组中间的元素进行比较,如果目标值小于中间元素,则在数组的左半部分继续查找,否则在右半部分查找, ...
- 「codeforces - 1481F」AB Tree
link. 理一下逻辑,主要讲一下我做题时的疑惑和其它题解没提到的细节. 首先容易看到,一个必然不劣的贪心策略是把尽量靠近根的层铺成同样的字符.也许会有疑惑,字符串是否本质不同的判定每个位置地位相等. ...