閱讀更多
引用
原文:JavaScript to Rust and Back Again: A wasm-bindgen Tale
鏈接:https://hacks.mozilla.org/2018/04/javascript-to-rust-and-back-again-a-wasm-bindgen-tale/
譯者:Tocy, 琪花億草, 雪落無痕xdj, 邊城

最近我們已經見識了WebAssembly如何快速編譯、加速JS庫以及生成更小的二進制格式。我們甚至為Rust和JavaScript社區以及其他Web編程語言之間的更好的互操作性制定了高級規劃。正如前面一篇文章中提到的,我想深入了解一個特定組件的細節,wasm-bindgen。

今天WebAssembly標準只定義了四種類型:兩種整數類型和兩種浮點類型。然而,大多數情況下,JS和Rust開發人員正在使用更豐富的類型! 例如,JS開發人員經常與互以添加或修改HTML節點相關的文檔交互,而Rust開發人員使用類似Result等類型進行錯誤處理,幾乎所有程序員都使用字符串。


被局限在僅使用由WebAssembly所提供的類型將會受到太多的限制,這就是wasm-bindgen出現的原因。


wasm-bindgen的目標是提供一個JS和Rust類型之間的橋接。它允許JS使用字符串調用Rust API,或Rust函數捕獲JS異常。wasm-bindgen抹平了WebAssembly和JavaScript之間的阻抗失配,確保JavaScript可以高效地調用WebAssembly函數,并且無需boilerplate,同時WebAssembly可以對JavaScript函數執行相同的操作。

wasm-bindgen項目在其README文件中有更多描述。要入門,讓我們深入到一個使用wasm-bindgen的例子中,然后探索它還有提供了什么。

1、Hello World!

學習新工具的最好也是最經典的方法之一就是探索下用它來輸出“Hello, World!”。在這里,我們將探索一個這樣的例子——在頁面里彈出“Hello World!”提醒框。

這里的目標很簡單,我們想要定義一個Rust的函數,給定一個名字,它會在頁面上創建一個對話框,上面寫著Hello,$name!在JavaScript中,我們可以將這個函數定義為:
export function greet(name) {
    alert(`Hello, ${name}!`);
}

不過在這個例子里要注意的是,我們將把它用Rust編寫。這里已經發生了很多我們必須要處理的事情:
  • JavaScript將會調用一個WebAssembly ??? ??槊?greetexport.
  • Rust函數將一個字符串作為輸入參數,也就是我們要打招呼的名字。
  • 在內部Rust會生成一個新的字符串,也就是傳入的名字。
  • 最后Rust會調用JavaScript的 alert函數,以剛創建的字符串作為參數。
啟動第一步,我們創建一個新的Rust工程:
$ cargo new wasm-greet --lib

