- 开始日期: 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 类型之间的未检查转换,作为开发人员的逃生舱。对于 proc-macro 前端,这是通过在派生类型上添加 #[wasm_bindgen(extends = Base)]
属性来完成的。对于 WebIDL 前端,使用 WebIDL 现有的接口继承语法。
动机
原型链和 ECMAScript 类允许 JavaScript 开发人员在类型之间定义单一继承关系。 WebIDL 接口可以相互继承, 并且 Web API 广泛使用此功能。我们希望支持在导入的派生类型上调用基方法,并将导入的派生类型传递给在 wasm-bindgen
中期望基类型的导入函数。我们希望支持动态检查某个 JS 值是否为 JS 类的实例,以及动态检查转换。最后,与 unsafe
为 Rust 的所有权和借用提供可封装的逃生舱的方式相同,我们希望提供未检查(但安全!)的 JS 类和值之间的转换。
利益相关者
直接或通过 web-sys
crate 间接使用 wasm-bindgen
的任何人都受到影响。这不会影响 Rust 之外的更广泛的 wasm 生态系统(例如 Webpack)。因此,在本周 Rust 和 WebAssembly 上以及在我们的工作组会议上通常发布此 RFC 就足以征求反馈。
详细说明
示例用法
考虑以下 JavaScript 类定义
class MyBase { }
class MyDerived extends MyBase { }
我们将它转换为 wasm-bindgen
proc-macro 导入,如下所示
# #![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
将发出此 __wbindgen_instanceof_Illmatic
的 JavaScript 定义,它只是包装了 JS instanceof
运算符
const __wbindgen_instanceof_Illmatic = function (idx) {
return getObject(idx) instanceof Illmatic;
};
JsCast
的提供特性方法
JsCast
特性的提供方法包装了不符合人体工程学的必需静态特性方法,并提供了符合人体工程学、可链接的版本,这些版本在 self
和另一个 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
的未检查转换方法,我们知道这些方法由于继承关系而有效
-
用于
self
消耗转换的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 {}
proc-macro 导入需要为每个传递基类添加 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
上可涡轮捕鱼的方法,这些方法可以在类型推断需要帮助时使用。相反,必须使用显式类型创建局部变量。# #![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
上可涡轮捕鱼的方法,但代价是使用非标准特性。 -
我们可以使用
TryFrom
进行动态检查转换,而不是JsCast::dyn_into
等。这将在使用wasm-bindgen
时引入一个新的夜间功能要求。我们通过不将我们的动态检查转换方法命名为JsCast::try_into
来保持与未来的兼容性,从而为TryFrom
稳定时留下可能性。 -
显式向上转换仍然没有提供很好的符合人体工程学的体验。我们可以在这里做几件事
-
使用
Deref
特性隐藏向上转换。这通常被认为是一种反模式。 -
为包含所有基类型方法的基类型自动创建一个
MyBaseMethods
特性,并为MyBase
和MyDerived
实现该特性?还发出一个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
进行向上转换,并对其他类型的转换进行动态检查。
未解决的问题
JsCast
特性是否应该在wasm_bindgen::prelude
中重新导出?我们没有在此 RFC 中指定它,并且我们最初可以在没有在 prelude 中重新导出它的情况下发布,看看感觉如何。根据经验,我们可能在将来决定将其添加到 prelude 中。