1. 程式人生 > >這一天,我用 Rust 重寫了已有 19 年曆史的 C++ 庫!

這一天,我用 Rust 重寫了已有 19 年曆史的 C++ 庫!

640?wx_fmt=gif

從版本 56 開始,Firefox 瀏覽器支援一種新的字元編碼轉換庫,叫做 encoding_rs。它是用 Rust 編寫的,代替了從 1999 年就開始使用的 C++ 編寫的字元編碼庫 uconv。最初,所有呼叫該字元編碼轉換庫的程式碼都是 C++,所以儘管新的庫是用 Rust 編寫的,它也必須能被 C++ 程式碼呼叫。實際上,在 C++ 呼叫者看來,這個庫跟現代的 C++ 庫沒什麼區別。下面是我實現這一點採用的開發方式。

640?wx_fmt=jpeg

相關閱讀:

  • 關於 encoding_rs 本身:https://hsivonen.fi/encoding_rs/

  • 演講視訊:https://media.ccc.de/v/rustfest18-5-a_rust_crate_that_also_quacks_like_a_modern_c_library

  • 幻燈片:https://hsivonen.fi/rustfest2018/


640?wx_fmt=png

怎樣寫現代 C++?


所謂“現代”C++的意思就是從 C++ 呼叫者來看,函式庫遵循 C++ 的核心指南(https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines),並具備以下新特性:

  • 通過返回 std::unique_ptr / mozilla::UniquePtr 中在堆上分配的物件的指標進行堆記憶體分配管理。

  • 呼叫者分配的緩衝區用 gsl::span / mozilla::Span 來表示,而不是用普通的指標和長度表示。

  • 多個返回值使用 std::tuple / mozilla::Tuple 傳遞,而不是使用輸出引數。

  • 非空的普通指標用 gsl::not_null / mozilla::NotNull 表示。