這將初始化一個新的wasm-greet文件夾,我們的工作都在這里面完成。接下來我們要使用如下信息修改我們的Cargo.toml(在Rust里相當于package.json):
[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

我們先忽略[lib]節的內容,接下來的部分聲明了對wasm-bindgen的依賴。這里的依賴包含了我們使用wasm-bindgen需要的所有的支持包。

接下來,是時候編寫一些代碼了!我們使用下列內容替換了自動創建的src/lib.rs:
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

如果你不熟悉Rust,這可能看起來有點啰嗦,但不要害怕!隨著時間的推移,wasm-bindgen項目不斷改進,而且可以肯定的是,所有這些并不總是必要的。

要注意的最重要的一點是#[wasm_bindgen]屬性,這是一個在Rust代碼中的注釋,這里的意思是“請在必要時用wrapper處理這個”。我們對alert函數的導入和greet函數的導出都被標注為這個屬性。稍后,我們將看到在引擎蓋下發生了什么。

首先,我們從在瀏覽器中打開作為例子來切入正題!我們先編譯wasm代碼:
$ rustup target add wasm32-unknown-unknown --toolchain nightly # only needed once
$ cargo +nightly build --target wasm32-unknown-unknown

這段代碼會生成一個wasm文件,路徑為target/wasm32-unknown-unknown/debug/wasm_greet.wasm。如果我們使用工具如wasm2wat來看這個wasm文件里面的內容,可能會有點嚇人。

結果發現這個wasm文件實際上還不能直接被JS調用!為了能讓我們使用,我們需要執行一個或更多步驟:
$ cargo install wasm-bindgen-cli # only needed once
$ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir .

很多不可思議的事情發生都發生在這個步驟中:wasm-bindgen CLI工具對輸入的wasm文件做后期處理,使它變的“suitable”可用。

我們待會再來看“suitable”的意思,現在我們可以肯定的說,如果我們引入剛創建的wasm_greet.js文件(wasm-bindgen工具創建的),我們已經獲取到了在Rust中定義的greet函數。

最終我們接下來要做的是使用bundler對其打包,然后創建一個HTML頁面運行我們的代碼。

在寫這篇文章的時候,只有Webpack’s 4.0 release對WebAssembly的使用有足夠的支持(盡管暫時已經有了 Chrome caveat)。

總有一天,更多的bundler也會接著支持WebAssmbly。在這我不再描述細節,但是你可以看一下在Github倉庫里的example配置。不過如果我們看內容,這個頁面中我們的JS在看起來是這樣的:
const rust = import("./wasm_greet");
rust.then(m => m.greet("World!"));

…就是這些了!現在打開我們的網頁就會顯示一個不錯的“Hello, World!”對話框,這就是Rust驅動的。

2、wasm-bindgen是如何工作的
唷,那是一個巨大的“Hello, World!”。讓我們深入了解一下更多的細節,以了解后臺發生了什么以及該工具是如何工作的。

wasm-bindgen最重要的方面之一就是它的集成基本上是建立在一個概念之上的,即一個wasm??榻鍪橇硪恢諩S???。例如,在上述中我們想要一個帶有如下簽名的ES??椋ㄔ赥ypescript中):
export function greet(s: string);

WebAssembly無法在本地執行此操作(請記住,它目前只支持數字),所以我們依靠wasm-bindgen來填補空白。

在上述的最后一步中,當我們運行wasm-bindgen工具時,你會注意到wasm_greet.js文件與wasm_greet_bg.wasm文件一起出現。前者是我們想要的實際JS接口,執行任何必要的處理以調用Rust。* _bg.wasm文件包含實際的實現和我們所有的編譯后的代碼。

我們可以通過引入 ./wasm_greet ??櫚玫?Rust 代碼愿意暴露出來的東西。我們已經看到了是如何集成的,可以繼續看看執行的結果如何。首先是我們的示例:
const rust = import("./wasm_greet");
rust.then(m => m.greet("World!"));

我們在這里以異步的方式導入接口,等待導入完成(下載和編譯 wasm)。然后調用??櫚?greet 函數。

注: 這里用到的異步加載目前需要 Webpack 來實現,但總會不需要的。而且,其它打包工具可能沒有此功能。

如果我們看看由 wasm-bindgen 工具為 wasm_greet.js 文件生成的內容,會看到像這樣的代碼:
import * as wasm from './wasm_greet_bg';

// ...

export function greet(arg0) {
    const [ptr0, len0] = passStringToWasm(arg0);
    try {
        const ret = wasm.greet(ptr0, len0);
        return ret;
    } finally {
        wasm.__wbindgen_free(ptr0, len0);
    }
}

export function __wbg_f_alert_alert_n(ptr0, len0) {
    // ...
}

注: 記住這是生成的,未經優化的代碼,它可能既不優雅也不簡潔??!在 Rust 中通過 LTO(Link Time Optimization,連接時優化)創建新的發行版,再通過 JS 打包工具流程(壓縮)之后,可能會精簡一些。

現在可以了解如何使用wasm-bindgen來生成greet函數。在底層它仍然調用wasm的greet函數,但是它是用一個指針和長度來調用的而不是用字符串。

了解passStringToWasm的更多細節可以訪問Lin Clark’s previous post。它包含了所有的模板,對我們來說這是除了wasm-bindgen工具以外還需要去寫的東西!然后我們接下來看__wbg_f_alert_alert_n函數。

進入更深一層,下一個我們感興趣的就是WebAssmbly中的greet函數。為了了解這個,我們先來看Rust編譯器能訪問到的代碼。注意像上面生成的這種JS wrapper,在這里你不用寫greet的導出符號,#[wasm_bindgen]屬性會生成一個shim,由它來為你翻譯,命名如下:
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) {
    let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) }
    let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) };
    greet(arg0);
}

