• 開始日期: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-bindgenMyDerived 會從 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`.
    }
}

範例未檢查轉型

如果我們確實知道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();
#}

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. 一個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 {}

處理巨集匯入需要每個遞移基底有一個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 上提供可鏈接的 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 特質來隱藏上轉型。這通常被視為一種反範例

    • 對於包含所有基本類型方法,並實作該特質的 MyBaseMyDerived,自動建立一個 MyBaseMethods 特質給基本類型?此外,發射一個 MyDerivedMethods 特質,它要求 MyBase 作為一個父特質,在特質層級上表示繼承?這是 Rust 的實作方式,並允許我們以特質限制編寫一般函式。這是 stdwebHTMLElement 的方法所使用的 IHTMLElement 特質。

      我們是否這樣做,也與在基本類型和派生類型之間轉換正交。我們留待後續的 RFC 探索此設計空間,並希望逐步只進行轉換。

  • 有時候,特質會阻礙學習對某事物能做什麼。它們在產生的文件說明中不如預期般突出,並且可能讓人們認為,即使在非必要的情況下,必須撰寫對特質通用的程式碼。我們可以透過兩種方法來消除 JsCast 特質

    1. 僅對 JsValue 實作其方法並要求將類似的轉換,從 ImportedJsClassUno -> ImportedJsClassDos,在中間轉到 JsValueImportedJsClassUno -> JsValue -> ImpiortedJsClassDos

    2. 我們可以重複地對 JsValue 和已匯入的 JS 類別直接實作其所有方法。

  • 未檢查的強制轉換可標記為 unsafe,以反映在這些情況下正確性依賴程式設計人員。然而,誤用未檢查的 JS 強制轉換與 Rust 的意義中不會造成記憶體不安全性,因此這將使用 unsafe 作為一般的「你大概不應該使用這個」警告,這並非 unsafe 的既定目的。

  • 我們只能在任何時候為所有內容實作未檢查的強制轉換。這將鼓勵一種隨意的、憑直覺的程式設計風格。我們偏好適時地利用類型。我們知道有時候仍然需要逃生艙口,並且我們確實提供任意的未檢查強制轉換,但引導人們使用 FromAsRefAsMut 升級強制轉換,並動態地檢查其他類型的強制轉換。

未解決的問題

  • 是否應該在 wasm_bindgen::prelude 中重新匯出 JsCast 特質?我們未在此 RFC 中指定應該,且我們一開始會在不於 prelude 中重新匯出它公開,並看看感覺如何。根據經驗,我們可能決定在未來將其加入 prelude。