做者: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 代码中没有本地字符串。 更具体地说,Wasm 没有字符串数据类型。java
Wasm的MVP(只支持wasm32)有一个ILP32数据模型,目前提供如下4种数据类型,分别是:node
虽然咱们很快就会开始讨论在浏览器中使用 Wasm,但关键是要始终记住,从根本上讲,Wasm 的执行是用堆栈机器来定义的。 其基本想法是,每种类型的指令都会将必定数量的 i3二、 i6四、 f3二、 f64值从堆栈中推入或弹出(MDN Web Docs ——理解 WebbAssembly 文本格式,2020)。jquery
正如咱们所看到的,上面的四种数据类型都属于数字。 那么,若是是这种状况,咱们如何在 WebAssembly (Wasm)中促成(facilitate)字符串呢?git
如今,能够将高级值(如字符串)转换为一组数字。 若是实现了这一点,那么咱们就能够在函数之间来回传递这些数字集(表明字符串)。 github
然而,这里有几个问题。web
对于通常的高级编码来讲,老是须要这种常量的显式编码 / 解码是很麻烦的,所以这不是一个很好的长期解决方案。 apache
此外,事实证实,这种方法目前在 Wasm 实际上不可能实现。 缘由是,尽管 Wasm 函数能够接受函数中的许多值(做为参数) ,可是目前 Wasm 函数只能返回一个值。而Wasm会有不少信息。
如今,让咱们经过看看 Rust 中的字符串的工做机制,来说一下基础知识。
Rust中的String 能够被认为是一个保证了拥有良好的 UTF-8 Vec<u8>(Blandy and Orendorff,2017)。
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]
有了全部这些信息,咱们如何为 Web 用 Wasm 编写“ Hello World! ” ? 例如,咱们如何在用户界面和 Wasm 执行环境之间来回传递字符串?
问题的核心是... WebAssembly 须要很好地使用 JavaScript... 咱们须要使用Javascript并将 JavaScript 对象传递到 WebAssembly,但 WebAssembly 根本不支持这一点。 目前,WebAssembly 只支持整数和浮点数(Williams,2019)。将 JavaScript 对象硬塞进 u32以便用于 Wasm,须要费些力气。
摔跤图案,看起来很像甲壳类动物。
这是个巧合吗? 我不这么认为。
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”中的第一个插槽被认为是一个堆栈。 这个堆栈,像典型的程序执行堆栈同样,是向下增加。
短时间的 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) { // ... }
相对于上面编写的 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。
// 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; } }
咱们能够看到, 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 对象。
在这里,咱们将讨论 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 工做流工具,易于使用。
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
在 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 文件。
<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/
若是访问服务器的地址,咱们会看到下面的页面。
当咱们添加咱们的名字并单击按钮时,获得如下响应。
如今咱们已经看到了使用 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。
参考文献
wasm-bindgen
Guide. [在线] 请访问: https://rustwasm.github.io/do... [Accessed 27 Jan. 2020].