1. 程式人生 > >【WebAssembly 的未來】成長技能樹(下)

【WebAssembly 的未來】成長技能樹(下)

技能: JS 和 WebAssembly 之間的快速呼叫

首先,我們需要在 JS 和 WebAssembly 之間進行快速呼叫,因為如果你將一個小模組整合到現有的 JS 系統中,那麼很有可能你需要在兩者之間進行大量呼叫。所以你需要這些調用盡可能快。

但是當 WebAssembly 首次出現時,這些呼叫並不夠快。這是我們回到整個 MVP 的事情 —— 該引擎對兩者之間的呼叫的支援是最小的。他們只是讓呼叫可工作,他們沒有讓呼叫夠快。因此引擎需要優化這些邏輯。

我們近期在 Firefox 中完成了這方面的工作。現在,其中一些呼叫實際上比非內聯 JavaScript 到 JavaScript 的呼叫更快。而其他引擎也在努力解決這個問題。

技能:快速簡潔的資料交換

然而,這帶來了另一件事。當你在 JavaScript 和 WebAssembly 之間進行呼叫時,你通常需要在它們之間傳遞資料。

你需要將值傳遞到 WebAssembly 函式或從中返回一個值。這也可能很慢,並且也很困難。

它很難是有幾個原因的。一是因為,目前 WebAssembly 只能理解數值。這意味著你不能將更復雜的值(如物件)作為引數傳遞。你需要將該物件轉換為數值並將其放入線性記憶體中。然後將該線性記憶體中的位置傳遞給 WebAssembly。

這有點複雜。將資料轉換為線性記憶體需要一些時間。因此,我們需要它更容易、更快速。

技能:ES 模組整合

我們需要的另一件事是整合瀏覽器內建的支援 ES 的模組。目前,你使用命令式 API 例項化 WebAssembly 模組。你呼叫一個函式,它會返回給你一個模組。

但這意味著 WebAssembly 模組實際上並不是 JS 模組圖的一部分。為了像使用 JS 模組一樣使用匯入和匯出,你需要整合 ES 模組。

技能:整合工具鏈

但是,僅能夠匯入和匯出並不能讓我們一路坦途。我們需要一個用來分發這些模組的地方,並從中下載它們,以及將它們繫結起來的工具。

WebAssembly 的 npm 怎麼樣?好吧,npm 怎麼樣呢?

WebAssembly 的 webpack 或 Parcel 怎麼樣?同理,webpack 和 Parcel 怎麼樣?

這些模組對於使用它們的人來說不應該有任何不同,因為沒有理由建立一個單獨的生態系統。我們只需要工具來整合它們。

技能: 後向相容性

還有一件事我們需要在現有的JS應用程式中做得很好 - 支援舊版本的瀏覽器,甚至那些不知道WebAssembly是什麼的瀏覽器。 我們需要確保:在需要支援IE11時,你不必在JavaScript中編寫相關模組的第二次完整實現。

對於這方面我們在什麼位置?

那麼在這些方面我們處於什麼狀態呢?

JS 和 WebAssembly 之間的快速呼叫

JS 和 WebAssembly 之間的呼叫目前在 Firefox 上是快速的,並且其他瀏覽器也在處理此問題。

簡便快速的資料交換

為了實現簡便快速的資料交換,目前有一些提案可以幫助解決這個問題。
正如我之前提到的,你必須使用線性記憶體來處理更復雜的資料型別的一個原因是因為 WebAssembly 僅理解數值。它擁有的唯一型別是整數和浮點數。
在引用型別提案中,這將有所改變。該提案添加了一個新型別,WebAssembly 函式可將其作為引數和返回值。此型別是對 WebAssembly 外部物件的引用 - 例如,JavaScript 物件。
但 WebAssembly 無法直接操作此物件。要實際執行諸如呼叫方法之類的操作,它仍然需要使用一些 JavaScript 膠水程式碼。這意味著它可以執行,但它比它被需要的速度慢。
為了加快速度,有一項我們曾稱之為主機繫結的提案。它讓 wasm 模組宣告必須將哪些膠水程式碼應用於在匯入和匯出之上,這樣膠水程式碼就不需要用 JS 編寫。通過將 JS 中的膠水程式碼用到 wasm 中,在呼叫內建 Web API 時可以完全優化此膠水程式碼。
我們可以更輕鬆地完成互動的另一部分。這就和跟蹤資料在記憶體中需要保留多長時間有關。如果你在 JS 中需要訪問線性記憶體中一些資料,那麼你必須把它保留在那裡直到 JS 讀取資料為止。但如果你把它永遠保留在那裡,你會得到所謂的記憶體洩漏。你怎麼知道何時可以刪除資料呢?你怎麼知道 JS 什麼時候完成訪問呢?目前,你必須自己管理它們。
 