現在可以看到原始代碼,greet,也就是由#[wasm_bindgen]屬性插入的看起來有意思的函數__wasm_bindgen_generated_greet。這是一個導出函數(用#[export_name]和extern關鍵詞來指定的),參數為JS傳進來的指針/長度對。在函數中它會將這個指針/長度轉換為一個&str (Rust中的一個字符串),然后將它傳遞給我們定義的greet函數。

從另一個方面看,#[wasm_bindgen]屬性生成了兩個wrappers:一個是在JavaScript中將JS類型的轉換為wasm,另外一個是在Rust中接收wasm類型并將其轉為Rust類型。

現在我們來看wrappers的最后一塊,即alert函數。Rust中的greet函數使用標準format!宏來創建一個新的字符串然后傳給alert?;叵氳蔽頤巧鱝lert方法的時候,我們是使用 #[wasm_bindgen]聲明的,現在我們看看在這個函數中暴露給rustc的內容:
fn alert(s: &str) {
    #[wasm_import_module = "__wbindgen_placeholder__"]
    extern {
        fn __wbg_f_alert_alert_n(s_ptr: *const u8, s_len: usize);
    }
    unsafe {
        let s_ptr = s.as_ptr();
        let s_len = s.len();
        __wbg_f_alert_alert_n(s_ptr, s_len);
    }
}

這并不是我們寫的,但是我們可以看看它是怎么變成這樣的。alert函數事實上是一個簡化的wrapper,它帶有Rust的 &str然后將它轉換為wasm類型(數字)。它調用了我們在上面看到過的比較有意思的函數__wbg_f_alert_alert_n,然而它奇怪的一點就是#[wasm_import_module]屬性。

在WebAssembly中所有導入的函數都有一個其存在的???,而且由于wasm-bindgen構建在ES??櫓?,所以這也將被轉譯為ES??櫚既?!

目前__wbindgen_placeholder__??槭導噬喜⒉淮嬖?,但它表示該導入將被wasm-bindgen工具重寫,以從我們生成的JS文件中導入。

最后,對于最后一部分的疑惑,我們得到了我們所生成的JS文件,其中包含:
export function __wbg_f_alert_alert_n(ptr0, len0) {
    let arg0 = getStringFromWasm(ptr0, len0);
    alert(arg0)
}

哇! 事實證明,這里隱藏著相當多的東西,我們從JS中的瀏覽器中的警告都有一個相對較長的知識鏈。不過,不要害怕,wasm-bindgen的核心是所有這些基礎設施都被隱藏了! 你只需要在隨便使用幾個#[wasm_bindgen]編寫Rust代碼即可。然后你的JS可以像使用另一個JS包或??橐謊褂肦ust了。

wasm-bindgen還能做什么

wasm-bindgen項目在這個領域內志向遠大,我們在此不再詳細贅述。探索wasm-bindgen中的功能一個有效的方法就是探索示例目錄,這些示例涵蓋了從我們之前看到的Hello World! 到在Rust中對DOM節點的完全操作。

wasm-bindgen高級特性如下:

  • 引入JS結構,函數,對象等來在wasm中調用。你可以在一個結構中調用JS方法,也可以訪問屬性,這給人一種Rust是“原生”的感覺,讓人覺得你曾經寫過的Rust #[wasm_bindgen] annotations都可以連接了起來。
  • 將Rust結構和函數導出到JS。與只用JS使用數字類型來工作相比,你可以導出一個Rust結構并在JS中轉換成一個類。然后可以將結構傳遞,而不是只使用整形數值來傳遞。 smorgasboard 這個例子可以讓你體會支持的互操作特性。
  • 其他各種各樣的特性例如從全局范圍內導入(就像alert函數),在Rust中使用一個Result來獲取JS異常,以及在Rust程序中通用方法模擬存儲JS值。
如果你想了解更多的功能,繼續閱讀 issue tracker。

3、wasm-bindgen接下來做什么?

在我們結束之前,我想花一點時間來下描述wasm-bindgen的未來愿景,因為我認為這是當今項目最激動人心的一方面。

不僅僅支持Rust

