WebAssembly(Wasm)中的字符串

做者:Timothy McCallum Second State 核心开发

这篇文章详细解释了 WASM 中如何实现字符串,文章有点长,慢慢读~javascript

字符串的重要性

计算机程序只用数字就能够成功执行。 然而,为了方便人机交互,人类可读的字符和文字是必需的。 当咱们思考人类如何与 Web 上的应用程序进行交互时,状况尤为如此。 绝佳的例子是,人们在访问Web 时选择使用域名,而非数字 IP 地址。css

正如本文的标题所宣称的,咱们将讨论 WebAssembly (Wasm)中的字符串。 WASM是最近咱们看到的最使人兴奋的计算机编程技术之一。 Wasm 是一种接近机器的、支持多平台的、低级的、类汇编语言(Reiser and bl ser,2017) ,它从一开始就是第一个实现形式语义学的主流编程语言(Rossberg et al. ,2018)。html

WebAssembly 中的字符串

有趣的是,WebAssembly 代码中没有本地字符串。 更具体地说,Wasm 没有字符串数据类型。java

Wasm的MVP(只支持wasm32)有一个ILP32数据模型,目前提供如下4种数据类型,分别是:node

  • i32,一个32位的整数(至关于 c + + 的带符号 long int)
  • i64,一个64位的整数(至关于 c + + 的带符号 long int)
  • f32,32位浮点数(至关于 c + + 的浮点数)
  • f64,64位浮点数(至关于 c + + 的 double)

虽然咱们很快就会开始讨论在浏览器中使用 Wasm,但关键是要始终记住,从根本上讲,Wasm 的执行是用堆栈机器来定义的。 其基本想法是,每种类型的指令都会将必定数量的 i3二、 i6四、 f3二、 f64值从堆栈中推入或弹出(MDN Web Docs ——理解 WebbAssembly 文本格式,2020)。jquery

正如咱们所看到的,上面的四种数据类型都属于数字。 那么,若是是这种状况,咱们如何在 WebAssembly (Wasm)中促成(facilitate)字符串呢?git

WebAssembly 中的字符串ーー怎样解决?

如今,能够将高级值(如字符串)转换为一组数字。 若是实现了这一点,那么咱们就能够在函数之间来回传递这些数字集(表明字符串)。 github

然而,这里有几个问题。web

对于通常的高级编码来讲,老是须要这种常量的显式编码 / 解码是很麻烦的,所以这不是一个很好的长期解决方案。 apache

此外,事实证实,这种方法目前在 Wasm 实际上不可能实现。 缘由是,尽管 Wasm 函数能够接受函数中的许多值(做为参数) ,可是目前 Wasm 函数只能返回一个值。而Wasm会有不少信息。

如今,让咱们经过看看 Rust 中的字符串的工做机制,来说一下基础知识。

Rust字符串

字符串

Rust中的String 能够被认为是一个保证了拥有良好的 UTF-8 Vec<u8>(Blandy and Orendorff,2017)。

& str

Rust 中的 &str 是对其余人拥有的一组 UTF-8文本的引用。&str 是一个宽指针(fat pointer),包含实际数据的地址及其长度。 您能够将 &str 看做是一个保证包含格式良好的 UTF-8的 &[u8] (Blandy and Orendorff,2017)。

编译时的字符串——存储在可执行文件中

字符串文本是一个指预先分配的文本的 &str,一般与程序机器代码一块儿存储在只读内存文档中; 程序开始执行时建立字节,一直到程序结束。 所以,修改 &str 是不可能的(Blandy 和 Orendorff,2017)。

&str 能够引用任何字符串的任何片断,所以使用 &str 做为函数参数的一部分是合适的; 调用者能够传递 String&str (Klabnik 和 Nichols,2019)。

像这样的代码这样:

fn my_function(the_string: &str) -> &str {
 // code ...
}

运行时的字符串ー在运行时分配和释放

能够在运行时使用 String 建立新字符串。 可使用如下方法将字符串文本转换为 String To String ()String::from 作一样的事情,所以您选择哪一个只是风格上的区别(Klabnik 和 Nichols,2019)。

let s = "the string literal".to_string();
let s = String::from("the string literal");

将字符串转换为数字

下面的 Rust 代码获取字符串 hello 并将其转换为字节,而后将该字符串的两个版本输出到终端。

fn main() {
    let s: String = String::from("hello");
    println!("String: {:?}", &s);
    println!("Bytes: {:?}", &s.as_bytes());
}

