JS Binding technology

The main purpose of JS Binding technology of Lynx (an efficient cross-platform framework) is to build an efficient communication bridge decouple from JS engine and have the ability to switch JS engine. The technology has gone through several iterations, and finally decouples the JS engine at the code level through the abstract engine interface layer design. Lynx currently supports switching between V8 and JSC engines on Android.

See the previous article for the basics of JSC and V8 engines

Problems encountered

Lynx is a JS-driven cross-platform framework, which provides rendering capability of JS to call Android and iOS platform layers, and also allows developers to expand platform capabilities. Therefore, besides core Runtime layer, Lynx communicates with JS. It also includes Module and RenderObjectImpl at the platform level, as well as interthread communication within the framework. Combined with the above features of Lynx framework, the main problems encountered during JS Binding iteration are as follows:

  1. Code decoupling: unification of processes such as initialization of different JS engines with Extension (which requires defining static methods)
  2. C++ object lifecycle management
  3. Parameter conversion across threads and platforms

design

The overall design code is in the Runtime directory.

For cross-thread and cross-platform parameter conversion, LynxValue is defined as a general transfer parameter in order to facilitate parameter conversion in upstream and downstream (JS and core C++ layer, core layer and platform layer Android and iOS). In addition, LynxValue transformation rules are formulated according to different platforms to reduce tedious transformation steps when parameters are called across layers. Transformation rules now include JSCHelper from JSC to core, V8Helper from V8 to core, OCHelper from core to iOS, and JNIHelper from core to Android. The following figure shows LynxValue circulation and different levels.

Primary data structure

  • LynxValue is the base class of parameter passing rules, in which unions are used to define parameters that support transformation. Including basic data types, arrays, key-value pairs, LynxObject and LynxFunction. Except LynxFunxtion and LynxObject, other parameters cannot communicate with JS directly. They are only used for parameter transformation and support cross-thread cross-platform transfer.
  • LynxArray is a set of ordered limited Lynxvalues, corresponding to the array of JS end and platform layer.
  • LynxMap key-value pairs only support strings as keys, corresponding to Map or Object on the JS end and key-value pairs on the platform layer.
  • LynxFunction stores functions on the JS side, which can be used to call back JS functions when appropriate.
  • LynxObject communication base class, with the help of ClassTemplate to build a communication bridge with JS objects (see the subsequent analysis), can perform indirect operations on JS objects, such as ProtectJSObject operation, make JS objects out of GC.

In terms of JS engine code decoupling, JSC and V8 have similar logic in JS prototype and Extension design, but differ in implementation details. For example, in JSC, JSClassRef is used to describe the properties and methods on the prototype, and the prototype chain can be constructed, while in V8, FunctionTemplate and PrototypeTemplate are used instead. Used in JSC JSObjectSetPrivate interface for JS object binding a c + + object, while V8 using ObjectTemplate: : SetInternalField method instead. Based on the above characteristics, Lynx JSBinding abstracts a layer of JS prototype constructor and method hook interface to meet the communication function with JS.

JSVM is a virtual machine running on behalf of JS, and the real implementation files are handed over to their respective engines for implementation.

JSContext is the control context of the JS engine and a template class that contains the Global object Global. Operations on real V8 and JSC are determined by its implementation classes V8Context and JSCContext. The external interface ClassTemplate and internal interface ObjectWrap are used to communicate with JS.

ClassTemplate is used to construct a template for JS prototypes through which functions and variable hooks can be registered (Extension functionality). This object holds the PrototypeBuilder, which is implemented by the corresponding JS engine and is used to build JSC’s JSClassRef or V8 FunctionTemplate, while creating JS objects from the prototype. ClassTemplate provides macro definitions to help define static methods of the default ClassTemplate. Here’s what they mean and how to use them:

  • DEFINE_CLASS_TEMPLATE_STARTThe start of the method definition for the default ClassTemplate build
  • REGISTER_PARENTDefine the parent (prototype chain) of the ClassTemplate to be used between START and END.
  • EXPOSE_CONSTRUCTORExpose the ClassTemplate in the JS context as a constructor, used between START and END.
  • REGISTER_METHOD_CALLBACKRegister function hooks with the ClassTemplate to be used between START and END.
  • REGISTER_GET_CALLBACK REGISTER_SET_CALLBACKRegister variable hooks with the ClassTemplate to be used between START and END.
  • DEFINE_CLASS_TEMPLATE_ENDEnd of method definition for the default ClassTemplate build.
  • DEFAULT_CLASS_TEMPLATEGet the default ClassTemplate.

