Lynx技術分析-JS引擎擴充套件設計

hxxft發表於2019-02-25

JS Binding 技術

Lynx(一個高效的跨平臺框架) 的 JS Binding 技術最主要的目的是搭建一個高效的與 JS 引擎解耦的通訊橋樑,同時具備 JS 引擎切換的能力。該技術經歷了多次迭代,最終通過抽象的引擎介面層設計,在程式碼層面做到對於 JS 引擎的解耦。目前 Lynx 在 Android 端支援 V8 和 JSC 引擎的切換。

關於 JSC 和 V8 引擎的相關基礎知識可以瀏覽上一篇文章

遇到的問題

Lynx 是一個 JS 驅動的跨平臺框架,提供了 JS 呼叫 Android 和 iOS 等平臺層的渲染能力,同時允許開發者擴充平臺能力,因此在 Lynx 中和 JS 通訊的除了核心 Runtime 層,還包括了處於平臺層的 Module 和 RenderObjectImpl,同時在框架中存線上程間通訊的情況。結合上述 Lynx 框架的特性,在 JS Binding 迭代時遇到的主要的問題:

  1. 程式碼解耦:對於不同的 JS 引擎的初始化等流程和 Extension (需要定義靜態方法)方式的統一
  2. C++ 物件生命週期管理
  3. 跨執行緒和跨平臺的引數轉化

設計

整體設計程式碼在 runtime 目錄

對於跨執行緒和跨平臺的引數轉化的問題,為了便於引數在上下游的轉化(JS 與核心 C++ 層的轉化,核心層與平臺層 Android & iOS 的轉化),定義了 LynxValue 作為通用傳遞引數,並根據不同平臺制定 LynxValue 的轉化規則,減少引數在跨層呼叫時繁瑣的轉化步驟。轉化規則現在包括 JSC 到核心層的 JSCHelper,V8 到核心層的 V8Helper,核心層到 iOS 層的 OCHelper,以及核心層到 Android 層的 JNIHelper。下面的圖可以看出 LynxValue 流通與不同層次。

Lynx技術分析-JS引擎擴充套件設計

主要資料結構

  • LynxValue 是引數傳遞規則的基類,其中使用了聯合體定義了支援轉化的引數。包括基本資料型別,陣列,鍵值對,LynxObject 和 LynxFunction 等。除了 LynxFunxtion 和 LynxObject,其餘引數均不能直接和 JS 通訊,僅用於引數轉化,同時支援跨執行緒跨平臺傳遞。
  • LynxArray 有序的有限個的 LynxValue 的集合,對應 JS 端和平臺層的陣列。
  • LynxMap 鍵值對,僅支援字串作為 key,對應 JS 端的 Map 或者 Object 和平臺層的鍵值對。
  • LynxFunction 儲存了 JS 端的 function,用於在合適時機回撥 JS function。
  • LynxObject 通訊基類,藉助 ClassTemplate 構建與 JS 通訊橋樑的物件(請看後續分析),可以對 JS 物件進行間接的操作,如 ProtectJSObject 的操作,使 JS 物件脫離 GC。

在 JS 引擎程式碼解耦方面,JSC 和 V8 在 JS 原型和 Extension 上的設計都是相似的邏輯,只是在實現的細節上不一致。如在 JSC 中利用 JSClassRef 描述原型上所具有的屬性和方法,同時可以構造原型鏈,而 V8 中利用 FunctionTemplate 和 PrototypeTemplate 代替;JSC 中使用 JSObjectSetPrivate 介面為 JS 物件繫結一個 C++ 物件,而 V8 則利用 ObjectTemplate::SetInternalField 方法代替。基於上述特點,Lynx 的 JSBinding 抽象了一層 JS 的原型構造器和方法鉤子的介面,以滿足與 JS 的通訊功能。

Lynx技術分析-JS引擎擴充套件設計

JSVM 是代表 JS 執行的虛擬機器,真正的實現檔案交由各自引擎實現。

JSContext 為 JS 引擎的控制上下文,同時是一個模板類,其中包含全域性物件 Global,對於真正的 V8 和 JSC 的操作由其實現類 V8ContextJSCContext 決定。而與 JS 通訊主要使用對外介面 ClassTemplate 和內部介面 ObjectWrap。

