作者:Timothy McCallum Second State 核心开发字符串的重要性 计算机程序只用数字就可以成功执行。 然而,为了方便人机交互,人类可读的字符和文字是必需的。 当我们思考人类如何与 Web 上的应用程序进行交互时,情况尤其如此。 绝佳的例子是,人们在访问Web 时选择使用域名,而非数字 IP 地址。
这篇文章详细解释了 WASM 中如何实现字符串,文章有点长,建议收藏后慢慢读~
正如本文的标题所宣称的,我们将讨论 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)
正如我们所看到的,上面的四种数据类型都属于数字。 那么,如果是这种情况,我们如何在 WebAssembly (Wasm)中促成(facilitate)字符串呢?
WebAssembly 中的字符串ーー怎样解决?
现在,可以将高级值(如字符串)转换为一组数字。 如果实现了这一点,那么我们就可以在函数之间来回传递这些数字集(代表字符串)。
然而,这里有几个问题。
对于一般的高级编码来说,总是需要这种常量的显式编码 / 解码是很麻烦的,因此这不是一个很好的长期解决方案。
此外,事实证明,这种方法目前在 Wasm 实际上不可能实现。 原因是,尽管 Wasm 函数可以接受函数中的许多值(作为参数) ,但是目前 Wasm 函数只能返回一个值。而Wasm会有很多信息。
现在,让我们通过看看 Rust 中的字符串的工作机制,来讲一下基础知识。
Rust字符串 字符串
【WebAssembly|WebAssembly(Wasm)中的字符串】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)。编译时的字符串——存储在可执行文件中
字符串文本是一个指预先分配的文本的
&st
r,通常与程序机器代码一起存储在只读内存文档中;
程序开始执行时创建字节,一直到程序结束。 因此,修改 &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,需要费些力气。
文章图片
摔跤图案,看起来很像甲壳类动物。
这是个巧合吗? 我不这么认为。
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个项。
文章图片
一旦创建,在 Javascript 中,这个
heap
变量将在执行时存储来自 Wasm 的所有可引用的 Javascript 值。如果我们再看一下生成的 JavaScript,我们可以看到被导出的函数
foo
接受一个任意的参数 arg0
。 foo
函数调用 addBorrowedObject
,将其传递到 arg0
。 addBorrowedObject 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
文章图片
Wasm-pack 是一个非常棒的 Wasm 工作流工具,易于使用。
Wasm-pack (https://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-pack
的 pkg
目录的内容复制到我们提供 Apache2 的地方。在
~ / greet / pkg
目录中创建以下索引 . html 文件。Wasm - Say helloWhat is your name?
Click the button
将 pkg 目录的内容复制到我们在运行Apache2的地方
cp -rp pkg/* /var/www/html/
如果访问服务器的地址,我们会看到下面的页面。
文章图片
当我们添加我们的名字并单击按钮时,得到以下响应。
文章图片
Wasm-pack (服务器端- Node.js)
现在我们已经看到了使用 html / js 和 Apache2的实际应用,让我们继续并创建另一个演示。 这一次是在 Node.js 的环境中,遵循
wasm-pack
的 npm-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
文章图片
更广泛的应用 预计“ 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/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md
- GitHub — RustWasm. (2020). rustwasm/wasm-bindgen. [在线] 请访问: https://github.com/rustwasm/wasm-bindgen/blob/master/guide/src/contributing/design/js-objects-in-rust.md
- 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/en-US/docs/WebAssembly/Understanding_the_text_format
- MDN Web Docs — Web APIs. (2020). Web APIs. [在线] 请访问: https://developer.mozilla.org/en-US/docs/Web/API
- 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/docs/wasm-bindgen/ [Accessed 27 Jan. 2020]. - Wasm By Example. (2019). WebAssembly Linear Memory. [在线] 请访问: https://wasmbyexample.dev/examples/webassembly-linear-memory/webassembly-linear-memory.rust.en-us.html
- Williams, A. (2019). Rust, WebAssembly, and Javascript Make Three: An FFI Story. [在线] 请访问: https://www.infoq.com/presentations/rust-webassembly-javascript/
推荐阅读
- java|【Rust日报】2022-08-17 在 Rust 和 C 之间传递字符串的 7 种方法
- 大数据|80岁还在写代码!Hello World发明人、UNIX命名者项目登上GitHub热榜
- 数据库|超过100M的SQL脚本怎么执行呢()
- 大数据|计算机科学领域 直接 间接_为什么计算机科学领域的女性很少()
- 计算机视觉|基于matlab的图像形状与分类毕业设计(含源文)
- 大数据开发|大数据是什么(0基础大数据怎么进行入门学习?基础知识总纲)
- 字符串|7行代码让B站崩溃3小时,竟因“一个诡计多端的0”
- spring|spring,springboot,springmvc底层原理解析