改进 JavaScript 和 Rust 的互操作性并深入认识 wasm-bindgen 组件


Posted in Javascript onJuly 13, 2019

前言

最近我们已经见识了WebAssembly如何快速编译、加速JS库以及生成更小的二进制格式。我们甚至为Rust和JavaScript社区以及其他Web编程语言之间的更好的互操作性制定了高级规划。正如前面一篇文章中提到的,我想深入了解一个特定组件的细节,wasm-bindgen。

今天WebAssembly标准只定义了四种类型:两种整数类型和两种浮点类型。然而,大多数情况下,JS和Rust开发人员正在使用更丰富的类型! 例如,JS开发人员经常与互以添加或修改HTML节点相关的文档交互,而Rust开发人员使用类似Result等类型进行错误处理,几乎所有程序员都使用字符串。

被局限在仅使用由WebAssembly所提供的类型将会受到太多的限制,这就是wasm-bindgen出现的原因。

wasm-bindgen的目标是提供一个JS和Rust类型之间的桥接。它允许JS使用字符串调用Rust API,或Rust函数捕获JS异常。

wasm-bindgen抹平了WebAssembly和JavaScript之间的阻抗失配,确保JavaScript可以高效地调用WebAssembly函数,并且无需boilerplate,同时WebAssembly可以对JavaScript函数执行相同的操作。

wasm-bindgen项目在其README文件中有更多描述。要入门,让我们深入到一个使用wasm-bindgen的例子中,然后探索它还有提供了什么。

1、Hello World!

学习新工具的最好也是最经典的方法之一就是探索下用它来输出“Hello, World!”。在这里,我们将探索一个这样的例子——在页面里弹出“Hello World!”提醒框。

这里的目标很简单,我们想要定义一个Rust的函数,给定一个名字,它会在页面上创建一个对话框,上面写着Hello,$name!在JavaScript中,我们可以将这个函数定义为:

代码

export function greet(name) {
  alert(`Hello, ${name}!`);
}

不过在这个例子里要注意的是,我们将把它用Rust编写。这里已经发生了很多我们必须要处理的事情:

  • JavaScript将会调用一个WebAssembly 模块, 模块名是 greetexport.
  • Rust函数将一个字符串作为输入参数,也就是我们要打招呼的名字。
  • 在内部Rust会生成一个新的字符串,也就是传入的名字。
  • 最后Rust会调用JavaScript的 alert函数,以刚创建的字符串作为参数。

启动第一步,我们创建一个新的Rust工程:

代码

$ cargo new wasm-greet --lib

这将初始化一个新的wasm-greet文件夹,我们的工作都在这里面完成。接下来我们要使用如下信息修改我们的Cargo.toml(在Rust里相当于package.json):

代码

[lib] 
crate-type = ["cdylib"] 
 
[dependencies] 
wasm-bindgen = "0.2"

我们先忽略[lib]节的内容,接下来的部分声明了对wasm-bindgen的依赖。这里的依赖包含了我们使用wasm-bindgen需要的所有的支持包。

接下来,是时候编写一些代码了!我们使用下列内容替换了自动创建的src/lib.rs:

代码

#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
  fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
  alert(&format!("Hello, {}!", name));
}

如果你不熟悉Rust,这可能看起来有点??拢??灰?ε拢∷孀攀奔涞耐埔疲?asm-bindgen项目不断改进,而且可以肯定的是,所有这些并不总是必要的。

要注意的最重要的一点是#[wasm_bindgen]属性,这是一个在Rust代码中的注释,这里的意思是“请在必要时用wrapper处理这个”。我们对alert函数的导入和greet函数的导出都被标注为这个属性。稍后,我们将看到在引擎盖下发生了什么。

首先,我们从在浏览器中打开作为例子来切入正题!我们先编译wasm代码:

代码

$ rustup target add wasm32-unknown-unknown --toolchain nightly # only needed once 
$ cargo +nightly build --target wasm32-unknown-unknown

