- 開始日期:2018-07-10
- RFC PR:https://github.com/rustwasm/rfcs/pull/2
- 追蹤問題:https://github.com/rustwasm/wasm-bindgen/pull/640
摘要
支援在 wasm-bindgen
的已導入類型中,定義單一繼承關係。具體來說,我們定義了從派生類型到其基本類型之靜態向上轉型,從類型到使用 JavaScript 之 instanceof
算子之任何其他類型的動態檢查轉型,以及最終作為開發人員之逃逸艙的任何 JavaScript 類型之間的未檢查轉型。對於程序巨集前端來說,這會透過將 #[wasm_bindgen(extends = Base)]
屬性新增到派生類型來完成。對於 WebIDL 前端,則使用 WebIDL 現有的介面繼承語法。
動機
原型鍊和 ECMAScript 類別讓 JavaScript 開發人員能夠在類型之間定義單一繼承關係。WebIDL 介面可以彼此繼承,而 Web API 廣泛使用此功能。我們希望支援在 wasm-bindgen
中,針對已導入的派生類型呼叫 basic 方法,並將已導入的派生類型傳遞給預料會接受基本類型的已導入函式。我們希望支援動態檢查某些 JS 值是否為 JS 類別的執行個體,以及動態檢查的轉型。最後,就如同 unsafe
為 Rust 的擁有權和借用提供了可封裝的逃逸艙,我們希望提供 JS 類別與值之間未檢查(但安全!)的轉換。
利害關係人
任何直接或透過 web-sys
板條箱,使用 wasm-bindgen
的人都會受到影響。這不會影響 Rust 之外的較大型 wasm 生態系統(例如 Webpack)。因此,通常在「Rust 與 WebAssembly 這一週」以及工作小組會議中宣傳此 RFC,就應該足夠徵詢意見回饋。
詳細說明
範例使用
請考慮下列 JavaScript 類別定義
class MyBase { }
class MyDerived extends MyBase { }
我們透過這種方式,將其轉換為 wasm-bindgen
程序巨集導入
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { pub extern type MyBase; #[wasm_bindgen(extends = MyBase)] pub extern type MyDerived; } #}
請注意 extern type MyDerived
上的 #[wasm_bindgen(extends = MyBase)]
註解。此註解會告訴 wasm-bindgen
,MyDerived
會從 MyBase
繼承。
或者,我們可以將這些相同的類別描述為 WebIDL 介面
interface MyBase {}
interface MyDerived : MyBase {}
範例向上轉型
我們可以使用一般的 From
、AsRef
、AsMut
和 Into
轉換,從 MyDerived
類型向上轉型到 MyBase
# #![allow(unused_variables)] #fn main() { let derived: MyDerived = get_derived_from_somewhere(); let base: MyBase = derived.into(); #}
範例動態檢查轉型
我們可以使用 dyn_{into,ref,mut}
方法,從 MyBase
向下動態檢查轉型(使用 JavaScript 的 instanceof
算子檢查)到 MyDerived
let base: MyBase = get_base_from_somewhere();
match base.dyn_into::<MyDerived>() {
Ok(derived) => {
// It was an instance of `MyDerived`!
}
Err(base) => {
// It was some other kind of instance of `MyBase`.
}
}
範例未檢查轉型
如果我們確實知道MyBase
是一個MyDerived
的實例,並且不願支付動態檢查的開銷,我們也可以使用未檢查的轉換
# #![allow(unused_variables)] #fn main() { let derived: MyDerived = get_derived_from_somewhere(); let base: MyBase = derived.into(); // We know that this is a `MyDerived` since we *just* converted it into `MyBase` // from `MyDerived` above. let derived: MyDerived = base.unchecked_into(); #}
未檢查的強制轉換是一種開發人員的應急手段,儘管它可能導致 JavaScript 例外,但它不會造成內存不安全。
JsCast
特質
對於任意 JavaScript 類型之間的動態檢查和未檢查強制轉換,我們引入了JsCast
特質。它要求實作提供一個布林斷言,來查詢 JavaScript 的instanceof
運算式,以及 JavaScript 值的未檢查轉換
# #![allow(unused_variables)] #fn main() { pub trait JsCast { fn instanceof(val: &JsValue) -> bool; fn unchecked_from_js(val: JsValue) -> Self; fn unchecked_from_js_ref(val: &JsValue) -> &Self; fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self; // ... provided methods elided ... } #}
JsCast
所需特質方法並非用於直接使用,而是由其提供的特質方法所利用。wasm-bindgen
的使用者大多數情況下可以忽略JsCast
所需特質方法,因為其實作將會自動產生,而且他們只會透過較為簡潔,提供的特有方法間接使用所需特質方法。
對於使用wasm-bindgen
匯入的每個extern { type Illmatic; }
,我們會傳出類似的JsCast
實作
# #![allow(unused_variables)] #fn main() { impl JsCast for Illmatic { fn instanceof(val: &JsValue) -> bool { #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))] #[wasm_import_module = "__wbindgen_placeholder__"] extern { fn __wbindgen_instanceof_Illmatic(idx: u32) -> u32; } #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))] unsafe extern fn __wbindgen_instanceof_Illmatic(_: u32) -> u32 { panic!("function not implemented on non-wasm32 targets") } __wbindgen_instance_of_MyDerived(val.idx) == 1 } fn unchecked_from_js(val: JsValue) -> Illmatic { Illmatic { obj: val, } } fn unchecked_from_js_ref(val: &JsValue) -> &Illmatic { unsafe { &*(val as *const JsValue as *const Illmatic) } } fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Illmatic { unsafe { &mut *(val as *mut JsValue as *mut Illmatic) } } } #}
此外,wasm-bindgen
將會傳出這個簡單地封裝 JS instanceof
運算式的__wbindgen_instanceof_Illmatic
的 JavaScript 定義
const __wbindgen_instanceof_Illmatic = function (idx) {
return getObject(idx) instanceof Illmatic;
};
JsCast
的特有提供方法
JsCast
特質提供的特質方法會封裝難以使用的靜態特質方法,並提供簡潔,可串連且作業於自我
以及另一個T: JsCast
的版本。例如,JsCast::is_instance_of
方法會詢問&self
是否為某個也實作JsCast
的另一個T
實例。
# #![allow(unused_variables)] #fn main() { pub trait JsCast where Self: AsRef<JsValue> + AsMut<JsValue> + Into<JsValue>, { // ... required trait methods elided ... // Unchecked conversions from `Self` into some other `T: JsCast`. fn unchecked_into<T>(self) -> T where T: JsCast, { T::unchecked_from_js(self.into()) } fn unchecked_ref<T>(&self) -> &T where T: JsCast, { T::unchecked_from_js_ref(self.as_ref()) } fn unchecked_mut<T>(&mut self) -> &mut T where T: JsCast, { T::unchecked_from_js_mut(self.as_mut()) } // Predicate method to check whether `self` is an instance of `T` or not. fn is_instance_of<T>(&self) -> bool where T: JsCast, { T::instanceof(self.as_ref()) } // Dynamically-checked conversions from `Self` into some other `T: JsCast`. fn dyn_into<T>(self) -> Result<T, Self> where T: JsCast, { if self.is_instance_of::<T>() { Ok(self.unchecked_into()) } else { Err(self) } } fn dyn_ref<T>(&self) -> Option<&T> where T: JsCast, { if self.is_instance_of::<T>() { Some(self.unchecked_ref()) } else { None } } fn dyn_mut<T>(&mut self) -> Option<&mut T> where T: JsCast, { if self.is_instance_of::<T>() { Some(self.unchecked_mut()) } else { None } } } #}
使用這些方法會提供比直接使用JsCast
所需特質方法更好的快速查錯語法。
# #![allow(unused_variables)] #fn main() { fn get_it() -> JsValue { ... } // Tired -_- SomeJsThing::unchecked_from_js(get_it()).method(); // Wired ^_^ get_it() .unchecked_into::<SomeJsThing>() .method(); #}
JsValue
的JsCast
實作
我們也為JsValue
實作了一個無動作的平凡JsCast
,並為JsValue
本身加入AsRef<JsValue>
和AsMut<JsValue>
實作,以滿足JsCast
超級特質界限
# #![allow(unused_variables)] #fn main() { impl AsRef<JsValue> for JsValue { fn as_ref(&self) -> &JsValue { self } } impl AsMut<JsValue> for JsValue { fn as_mut(&mut self) -> &mut JsValue { self } } impl JsCast for JsValue { fn instanceof(_: &JsValue) -> bool { true } fn unchecked_from_js(val: JsValue) -> Self { val } fn unchecked_from_js_ref(val: &JsValue) -> &Self { val } fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self { val } } #}
上拋強制轉換實作
對於使用extern type MyDerived
匯入的類型的每個extends = MyBase
,以及 WebIDL 介面繼承鏈中的每個基底和衍生介面,wasm-bindgen
會傳出這些特質實作,以封裝從JsCast
傳出的未檢查轉換方法,我們知道這是因為繼承關係而有效
-
一個
From
實作,用於自費轉換# #![allow(unused_variables)] #fn main() { impl From<MyDerived> for MyBase { fn from(my_derived: MyDerived) -> MyBase { let val: JsValue = my_derived.into(); <MyDerived as JsCast>::unchecked_from_js(val) } } #}
-
一個
AsRef
實作,用於共用引用轉換# #![allow(unused_variables)] #fn main() { impl AsRef<MyBase> for MyDerived { fn as_ref(&self) -> &MyDerived { let val: &JsValue = self.as_ref(); <MyDerived as JsCast>::uncheck_from_js_ref(val) } } #}
-
一個
AsMut
實作,用於獨佔引用轉換# #![allow(unused_variables)] #fn main() { impl AsMut<MyBase> for MyDerived { fn as_mut(&mut self) -> &mut MyDerived { let val: &mut JsValue = self.as_mut(); <MyDerived as JsCast>::uncheck_from_js_mut(val) } } #}
深繼承鏈範例
對於較深的繼承鏈,例如這個範例
class MyBase {}
class MyDerived extends MyBase {}
class MyDoubleDerived extends MyDerived {}
處理巨集匯入需要每個遞移基底有一個extends
屬性
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] extern { pub extern type MyBase; #[wasm_bindgen(extends = MyBase)] pub extern type MyDerived; #[wasm_bindgen(extends = MyBase, extends = MyDerived)] pub extern type MyDoubleDerived; } #}
另一方面,WebIDL 前端可以瞭解完整的繼承鏈,只需要一般的介面繼承語法即可
interface MyBase {}
interface MyDerived : MyBase {}
interface MyDoubleDerived : MyDerived {}
給定這些定義,我們可以將一個MyDoubleDerived
上拋強制轉換為一個MyBase
# #![allow(unused_variables)] #fn main() { let dub_derived: MyDoubleDerived = get_it_from_somewhere(); let base: MyBase = dub_derived.into(); #}
缺點
- 我們可能會誤將使用此繼承視為 Rust 較慣用的特質用法,而加以鼓勵。
理由及替代方案
-
我們可以定義一個
Upcast
特質,取代標準的From
和As{Ref,Mut}
特質。這將使得明顯看出我們正在進行與繼承相關的轉換,但這也會是一個新的特質,人們必須理解新特質,相較之下,大多數 Rust 程式設計師都熟悉那些std
特質。 -
使用
From
和As{Ref,Mut}
特質進行上轉型不會在self
上提供可鏈接的 turbofishing 方法,而這些方法可在類型推論需要幫助時使用。相反地,必須使用一個具有明確類型的區域變數。# #![allow(unused_variables)] #fn main() { // Can't do this with upcasting. get_some_js_type() .into::<AnotherJsType>() .method(); // Have to do this: let another: AnotherJsType = get_some_js_type().into(); another.method(); #}
倘若使用自訂的
Upcast
特質,我們可以在self
上提供可 turbofish 的方法,但代價是使用非標準特質。 -
我們可以使用
TryFrom
,以在動態檢查轉換時取代JsCast::dyn_into
等方法。這會在使用wasm-bindgen
時,引入新的不穩定性功能需求。我們保留了在TryFrom
穩定之後的可能性,屆時可以將我們的動態檢查轉換方法命名為JsCast::try_into
,以利於未來相容性。 -
明確上轉型仍然提供不了非常好的語法人體工學。我們可以採取以下一些措施
-
使用
Deref
特質來隱藏上轉型。這通常被視為一種反範例。 -
對於包含所有基本類型方法,並實作該特質的
MyBase
和MyDerived
,自動建立一個MyBaseMethods
特質給基本類型?此外,發射一個MyDerivedMethods
特質,它要求MyBase
作為一個父特質,在特質層級上表示繼承?這是 Rust 的實作方式,並允許我們以特質限制編寫一般函式。這是stdweb
對HTMLElement
的方法所使用的IHTMLElement
特質。我們是否這樣做,也與在基本類型和派生類型之間轉換正交。我們留待後續的 RFC 探索此設計空間,並希望逐步只進行轉換。
-
-
有時候,特質會阻礙學習對某事物能做什麼。它們在產生的文件說明中不如預期般突出,並且可能讓人們認為,即使在非必要的情況下,必須撰寫對特質通用的程式碼。我們可以透過兩種方法來消除
JsCast
特質-
僅對
JsValue
實作其方法並要求將類似的轉換,從ImportedJsClassUno
->ImportedJsClassDos
,在中間轉到JsValue
:ImportedJsClassUno
->JsValue
->ImpiortedJsClassDos
。 -
我們可以重複地對
JsValue
和已匯入的 JS 類別直接實作其所有方法。
-
-
未檢查的強制轉換可標記為
unsafe
,以反映在這些情況下正確性依賴程式設計人員。然而,誤用未檢查的 JS 強制轉換與 Rust 的意義中不會造成記憶體不安全性,因此這將使用unsafe
作為一般的「你大概不應該使用這個」警告,這並非unsafe
的既定目的。 -
我們只能在任何時候為所有內容實作未檢查的強制轉換。這將鼓勵一種隨意的、憑直覺的程式設計風格。我們偏好適時地利用類型。我們知道有時候仍然需要逃生艙口,並且我們確實提供任意的未檢查強制轉換,但引導人們使用
From
、AsRef
和AsMut
升級強制轉換,並動態地檢查其他類型的強制轉換。
未解決的問題
- 是否應該在
wasm_bindgen::prelude
中重新匯出JsCast
特質?我們未在此 RFC 中指定應該,且我們一開始會在不於 prelude 中重新匯出它公開,並看看感覺如何。根據經驗,我們可能決定在未來將其加入 prelude。