js⼆进制流转换成图⽚_实现⼀个简单的基于WebAssembly的
图⽚处理应⽤
图⽚来源: rustwasm.github.io/
本⽂作者:刘家隆
写在前边
本⽂希望通过 Rust 敲⼀敲 WebAssembly 的⼤门。作为⼀篇⼊门⽂章,期望能够帮你了解 WebAssembly 以及构建⼀个简单的WebAssembly 应⽤。在不考虑IE的情况,⽬前⼤部分主流的浏览器已经⽀持 WebAssembly,尤其在移动端,主流的UC、X5内核、Safari等都已⽀持。读完本⽂,希望能够帮助你将 WebAssembly 应⽤在⽣产环境中。
WebAssembly(wasm)简介
如果你真的了解了 WebAssembly, 可以跳过这⼀节。
可以先看两个 wasm ⽐较经典的 demo: /demo /Tanks/ inuation-labs /d3demo/
快速总结⼀下: WebAssembly(wasm) 是⼀个可移植、体积⼩、加载快并且兼容 Web 的全新格式,由 w3c 制定出的新的规范。⽬的是在⼀些场景下能够代替 JS 取得更接近原⽣的运算体验,⽐如游戏、图⽚/视频编辑、AR/VR。说⼈话,就是可以体积更⼩、运⾏更快。
wasm 有两种表⽰格式,⽂本格式和⼆进制格式。⼆进制格式可以在浏览器的 js 虚拟机中沙箱化运⾏,也可以运⾏在其他⾮浏览器环境中,⽐如常见的 node 环境中等;运⾏在 Web 上是 wasm ⼀开始设计的初衷,所以实现在浏览器上的运⾏⽅法⾮常简单。
通过⼀个简单的例⼦实现快速编译 wasm ⽂本,运⾏⼀个 wasm ⼆进制⽂件:
wasm ⽂本格式代码:
(module
(import "js" "import1" (func $i1)) // 从 js 环境中导⼊⽅法1
(import "js" "import2" (func $i2)) // 从 js 环境中导⼊⽅法2
(func $main (call $i1)) // 调⽤⽅法1
(start $main)
(func (export "f") (call $i2)) // 将⾃⼰内部的⽅法 f 导出,提供给 js,当 js 调⽤,则会执⾏⽅法2
)
上述内容看个⼤概即可,参阅代码中注释⼤致了解主要功能语法即可。主要功能就是从 js 环境中导⼊两个⽅法 import1 和 import2; 同时⾃⾝定义⼀个⽅法 f 并导出提供给外部调⽤,⽅法体中执⾏了 import2。
⽂本格式本⾝⽆法在浏览器中被执⾏,必须编译为⼆进制格式。可以通过 wabt 将⽂本格式编译为⼆进制,注意⽂本格式本⾝不⽀持注释的写法,编译的时候需要将其去除。这⾥使⽤ wat2wasm 在线⼯具快速编译,将编译结果下载就是运⾏需要的 wasm ⼆进制⽂件。
有了⼆进制⽂件,剩下的就是在浏览器中进⾏调⽤执⾏。
// 定义 importObj 对象赋给 wasm 调⽤
var importObj = {js: {
import1: () => console.log("hello,"), // 对应 wasm 的⽅法1
import2: () => console.log("world!") // 对应 wams 的⽅法2
}};
// demo.wasm ⽂件就是刚刚下载的⼆进制⽂件
fetch('demo.wasm').then(response =>
response.arrayBuffer() // wasm 的内存 buffer
).then(buffer =>
/**
* 实例化,返回⼀个实例 dule 和⼀个 WASM.instance,
* module 是⼀个⽆状态的带有 dule 占位的对象;
* 其中instance就是将 module 和 ES 相关标准融合,可以最终在 JS 环境中调⽤导出的⽅法
*/
WebAssembly.instantiate(buffer, importObj)
)
.then(({module, instance}) =>在线二进制转换
);
⼤概简述⼀下功能执⾏流程:
在 js 中定义⼀个 importObj 对象,传递给 wasm 环境,提供⽅法 import1import2 被 wasm 引⽤;
通过 fetch 获取⼆进制⽂件流并获取到内存 buffer;
通过浏览器全局对象 WebAssembly 从内存 buffer 中进⾏实例化,即 WebAssembly.instantiate(buffer, importObj),此时会执⾏wasm 的 main ⽅法,从⽽会调⽤ import1 ,控制台输出 hello;
实例化之后返回 wasm 实例,通过此实例可以调⽤ wasm 内的⽅法,从⽽实现了双向连接,执⾏ ports.f() 会调⽤ wasm 中的⽅法 f,f 会再调⽤ js 环境中的 import2,控制台输出 world。
细品这段实现,是不是就可以达到 wasm 内调⽤ js,从⽽间接实现在 wasm 环境中执⾏浏览器相关操作呢?这个下⽂再展开。
通过直接编写⽂本格式实现 wasm 显然不是我们想要的,那么有没有“说⼈话的”实现⽅式呢,⽬前⽀持⽐较好的主要包括 C、 C++、Rust、 Lua 等。
颇有特点的Rust
如果你了解 Rust,这⼀节也可以跳过了。
A language empowering everyone to build reliable and efficient software. ——from rust-lang
Rust 被评为 2019 最受欢迎的语⾔。
截图⾃ insights.stackoverflow /survey/2019#technology-_-most-loved-dreaded-and-wanted-languages
Rust 正式诞⽣于 15 年,距今仅仅不到五年的时间,但是⽬前已覆盖各⼤公司,国外有 Amazon、Google、Facebook、Dropbox 等巨头,国内有阿⾥巴巴、今⽇头条、知乎、Bilibili 等公司。那是什么让如此年轻的语⾔成长这么快?
Rust 关注安全、并发与性能,为了达成这⼀⽬标,Rust 语⾔遵循内存安全、零成本抽象和实⽤性三⼤设计哲学
借助 LLVM 实现跨平台运⾏。
Rust 没有运⾏时 gc,并且⼤部分情况不⽤担⼼内存泄漏的问题。
…
你内⼼ OS 学不动了?别急,先简单领略⼀下 Rust 的魅⼒,或许你会被他迷住。
下边看似很简单的问题,你能否答对?⼀共三⾏代码,语法本⾝没有问题,猜打印的结果是啥?
fn main() {
let s1 = String::from("hello word"); // 定义⼀个字符串对象
let s2 = s1; // 赋值
println!("{}", s1); // log输出
}
思考⼀会 点击查看答案
这其实是 Rust 中⼀个⽐较重要的特性——所有权。当将 s1 赋值给 s2 之后,s1 的所有权便不存在了,可以理解为 s1 已经被销毁。通过这种特性,实现内存的管理被前置,代码编写过程中实现内存的控制,同时,借助静态检查,可以保证⼤部分编译正确的程序可以正常运⾏,提⾼内存安全之外,也提⾼了程序的健壮性,提⾼开发⼈员的掌控能⼒。
所有权只是 Rust 的众多特性之⼀,围绕⾃⾝的三⼤哲学(安全、并发与性能)其有很多优秀的思想,也预⽰着其上⼿成本还是⽐较⾼的,感兴趣的可以深⼊了解⼀下。之前 Rust 成⽴过 CLI、⽹络、WASM、嵌⼊式四⼤⼯作组,预⽰着 Rust 希望发⼒的四⼤⽅向。截⽌⽬前已经在很多领域有⽐较完善的实现,例如在服务端⽅向有 actix-web、web 前端⽅向有 yew、wasm ⽅⾯有 wasm-pack 等。总之,Rust 是⼀门可以拓宽能⼒边界的⾮常有意思的语⾔,尽管⼊门陡峭,也建议去了解⼀下,或许你会深深
的爱上它。
除 wasm 外的其他⽅向(cli、server等),笔者还是喜欢 go,因为简单,^_^逃…
⾏了,扯了这么多,Rust 为何适合 wasm:
没有运⾏时 GC,不需要 JIT,可以保证性能
没有垃圾回收代码,通过代码优化可以保证 wasm 的体积更⼩
⽀持⼒度⾼(官⽅介⼊),⽬前⽽⾔相⽐其他语⾔⽣态完善,保证开发的低成本
Rust -> wasm
Rust编译⽬标
rustc 本⾝是⼀个跨平台的编译器,其编译的⽬标有很多,具体可以通过 rustup target list 查看,和编译 wasm 相关的主要有三个:wasm32-wasi:主要是⽤来实现跨平台,通过 wasm 运⾏时实⾏跨平台模块通⽤,⽆特殊 web 属性
wasm32-unknown-emscripten:⾸先需要了解 emscripten,借助 LLVM 轻松⽀持 rust 编译。⽬标产物
通过 emscripten 提供标准库⽀持,保证⽬标产物可以完整运⾏,从⽽实现⼀个独⽴跨平台应⽤。
wasm32-unknown-unknown:主⾓出场,实现 rust 到 wasm 的纯粹编译,不需要借助庞⼤的 C 库,因⽽产物体积更加⼩。通过内存分配器(wee_alloc)实现堆分配,从⽽可以使⽤我们想要的多种数据结构,例如 Map,List 等。利⽤ wasm-bindgen、web-sys/js-sys 实现与 js、ECMAScript、Web API 的交互。该⽬标链⽬前也是处于官⽅维护中。
或许有⼈对 wasm32-unknown-unknown 的命名感觉有些奇怪,这⾥⼤概解释⼀下:wasm32 代表地址宽度为 32 位,后续可能也会有 wasm64 诞⽣,第⼀个 unknow 代表可以从任何平台进⾏编译,第⼆个 unknown 表⽰可以适配任何平台。
wasm-pack
以上各个⼯具链看着复杂,官⽅开发⽀持的 wasm-pack ⼯具可以屏蔽这⼀切细节,基于 wasm32-unknown-unknown ⼯具链可快速实
现 Rust -> wasm -> npm 包的编译打包,从⽽实现在 web 上的快速调⽤,窥探 wasm-npm 包这头“⼤象”只需要如下⼏步:
1. 使⽤ rustup 安装rust
2. 安装 wasm-pack
3. wasm-pack new hello-wasm.
4. cd hello-wasm
5. 运⾏ wasm-pack build.
6. pkg ⽬录下产物就是可以被正常调⽤的 node_module 了
⼀个真实例⼦看⼀下 wasm 运⾏优势
路指好了,准备出发!接下来可以愉快的利⽤ rust 编写 wasm 了,是不是⼿痒了;下边通过实现⼀个 MD5 加密⽅法来对⽐⼀下 wasm
和 js 的运⾏速度。
⾸先修改 l,添加依赖包
[dependencies]
wasm-bindgen = "0.2"
md5 = "0.7.0"
Cargo 是 Rust 的包管理器,⽤于 Rust 包的发布、下载、编译等,可以按需索取你需要的包。其中 md5 就是⼀会要进⾏ md5 加密的算
法包,wasm-bindgen 是帮助 wasm 和 js 进⾏交互的⼯具包,抹平实现细节,⽅便两个内存空间进⾏通讯。
编写实现(src/lib.rs)
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn digest(str: &str) -> String {
let digest = md5::compute(str);
let res = format!("{:x}", digest);
return res;
}
借助 wasm_bindgen 可以快速将⽅法导出给 js 进⾏调⽤,从⽽不需要关⼼内存通信的细节。最终通过 wasm-pack build 构建出包(在⽬
录 pkg 下),可以直接在 web 进⾏引⽤了,产物主要包含以下⼏部分
├── package.json
├── README.md
├── *.ts
├── index_bg.wasm:⽣成 wasm ⽂件,被index.js进⾏调⽤
├── index.js:这个就是最终被 ECMAScript 项⽬引⽤的模块⽂件,⾥边包含我们定义的⽅法以及⼀些⾃动⽣成的胶⽔函数,利⽤ TextEncoder 实现内存之间的数据通js 调⽤
import * as wasm from "./pkg";
wasm.digest('xxx');
构建出的 wasm pkg 包引⼊ web 项⽬中,使⽤ webpack@4 进⾏打包编译,甚⾄不需要任何其他的插件便可⽀持。
速度对⽐
针对⼀个⼤约 22 万字符长度的字符串进⾏ md5 加密,粗略的速度对⽐:
从数据层⾯来看,wasm 的性能优势显⽽易见。但同时也发现在 100 次的时候,性能数据差值虽然扩⼤,但是⽐值却相⽐⼀次加密缩⼩。原因是在多次加密的时候,js 和 wasm 的通信成本的占⽐逐渐增⾼,导致加密时间没有按⽐例增长,也说明 wasm 实际加密运算的时间⽐结果更⼩。这其实也表明了了 wasm 在 web 上的应⽤场景:重计算、轻交互,例如⾳视频/图像处理、游戏、加密。但在将来,这也会得到相应的改善,借助 interface-type 可实现更⾼效的值传递,未来的前端框架或许会真正迎来⼀场变⾰。
利⽤ wasm 实现⼀个完整 Web 应⽤
借助 wasm-bindgen,js-sys和web-sys crates,我们甚⾄可以极⼩的依赖 js,完成⼀个完整的 web 应⽤。以下是⼀个本地彩⾊ png 图⽚转换为⿊⽩图⽚的 web-wasm 应⽤。
效果图:
在线体验:点我
⼤致功能是通过 js 读取⽂件,利⽤ wasm 进⾏图⽚⿊⽩处理,通过 wasm 直接创建 dom 并进⾏图⽚渲染。
1. 利⽤ js 实现⼀个简单的⽂件读取:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论