输出

String: "hello"
Bytes: [104, 101, 108, 108, 111]

Wasm 的“ Hello World! ”例子

有了全部这些信息,咱们如何为 Web 用 Wasm 编写“ Hello World! ” ? 例如,咱们如何在用户界面和 Wasm 执行环境之间来回传递字符串?

问题的核心是... WebAssembly 须要很好地使用 JavaScript... 咱们须要使用Javascript并将 JavaScript 对象传递到 WebAssembly,但 WebAssembly 根本不支持这一点。 目前,WebAssembly 只支持整数和浮点数(Williams,2019)。

将 JavaScript 对象硬塞进 u32以便用于 Wasm,须要费些力气。

file

摔跤图案,看起来很像甲壳类动物。

这是个巧合吗? 我不这么认为。

Bindgen

Wasm-bindgen 是 Rust 的 build time 依赖项。 它可以在编译时生成 Rust 和 JavaScript 代码。 它也能够用做一个可执行文件,在命令行中称为 bindgen。 实际上,Wasm-bindgen 工具容许 JavaScript 和 Wasm 交流像字符串这样的高级 JavaScript 对象。 与专门通讯的数字数据类型相反( rustwasm.github.io ,2019)。

这是如何实现的呢?

内存

“ WebAssembly 程序的主要存储是大量的原始字节数组、线性内存或单纯的内存 (Rossberg et al. ,2018)。

Wasm-bindgen 工具抽象出线性内存,并容许在 Rust 和 JavaScript 之间使用本地数据结构(Wasm By Example,2019)。

当前的策略是让 wasm-bindgen 维护一个“heap”。 这个“ heap”是一个由 wasm-bindgen 建立的模块本地变量,位于 wasm-bindgen 生成的 JavaScript 文件中。

接下来的部分可能看起来有点很差懂,请坚持下去。 事实证实,这个“heap”中的第一个插槽被认为是一个堆栈。 这个堆栈,像典型的程序执行堆栈同样,是向下增加。

“stack” 上的临时 JS 对象

短时间的 JavaScript 对象被推送到堆栈上,它们的索引(堆栈中的位置和长度)被传递给 Wasm。 一个栈指针用来指出下一个项目的推送位置(GitHub ー RustWasm,2020)。

删除只是存储未定义 / null。 因为这种方案的 “栈-y” 特性,它只适用于 Wasm 没有保留 JavaScript 对象的状况(GitHub ー RustWasm,2020)。

JsValue
Wasm-bindgen 库的 Rust 代码库自己使用一个特殊的 JsValue。 编写的导出函数(以下图所示)能够引用这个特殊的 JsValue。
#[wasm_bindgen]
pub fn foo(a: &JsValue) {
 // ...
}

wasm-bindgen 生成的 Rust

相对于上面编写的 Rust,#[wasm_bindgen] 生成的 Rust 代码看起来是这样的。

#[export_name = "foo"] 
pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) {
    let arg0 = unsafe {
        ManuallyDrop::new(JsValue::__from_idx(arg0))
    };
    let arg0 = &*arg0;
    foo(arg0);
}

而外部可调用的标识符仍然称为 foo。 调用时,wasm_bindgen-generated Rust 函数的内部代码即 Wasm bindgen generated foo 其实是从 Wasm 模块导出的。 Wasm bindgen-generated 函数接受一个整数参数,并将其包装为 JsValue

点要记住,因为 Rust 的全部权属性,对 JsValue 的引用不能持续到函数调用的生命周期以后。 所以,wasm-bindgen 生成的 Javascript 须要释放做为该函数执行的一部分而建立的堆栈槽。 接下来让咱们看看生成的 Javascript。

Wasm-bindgen 生成的 JavaScript

// foo.js
import * as wasm from './foo_bg';
const heap = new Array(32);
heap.push(undefined, null, true, false);
let stack_pointer = 32;
function addBorrowedObject(obj) {
  stack_pointer -= 1;
  heap[stack_pointer] = obj;
  return stack_pointer;
}
export function foo(arg0) {
  const idx0 = addBorrowedObject(arg0);
  try {
    wasm.foo(idx0);
  } finally {
    heap[stack_pointer++] = undefined;
  }
}

heap