一旦 JS 處理完這些資料,JS 程式碼就必須呼叫類似 free 函式的東西來釋放記憶體。但這很冗餘並且極易出錯。為了簡化此過程,我們將 WeakRef 新增到 JavaScript 中。有了這個,你將能夠從 JS 方面檢視此物件。然後,當該物件被垃圾回收時,你可以在 WebAssembly 端進行清理。
所以這些建議都在處理中。 與此同時,Rust 生態系統已經建立了一些工具,可以為你自動完成這一切,並且可以新增到正在處理的提案中。
 
特別值得一提的是一個工具,因為其他語言也可以使用它。它被稱為 wasm-bindgen。當它看到你的 Rust 程式碼應在完成類似接收或返回某些型別的 JS 值或 DOM 物件之類的東西時,它會自動建立為你做這個的 JavaScript 膠水程式碼,所以你不需要為此考慮。並且因為它是以與語言無關的方式編寫的,其他語言工具鏈可以採用它。

整合 ES 模組

對於整合 ES 模組,提案還為之尚早。我們正著手和瀏覽器廠商實現該功能。

工具鏈支援

在工具鏈支援上,目前有些工具,例如 Rust 生態系統中的 wasm-pack,它自動完成為 npm 打包程式碼的所有事情。並且中間商也在積極為其提供支援。

後向相容性

最後,對於後向相容性,目前有wasm2js工具。這需要一個 wasm 檔案,然後輸出一個等價的 JS 檔案。此 JS 檔案不是很快速,但最起碼這意味著在那些不理解 WebAssembly 的老版本瀏覽器上是可工作的。

所以我們正在接近解鎖這一成就。一旦我們解鎖它,我們就可以為另外兩個成就開闢新道路。

JS 框架和編譯為 JS 的語言

其中之一是重寫大部分內容,諸如使用 WebAssembly 編寫 JavaScript 框架

另一件事是使靜態型別的編譯為 JS 的語言可以編譯為 WebAssembly —— 例如,使 Scala.js、Reason 或 Elm 等語言可編譯為 WebAssembly。

技能:GC

出於幾個原因,我們需要與瀏覽器的垃圾回收器進行整合。

首先,讓我們看看重寫 JS 框架的部分內容。出於幾個原因,這可能是好事。例如,在 React 中,你可以做的一件事是在 Rust 中重寫 DOM diffing 演算法,它具有非常符合人體工程學的多執行緒支援,並且並行化了此演算法。

你也可以通過不同地分配記憶體來加快其速度。在虛擬 DOM 中,你可以使用特殊的記憶體分配方案,而不是建立需要進行垃圾回收的一堆物件。例如,你可以使用具有極低開銷的分配和一次性解除分配的 bump 分配器方案。這可能有助於加快執行速度並減少記憶體使用量。

但你仍然需要與該程式碼中的 JS 物件(例如元件之類的東西)進行互動。你不能只是不斷地將所有內容複製到和複製出線性記憶體,因為這樣做既困難又低效。

因此,你需要能夠與瀏覽器的 GC 整合,以便你可以使用由 JavaScript VM 管理的元件。其中一些 JS 物件需要指向線性記憶體中的資料,有時線性記憶體中的資料需要指向 JS 物件。

如果這最終建立了迴圈引用,則可能意味著垃圾回收器出現了問題。這意味著垃圾收集器將無法判斷此物件是否已被使用,因此永遠不會回收它們。WebAssembly 需要與 GC 整合,以確保這類跨語言資料依賴性可正常使用。

這也將對編譯為 JS 的靜態型別語言,如 Scala.js,Reason,Kotlin 或 Elm 等有所幫助。這些語言在編譯為 JS 時使用 JavaScript 的垃圾回收器。因為 WebAssembly 可以使用相同的 GC —— 內置於引擎中的 GC —— 這些語言將能夠編譯為 WebAssembly 並使用相同的垃圾回收器。他們不需要改變 GC 的工作方式。

技能:異常處理

