WebAssembly(Wasm)中的字符串

3,323 阅读16分钟

作者:Timothy McCallum Second State 核心开发

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

字符串的重要性

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

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

WebAssembly 中的字符串

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

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

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

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

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

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

现在,可以将高级值(如字符串)转换为一组数字。 如果实现了这一点,那么我们就可以在函数之间来回传递这些数字集(代表字符串)。

然而,这里有几个问题。

对于一般的高级编码来说,总是需要这种常量的显式编码 / 解码是很麻烦的,因此这不是一个很好的长期解决方案。

此外,事实证明,这种方法目前在 Wasm 实际上不可能实现。 原因是,尽管 Wasm 函数可以接受函数中的许多值(作为参数) ,但是目前 Wasm 函数只能返回一个值。而Wasm会有很多信息。

现在,让我们通过看看 Rust 中的字符串的工作机制,来讲一下基础知识。

Rust字符串

字符串

Rust中的String 可以被认为是一个保证了拥有良好的 UTF-8 Vec(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 创建新字符串。 可以使用以下方法将字符串文本转换为 StringTo 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 (rustwasm.github.io/wasm-pack/) 在幕后使用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. [在线] 请访问: github.com/WebAssembly…
  • GitHub — RustWasm. (2020). rustwasm/wasm-bindgen. [在线] 请访问: github.com/rustwasm/wa…
  • 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. [在线] 请访问: developer.mozilla.org/en-US/docs/…
  • MDN Web Docs — Web APIs. (2020). Web APIs. [在线] 请访问: developer.mozilla.org/en-US/docs/…
  • 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. [在线] 请访问: rustwasm.github.io/docs/wasm-b… [Accessed 27 Jan. 2020].
  • Wasm By Example. (2019). WebAssembly Linear Memory. [在线] 请访问: wasmbyexample.dev/examples/we…
  • Williams, A. (2019). Rust, WebAssembly, and Javascript Make Three: An FFI Story. [在线] 请访问: www.infoq.com/presentatio…