咱们能够看到, JavaScript 文件从 Wasm 文件导入。
而后咱们能够看到前面提到的“heap”模块-本地变量被建立。 重要的是要记住这个 JavaScript 是由 Rust 代码生成的。 若是您想了解这是如何作到的,请参阅此 mod.rs文件中的第747行。

我提供了 Rust 的一小段代码,这段代码能够生成 JavaScript,代码以下。

self.global(&format!("const heap = new Array({});", INITIAL_HEAP_OFFSET));

在 Rust 文件中,INITIAL heap offset 被硬编码为32。 所以,数组默认有32个项。

file

一旦建立,在 Javascript 中,这个 heap 变量将在执行时存储来自 Wasm 的全部可引用的 Javascript 值。
若是咱们再看一下生成的 JavaScript,咱们能够看到被导出的函数 foo 接受一个任意的参数 arg0foo 函数调用 addBorrowedObject ,将其传递到 arg0addBorrowedObject function 将堆栈指针位置递减1(为32,如今为31) ,而后将对象存储到该位置,同时还将该特定位置返回给调用 foo 函数。

堆栈位置存储为一个名为 idx0的常量。 而后将 idx0传递给由 bindgen 生成的 Wasm,以便 Wasm 能够对其进行操做(GitHub ー RustWasm,2020)。

正如咱们提到的,咱们仍然在讨论“堆栈”上的 Temporary JS 对象。

若是咱们查看生成的 JavaScript 代码的最后一行文本,咱们会看到堆栈指针位置的堆被设置为未定义,而后自动(感谢 ++ 语法)堆栈指针变量被递增回原来的值。

到目前为止,咱们已经介绍了一些只是临时使用的对象,即只在一次函数调用期间使用。 接下来让咱们看看长期存在的 JS 对象。

长期存在的 JS 对象

在这里,咱们将讨论 JavaScript 对象管理的后半部分,再次引官方的 bindgen 文档( rustwasm.github.io,2019)。

栈的严格的 push / pop 不适用于长期存在的 JavaScript 对象,所以咱们须要一种更为永久的存储机制。

若是咱们回顾一下最初编写的 foo 函数示例,咱们能够看到稍微的更改就会改变 JsValue 的全部权,从而改变其生命周期。 具体来讲,经过删除 & (在咱们编写的 Rust 中) ,咱们使 foo 函数得到了对象的所有全部权,而不仅是借用一个refference。

// foo.rs
#[wasm_bindgen]
pub fn foo(a: JsValue) {
    // ...
}

如今,在生成的 Rust 中,咱们调用 addHeapObject,而不是 addBorrowedObject

import * as wasm from './foo_bg'; // imports from wasm file
const heap = new Array(32);
heap.push(undefined, null, true, false);
let heap_next = 36;
function addHeapObject(obj) {
  if (heap_next === heap.length)
    heap.push(heap.length + 1);
  const idx = heap_next;
  heap_next = heap[idx];
  heap[idx] = obj;
  return idx;
}
T

addHeapObject 使用 heap 和 heap_next 函数来获取一个 slot 来存储对象。

如今咱们已经对使用 JsValue 对象有了一个大体的了解,接下来让咱们关注字符串。

字符串经过两个参数,一个指针和一个长度传递给 wasm。(GitHub ー RustWasm,2020)

字符串使用 TextEncoder API 进行编码,而后复制到 Wasm 堆上。 下面是一个使用 TextEncoder API 将字符串编码为数组的快速示例。 你能够在你的浏览器控制台上尝试一下。

const encoder = new TextEncoder();
const encoded = encoder.encode('Tim');
encoded
// Uint8Array(3) [84, 105, 109]

只传递索引(指针和长度),而不是传递整个高级对象,是颇有意义的。 正如咱们在本文开头所提到的,咱们可以将许多值传递到一个 Wasm 函数中,但只容许返回一个值。 那么咱们如何从一个 Wasm 函数返回指针和长度呢?

目前 WebAssembly GitHub 上有一个公开的 issue,是正在实现和标准化 Wasm 函数的多个返回值。

同时导出一个返回字符串的函数,须要一个涉及到的两种语言的 shim。 在这种状况下,JavaScript 和 Rust 都须要就每一方如何转换成和转换成 Wasm (用他们各自的语言)达成一致。

Wasm-bindgen 工具能够链接全部这些shim,而 #[wasm_bindgen] 宏也能够处理 Rust shim (GitHub ー RustWasm,2020)。