这段代码会生成一个wasm文件,路径为target/wasm32-unknown-unknown/debug/wasm_greet.wasm。如果我们使用工具如wasm2wat来看这个wasm文件里面的内容,可能会有点吓人。

结果发现这个wasm文件实际上还不能直接被JS调用!为了能让我们使用,我们需要执行一个或更多步骤:

代码

$ cargo install wasm-bindgen-cli # only needed once 
$ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir .

很多不可思议的事情发生都发生在这个步骤中:wasm-bindgen CLI工具对输入的wasm文件做后期处理,使它变的“suitable”可用。

我们待会再来看“suitable”的意思,现在我们可以肯定的说,如果我们引入刚创建的wasm_greet.js文件(wasm-bindgen工具创建的),我们已经获取到了在Rust中定义的greet函数。

最终我们接下来要做的是使用bundler对其打包,然后创建一个HTML页面运行我们的代码。

在写这篇文章的时候,只有Webpack's 4.0 release对WebAssembly的使用有足够的支持(尽管暂时已经有了 Chrome caveat)。

总有一天,更多的bundler也会接着支持WebAssmbly。在这我不再描述细节,但是你可以看一下在Github仓库里的example配置。不过如果我们看内容,这个页面中我们的JS在看起来是这样的:
代码

const rust = import("./wasm_greet"); 
rust.then(m => m.greet("World!"));

…就是这些了!现在打开我们的网页就会显示一个不错的“Hello, World!”对话框,这就是Rust驱动的。

2、wasm-bindgen是如何工作的

唷,那是一个巨大的“Hello, World!”。让我们深入了解一下更多的细节,以了解后台发生了什么以及该工具是如何工作的。

wasm-bindgen最重要的方面之一就是它的集成基本上是建立在一个概念之上的,即一个wasm模块仅是另一种ES模块。例如,在上述中我们想要一个带有如下签名的ES模块(在Typescript中):

代码

export function greet(s: string);

WebAssembly无法在本地执行此操作(请记住,它目前只支持数字),所以我们依靠wasm-bindgen来填补空白。

在上述的最后一步中,当我们运行wasm-bindgen工具时,你会注意到wasm_greet.js文件与wasm_greet_bg.wasm文件一起出现。前者是我们想要的实际JS接口,执行任何必要的处理以调用Rust。* _bg.wasm文件包含实际的实现和我们所有的编译后的代码。

我们可以通过引入 ./wasm_greet 模块得到 Rust 代码愿意暴露出来的东西。我们已经看到了是如何集成的,可以继续看看执行的结果如何。首先是我们的示例:

代码

const rust = import("./wasm_greet"); 
rust.then(m => m.greet("World!"));

我们在这里以异步的方式导入接口,等待导入完成(下载和编译 wasm)。然后调用模块的 greet 函数。

注: 这里用到的异步加载目前需要 Webpack 来实现,但总会不需要的。而且,其它打包工具可能没有此功能。

如果我们看看由 wasm-bindgen 工具为 wasm_greet.js 文件生成的内容,会看到像这样的代码:

代码

import * as wasm from './wasm_greet_bg';

// ...

export function greet(arg0) {
  const [ptr0, len0] = passStringToWasm(arg0);
  try {
    const ret = wasm.greet(ptr0, len0);
    return ret;
  } finally {
    wasm.__wbindgen_free(ptr0, len0);
  }
}

export function __wbg_f_alert_alert_n(ptr0, len0) {
  // ...
}

注: 记住这是生成的,未经优化的代码,它可能既不优雅也不简洁!!在 Rust 中通过 LTO(Link Time Optimization,连接时优化)创建新的发行版,再通过 JS 打包工具流程(压缩)之后,可能会精简一些。

现在可以了解如何使用wasm-bindgen来生成greet函数。在底层它仍然调用wasm的greet函数,但是它是用一个指针和长度来调用的而不是用字符串。

了解passStringToWasm的更多细节可以访问Lin Clark's previous post。它包含了所有的模板,对我们来说这是除了wasm-bindgen工具以外还需要去写的东西!然后我们接下来看__wbg_f_alert_alert_n函数。

