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

前言

最近我们已经见识了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,这可能看起来有点啰嗦,但不要害怕!随着时间的推移,wasm-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的基础架构。

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

(0)

相关推荐

  • 完美解决node.js中使用https请求报CERT_UNTRUSTED的问题

    只要调用了没有受信的https就会报错:CERT_UNTRUSTED 简单的解决方法就是设置环境变量回避非授信证书的问题. 只要在请求的代码之前加上如下代码即可: process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 结束!!! 以上就是小编为大家带来的完美解决node.js中使用https请求报CERT_UNTRUSTED的问题全部内容了,希望大家多多支持我们~

  • 在PHP程序中使用Rust扩展的方法

     C或PHP中的Rust 我的基本出发点就是写一些可以编译的Rust代码到一个库里面,并写为它一些C的头文件,在C中为被调用的PHP做一个拓展.虽然并不是很简单,但是很有趣. Rust FFI(foreign function interface) 我所做的第一件事情就是摆弄Rust与C连接的Rust的外部函数接口.我曾用简单的方法(hello_from_rust)写过一个灵活的库,伴有单一的声明(a pointer to a C char, otherwise known as a strin

  • python实现上传样本到virustotal并查询扫描信息的方法

    本文实例讲述了python实现上传样本到virustotal并查询扫描信息的方法.分享给大家供大家参考.具体方法如下: import simplejson import urllib import urllib2 import os MD5 = "5248f774d2ee0a10936d0b1dc89107f1" MD5 = "12fa5fb74201d9b6a14f63fbf9a81ff6" #do not have report on virustotal.co

  • python实现封装得到virustotal扫描结果

    本文实例讲述了python实现封装得到virustotal扫描结果的方法.分享给大家供大家参考.具体方法如下: import simplejson import urllib import urllib2 import os, sys import logging try: import sqlite3 except ImportError: sys.stderr.write("ERROR: Unable to locate Python SQLite3 module. " \ &qu

  • IntelliJ安装并使用Rust IDE插件

    Rust 是一个由Mozilla主导开发的通用编译型编译语言.它的设计准则为"安全,并发,实用",支持函数式,并发式,过程式以及面向对象的编程风格. Rust插件的主要特性如下: 导航特性:Go to Class.Go to Symbol.Go to Super Module.Structure.Go to Definition. 编辑器特性:代码自动完成.格式化(计划支持rustfmt).合并行.智能按键(如自动插入匹配的符号).自动填充后缀.基本的Intention和重构(如引入变

  • you *might* want to use the less safe log_bin_trust_function_creators variable

    恢复MySQL数据库创建存储过程是遇到错误 you *might* want to use the less safe log_bin_trust_function_creators variable 需要执行 SET GLOBAL log_bin_trust_function_creators = 1;

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

    前言 最近我们已经见识了WebAssembly如何快速编译.加速JS库以及生成更小的二进制格式.我们甚至为Rust和JavaScript社区以及其他Web编程语言之间的更好的互操作性制定了高级规划.正如前面一篇文章中提到的,我想深入了解一个特定组件的细节,wasm-bindgen. 今天WebAssembly标准只定义了四种类型:两种整数类型和两种浮点类型.然而,大多数情况下,JS和Rust开发人员正在使用更丰富的类型! 例如,JS开发人员经常与互以添加或修改HTML节点相关的文档交互,而Rus

  • 动态规划之使用备忘录来改进Javascript函数

    目录 什么是备忘录? 备忘录的概念 1.引用透明 2.查找表 比较函数使用备忘录和不用备忘录 解决方法是记录调用函数的返回结果 备忘录的意义 结论:什么是备忘录? 前言; 动态规划已出现了十多年.根据维基百科,它既是一种数学优化方法,也是一种计算机编程方法.一个问题要真正应用动态规划,必须具有两个关键属性:最优结构和重叠子结构.本文不会细讲动态规划,而是将关注重叠子结构如何成为动态规划的关键属性之一.由于这关系到接下来的存储解决方案问题,所以对它的讨论非常重要. 本文将介绍什么是备忘录,备忘录对

  • 详解JavaScript如何创建一个非自动播放的GIF网络组件

    目录 一些很可爱的测试数据 构建Web组件 逻辑 结果 今天,我将向您展示如何创建一个允许您的用户决定是否要播放 gif 的 Web 组件!让我们开始吧. 一些很可爱的测试数据 这里用的gif是小骆驼和猫的这种可爱互动: 哇,太可爱了!我可以看一天这个 构建 Web 组件 对于这个 Web 组件,我们需要一些东西: 画布(“缩略图”所在的位置) 一张图片(实际的 gif) 标有“gif”的标签 一些造型 让我们这样做: const noAutoplayGifTemplate = document

  • 如何改进javascript代码的性能

    本来在那片编写可维护性代码文章后就要总结这篇代码性能文章的,耽搁了几天,本来也是决定每天都要更新一篇文章的,因为以前欠下太多东西没总结,学过的东西没去总结真的很快就忘记了,记录一下在你脑力留下更深的印象,特别是这些可维护性代码,性能什么的,当在你脑子里形成一种习惯了,那你就牛了!这里也要给初学者一个建议:多总结你学过的东西,因为这其实也是在学习新知识! 好,进入我们的主题:如何提高JS代码的性能. 1.优化DOM交互 DOM与我们的页面紧密相关,浏览器渲染页面也就是在渲染解析后的DOM元素,DO

  • JavaScript如何调试有哪些建议和技巧附五款有用的调试工具

    以下内容是关于javascript如何调试有哪些建议和技巧的相关知识,具体详情请看下文吧. 浏览器开发者工具 我个人最喜欢Chrome开发者工具.虽然Safari和Firefox无法达到Chrome那么高的标准,但它们也在逐渐改善.在Firefox中,可以将Firebug和Firefox开发者工具组合使用.如果Firefox小组在改进内置开发者工具方面继续表现优异的话,Firebug有一天可能会被淘汰. 先把个人偏好放在一边,你应该能够在目标浏览器中对任意代码进行试验和调试.你的目标浏览器可能包

  • Rust 搭建一个小程序运行环境的方法详解

    目录 从零到一:构建一个能运行小程序的App FinClip 安全沙箱的初始化 获得 SDK Key 以及 SDK Secret 的两种方式 方式一:采用 FinClip.com 托管服务 方式二:自行部署 FinClip 社区版 FinClip SDK 在 App 中的初始化 Rust 开发环境的准备 Rust 代码编译成 iOS 静态库的验证 搭建一个FinClip社区版docker运行环境,安装设置Rust开发编译iOS代码的环境,设置xcode的项目配合,集成FinClip SDK,准备

  • 使用Rust制作康威生命游戏的实现代码

    目录 前言 安装准备 初始项目 Cargo.toml 初始web项目 游戏规则 游戏设计 Rust实现 测试 调试 前言 之前学了几遍,后来忘记了,通过制作该游戏再复习复习. 安装准备 wasm-pack : https://rustwasm.github.io/wasm-pack/installer/ cargo-generate: cargo install cargo-generate 初始项目 初始rust项目 使用wasm的项目模板: cargo generate --git http

  • JavaScript实现网页截图功能

    使用JavaScript截图,这里我要推荐两款开源组件:一个是Canvas2Image,它可以将Canvas绘图编程PNG/JPEG/BMP的图像:但是光有它还不够,我们需要给任意DOM(至少是绝大部分)截图,这就需要html2canvas,它可以将DOM对象转换成一个canvas对象.两者的功能结合起来,就可以把页面上的DOM截图成PNG或者JPEG图像了,很酷. Canvas2Image 它的原理是利用了HTML5的canvas对象提供了toDataURL()的API: 复制代码 代码如下:

  • 改进 ASP 的字符串处理性能

    大多数 Active Server Pages (ASP) 应用程序都要通过字符串连接来创建呈现给用户的 HTML 格式的数据.本文对几种创建此 HTML 数据流的方法进行了比较,在特定情况下,某些方法在性能方面要优于其他方法.本文假定您已经具备一定的 ASP 和 Visual Basic 编程方面的知识. 目录 简介 ASP 设计 字符串连接 快捷的解决方案 StringBuilder 内置方法 测试 结果 小结 简介 编写 ASP 页面时,开发人员实际上是创建一个格式化的文本流,通过 ASP

  • 用JavaScript编写COM组件的步骤

    支持这些接口的脚本语言有:JavaScript/JScript.VBScript.Perl和Python等. WSC有以下特点.  小巧高效:  易于创建.维护和部署:  提供了创建COM部件的能力:  提供了访问大量系统服务的能力. 用JavaScript编写COM组件的核心工作是完成一个.wsc(Windows Scripting Component)文件..wsc文件是一种标准的XML文件,其格式如下: <?xml version="1.0" encoding=&q

随机推荐