这一创新以一种很是聪明的方式解决了 WebAssembly 中的字符串问题。 这当即为无数的 Web 应用程序打开了大门,使之能够利用 Wasm 的出色特性。 随着开发的继续,即多值提议的正规化,Wasm 在浏览器内外的功能将大大提高。

让咱们来看一些在 WebAssembly 中使用字符串的具体例子。 这些都是你能够本身尝试的成功例子。

具体的例子

正如 bindgen 文档所说。 “经过添加 wasm-pack,您能够在本地 web 上运行 Rust,将其做为更大应用程序的一部分发布,甚至能够在 NPM 上发布 Rust-compiled to-webassembly! ”

Wasm-pack

file

Wasm-pack 是一个很是棒的 Wasm 工做流工具,易于使用。

Wasm-pack (https://rustwasm.github.io/wa... 在幕后使用wasm-bindgen。

简而言之,wasm-pack 在编译到 WebAssembly 的同时生成 Rust 代码和 JavaScript 代码。 Wasm-pack 容许您经过 JavaScript 与 WebAssembly 交流,就像它是 JavaScript 同样(Williams,2019)。

Wasm使用 wasm32-unknown-unknown目标编译您的代码。

Wasm-pack (客户端-网页)

下面是一个使用 wasm-pack 在 web 上实现字符串链接的例子。

若是咱们启动一个 Ubuntu Linux 系统并执行如下操做,咱们能够在几分钟内开始构建这个演示。

#System housekeeping
sudo apt-get update
sudo apt-get -y upgrade
sudo apt install build-essential
#Install apache
sudo apt-get -y install apache2
sudo chown -R $USER:$USER /var/www/html
sudo systemctl start apache2
#Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
#Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

一旦系统设置好咱们能够用Rust建立一个新项目

cd ~
cargo new --lib greet
cd greet

而后咱们执行一些 Rust 配置,以下所示(打开 Cargo.toml 文件并在文件底部添加如下内容)

[lib]
name = "greet_lib"
path = "src/lib.rs"
crate-type =["cdylib"][dependencies]

最后,咱们使用 wasm-pack 构建程序

wasm-pack build --target web

一旦代码被编译,咱们只须要建立一个 HTML 文件来进行交互,而后将 HTML 以及 wasm-packpkg 目录的内容复制到咱们提供 Apache2 的地方。

~ / greet / pkg 目录中建立如下索引 . html 文件。

<html>
<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
    <script type="module">import init, { greet } from './greet_lib.js';async function run() {await init();var buttonOne = document.getElementById('buttonOne');buttonOne.addEventListener('click', function() {var input = $("#nameInput").val();alert(greet(input));}, false);}run();</script>
</head>
<body>
    <div class="row">
        <div class="col-sm-4"></div>
        <div class="col-sm-4"><b>Wasm - Say hello</b></div>
        <div class="col-sm-4"></div>
    </div>
    <hr />
    <div class="row">
        <div class="col-sm-2"></div>
        <div class="col-sm-4">What is your name?</div>
        <div class="col-sm-4"> Click the button</div>
        <div class="col-sm-2"></div>
    </div>
    <div class="row">
        <div class="col-sm-2"></div>
        <div class="col-sm-4">
            <input type="text" id="nameInput" placeholder="1" , value="1">
        </div>
        <div class="col-sm-4">
            <button class="bg-light" id="buttonOne">Say hello</button>
        </div>
        <div class="col-sm-2"></div>
    </div>
</body>
<scriptsrc="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous">
    </script>
</html>

将 pkg 目录的内容复制到咱们在运行Apache2的地方

cp -rp pkg/* /var/www/html/

若是访问服务器的地址,咱们会看到下面的页面。

file

当咱们添加咱们的名字并单击按钮时,获得如下响应。

file

Wasm-pack (服务器端- Node.js)

如今咱们已经看到了使用 html / js 和 Apache2的实际应用,让咱们继续并建立另外一个演示。 这一次是在 Node.js 的环境中,遵循 wasm-packnpm-browser-packages 文档

sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install build-essential
sudo apt-get -y install curl
#Install Node and NPM
curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install npm
#Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
#Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf |
sudo apt-get install pkg-config
sudo apt-get install libssl-dev
cargo install cargo-generate
cargo generate --git https://github.com/rustwasm/wasm-pack-template

感兴趣的话, 该demo(是用官方demo软件生成的)的Rust代码以下

mod utils;
use wasm_bindgen::prelude::*;// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;#[wasm_bindgen]
extern {
fn alert(s: &str);
}#[wasm_bindgen]
pub fn greet() {
    alert("Hello, tpmccallum-greet!");
}

您可使用如下命令构建项目,最后一个参数是 npmjs. com 用户名

wasm-pack build --scope tpmccallum

要登陆到您的 npm 账户,只需经过 wasm-pack 键入如下命令

wasm-pack login

要发布,只需切换到 pkg 目录并运行如下命令

cd pkg
npm publish --access=public

好的,咱们已经发布了一个包。

如今,让咱们继续建立一个新的应用程序,咱们能够在其中使用咱们的包。

请注意,咱们使用的是模板,因此不要为下面的命令建立本身的应用程序名,而是使用以下所示的 create-wasm-app 文本。

cd ~
npm init wasm-app create-wasm-app

在这个阶段,咱们想从 npmjs. com 安装这个软件包。 咱们使用如下命令来实现这一点

npm i @tpmccallum/tpmccallum-greet

如今打开 index.js,按照名称导入包,以下所示

import * as wasm from "tpmccallum-greet";
  
wasm.greet();

最后,启动演示并访问 localhost: 8080

npm install
npm start

file

更普遍的应用

预计“ WebAssembly 将在其余领域发现普遍的用途。 事实上,其余多种嵌入方式已经在开发中: 内容传输网络中的沙箱,区块链上的智能合约或去中心化的云计算,移动设备的代码格式,甚至做为提供可移植语言运行时的独立引擎” (Rossberg et al. ,2018)。

这里详细解释的 MutiValue 提议颇有可能最终容许一个 Wasm 函数返回许多值,从而促进一组新接口类型的实现。

实际上,有一个提议,正如这里所解释的,在 WebAssembly 中添加了一组新的接口类型,用于描述高级值(好比字符串、序列、记录和变量)。 这种新的方法能够实现这一点,而无需提交到单一的内存表示或共享模式。 使用这种方法,接口类型只能在模块的接口中使用,而且只能由声明性接口适配器生成或使用。

该提案代表,它是在 WebAssembly 核心规范的基础上进行语义分层的(经过多值和引用类型提案进行扩展)。 全部的适应都在一个自定义部分中指定,而且可使用 javascript api 进行polyfill。

参考文献

  • Blandy, J. and Orendorff, J. (2017). 《Rust 编程》. O’Reilly Media Inc.
  • GitHub — WebAssembly. (2020). WebAssembly/interface-types. [在线] 请访问: https://github.com/WebAssembl...
  • GitHub — RustWasm. (2020). rustwasm/wasm-bindgen. [在线] 请访问: https://github.com/rustwasm/w...
  • Haas, A., Rossberg, A., Schuff, D.L., Titzer, B.L., Holman, M., Gohman, D., Wagner, L., Zakai, A. and Bastien, J.F., 2017, June. 《使用WebAssembly加快网络速度》在第38届ACM SIGPLAN会议上有关编程语言设计和实现的会议论文集(第185–200页)。
  • Klabnik, S. and Nichols, C. (2019). The Rust Programming Language (Covers Rust 2018). San Francisco: No Starch Press Inc.
  • MDN Web Docs — Understanding WebAssembly text format. (2020). Understanding WebAssembly text format. [在线] 请访问: https://developer.mozilla.org...
  • MDN Web Docs — Web APIs. (2020). Web APIs. [在线] 请访问: https://developer.mozilla.org...
  • Reiser, M. and Bläser, L., 2017, October. 经过交叉编译到WebAssembly来加速JavaScript应用程序。在第9届ACM SIGPLAN虚拟机和中间语言国际研讨会论文集(第10-17页)中。
  • Rossberg, A., Titzer, B., Haas, A., Schuff, D., Gohman, D., Wagner, L., Zakai, A., Bastien, J. and Holman, M. (2018). * 使用WebAssembly加快网络速度。 ACM通信,61(12),第107–115页。
  • Rustwasm.github.io. (2019). Introduction — The wasm-bindgen Guide. [在线] 请访问: https://rustwasm.github.io/do... [Accessed 27 Jan. 2020].
  • Wasm By Example. (2019). WebAssembly Linear Memory. [在线] 请访问: https://wasmbyexample.dev/exa...
  • Williams, A. (2019). Rust, WebAssembly, and Javascript Make Three: An FFI Story. [在线] 请访问: https://www.infoq.com/present...
相关文章
相关标签/搜索