我們還需要更好的支援來對異常進行處理。

有些語言,比如 Rust,沒有異常。但在其他語言中,例如 C++,JS 或 C#,異常處理有時會被廣泛使用。

你目前可以填加異常處理邏輯,但這些新增會使程式碼執行得非常慢。因此,在編譯到 WebAssembly 時其預設值是在沒有異常處理的情況下編譯的。

但是,由於 JavaScript 中存在異常,即使你編譯程式碼時不使用異常,JS 也可能會丟擲一個異常。如果 WebAssembly 函式呼叫了丟擲異常的 JS 函式,則 WebAssembly 模組將無法正確處理異常。 因此像 Rust 這樣的語言在這種情況下會選擇中止執行。我們需要讓它做得更好。

技能: 除錯

使用JS和編譯到JS語言的人們習慣擁有的另一件事是良好的除錯支援。所有主流瀏覽器中的Devtools都可以輕鬆地逐步除錯JS。我們需要同樣級別的支援以在瀏覽器中除錯WebAssembly。

技能: 尾呼叫(Tail calls)

最後,對於許多函式式語言來說,你需要支援稱為尾呼叫的東西。 我不打算詳細介紹這個細節,但基本上它可以讓你呼叫一個新函式,而無需在堆疊中新增新的棧幀。因此,對於支援此功能的函式式語言,我們希望WebAssembly也支援它。

對於這些我們處於什麼狀態?

那麼在此我們處於什麼狀態呢?

垃圾回收

對於垃圾回收,目前有兩個提案正在進行中:

JS的Typed Objects提案和WebAssembly的GC提案。Typed Object可以實現對Object固定結構的描述。對此有一篇解釋文章,並且該提案將在即將召開的TC39會議上討論。
WebAssembly GC 提案使得直接訪問該結構成為可能。該提案正在積極撰稿中。

這兩個功能完成之後,JS和WebAssembly都將知道物件是什麼樣子的,並且可以共享該物件並高效地訪問儲存在其上的資料。 我們的團隊實際上已經完成了這個工作的原型。但是,這仍需要一段時間才能通過標準化,所以我們可能會在明年的某個時候審視之。

異常處理

異常處理目前仍處於研究和開發階段,目前正有人在分析是否可以參考其他提案,例如我之前提到的引用型別的提案。

除錯

對於除錯,在瀏覽器devtools中目前有一些支援。例如,你可以在Firefox偵錯程式中單步執行WebAssembly的文字格式。但它仍然不理想。我們希望能夠展示你在實際原始碼中的具體位置,而不是在assembly中。我們需要做的就是弄清楚源對映 - 或一個源對映型別的事物 - 如何在WebAssembly中工作的。因此,WebAssembly CG的一個小組正在從事細化它。

尾呼叫

尾呼叫提案也在進行中。

一旦完成了這些功能,我們將解鎖JS框架和許多編譯到JS的語言。

瀏覽器之外

現在,當我談到“瀏覽器之外”時,你可能會感到困惑。因為難道不是用來檢視網頁的瀏覽器嗎? 並且這個名字-WebAsmbly難道不是正確的嗎。

但真相是你在瀏覽器中所看到的東西 - HTML,CSS和JavaScript - 只是網路構成的一部分。它們是可見的部分 - 它們是你用來建立使用者介面的技術 - 因此它們是最明顯的。

但是,網路中另一個非常重要的部分具有不可見的屬性。

這就是連結。 這是一種非常特殊的連結。

這個連結的創新之處在於我可以連結到你的頁面而無需將其放在中央登錄檔中,並且無需詢問你甚至不知道你是誰。我可以僅把那個連結放在那。

這正是連結的簡易性,沒有任何監管或批准瓶頸,使我們的網路成為可能。這就是我們與不認識的人建立這些全球社群的原驅動力。

但如果我們所擁有的僅是連結,那麼我們有兩個問題尚待解決。

第一個問題是......你在訪問這個網站時,它會為你提供一些程式碼。它如何知道應該為你提供哪類程式碼?因為如果你在Mac上執行,那麼你需要的機器程式碼與在Windows上是不同的。這就是為什麼你在不同的作業系統上使用不同版本的程式。

那麼一個網站是否應該為每個可能的裝置提供不同版本的程式碼呢?不。

相反,該網站有單一版本的程式碼 - 原始碼。這是交付給使用者的程式碼。然後它被轉譯為使用者裝置上的機器程式碼。

