• 开始日期: 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 {}

示例向上转换

我们可以使用正常的 FromAsRefAsMutInto 转换,从 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`.
    }
}

示例未检查转换

如果我们确实知道 MyBaseMyDerived 的实例,并且我们不想支付动态检查的成本,我们也可以使用未检查转换


# #![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();
#}

JsValueJsCast 实现

我们还为 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 的未检查转换方法,我们知道这些方法由于继承关系而有效

  1. 用于 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)
        }
    }
    #}
  2. 用于共享引用转换的 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)
        }
    }
    #}
  3. 用于独占引用转换的 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 特性,而不是使用标准的 FromAs{Ref,Mut} 特性。这将使我们更清楚地了解我们正在进行与继承相关的转换,但也将是一个新的特性,人们必须理解它,而不是几乎所有 Rust 程序员对 std 特性的熟悉程度。

  • 使用 FromAs{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 特性,并为 MyBaseMyDerived 实现该特性?还发出一个 MyDerivedMethods 特性,该特性要求 MyBase 作为超特性,在特性级别表示继承?这是 Rust 风格的做法,它允许我们使用特性边界编写泛型函数。这就是 stdwebHTMLElement 的方法使用 IHTMLElement 特性的方式。

      我们是否这样做也恰好与基类型和派生类型之间的转换无关。我们将探索这个设计空间以供后续 RFC,并希望以增量的方式只实现转换。

  • 特性有时会妨碍人们了解可以对某件事做些什么。它们在生成的文档中并不那么直观,并且可能导致人们认为他们必须编写对特性进行泛化的代码,而实际上并非如此。我们可以通过两种方式摆脱 JsCast 特性

    1. 仅在 JsValue 上实现其方法,并要求像 ImportedJsClassUno -> ImportedJsClassDos 这样的转换在两者之间转到 JsValueImportedJsClassUno -> JsValue -> ImpiortedJsClassDos

    2. 我们可以冗余地在 JsValue 和导入的 JS 类上直接实现所有方法。

  • 未检查转换可以标记为 unsafe 以反映这些情况下正确性依赖于程序员。但是,错误使用未检查的 JS 转换不会在 Rust 意义上引入内存不安全,因此这将使用 unsafe 作为通用的“你可能不应该使用它”警告,这不是 unsafe 的预期用途。

  • 我们只可以为所有内容始终实现未检查转换。这将鼓励一种松散的、从臀部射击的编程风格。我们更愿意在可能的情况下利用类型。我们意识到有时仍然需要逃生舱,我们确实提供了任意未检查转换,但引导人们使用 FromAsRefAsMut 进行向上转换,并对其他类型的转换进行动态检查。

未解决的问题

  • JsCast 特性是否应该在 wasm_bindgen::prelude 中重新导出?我们没有在此 RFC 中指定它,并且我们最初可以在没有在 prelude 中重新导出它的情况下发布,看看感觉如何。根据经验,我们可能在将来决定将其添加到 prelude 中。