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...
相關文章
相關標籤/搜索