进入更深一层,下一个我们感兴趣的就是WebAssmbly中的greet函数。为了了解这个,我们先来看Rust编译器能访问到的代码。注意像上面生成的这种JS wrapper,在这里你不用写greet的导出符号,#[wasm_bindgen]属性会生成一个shim,由它来为你翻译,命名如下:

代码

pub fn greet(name: &str) {
  alert(&format!("Hello, {}!", name));
}

#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) {
  let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) }
  let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) };
  greet(arg0);
}

现在可以看到原始代码,greet,也就是由#[wasm_bindgen]属性插入的看起来有意思的函数__wasm_bindgen_generated_greet。这是一个导出函数(用#[export_name]和extern关键词来指定的),参数为JS传进来的指针/长度对。在函数中它会将这个指针/长度转换为一个&str (Rust中的一个字符串),然后将它传递给我们定义的greet函数。

从另一个方面看,#[wasm_bindgen]属性生成了两个wrappers:一个是在JavaScript中将JS类型的转换为wasm,另外一个是在Rust中接收wasm类型并将其转为Rust类型。

现在我们来看wrappers的最后一块,即alert函数。Rust中的greet函数使用标准format!宏来创建一个新的字符串然后传给alert。回想当我们声明alert方法的时候,我们是使用 #[wasm_bindgen]声明的,现在我们看看在这个函数中暴露给rustc的内容:

代码

fn alert(s: &str) {
  #[wasm_import_module = "__wbindgen_placeholder__"]
  extern {
    fn __wbg_f_alert_alert_n(s_ptr: *const u8, s_len: usize);
  }
  unsafe {
    let s_ptr = s.as_ptr();
    let s_len = s.len();
    __wbg_f_alert_alert_n(s_ptr, s_len);
  }
}

这并不是我们写的,但是我们可以看看它是怎么变成这样的。alert函数事实上是一个简化的wrapper,它带有Rust的 &str然后将它转换为wasm类型(数字)。它调用了我们在上面看到过的比较有意思的函数__wbg_f_alert_alert_n,然而它奇怪的一点就是#[wasm_import_module]属性。

在WebAssembly中所有导入的函数都有一个其存在的模块,而且由于wasm-bindgen构建在ES模块之上,所以这也将被转译为ES模块导入!

目前__wbindgen_placeholder__模块实际上并不存在,但它表示该导入将被wasm-bindgen工具重写,以从我们生成的JS文件中导入。

最后,对于最后一部分的疑惑,我们得到了我们所生成的JS文件,其中包含:

代码

export function __wbg_f_alert_alert_n(ptr0, len0) {
  let arg0 = getStringFromWasm(ptr0, len0);
  alert(arg0)
}

哇! 事实证明,这里隐藏着相当多的东西,我们从JS中的浏览器中的警告都有一个相对较长的知识链。不过,不要害怕,wasm-bindgen的核心是所有这些基础设施都被隐藏了! 你只需要在随便使用几个#[wasm_bindgen]编写Rust代码即可。然后你的JS可以像使用另一个JS包或模块一样使用Rust了。

wasm-bindgen还能做什么

wasm-bindgen项目在这个领域内志向远大,我们在此不再详细赘述。探索wasm-bindgen中的功能一个有效的方法就是探索示例目录,这些示例涵盖了从我们之前看到的Hello World! 到在Rust中对DOM节点的完全操作。

wasm-bindgen高级特性如下:

  • 引入JS结构,函数,对象等来在wasm中调用。你可以在一个结构中调用JS方法,也可以访问属性,这给人一种Rust是“原生”的感觉,让人觉得你曾经写过的Rust #[wasm_bindgen] annotations都可以连接了起来。
  • 将Rust结构和函数导出到JS。与只用JS使用数字类型来工作相比,你可以导出一个Rust结构并在JS中转换成一个类。然后可以将结构传递,而不是只使用整形数值来传递。 smorgasboard 这个例子可以让你体会支持的互操作特性。
  • 其他各种各样的特性例如从全局范围内导入(就像alert函数),在Rust中使用一个Result来获取JS异常,以及在Rust程序中通用方法模拟存储JS值。