這個概念的名稱是可移植性。

這很棒,你可以從不認識你的,也不知道你正在執行什麼樣的裝置的人那裡載入程式碼。

但這帶來了第二個問題。如果你不知道你正在載入的網頁來自於何人,你怎麼知道他們給你的程式碼是什麼型別的? 它可能是惡意程式碼。它可能試圖接管你的系統。

是否說這個網路願景——執行來自你所遵循的任何人的程式碼——意味著你必須盲目地信任網路上的任何人?

這是網路上其他核心概念的用武之地。

它是安全模型。我打算把它稱為沙箱。

基本上,瀏覽器獲取頁面 - 其他人的程式碼,並且不是讓它在你的系統中隨意地執行,而是將其放到沙箱中。它會將一些不危險的操作放入沙箱中,以便程式碼可以執行某些操作,但它會將危險的東西留在沙箱之外。

所以連結的實用性是基於以下兩點的:

  • 可移植性 - 向用戶分發程式碼並使其可執行在任意型別裝置上的瀏覽器的能力。

     

  • 沙箱 - 一種安全模型,可讓你在不會危及機器的完整性的情況下執行程式碼。

那麼為什麼這種區別很重要呢? 如果我們將Web視為瀏覽器使用HTML、CSS和JS向我們展示某些東西,或者如果我們從可移植性和沙箱方面審視Web,那這為什麼會有所不同呢?

因為它改變了你對WebAssembly的看法。

你可以將WebAssembly視為瀏覽器工具箱中的另一個工具......它本是如此。

它是瀏覽器工具箱中的另一個工具。但這不僅僅是這些。它還為我們提供了一種方法來獲取 Web 另外的兩個功能 - 可移植性和安全性模型 - 並將它們用到需要它們的其他用例中。

我們可以將網路擴充套件到瀏覽器之外。現在讓我們來看看這些網路屬性在何處有用。

Node.js

WebAssembly 如何幫助 Node 呢?它可以為 Node 帶來完全的可移植性。

Node 為你提供了 Web 上大部分 JavaScript 可移植性。但在很多情況下,Node 的 JS 模組還不夠 - 其中你需要提高效能或重用現有的程式碼,而這些程式碼並非用 JS 編寫的。

在這些情況下,你需要 Node 中的原生模組。這些模組使用 C 語言編寫,並需要針對使用者執行的特定型別的機器進行編譯。

原生模組可以在使用者安裝時進行編譯,也可以預編譯為用於不同系統的寬基體二進位制檔案。其中一種方法對於使用者來說是一種折磨,另一種方法是包維護者的痛苦之源。

現在,如果這些原生模組是用 WebAssembly 編寫的,那麼它們就不需要專門針對目標平臺架構進行編譯。相反,他們可以像 Node 中的 JavaScript 一樣執行。但他們幾乎是以原生效能來執行的。

因此,我們為 Node 中的執行程式碼提供了完全的可移植性。你可以使用完全相同的 Node 應用程式並在所有不同型別的裝置上執行它,而無需編譯任何東西。

但 WebAssembly 無法直接訪問系統的資源。Node 中的原生模組不是沙盒 - 它們可以完全訪問瀏覽器從沙箱中取走的所有危險功能。在 Node 中,JS 模組也可以訪問這些危險的玩具,因為 Node 使它們變得可用。例如,Node 提供了從系統讀取和寫入檔案的方法。

對於 Node 的用例來說,模組具有對危險系統 api 的這種訪問是有一定意義的。因此,如果 WebAssembly 模組沒有那樣的訪問(就像 Node 的當前模組那樣),我們如何給 WebAssembly 模組它們需要的訪問?我們需要傳遞函式,以便 WebAssembly 模組可以與作業系統一起工作,就像 Node 與 JS 一樣。

對於 Node,這可能會包含很多 C 標準庫中的功能。它還可能包括 POXSIX 的一部分 —— 行動式作業系統介面 —— 這是一種較早的有助於相容性的標準。它提供了一個 API,用於跨一組不同的類 unix 作業系統與系統互動。模組肯定需要一些類似 posix 的函式。

技能:行動式介面

Node 核心人員需要做的是找出要公開的函式集和要使用的 API。

但如果這是標準的,不是很好嗎?不是特定於 Node 的東西,但是也可以跨其他執行時和用例使用?