ClassTemplate 用於構造 JS 原型的模板,通過該模板可以註冊函式和變數鉤子等(Extension 功能。該物件持有PrototypeBuilder,PrototypeBuilder 由對應的 JS 引擎實現,用於構建 JSC 的 JSClassRef 或者是 V8 的 FunctionTemplate,同時可以根據原型建立 JS 物件。 ClassTemplate 提供了巨集定義幫助定義預設 ClassTemplate 的靜態方法,下面是巨集定義的意義和用法:

  • DEFINE_CLASS_TEMPLATE_START 預設 ClassTemplate 構建的方法定義的開始
  • REGISTER_PARENT 定義 ClassTemplate 的父親(原型鏈),在 START 和 END 之間使用。
  • EXPOSE_CONSTRUCTOR 在 JS 上下文中暴露該 ClassTemplate 作為構造器,在 START 和 END 之間使用。
  • REGISTER_METHOD_CALLBACK 向 ClassTemplate 中註冊函式鉤子,在 START 和 END 之間使用。
  • REGISTER_GET_CALLBACK REGISTER_SET_CALLBACK 向 ClassTemplate 中註冊變數鉤子,在 START 和 END 之間使用。
  • DEFINE_CLASS_TEMPLATE_END 預設 ClassTemplate 構建的方法定義的結束。
  • DEFAULT_CLASS_TEMPLATE 獲取預設 ClassTemplate。

defines.h 標頭檔案含有用於定義 JS 引擎鉤子函式的巨集規則,C++ 類需要根據巨集定義鉤子函式,並將函式指正註冊到 ClassTemplate 中,同時自身需要有對應的類方法(鉤子函式會進行回撥)進行真正實現,才能完成原型的構建。ClassTemplate.h 中通過巨集定義提供了快速構建一個與 C++ 物件預設的 ClassTemplate 物件。結合兩個巨集定義規則,可以實現快速構建與 JS 通訊的 C++ 類。下面是 defines.h 中巨集定義的意義:

  • DEFINE_METHOD_CALLBACK 用於定義 JS 引擎函式鉤子,DEFINE_GROUP_METHOD_CALLBACK 用於定義帶方法名稱作為引數的函式鉤子,METHOD_CALLBACK 用於獲取鉤子名稱
  • DEFINE_SET_CALLBACK DEFINE_GET_CALLBACK 用於定義 JS 引擎變數鉤子,SET_CALLBACK GET_CALLBACK 用於獲取鉤子名稱。

自定義類方法鉤子示例:

JS 變數的 Get 鉤子:base::ScopedPtr<LynxValue> Function();

JS 變數的 Set 鉤子: void Function(base::ScopedPtr<jscore::LynxValue>& value);

JS 方法鉤子:base::ScopedPtr<LynxValue> Function(base::ScopedPtr<LynxArray>& array);

ObjectWrap 用於建立 JS 物件和 C++ 物件(這裡指 LynxObject)的關係,即用於管理 C++ 物件生命週期,C++ 物件的生命週期是跟隨 JS 物件(當然 JS 物件只是對 C++ 物件進行引用計數上的增減,確保 C++ 物件在被其他類引用時可以被安全釋放或使用)。JS物件和C++物件繫結的時機在 ClassTemplate 建立 JS 物件時,這個時機由 JS 執行上下文決定(在 defines.h 中的鉤子函式中處理),無需開發者關心。

JS Binding 整體執行圖示,在 Lynx 開發中,JS 引擎的具體實現或者引數轉化規則對外無感知,利用 LynxObject 和 LynxValue 就可以與 JS 通訊,完成 API 呼叫工作。LynxValue 和 JSValue 的轉化均是在 JSObject 和 LynxObject 相互呼叫時進行。

Lynx技術分析-JS引擎擴充套件設計

例項:定義與 JS 物件 console 關聯的 Console 類,實現 console.log 的函式呼叫,主要步驟如下

  1. 繼承 LynxObject ,定義被鉤子函式呼叫的 Log 類方法
  2. 定義需要進行 Extension 的 Log 函式鉤子
  3. 根據 ClassTemplate 提供的巨集定義,快速建立預設的 ClassTemplate,在建構函式中傳入預設的 ClassTemplate。
namespace jscore {
    class Console : public LynxObject {
    public:
        Console(JSContext* context);
        virtual ~Console();
        // 定義 JS 引擎函式鉤子回撥的類方法
        base::ScopedPtr<LynxValue> Log(base::ScopedPtr<LynxArray>& array);
    };
}
複製程式碼
namespace jscore {

    #define FOR_EACH_METHOD_BINDING(V)    \
        V(Console, Log)                   

    // 定義需要進行 Extension 的函式鉤子
    FOR_EACH_METHOD_BINDING(DEFINE_METHOD_CALLBACK)

    // 定義預設的 ClassTemplate
    DEFINE_CLASS_TEMPLATE_START(Console)
        FOR_EACH_METHOD_BINDING(REGISTER_METHOD_CALLBACK)
    DEFINE_CLASS_TEMPLATE_END
    
    // 建構函式中傳入預設的 ClassTemplate
    Console::Console(JSContext* context) : LynxObject(context, DEFAULT_CLASS_TEMPLATE(context)) {
    }

    Console::~Console() {}

    base::ScopedPtr<LynxValue> Console::Log(base::ScopedPtr<LynxArray>& array) {
    	// Print log
        return base::ScopedPtr<LynxValue>(NULL);
    }

}
複製程式碼

優缺點

優點:隔離 JS 引擎程式碼,易於切換;無額外消耗的函式鉤子(通訊)實現,比 RN 的通訊更快;快速上手,相比於 Web IDL 沒有學習成本。

缺點:仍然需要在通訊類中手動編寫一定程式碼;暫時只滿足於和 JS 引擎通訊的功能,相比於 Web IDL 而言功能相對簡單,暫時無涉及多種外部語言。

嘗試

Git 拉 Lynx 工程原始碼,根據 How To Build 執行 Android 工程,在 Android 工程根目錄的 gradle.properties 中,通過設定 js_engine_type=v8/jsc 進行 V8 引擎和 JSC 引擎的切換。iOS 僅支援 JSC 引擎。

請持續關注 Lynx,一個高效能跨平臺開發框架。

相關文章