如果你想了解更多的功能,继续阅读 issue tracker。

3、wasm-bindgen接下来做什么?

在我们结束之前,我想花一点时间来下描述wasm-bindgen的未来愿景,因为我认为这是当今项目最激动人心的一方面。

不仅仅支持Rust

从第1天起,wasm-bindgen CLI工具就设计成了多语言支持的。尽管Rust目前是唯一被支持的语言,但该工具也可以嵌入C或C++。 #[wasm_bindgen]属性创建了可被wasm-bindgen工具解析并随后删除的输出(* .wasm)文件的自定义部分。

本节介绍要生成哪些JS绑定以及它们的接口是什么。这个描述中没有关于Rust的特定部分,因此C ++编译器插件可以很容易地创建该部分,并通过wasm-bindgen工具进行处理。

我觉得这个方面特别令人振奋,因为我相信它使像wasm-bindgen这样的工具成为WebAssembly和JS集成的标准做法。希望所有编译为WebAssembly的语言都能受益,并且可以被bundler自动识别,以避免上述几乎所有的配置和构建工具。

自动绑定JS生态

使用#[wasm_bindgen] 宏导入功能唯一不好的一面就是你必须将所有东西都写出来,还要保证没有任何错误。这种让人觉得很单调(而且易错)的操作的自动化技术已经成熟了。

所有的web APIs都由WebIDL指定,而且在generate #[wasm_bindgen] annotations from WebIDL是可行的。这个就意味着你不需要像前面一样定义alert函数,而是你只需要写下面这些:

代码

#[wasm_bindgen]
pub fn greet(s: &str) {
  webapi::alert(&format!("Hello, {}!", s));
}

在这个例子中,WebIDL对web APIs的描述可以完全自动生成webapi集合,保证没有错误。

我们甚至可以将自动化更进一步,TypeScript组织已经做了这方面的复杂工作,参照generate #[wasm_bindgen] from TypeScript as well。可以免费用npm上的TypeScript自动绑定任何包!

比 JS DOM 操作更快的性能

最后要说的事情对 wasm-bindgen 来说也很重要:超快的 DOM 操作 —— 这是很多 JS 框架的终极目标。如今需要使用一些中间工具来调用 DOM 函数,这些工具正在由 JavaScript 实现转向 C++ 引擎实现。然而,在 WebAssembly 来临之后,这些工具并非必须。WebAssembly 是有类型的。

从第一天起,wasm-bindgen 代码生成的设计就考虑到了将来的宿主绑定方案。当这一特征出现在 WebAssembly 之后,我们可以直接调用导入的函数,而不需要 wasm-bindgen 的中间工具。

此外,它使得 JS 引擎积极优化 WebAssembly 对 DOM 的操作,使其对类型的支持更好,而且在调用 JS 的时候不再需要进行参数验证。在这一点上,wasm-bindgen 不仅在操作像 string 这样的富类型变得容易,还提供了一流的 DOM 操作性能。

收工

我自己发现使用WebAssembly是异常令人振奋的,不仅仅是因为其社区,还因为其如此快速地在进度上突飞猛进。wasm-bindgen工具拥有光明的未来。它使JS和诸如Rust这样的编程语言之间的互操作性变成了一流的体验,并且随着WebAssembly的不断发展它也将提供了长期的好处。

试着给wasm-bindgen一次机会,因功能需求而创建一个问题,亦或继续保持参与Rust和WebAssembly!

关于Alex Crichton(作者)