從第1天起,wasm-bindgen CLI工具就設計成了多語言支持的。盡管Rust目前是唯一被支持的語言,但該工具也可以嵌入C或C++。 #[wasm_bindgen]屬性創建了可被wasm-bindgen工具解析并隨后刪除的輸出(* .wasm)文件的自定義部分。

本節介紹要生成哪些JS綁定以及它們的接口是什么。這個描述中沒有關于Rust的特定部分,因此C ++編譯器插件可以很容易地創建該部分,并通過wasm-bindgen工具進行處理。

我覺得這個方面特別令人振奮,因為我相信它使像wasm-bindgen這樣的工具成為WebAssembly和JS集成的標準做法。希望所有編譯為WebAssembly的語言都能受益,并且可以被bundler自動識別,以避免上述幾乎所有的配置和構建工具。

自動綁定JS生態

使用#[wasm_bindgen] 宏導入功能唯一不好的一面就是你必須將所有東西都寫出來,還要保證沒有任何錯誤。這種讓人覺得很單調(而且易錯)的操作的自動化技術已經成熟了。

所有的web APIs都由WebIDL指定,而且在generate #[wasm_bindgen] annotations from WebIDL是可行的。這個就意味著你不需要像前面一樣定義alert函數,而是你只需要寫下面這些:
#[wasm_bindgen]
pub fn greet(s: &str) {
    webapi::alert(&format!("Hello, {}!", s));
}

在這個例子中,WebIDL對web APIs的描述可以完全自動生成webapi集合,保證沒有錯誤。

我們甚至可以將自動化更進一步,TypeScript組織已經做了這方面的復雜工作,參照generate #[wasm_bindgen] from TypeScript as well??梢悅夥延胣pm上的TypeScript自動綁定任何包!

比 JS DOM 操作更快的性能

最后要說的事情對 wasm-bindgen 來說也很重要:超快的 DOM 操作 —— 這是很多 JS 框架的終極目標。如今需要使用一些中間工具來調用 DOM 函數,這些工具正在由 JavaScript 實現轉向 C++ 引擎實現。然而,在 WebAssembly 來臨之后,這些工具并非必須。WebAssembly 是有類型的。

從第一天起,wasm-bindgen 代碼生成的設計就考慮到了將來的宿主綁定方案。當這一特征出現在 WebAssembly 之后,我們可以直接調用導入的函數,而不需要 wasm-bindgen 的中間工具。

此外,它使得 JS 引擎積極優化 WebAssembly 對 DOM 的操作,使其對類型的支持更好,而且在調用 JS 的時候不再需要進行參數驗證。在這一點上,wasm-bindgen 不僅在操作像 string 這樣的富類型變得容易,還提供了一流的 DOM 操作性能。

收工

我自己發現使用WebAssembly是異常令人振奮的,不僅僅是因為其社區,還因為其如此快速地在進度上突飛猛進。wasm-bindgen工具擁有光明的未來。它使JS和諸如Rust這樣的編程語言之間的互操作性變成了一流的體驗,并且隨著WebAssembly的不斷發展它也將提供了長期的好處。

試著給wasm-bindgen一次機會,因功能需求而創建一個問題,亦或繼續保持參與Rust和WebAssembly!

關于Alex Crichton(作者)
Alex是Rust核心團隊的成員之一,自2012年底以來一直從事于Rust。目前他正在幫助WebAssembly Rust Working Group使得Rust + Wasm成為最佳體驗。Alex還幫助維護Cargo(Rust的包管理器),Rust標準庫以及Rust的發布和CI的基礎架構。
來自: oschina
0
0
評論 共 1 條 請登錄后發表評論
1 樓 xxbb77 2018-11-12 16:32
非常感謝 謝謝分享

發表評論

您還沒有登錄,請您登錄后再發表評論