上面的 gsl:: 表示 Guidelines Support Library(https://github.com/microsoft/GSL),這個庫能提供核心指南要求、但尚未存在於 C++ 標準庫中的東西。


640?wx_fmt=png

用 Rust 寫 C++ 庫?


“用 Rust”寫 C++ 庫的意思是指庫中的大部分是用 Rust 寫的,但提供給 C++ 呼叫者的介面至少在 C++ 呼叫者來看就像個真正的 C++ 庫一樣。


640?wx_fmt=png

C++ 和 Rust 都與 C 有互操作性


C++ 的 ABI 非常複雜,而 Rust ABI 尚未完全確定。但是,C++ 和 Rust 都支援一些使用 C ABI 的函式。因此,要想讓 C++ 和 Rust 擁有互操作性,就需要通過某種方法,讓 C++ 把 Rust 程式碼看成 C 程式碼,Rust 把 C++ 程式碼看成 C 程式碼。


640?wx_fmt=png

簡化的情形


這篇文章並不是 Rust 與 C++ 互聯的完整指南。encoding_rs 的介面非常簡單,缺乏兩種語言之間的互操作性上的常見問題。但是,encoding_rs 簡化 C++ 介面的例子可以作為一個指南,給那些希望在設計函式庫時瞭解跨語言互操作性的人們提供一些幫助。具體來說:

  • encoding_rs 從來不會呼叫 C++:跨語言呼叫是單向的。

  • encoding_rs 在呼叫返回後,不持有指向 C++ 物件的引用:因此 Rust 程式碼不需要管理 C++ 記憶體。

  • encoding_rs 不需要用 Rust 或 C++ 語言提供繼承層次結構:因此兩種語言中都沒有 vtables。

  • encoding_rs 操作的資料型別非常簡單:只有基本型別的連續緩衝區(u8 / uint8_t  和  u16 / char16_t  的緩衝區)。

  • 僅支援 panic=abort 配置(即 Rust 的崩潰會終止整個程式,無需回滾棧),而且這裡給出的程式碼只有在該配置下才是正確的。這裡給出的程式碼沒有去防止 Rust 崩潰跨越 FFI 邊界回滾,因此跨 FFI 邊界的崩潰是未定義的行為。


640?wx_fmt=png

API 快速概覽


為了理解我們討論的 Rust API(https://docs.rs/encoding_rs/0.8.13/encoding_rs/),先來從高層次看看。整個函式庫有三個公開的結構體(struct):Encoding,Decoder 和 Encoder。從函式庫的使用者角度來看,這些結構能為各種具體的編碼提供統一的介面,所以可以像 traits、父類或介面一樣使用, 但嚴格來說它們實際上是結構體。Encoding 的例項是靜態分配的。Decoder 和 Encoder 封裝了流轉換的狀態,是在執行時動態分配的。

Encoding 例項的引用(即&'static Encoding)可以通過標籤獲得(從協議文字中提取的文字識別資訊),或通過命名靜態變數(named static)獲得。然後 Encoding 可以作為 Decoder 的引數使用,後者是在棧上分配的。

let encoding: &'static Encoding =
    Encoding::for_label( // by label
        byte_slice_from_protocol
    ).unwrap_or(
        WINDOWS_1252     // by named static
    );

let decoder: Decoder =
    encoding.new_decoder();


在處理流時,Decoder 中有個方法可以將流從呼叫者分配的一個切片解碼到呼叫者分配的一個切片。解碼器不進行堆分配操作。

pub enum DecoderResult {
    InputEmpty,
    OutputFull,
    Malformed(u8, u8),
}

impl Decoder {
    pub fn decode_to_utf16_without_replacement(
        &mut self,
        src: &[u8],
        dst: &mut [u16],
        last: bool
    ) -> (DecoderResult, usize, usize)
}

在處理流之外的情況時,呼叫者完全不需要處理 Decoder 和 Encoder 的任何東西。Encoding 會提供方法在一個緩衝區中處理整個邏輯輸入流。

impl Encoding {
    pub fn decode_without_bom_handling_and_without_replacement<'a>(
        &'
static self,
        bytes: &'a [u8],
    ) -> Option<Cow<'
a, str>>
}



640?wx_fmt=png

處理過程


0. 對 FFI 友好的設計

有些設計來自於問題域本身的簡化因素。而有些只是選擇。

字元編碼庫可以合理地將編碼、解碼器和編碼器的概念表示成 traits(類似於 C++ 中沒有欄位的抽象父類),但是,encoding_rs 對這些概念採用了結構體(struct),以便在分發的時候能 match 成一個 enum,而不必依賴於 vtable(https://en.wikipedia.org/wiki/Virtual_method_table)。

pub struct Decoder { // no vtable
   variant: VariantDecoder,
   // ...
}

enum VariantDecoder { // no extensibility
    SingleByte(SingleByteDecoder),
    Utf8(Utf8Decoder),
    Gb18030(Gb18030Decoder),
    // ...
}

這樣做的主要動機並不是消除 vtable 本身,而是故意讓層次結構不能擴充套件。其背後反映的哲學是,新增字元編碼不應該是程式設計師應當關心的事情。相反,程式應當使用 UTF-8 作為資料交換,而且程式不應當支援古老的編碼,除非需要相容已有的內容。這種不可擴充套件的層次結構能帶來強型別安全。如果你從 encoding_rs 得到一個 Encoding 例項,那麼你可以信任它絕不會給出任何編碼標準中沒有給出的特性。也就是說,你可以相信它絕不會表現出 UTF-7 或 EBCDIC 的行為。

此外,通過分發 enum,一個編碼的解碼器可以在內部根據 BOM 嗅探的結果變成另一個編碼的解碼器。

有人可能會說,Rust 提供編碼轉換器的方式是將它變成迭代介面卡,接受位元組迭代器的輸入,然後輸出 Unicode 的標量值,或者相反。然而迭代器不僅在跨越 FFI 邊界時更復雜,還使得加速 ASCII 處理等技巧更難以實現。而直接接受一個切片進行讀取和寫入操作,不僅使得提供 C API 更容易(用 C 的術語來說,Rust 切片解構成對齊的非空指標和一個長度值),而且可以通過觀察多個程式碼單元能放入單個暫存器(ALU 暫存器或 SIMD 暫存器)的情況,實現一次處理多個程式碼單元,從而實現 ASCII 處理加速。

如果 Rust 的原生 API 只處理基本型別、切片和(非 trait 物件的)結構體,那麼與支援高階 Rust 特性的 API 相比,這個 API 更容易對映到 C API。(在 Rust 中,發生型別擦除時會產生一個 trait 物件。也就是說,你得到的是一個 trait 型別的引用,它並沒有給出該引用指向的那個結構體的型別資訊。)

1. 建立 C API

當涉及到的型別足夠簡單時,C 和 Rust之間的主要鴻溝,一是 C 語言缺乏方法、缺乏多返回值功能,二是不能以值形式傳送 C 結構體之外的型別。

  • 方法用函式包裹起來,該函式的第一個引數是指向該方法所屬結構體的指標。

  • 切片引數轉換為兩個引數:指向切片開頭的指標,以及切片的長度。

  • 函式的返回值中,第一個基本型別的返回值作為返回值返回,其他返回值作為輸出引數。當輸出引數與同類型的輸入引數相關時,使用 in/out 引數是合理的。

  • 如果 Rust 方法以值的形式返回一個結構體,那麼封裝函式將打包該結構體,並返回指向它的指標,因此 Rust 不必考慮該結構體。此外還要新增一個函式,用於釋放該指標指向的結構體。這樣,Rust 方法只需將指標打包,或者拆包。從 C 指標的角度來看,結構體是不透明的。

  • 作為特殊情況,獲取編碼名稱的方法在 Rust 中返回 &'static str,它被包裹在一個函式中,接收一個指向可寫入的緩衝區的指標,緩衝區的長度至少應當為最長的編碼名稱的長度。

  • enum 用來表示輸入緩衝區的枯竭、輸出緩衝區佔滿,或錯誤以及詳細情況,這些 enum 在 C API 中轉變成 uint32_t,並加上相應的常量來表示“輸入空”或“輸出滿”以及一系列解釋其他錯誤的規則。這種方式不是最理想的,但在這種情況下很好用。

  • 越界檢查時的長度計算改成飽和運算(saturating)。也就是說,呼叫者需要將 SIZE_MAX 當作越界的訊號。

2.在 C++ 中根據 C API 重建 API

即使是慣用的 C API(https://github.com/hsivonen/encoding_c/blob/master/include/encoding_rs.h)也不能當做現代 C++ API 使用。幸運的是,類似於多重返回值、切片等 Rust 概念可以在 C++ 中表示,只需將 C API 返回的指標解釋成指向 C++ 物件的指標,就能展示出 C++ 的優雅。

大部分例子來自一個使用了 C++17 標準庫型別的 API(https://github.com/hsivonen/encoding_c/blob/master/include/encoding_rs_cpp.h)。在 Gecko 中,我們一般會避免使用 C++ 標準庫,而使用一個 encoding_rs 的特別版本的 C++ API,該版本使用了 Gecko 特有的型別(https://searchfox.org/mozilla-central/source/intl/Encoding.h)。這裡我假設標準庫型別的例子更容易被更多讀者接受。

方法的優雅

對於每個 C 語言中不透明的構造體指標,C++ 中都會定義一個類,C 的標頭檔案也會修改,使得從 C++ 編譯器的角度來看,指標型別變成指向 C++ 類例項的指標。這些放在一起就相當於一個 reinterpret_cast 過的指標,而不需要實際寫出 reinterpret_cast。

由於指標並不真正指向它們看似指向的類的例項,而是指向 Rust 結構體的例項,因此應該事先做好預防措施。這些類中沒有定義任何欄位。預設的無引數建構函式和複製構造方法被刪除,預設的 operator= 也被刪除。此外,這些類還不能包含虛方法。(最後一點是個重要的限制條件,稍後會討論。)

class Encoding final {
// ...
private:
    Encoding() = delete;
    Encoding(const Encoding&) = delete;
    Encoding& operator=(const Encoding&) = delete;
    ~Encoding() = delete;
};

對於 Encoding 來說,所有例項都是靜態的,因此解構函式也被刪掉了。如果是動態分配的 Decoder 和 Encoder,還要新增一個空的解構函式和一個 static void operator delete。(後面會給一個例子。)這樣能讓這個偽 C++ 類的析構過程導向 C API 中相應型別的釋放函式。

這些基礎工作將指標變得看上去像是 C++ 類例項的指標。有了這些,就能在這些指標上實現方法呼叫了。(介紹完下一個概念後也會給出例項。)

返回動態分配的物件

前面說過,Rust API 以值方式返回 Encoder 或 Decoder,這樣呼叫者可以將返回值放在棧上。這種情況被 FFI 的包裹代替,因此 C API 只需通過指標暴露堆上分配的物件。而且,這些指標也被重新解釋為可 delete 的 C++ 物件指標。

不過還需要確保這些 delete 會在正確的時機被呼叫。在現代 C++ 中,如果物件在同一時刻只能有一個合法的所有者,那麼物件指標會被包裹在 std::unique_ptr 或 mozilla::UniquePtr 中。老的 uconv 轉換器支援引用計數,但在 Gecko 程式碼中所有實際的應用中,每個轉換器都只有一個所有者。由於編碼器和解碼器的使用方式使得同一時刻只有一個合法的所有者,因此 encoding_rs 的兩個 C++ 包裹就使用了 std::unique_ptr 和 mozilla::UniquePtr。

我們來看看 Encoding 中那個返回 Decoder 的工廠方法。在 Rust 中,這個方法接收 self 的引用,通過值返回 Decoder。

impl Encoding {
    pub fn new_decoder(&'static self) -> Decoder {
        // ...
    }
}

在 FFI 層,第一個引數是顯式的指標型別,對應於 Rust 的 &self 和 C++ 的 this(具體來說,是 const 版本的 this)。我們在堆上分配記憶體(Box::new())然後將 Decoder 放進分配好的記憶體中。然後忘記記憶體分配(Box::into_row),這樣可以將指標返回給 C,而不會在作用域結束時釋放。為了能夠釋放記憶體,我們引入了一個新的函式,將 Box 放回,然後將它賦給一個變數,以便立即離開作用域,從而釋放堆上分配的記憶體。

#[no_mangle]
pub unsafe extern "C" fn encoding_new_decoder(
    encoding: *const Encoding) -> *mut Decoder
{
    Box::into_raw(Box::new((*encoding).new_decoder()))
}

#[no_mangle]
pub unsafe extern "C" fn decoder_free(decoder: *mut Decoder) {
    let _ = Box::from_raw(decoder);
}

在 C 檔案頭中看起來像這樣:

ENCODING_RS_DECODER*
encoding_new_decoder(ENCODING_RS_ENCODING const* encoding)
;

void
decoder_free(ENCODING_RS_DECODER* decoder)
;

ENCODING_RS_DECODER 是一個巨集,用於在 C 標頭檔案在 C++ 環境中使用(而不是作為純 C API 使用)時將其替換成正確的 C++ 型別。

在 C++ 一側,我們使用 std::unique_ptr,相當於 Rust 的 Box。實際上它們也非常相似:

let ptr: Box<Foo>
std::unique_ptr<Foo> ptr
Box::new(Foo::new(a, b, c))
make_unique<Foo>(a, b, c)
Box::into_raw(ptr)
ptr.release()
let ptr = Box::from_raw(raw_ptr);
std::unique_ptr<Foo> ptr(raw_ptr);

我們把從 C API 獲得的指標包裹在 std::unique_ptr 中:

class Encoding final {
public:
    inline std::unique_ptr<Decoder> new_decoder() const
    {
        return std::unique_ptr<Decoder>(
            encoding_new_decoder(this));
    }
};

當 std::unique_ptr<Decoder> 離開作用域時,刪除操作會通過 FFI 導向回 Rust,這是因為定義是下面這樣的:

class Decoder final {
public:
    ~Decoder() {}
    static inline void operator delete(void* decoder)
    
{
        decoder_free(reinterpret_cast<Decoder*>(decoder));
    }
private:
    Decoder() = delete;
    Decoder(const Decoder&) = delete;
    Decoder& operator=(const Decoder&) = delete;
};

如何工作?

在 Rust 中,非 trait 的方法只不過是語法糖:

impl Foo {
    pub fn get_val(&self) -> usize {
        self.val
    }
}

fn test(bar: Foo) {
    assert_eq!(bar.get_val(), Foo::get_val(&bar));
}

對非 trait 型別的引用方法呼叫只不過是普通的函式呼叫,但第一個引數是指向 self 的引用。在 C++ 一側,非虛方法的呼叫原理相同:非虛 C++ 方法呼叫只不過是函式呼叫,但第一個函式是 this 指標。

在 FFI/C 層,我們可以將同樣的指標顯式地作為第一個引數傳遞。

在呼叫 ptr->Foo() 時,其中的 ptr 是 T* 型別,而如果方法定義為 void Foo()(它在 Rust 中對映到 &mut self),那麼 this 是 T* 型別,如果方法定義為 void Foo() const(在 Rust 中對映到 &self),則 this 是 const T* 型別,所以這樣也能正確處理 const。

fn foo(&self, bar: usize) -> usize
size_t foo(size_t bar) const
fn foo(&mut self, bar: usize) -> usize
size_t foo(size_t bar)

這裡“非 trait 型別”和“非虛”是非常重要的。要想讓上面的程式碼正確工作,那麼無論那一側都不能有 vtable。這就是說,Rust 不能有 trait,C++ 也不能有繼承。在 Rust 中,trait 物件(指向任何實現了 trait 的結構體的 trait 型別的引用)實現為兩個指標:一個指向結構體例項,另一個指向對應於資料的具體型別的 vtable。我們需要能夠把 self 的引用作為單一指標跨越 FFI 傳遞,所以在跨越 FFI 時無法攜帶 vtable 指標。為了讓 C++ 物件指標相容 C 的普通指標,C++ 將 vtable 指標放在了物件自身上。由於我們的指標指向的並不是真正帶有 vtable 的 C++ 物件,而是 Rust 物件,所以必須保證 C++ 程式碼不會在指標目標上尋找 vtable 指標。

其結果是,Rust 中的結構對應的 C++ 中的類不能從 C++ 框架中的通用基類繼承。在 Gecko 的情況中,C++ 類不能繼承 nsISupports。例如,在 Qt 的語境下,對應的 C++ 類不能從 QObject 繼承。

非空指標

Rust API 中有的方法會返回 &'static Encoding。Rust 的引用永遠不會為 null,因此最好是將這個資訊傳遞給 C++ API。C++ 中對應於此的是 gsl::not_null和mozilla::NotNull。

由於 gsl::not_null 和 mozilla::NotNull 只不過是型別系統層面的寫法,它並不會改變底層指標的機器表示形式,因此對於有 Rust 保證的指標,跨越 FFI 之後可以認為它們絕不會為 null,所以我們想做的是,利用與之前將 FFI 返回的指標重新解釋為指向無欄位、無虛方法的 C++ 物件的指標同樣的技巧來騙過 C++ 編譯器,從而在標頭檔案中宣告那些 FFI 返回的絕不會為 null 的指標為型別 mozilla::NotNull<const Encoding*>。不幸的是,實際上這一點無法實現,因為在 C++ 中,涉及模板的型別不能在 extern "C" 函式的定義中使用,所以 C++ 程式碼最後只能在從 C API 接收到指標、包裹在 gsl::not_null 或 mozilla::NotNull 時進行一系列的 null 檢查。

但是,也有一些定義是指向編碼物件常量的靜態指標(指向的目標是在 Rust 中定義的),而且恰巧 C++ 允許將這些定義為 gsl::not_null<const Encoding*>,所以我們這樣實現了。(感謝 Masatoshi Kimura 指出這一點的可行性。)

Rust 中靜態分配的 Encoding 例項的定義如下:

pub static UTF_8_INIT: Encoding = Encoding {
    name: "UTF-8",
    variant: VariantEncoding::Utf8,
};

pub static UTF_8: &'static Encoding = &UTF_8_INIT;

在 Rust 中,通用的規則(https://twitter.com/tshepang_dev/status/1051558270425591808)是 static 用來宣告不會改變的記憶體地址,const 用來宣告不會改變的值。因此,UTF_8_INIT 應當為 static,而 UTF_8 應當為 const:指向 static 例項的引用的值不會改變,但為這個引用靜態分配的記憶體地址則不一定。不幸的是, Rust 有一條規則說,const 的右側不能包含任何 static 的東西,因此這一條阻止了對 static 的引用,以確保 const 定義的右側可以被靜態檢查,確定它是否適合任何假想的 const 定義——甚至是那些在編譯時就試圖解引用(dereference)的定義。

但對於 FFI,我們需要為 UTF_8_INIT 分配一塊不會改變的記憶體,因為這種記憶體能在 C 的聯結器中使用,可以讓我們為 C 提供命名的指標型別的東西。上面說的 UTF_8 的表示形式已經是我們需要的了,但為了讓 Rust 更優雅,我們希望 UTF_8 能參與到 Rust 的名稱空間中。這意味著從 C 的角度來看,它的名字需要被改變(mangle)。我們浪費了一些空間來重新靜態分配指標來避免改變名稱,以供 C 使用:

pub struct ConstEncoding(*const Encoding);

unsafe impl Sync for ConstEncoding {}

#[no_mangle]
pub static UTF_8_ENCODING: ConstEncoding =
    ConstEncoding(&UTF_8_INIT);

這裡使用了指標型別,以明確 C 語言會將其當做指標(即使 Rust 引用型別擁有同樣的表現形式)。但是,Rust 編譯器拒絕編譯帶有全域性可視性指標的程式。由於全域性變數可以被任何執行緒訪問,多執行緒同時訪問指標指向的目標可能會引發問題。這種情況下,指標目標不會被修改,因此全域性可視性是沒問題的。為了告訴編譯器這一點,我們需要為指標實現 Sync 這個 marker trait。但是,trait 不能在指標型別上實現。作為迂迴方案,我們為*const Encoding建立了一個新的型別。新的型別擁有與它包裹的型別同樣的表現形式,但我們可以在新型別上實現 trait。實現 Sync 是 unsafe 的,因為我們告訴了編譯器某些東西可以接受,這並不是編譯器自己發現的。

在 C++ 中我們可以這樣寫(巨集擴充套件之後的內容):

extern "C" {
    extern gsl::not_null<const encoding_rs::Encoding*> const UTF_8_ENCODING;
}

指向編碼器和解碼器的指標也絕不會為 null,因為記憶體分配失敗會直接終止程式。但是 std::unique_ptr / mozilla::UniquePtr 和 gsl::nul / mozilla::NotNull 不能結合使用。

可選值

Rust 中常見的做法是用 Option<T> 表示返回值可能有值也可能沒有值。現在的 C++ 提供了同樣的東西:std::optional<T>。在 Gecko 中,我們使用的是 mozilla::Maybe<T>。

Rust 的 Option<T> 和 C++ 的 std::optional<T> 實際上是一樣的:

return None;
   return std::nullopt;
return Some(foo);
   return foo;
is_some()
   operator bool()
   has_value()
unwrap()
   value()
unwrap_or(bar)
   value_or(bar)

但不幸的是,C++ 保留了安全性。從 std::optional<T> 中提取出包裹值時最優雅的方法就是使用 operator*(),但這個也是沒有檢查的,因此也是不安全的。

多返回值

儘管 C++ 在語言層面缺少對於多返回值的支援,但多返回值可以從庫的層次實現。比如標準庫,相應的部分是 std::tuple,std::make_tuple 和 std::tie。在 Gecko 中,相應的庫是 mozilla::Tuple,mozilla::MakeTuple 和 mozilla::Tie。

fn foo() -> (T, U, V)
   std::tuple<T, U, V> foo()
return (a, b, c)
;
   return {a, b, c};
let (a, b, c) = foo();
   const auto [a, b, c] = foo();
let mut (a, b, c= foo();
   auto [a, b, c] = foo();

切片

Rust 切片包裹了一個自己不擁有的指標,和指標指向內容的長度,表示陣列中的一段連續內容。相應的 C 程式碼為:

src: &[u8]
   const uint8_t* src, size_t src_len
dst: &mut [u8]
   uint8_t* dst, size_t dst_len

C++ 的標準庫中並沒有對應的東西(除了 std::string_view 可以用來表示只讀字串切片之外),但 C++ 核心指南中已經有一部分叫做 span 的東西(https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#i13-do-not-pass-an-array-as-a-single-pointer):

src: &[u8]
   gsl::span<const uint8_tsrc
dst: &mut [u8]
   gsl::span<uint8_tdst
&mut vec[..]
   gsl::make_span(vec)
std::slice::from_raw_parts(ptrlen)
   gsl::make_span(ptrlen)
for item in slice {}
   for (auto&& item : span) {}
slice[i]
   span[i]
slice.len()
   span.size()
slice.as_ptr()
   span.data()

GSL 依賴於 C++14,但在 encoding_rs 釋出時,Gecko 由於 Android 的阻礙,不得不停留在 C++11 上(https://bugzilla.mozilla.org/show_bug.cgi?id=1325632#c25)。因此,GSL 不能原樣在 Gecko 中使用,我將 gsl::span 移植到了 C++11 上,變成 mozilla::Span(https://searchfox.org/mozilla-central/source/mfbt/Span.h#375)。移植的過程主要是去掉 constexpr 關鍵字,並用 mozilla:: 的型別和型別 trait 代替標準庫中的型別。在 Gecko 改成 C++14 後,部分 constexpr 關鍵字被恢