如果你願意,可以使用 POSIX for WebAssembly。PWSIX?是的,一個可移植的 WebAssembly 系統介面。

如果用正確的方法,你甚至可以為 Web 實現相同的 API。這些標準 API 可以被填充到現有的 Web API 中。

這些函式不會是 WebAssembly 規範的一部分,而且會有 WebAssembly 的主機無法使用它們。但是對於那些可以使用這些函式的平臺,不管程式碼執行在哪個平臺上,都將有一個統一的 API 來呼叫這些函式。這將使通用模組 —— 同時在 Web 和 Node 上執行的模組變得更加容易。

擁有這些之後我們目前處於什麼狀態?

那麼,這是否真的會發生呢?

有一些事情正在朝著此想法努力。有一個名為包名對映的提案,將提供一種機制,用於將模組名稱對映到載入對應模組所在的路徑上。這可能會同時得到瀏覽器和 Node 的支援,因此可以使用它在相同 API 中提供不同的路徑以載入完全不同的模組。這樣,.wasm 模組本身可以指定一個(模組名,函式名)匯入對,它可以在不同的環境甚至 Web 上執行。

有了這種機制之後,剩下要做的就是弄清楚哪些函式是意義的以及它們的介面應該是什麼。

目前對此還沒有相關研究。但目前已有很多朝著此方向發展的探討。它看起來很可能以某種形式正在發生中。

還有一點好處是,因為解鎖這個功能讓我們處在解鎖瀏覽器之外的其他一些用例的半道中。並且有了這個之後,我們就可以加快步伐。

CDNs, Serverless, 以及邊緣計算

其中一個例子是諸如 CDN、Serverless 和邊緣計算等事物。在這些情況下,你將程式碼放到他人的伺服器上,並由其確保該伺服器的維護並保證程式碼放到所有使用者臨近的地方。

技能: 執行時

他們需要建立自己的執行時。這意味著使用 WebAssembly 編譯器 - 可以將 WebAssembly 編譯為機器碼 - 並將其與用在與我之前提到的系統互動的函式組合。

對於 WebAssembly 編譯器,Fastly 使用 Cranelift ,我們在構建 Firefox 時也使用此編譯器。它被設計為非常快速,並且不會佔用太多記憶體。

現在,對於與系統其餘部分互動的函式,他們必須建立自己的介面,因為我們還沒有可用的可移植介面。

因此,目前是可以建立自己的執行時的,但需要花費一些投入。它是必須在不同公司之間重複的投入。

如果我們不止擁有可移植介面,而且我們還有一個可以在所有此類公司和其他用例中使用的通用執行時會如何呢?這肯定會加速開發進度。

如此,其他公司可以使用這個執行時 - 就像他們目前使用 Node 一樣 - 而不需要從頭開始建立自己的執行時。

行動式 CLI 工具

WebAssembly 也可以在更傳統的作業系統中使用。現在需要說明的是,我不是在核心中討論(儘管勇敢的人也在嘗試),而是在 Ring3 使用者模式下執行的 WebAssembly。

然後,您可以做一些事情,比如擁有可移植的CLI工具,可以跨所有不同型別的作業系統使用。

這與另一個用例非常接近……

物聯網包括可穿戴技術和智慧家電等裝置

這些裝置通常是資源受限的 —— 它們沒有太多的計算能力,也沒有太多的記憶體。這種情況下,像 Cranelift 這樣的編譯器和 wasmtime 這樣的執行時會很出色,因為它們效率高,記憶體低。在資源極度受限的情況下,WebAssembly 可以在將應用程式載入到裝置之前完全編譯成機器碼。

還有一個事實是,目前有很多不同的裝置,它們都略有不同。WebAssembly 的可移植性確實有助於這一點。

這就是 WebAssembly 值得期望的地方。

結論

現在讓我們縮小下並檢視下這個技能樹。

我在這篇文章的開頭說過,人們對WebAssembly有一種誤解 - 在MVP中的WebAssembly是WebAssembly的最終版本。

我想你現在可以看出為什麼這是一種誤解了。

是的,MVP開闢了很多機會。它使得將大量桌面應用程式帶入Web成為可能。但我們仍然有很多用例待解鎖,從重量級桌面應用程式到小型模組,JS框架,瀏覽器以外的所有東西...... Node.js、serverless、區塊鏈和可移植CLI工具,以及物聯網。

所以我們今天所擁有的WebAssembly並不是故事的結尾 - 它僅是一個開始。