The cbmc.h header contains the macro rules that define the hook functions of the JS engine. The C++ class needs to define the hook functions according to the macro rules and register the function indexes in the ClassTemplate. At the same time, it needs the corresponding class methods (hook functions callback) to implement the prototype. Classtemplate. h provides the ability to quickly build a default ClassTemplate object with C++ objects through macro definitions. Combined with two macro definition rules, it is possible to quickly build C++ classes that communicate with JS. The following are the definitions of defines defines defines defines defines

  • DEFINE_METHOD_CALLBACKFor defining the JS engine function hooks,DEFINE_GROUP_METHOD_CALLBACKUsed to define function hooks that take method names as arguments,METHOD_CALLBACKUsed to get the hook name
  • DEFINE_SET_CALLBACK DEFINE_GET_CALLBACKUsed to define JS engine variable hooks,SET_CALLBACK GET_CALLBACKUsed to get the hook name.

Examples of custom class method hooks:

JS variable Get hook: base::ScopedPtr<LynxValue> Function();

Void Function(base::ScopedPtr<jscore::LynxValue>& value);

ScopedPtr<LynxValue> Function(base::ScopedPtr<LynxArray>& array);

ObjectWrap is used to establish relationships between JS objects and C++ objects (LynxObject in this case). It is used to manage the C++ object life cycle, which follows JS objects (of course, JS objects just add or subtract references from C++ objects, Ensure that C++ objects can be safely released or used when referenced by other classes. When the ClassTemplate creates the JS object, the timing is determined by the context in which the JS object is run (handled by the chbm. h hook function) and is not the developer’s concern.

The overall operation diagram of JS Binding shows that in Lynx development, the specific implementation of JS engine or parameter transformation rules are not perceived externally. LynxObject and LynxValue can be used to communicate with JS and complete API call. LynxValue and JSValue are converted when JSObject and LynxObject call each other.

Example: Define the console class associated with the JS object Console, and implement the function call console.log. The main steps are as follows

  1. Inheriting LynxObject, define Log class methods called by hook functions
  2. Define the Log hook for Extension
  3. Quickly create a default ClassTemplate from the macro definition provided by ClassTemplate, passing the default ClassTemplate in the constructor.
namespace jscore {
    class Console : public LynxObject {
    public:
        Console(JSContext* context);
        virtual ~Console();
        // Define the class method called by the JS engine function hook callback
        base::ScopedPtr<LynxValue> Log(base::ScopedPtr<LynxArray>& array);
    };
}
Copy the code
namespace jscore {

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

    // Define the function hook for Extension
    FOR_EACH_METHOD_BINDING(DEFINE_METHOD_CALLBACK)

    Define the default ClassTemplate
    DEFINE_CLASS_TEMPLATE_START(Console)
        FOR_EACH_METHOD_BINDING(REGISTER_METHOD_CALLBACK)
    DEFINE_CLASS_TEMPLATE_END
    
    // The default ClassTemplate is passed in the constructor
    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); }}Copy the code

The advantages and disadvantages

Advantages: Isolated JS engine code, easy to switch; A no-cost implementation of functional hooks (communication) that communicates faster than RN; Get started quickly and have no learning costs compared to Web IDL.

Disadvantages: You still need to write some code manually in the communication class; For the time being, it is only satisfied with the function of communicating with JS engine. Compared with Web IDL, the function is relatively simple and does not involve a variety of external languages for the time being.

try

Git Lynx project source code, run the Android project according To How To Build, in the Android project root directory gradle.properties, Js_engine_type = V8 / JSC To switch between v8 and JSC engines. IOS supports only the JSC engine.

Stay tuned for Lynx, a high-performance cross-platform development framework.