JS Binding机制

JS Binding机制

JS引擎通常会抽象出VM、JSContext、JSValue、GlobalObject等概念,VM代表一个JS虚拟机实例,拥有独立的堆栈空间,有点类似进程的概念,不同的VM相互是隔离的(因此在v8中以v8::Isolate命名),一个VM中可以有多个JSContext,JSContext代表一个JS的执行上下文,可以执行JS代码,JSValue代表一个JS值类型,可以是基础数据类型也可以是Object类型,每个JSContext中都会拥有一个GlobalObject对象,GlobalObject在JSContext整个生命周期内,都可以直接进行访问,它默认是可读可写的,因此可以在GlobalObject上绑定属性或者函数等,这样就可以在JSContext执行上下文中访问它们了。

要想在JS环境中使用Canvas,需要将Canvas相关接口注入到JS环境,正如Java JNI、Python Binding、Lua Binding等类似,JS引擎也提供了Extension机制,称之为JS Binding,它允许开发者使用c++等语言向JS上下文中注入变量、函数、对象等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// V8函数绑定示例
static void LogCallback(const v8::FunctionCallbackInfov8::Value& args){…}



// Create a template for the global object and set the

// built-in global functions.

v8::Localv8::ObjectTemplate global = v8::ObjectTemplate::New(isolate);

global->Set(v8::String::NewFromUtf8(isolate, “log”),

v8::FunctionTemplate::New(isolate, LogCallback));

// Each processor gets its own context so different processors

// do not affect each other.

v8::Persistentv8::Context context =

v8::Context::New(isolate, nullptr, global);

以小程序环境为例,小程序容器初始化时,会分别创建Render和Worker,Render负责界面渲染,Worker负责执行业务逻辑,拥有独立JSContext,Canvas提供了createCanvas()和createOffscreenCanvas() 全局函数需要绑定到该JSContext的GlobalObject上,因此Worker需要有一个时机通知canvas注入API,从小程序视角来看,Worker依赖Canvas显然不合理,因此小程序提供了插件机制,每个插件都是一个动态库,Canvas作为插件先注册到Worker,随后Worker创建之后会扫描一遍插件,依次dlopen每个插件并执行插件的初始化函数,将JSContext作为参数传给插件,这样插件就可以向JSContext中绑定API了。

关于JSEngine和Binding有两个需要注意的点(以V8为例):

关于线程安全。JSContext通常设计为非线程安全的,需要注意不要在非JS线程中访问JS资源。其次,在V8中一个线程可能有多个JSContext,需要使用v8::Context::Scope切换正确的JSContext;

关于Binding对象的生命周期。众所周知,C与JS语言内存管理方式不一样,C需要开发者手动管理内存,JS由虚拟机管理。对于C++ Binding的JS对象的生命周期理论上需要跟普通JS对象一致,因此需要有一种机制,当JS对象被GC回收时,需要通知到C++ Binding对象,以便执行相应的析构函数释放内存。事实上,JS引擎通常会提供让一个JS对象脱离/回归GC管理的机制,且JS对象的生命周期均有钩子函数可以进行监听。V8中有Handle(句柄)的概念,Handle分为LocalHandle、PersistentHandle、Weak PersistentHandle。LocalHandle在栈上分配,由HandleScope控制其作用域,超出作用域即被标记为可释放,PersistentHandle在堆上分配,生命周期长,通常需要开发者显式通过PersistentHandle#Reset的方式释放对象。通过SetWeak函数可以让一个PersistentHandle转为一个Weak PersistentHandle,当没有其他引用指向Weak句柄时就会触发回调,开发者可以在回调中释放内存。

最后再讨论下Binding代码如何跨JSEngine的问题。

当前主流的JSEngine有V8、JavaScriptCore、QuickJS等,如果需要更换JSEngine的话,Binding代码需要重写,成本有点高(Canvas接口非常多),因此理论上可以再封装一个抽象层,屏蔽不同引擎的差异,对外提供一致接口,基于抽象层编写一次Binding代码,就可以适配到多个JSEngine(使用IDL生成代码是另外一条路),目前我们使用了UC团队提供的JSI SDK适配多JS引擎。

平台窗体抽象层设计

要想做到跨平台,就需要设计一个抽象的平台胶水层,胶水层的职责是对下屏蔽各个平台间的实现差异,对上为Canvas提供统一的接口操作Surface,封装MakeCurrent、SwapBuffer等行为。实现上可以借鉴Flutter Engine,Flutter Engine的Shell模块对GL胶水层做了较好的封装,可以无缝接入到Android、iOS等主流平台,扩展到新平台比如鸿蒙OS也不在话下。原文链接:https://blog.csdn.net/2401_84446919/article/details/138868221