Alex是Rust核心团队的成员之一,自2012年底以来一直从事于Rust。目前他正在帮助WebAssembly Rust Working Group使得Rust + Wasm成为最佳体验。Alex还帮助维护Cargo(Rust的包管理器),Rust标准库以及Rust的发布和CI的基础架构。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript/jQuery 表单美化插件小结
Feb 14 Javascript
js获取html文件的思路及示例
Sep 17 Javascript
当鼠标滑过文本框自动选中输入框内容的JS代码分享
Nov 26 Javascript
javascript里使用php代码实例
Dec 13 Javascript
jQuery中attr()方法用法实例
Jan 05 Javascript
JS模拟的Map类实现方法
Jun 17 Javascript
JS实现简易的图片拖拽排序实例代码
Jun 09 Javascript
vue2.0 自定义 饼状图 (Echarts)组件的方法
Mar 02 Javascript
Vue中mintui的field实现blur和focus事件的方法
Aug 25 Javascript
深入了解JavaScript代码覆盖
Jun 13 Javascript
Vue.js获取手机系统型号、版本、浏览器类型的示例代码
May 10 Javascript
多页vue应用的单页面打包方法(内含打包模式的应用)
Jun 11 Javascript
微信小程序解析富文本过程详解
Jul 13 #Javascript
微信小程序wx.navigateTo中events属性实现页面间通信传值,数据同步
Jul 13 #Javascript
微信小程序Echarts覆盖正常组件问题解决
Jul 13 #Javascript
微信小程序图片左右摆动效果详解
Jul 13 #Javascript
vue iview多张图片大图预览、缩放翻转
Jul 13 #Javascript
vue实现图片预览组件封装与使用
Jul 13 #Javascript
微信小程序基于Taro的分享图片功能实践详解
Jul 12 #Javascript
You might like
PHP 巧用数组降低程序的时间复杂度
2010/01/01 PHP
基于PHP开发中的安全防范知识详解
2013/06/06 PHP
PHP使用Mysql事务实例解析
2014/09/08 PHP
PHP实现上传文件并存进数据库的方法
2015/07/16 PHP
yii2局部关闭(开启)csrf的验证的实例代码
2017/07/10 PHP
phpStudy配置多站点多域名和多端口的方法
2017/09/01 PHP
javascript实现的动态文字变换
2007/07/28 Javascript
javascript优先加载笔记代码
2008/09/30 Javascript
javascript 表单规则集合对象
2009/07/21 Javascript
JavaScript全局函数使用简单说明
2011/03/11 Javascript
js中传递特殊字符(+,&)的方法
2014/01/16 Javascript
javascript作用域和闭包使用详解
2014/04/25 Javascript
浅谈JavaScript数据类型及转换
2015/02/28 Javascript
Java框架SSH结合Easyui控件实现省市县三级联动示例解析
2016/06/12 Javascript
JS在Chrome浏览器中showModalDialog函数返回值为undefined的解决方法
2016/08/03 Javascript
IONIC自定义subheader的最佳解决方案
2016/09/22 Javascript
layui 弹出删除确认界面的实例
2019/09/06 Javascript
[01:06] DOTA2英雄背景故事第三期之秩序法则光之守卫
2020/07/07 DOTA
Python中文件I/O高效操作处理的技巧分享
2017/02/04 Python
python日期时间转为字符串或者格式化输出的实例
2018/05/29 Python
Python中pymysql 模块的使用详解
2019/08/12 Python
如何运行带参数的python脚本
2019/11/15 Python
Python模块zipfile原理及使用方法详解
2020/08/04 Python
Anthropologie英国:美国家喻户晓的休闲服装和家居产品品牌
2018/12/05 全球购物
中专毕业生的自我鉴定
2013/12/01 职场文书
电脑销售顾问自荐信
2014/01/29 职场文书
入党申请自荐书范文
2014/02/11 职场文书
签约仪式策划方案
2014/06/02 职场文书
设计大赛策划方案
2014/06/13 职场文书
井冈山红色之旅心得体会
2014/10/07 职场文书
四年级小学生评语
2014/12/26 职场文书
试用期工作表现自我评价
2015/03/06 职场文书
敬老院活动感想
2015/08/07 职场文书
新学期家长寄语2016
2015/12/03 职场文书
初中运动会闭幕词范本3篇
2019/12/09 职场文书
Nginx实现高可用集群构建(Keepalived+Haproxy+Nginx)
2021/05/27 Servers