- 開始日期:2018-01-08
- RFC 公關: (留空)
- 追蹤問題: (留空)
摘要
新增 #[wasm_bindgen]
處理、載入和控管對本機 JS 檔案之相依關係。
-
現在可以用
module
屬性明確載入檔案# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... } #}
-
現在可以用
inline_js
屬性加入 JS 模組# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
-
--browser
旗標改用為瀏覽器產生 ES 模組,而且--no-modules
已不再使用,取而代之的是此旗標。 -
--nodejs
將不支援本機 JS 程式片段,但未來會支援。
動機
wasm-bindgen
的目標是讓 Rust 和 JS 輕鬆地互通有無。雖然撰寫自訂的 Rust 程式碼非常容易,但撰寫自訂的 JS 並將它連接到 #[wasm_bindgen]
其實相當困難(請見 rustwasm/wasm-bindgen#224)。#[wasm_bindgen]
屬性目前僅支援從 ES 模組載入函式,但即使如此,支援仍然有限,而且僅假設 ES 模組字串存在於最後的應用程式組建步驟中。
目前沒有可組合的方式讓一個 crate 有其所建置的輔助 JS,最後能順利地包含到最終建置的應用程式中。例如,rand
crate 無法輕易包含本機 JS(可能是用來偵測它應使用的隨機 API),而又不會對最終成果強加嚴格的要求。
若要支援從自訂 JS 檔案匯入,在人體工學上看似也是 stdweb
等架構所需要的,以建置類似 js!
的巨集。這需要在編譯時產生 JS 程式片段,並包含到最終的套件中,而這應是由此新屬性來驅動。
利益相關者
這份 RFC 的一些主要利益相關者有
#[wasm_bindgen]
的使用者- 想要為其 crate 加入 wasm 支援的 crate 作者。
stdweb
作者- 封裝器(webpack)和
wasm-bindgen
整合人員。
在這裡的大量各式各樣的人員將會被公開發布於 RFC 上,而且歡迎與更多人聯繫!
詳細說明
這項提議包含大量動態元素,它們的用意全部都是協同合作,以提供一條簡化的流程,以便將本機 JS 檔案包含至最終的 #[wasm_bindgen]
成果。我們將在此逐一檢視每一部分。
新的語言語法特色
此處建議最面向使用者的變更,是重新詮釋 #[wasm_bindgen]
內的 module
屬性,並新增一個 inline_js
屬性。它們現在可以用於匯入本機檔案,並定義本機匯入內容,如下所示
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "/js/foo.js")] extern "C" { // ... definitions } #[wasm_bindgen(inline_js = "export function foo() {}")] extern "C" { fn foo(); } #}
第一個宣告表示,此區塊的函式、類型等等,全都是從 /js/foo.js
檔案匯入的,它是相對於目前的檔案,並根植於 crate 根目錄。第二個宣告將 JS 列為字串,並以 extern
區塊描述內嵌模組的匯出項目。
建議透過下述規則,來詮釋 module
屬性。
-
如果字串開頭是 cargo 建置目錄的絕對路徑(由
$OUT_DIR
識別),此字串會被詮釋為輸出目錄中的檔案路徑。這是針對建置腳本,在建置期間會產生 JS 檔案用的。 -
如果字串開頭是
/
、./
或../
,它會被視為本機檔案的路徑。如果不是,它就會按字面值傳遞為 ES 模組匯入。 -
所有路徑都會相對於目前的檔案解析,類似 Rust 本身的
#[path]
、include_str!
等。不過,目前我們還不知道,要如何對相對檔案進行處理。因此,所有路徑都需要以/
開頭。當proc_macro
有穩定的 API(或是我們想出方法時),我們可以開始允許以./
和../
開頭的路徑。
我們希望這樣能大致符合程式設計師的預期,以及瀏覽器和打包器中既有的約定。
inline_js
屬性並非真正打算用於一般目的開發,而是提供一個方式,讓程序宏(目前沒辦法依賴 $OUT_DIR
)產生供 JS 匯入的內容。
匯入 JS 的格式
所有匯入的 JS 都必須使用 ES 模組語法撰寫。一開始,JS 必須手寫,無法後製處理。例如,JS 不能寫成 TypeScript,也不能使用 Babel 或類似的工具編譯。
舉例來說,某資源庫可能包含
# #![allow(unused_variables)] #fn main() { // src/lib.rs #[wasm_bindgen(module = "/js/foo.js")] extern "C" { fn call_js(); } #}
並搭配
// js/foo.js
export function call_js() {
// ...
}
請注意,js/foo.js
使用 ES 模組語法來匯出函式 call_js
。當 Rust 呼叫 call_js
時,它會呼叫 foo.js
中的 call_js
函式。
透過依賴項傳播
file
屬性的目的是與依賴項順利合作。當使用 #[wasm_bindgen]
建置專案時,您不應該需要知道依賴項是否使用本機 JS 片段!
#[wasm_bindgen]
巨集會在編譯階段讀取提供的檔案內容(如有)。此檔案會使用特定的 wasm-bindgen 格式序列化成 wasm-bindgen 自訂區段。Rustc 產生的最終 WebAssembly (Wasm) 產出物會包含所有在自訂區段中參照到的 JS 檔案內容。
wasm-bindgen
CLI 工具將會解壓縮所有 JS 並將其寫入檔案系統。發出的 Wasm 檔案(或 wasm-bindgen 產生的墊片 JS 檔案)會匯入所有發出的 JS 檔案,且使用相對匯入。
更新 wasm-bindgen
產出模式
wasm-bindgen
目前有幾種產出產生模式。這些產出模式主要圍繞著模組與非模組,以及如何定義模組。此 RFC 提議我們轉而更偏向於環境,例如與 Node.js 相容的程式碼與與瀏覽器相容的程式碼(其包含的不只模組格式)。表示在環境支援多個模組系統,或模組系統為選用時(瀏覽器同時支援 ES 模組和非模組),只要與該環境相容,wasm-bindgen
將會選擇它認為最適的模組系統。
wasm-bindgen
目前的產出模式為
-
預設 - 預設情況下,
wasm-bindgen
發出的產出設定 Wasm 模組本身為 ES 模組。這自然會與本身為 ES 模組的自訂 JS 片段搭配使用,因為它們最終只會是圖表中的更多模組,而這些模組都位在本機產出目錄中。此產出模式目前只能由打包工具(如 Webpack)使用,預設產出無法載入至瀏覽器或 Node.js 中。 -
--no-modules
-wasm-bindgen
的--no-modules
旗標與 ES 模組不相容,因為此旗標預計透過不屬於模組的<script>
標籤包含。在上一層 Crate 包含本機 JS 片段時,此模式會像現今一樣無法使用。 -
--nodejs
-wasm-bindgen
的此旗標表示產出應針對 Node.js 量身打造,特別是使用 CommonJS 模組慣例。在此模式中,wasm-bindgen
最後會在 Rust 中使用 JS 剖析器將本機匯入的 JS 模組的 ES 語法改寫成 CommonJS 語法。 -
--browser
- 目前,此旗標與預設產出模式相同,只是產出經過微調而稍微適合瀏覽器環境(例如假設TextEncoder
環境中可用)。此 RFC 建議修改此旗標用途(中斷此旗標),改為在網路瀏覽器中原生載入 ES 模組,但其他方面具有與目前的
--no-modules
相似的介面,詳情如下。
此 RFC 建議重新思考這些產出模式,如下所述
目標環境 | CLI 旗標 | 模組格式 | 使用者體驗 | 本機 JS 片段如何載入? |
---|---|---|---|---|
沒有打包工具的 Node.js | --nodejs | Common.js | require() 主要 JS 黏合檔案 | 主要的 JavaScript 黏合檔案會 require() crates 的本地 JavaScript 片段。 |
無打包器的 Web | --browser | ES 模組 | 指向主要 JavaScript 黏合檔案的 <script> 使用 type=module | import 陳述會造成對 crates 的本地片段增加網路請求。 |
含打包器的 Web | 無 | ES 模組 | 指向主要 JavaScript 黏合檔案的 <script> | 打包器會將 crates 的本地片段連結成主要 JavaScript 黏合檔案。除了 wasm 模組本身以外,沒有其他額外的網路請求。 |
值得注意的是,對於 wasm-bindgen
而言,含打包器和無打包器的瀏覽器幾乎是相同的:唯一的差別在於,如果我們假設有一個打包器,則我們可以依賴打包器為我們補充 wasm 作為 ES 模組。請注意,此處的 --browser
如今有根本上的不同,因此會造成重大變更。我們認為 --browser
的使用頻率低到我們可以免除,但我們歡迎就此點提供回饋!
--no-modules
旗標不再有適用了,因為 --browser
使用範例打算納入其中。請注意,此 RFC 提議目前僅提供以打包器為導向和以瀏覽器為導向的模式,以支援本機 JavaScript 片段,同時也為最終支援 Node.js 中的本機 JavaScript 片段鋪路。--no-modules
最終也可以用與 Node.js 相同的方式獲得支援(一旦我們解析了 JS 檔案並改寫了外匯功能),但在此建議逐步從 --no-modules
轉向 --browser
。
--browser
輸出目前被認為用於匯出初始化函數,這個函數在呼叫並解決回傳的承諾(就像目前的 --no-modules
)之後,會導致在呼叫時所有的外匯功能都能運作。在承諾解決之前,所有外匯功能在呼叫時都會擲回錯誤。
依賴其他 JS 檔案的 JS 檔案
關於此 RFC 的一個棘手之處在於,當本機 JavaScript 片段依賴其他 JS 檔案時。例如,您的 JS 可能看起來像
// js/foo.js
import { foo } from '@some/npm-package';
import { bar } from './bar.js'
// ...
依照上述設計,這些輸入將無法運作。我們打算明確地表示這是此設計的初始限制。在此我們還不支援 JS 片段之間的輸入,但我們最終應該能夠支援。
長久來看,若要支援 --nodejs
,我們將需要 JS 的某些層級 ES 模組解析器。一旦我們可以解析輸入本身,則在擴充期間對於 #[wasm_bindgen]
而言,要載入暫時包含的檔案會相對簡單。例如,在上方檔案中,我們會將 ./bar.js
納入 wasm 自訂區段。在這個未來的世界裡,我們會在產生最終輸出製品時,僅改寫(如果必要)./bar.js
。此外,在 wasm-pack
和 wasm-bindgen
中透過 NPM 套件支援(這是未來目標),我們可以驗證 package.json
中是否有輸入可使用。
存取 wasm 記憶體/表格
взаимодействующие с модулем wasm фрагменты JS обычно должны работать с экземплярами WebAssembly.Memory и WebAssembly.Table, связанными с модулем wasm. Этот RFC предлагает использовать сам wasm, чтобы передавать эти объекты как указано ниже:
# #![allow(unused_variables)] #fn main() { // lib.rs #[wasm_bindgen(module = "/js/local-snippet.js")] extern { fn take_u8_slice(memory: &JsValue, ptr: u32, len: u32); } #[wasm_bindgen] pub fn call_local_snippet() { let vec = vec![0,1,2,3,4]; let mem = wasm_bindgen::memory(); take_u8_slice(&mem, vec.as_ptr() as usize as u32, vec.len() as u32); } #}
// js/local-snippet.js
export function take_u8_slice(memory, ptr, len) {
let slice = new UInt8Array(memory.arrayBuffer, ptr, len);
// ...
}
Здесь встроенный существующий wasm_bindgen::memory() используется, чтобы передать объект памяти в импортированный фрагмент JS. Чтобы отразить это, мы добавим также wasm_bindgen::function_table() в crate wasm-bindgen как встроенный для доступа к таблице функций и возврата ее в виде JsValue.
В конечном итоге нам может понадобиться более явный способ импорта памяти/таблиц, но сейчас этого должно быть достаточно для выразительности.
Недостатки
-
Начальный RFC довольно консервативный. Он не работает напрямую с --nodejs или --no-modules. Кроме того, изначально он не поддерживает импорт фрагментов JS в другой JS. Обратите внимание, что все это планируется поддерживать в будущем, просто разумно предполагать, что на старте для этого может потребоваться больше разработок, чем нам необходимо.
-
Фрагменты JS должны быть написаны в синтаксисе базового модуля ES. Обычные препроцессоры, например TypeScript, нельзя использовать. Пока не ясно, как будут импортироваться такие препроцессированные JS. Предполагается, что фрагменты JS будут настолько маленькими, что это не станет большой проблемой. Более объемные фрагменты JS всегда можно извлечь в пакет NPM и там их пост-обработать. Обратите внимание, что авторы всегда могут запустить компилятор TypeScript вручную для таких случаев.
-
Предлагается отказаться от относительно популярного флага --no-modules в пользу флага --browser, у которого относительно сегодня будет собственное знаковое изменение. Однако считается, что --browser используется крайне редко, поэтому его безопасно менять, а также что нам стоит избежать изменения --no-modules в его сегодняшнем виде.
-
Для локальных фрагментов JS требуется синтаксис модуля ES. Может, это и субъективно, но целью является упрощение добавления будущих функций в wasm-bindgen при одновременной работе с JS. Тем не менее, система модулей ES — единственный официальный стандарт в экосистеме, поэтому эта система должна стать очевидным выбором для написания локальных фрагментов JS.
Обоснование и альтернативы
此系統的主要替代方案為巨集,例如 stdweb 中的 js!
。這允許在 Rust 程式碼中直接編寫 JS 程式碼的小片段,然後 wasm-bindgen
會具有產生適當的 shim 的知識。此 RFC 提議識別 module
路徑,而不是採用此方法,因為它被認為是一個更通用的方法。此外,預計可以在 module
指令(包含本地檔案路徑)之上建立 js!
巨集。wasm-bindgen
Crate 有朝一日可能會發展出類似的 js!
巨集,但認為最好從更保守的方法開始。
ES 模組的一個替代方案是簡單地串接所有 JS。這樣我們就不必解析任何東西,而是只將所有內容放入一個檔案中。但是,這種方法的缺點是,它很容易導致命名空間衝突,而且它也迫使所有人都同意模組格式,並有可能強制模組格式最終成品。
發射小檔案的另一個替代方案是在 wasm-bindgen 時間,改為在 執行時期 解壓縮所有檔案,方法是將它們留在 wasm 可執行檔的自訂區段。但是,這反過來可能會違反某些 CSP 設定(特別是嚴格設定)。
未解決的問題
-
最初有必要支援
--nodejs
嗎? -
最初有必要在本地 JS 片段中支援本地 JS 匯入嗎?
-
如今是否有已知的 JS ES 模組剖析器?我們是否被迫包含一個完整的 JS 剖析器,或者我們可以僅處理 ES 語法的一個最小版本呢?
-
我們將如何處理其他資產(如 CSS、HTML 或影像),而這些資產希望由最終的 wasm 檔案參考?