相關推薦

  • 深入認識javascript中得eval函數

    深入認識javascript中得eval函數深入認識javascript中得eval函數深入認識javascript中得eval函數

  • 深入認識javascript中的eval函數

    發現為本文起一個合適的標題還不是那么容易,呵呵,所以在此先說明下本文的兩個目的:(1)介紹javascript中的eval函數的用法(2)如何在函數內執行全局代碼 ?先來說eval的用法,內容比較簡單,熟悉的可以跳過。eval函數接收一個參數s,如果s不是字符串,則直接返回s。否則執行s語句。如果s語句執行結果是一個值,則返回此值,否則返回undefined。需要特別注意的是對象聲明語法“{}”...

  • Rust租借和生命周期深入剖析

    資源的所有權和租借 Rust通過一個成熟的租借系統而不是GC來達到內存安全的目的。對于和種資源(棧內存,堆內存,文件句柄等),都確定只有一個擁有者來確保其正確的解構(如果資源需要解構的話)。你可以利用&或者&mut創建對資源新的綁定,我們把這種綁定叫做租借。編譯器會保證所有的所有者和租借都正常工作。 復制和所有權轉移(Move) 在我們進入租借系統的討論前,我們還知道,Rust

  • 認識組件

  • 認識JavaScript

    前言: 學習的興趣在于自己不斷的突破,發現自己需要學習的內容還有很多,之前在牛腩新聞發布系統的項目中初步了解了JavaScript,但是當時的自己還不太了解,現在步入JavaScript階段的學習,發現學習知識真是很有趣!這對于滿足感很足的我來說,就是這樣一個狀態吧!下面從宏觀角度來了解JavaScript: 核心: 一、定義 是一種具有面向對象能力的、解釋

  • 認識 JavaScript

    在這個課程中,我會按照有效的學習方法,從零開始,幫你的把 JavaScript 作為一個工具用起來。記??!是用起來,可不是從頭到尾講一大堆語法了事。我們學習的目的難道不是“去用”嗎?

  • 認識JavaScript

    在本季課程的講解部分結合了大量的案例演示,通過案例演示可以清楚的加深對JavaScript基本語言的理解; 建議在學習的過程中,一定要動手實戰,這樣才能真正的領悟到JavaScript的語法細節。

  • 改進JAVASCRIPT

    下面的validate()方法有點長, 幫我看一下還能怎么改進一下rnrn[code=JScript]rn rnrn[/code]rnrn[code=HTML]rn rn rn NOTE: Inactive status only applies to Activity for entry screens. Once set, it will not be shown in the selection list.rn rn Display By Alpharn Display By Sequence Norn rn rn rn Reference Codern Reference Descriptionrn Sequence Norn Inactive rn rn rn rn rn rn rn rn AGTrn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn BOrn rn rn rn rn rn rn rn rn rn rn rn rn rn TRADErn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rn rnrn[/code]

  • 深入認識Redux(一)

  • js對象的深入認識

  • 深入認識AsyncTask

    深入認識AsyncTask

  • 深入認識DDD

  • 深入認識字符串

    一門初級、從入門到精通的C語言C++語法教程,由畢業于清華大學的業內人士執課。從簡單的HelloWorld入門程序,到深入的C語言C++核心概念,均為您娓娓道來,言之必詳、聽之必懂。讓C語言C++編程變得簡單,讓C語言C++編程變得有趣,讓喜歡C語言C++的人學會C語言C++!

  • 深入認識Redux(四)

  • 深入認識adb工具

  • 深入認識Redux(三)

  • 深入認識CPU

    學習匯編的時候,對cpu的認識有助于對匯編語言的理解,因為匯編語言功能可以直接對cpu中的寄存器進行操作。 以8086cpu為例子,8086cpu是16位的,那么16位cpu具有哪些性質呢? 運算器一次最多可以處理16位的數據 寄存器的最大寬度為16位 寄存器和運算器之間的通路為16位 1.cpu組成 控制部件單元(Control unit):主要是負責對指令,并且發出為完成每條指令所要執行的各個

  • 類的初始化的深入認識

    認識類的初始化,首先說一下,類的加載。 類的加載分五個階段:加載階段,驗證階段,準備階段,解析階段,初始化階段。 這五個階段具體做什么這里就不詳細說了。類文件加載文章詳細說了。 初始化JVM做了什么? 初始化階段是執行類構造器()方法的過程。(注意這里的類構造器和我們通常所說的類的構造方法是不一樣的,構造方法用于實例化一個對象) 舉個例子:這里SupCl

  • 深入認識Joomla CMS

    通過對Joomla的擴展,Joomla???,以及Joomla菜單的詳細介紹來深入認識Joomla CMS。

  • 組件生產數學建模模擬題中使用遺傳算法的改進認識

Global site tag (gtag.js) - Google Analytics