QuickJS 在IOT上的一些初步尝试

关于QuickJS

QuickJS是一个小型且可嵌入的Javascript引擎。它支持 ES2020规范,包括模块,异步生成器,代理和BigInt。
它可选地支持数学扩展,例如大十进制浮点数(BigDecimal),大二进制浮点数(BigFloat)和运算符重载。
主要特点:

  • 体积小且易于嵌入:只有几个C文件,没有外部依赖关系,一个简单的hello world 程序的210 KiB x86代码。
  • 快速解释器,启动时间非常短:在台式机的单个内核上,可以在大约100秒内运行ECMAScript Test Suite的75000个测试。运行时实例的完整生命周期在不到300微秒内即可完成。
  • 几乎完整的ES2020 支持,包括模块,异步生成器和完整的附件B支持(旧版Web兼容性)。
  • 选择ES2020功能时,将通过近100%的ECMAScript Test Suite测试。可在Test262报告中获得摘要。
  • 可以将Javascript源代码编译为没有外部依赖关系的可执行文件。
  • 使用引用计数进行垃圾收集(以减少内存使用并具有确定性的行为),并进行循环删除。
  • 数学扩展:BigDecimal,BigFloat,运算符重载,bigint模式,数学模式。
  • 使用Java语言实现的带有上下文着色的命令行解释器。
  • 小型内置标准库,带有C库包装器。

QuickJS 的几个重要特性: 小型,可嵌入等都和V8引擎 有鲜明的对比,因此QuickJs在一些领域必然可以大有作为,IOT设备上 可能就是QuickJs大展拳脚的一地。
本文尝试使用QuickJS作为执行引擎,尝试打造一个可以让开发者用JavaScript 就可以开发IOT UI 的渲染容器。

设计目标

贴合 IOT 图形化设备的一些特性

  • 主要展示2d为主
  • 多为单屏为主,滚动较少
  • 提供的功能和设备有强相关性
  • 操作多为点击等简单操作
  • 设备配置较低

基于对于IOT图形化设备的特性总结和了解,本文这里最终设计目标也并不是一个完善的类似浏览器的UI 渲染容器,只是尽可能的去贴合IOT 图形化设备。

贴合Web开发

为了贴合目前主流的Web开发形式,本文设计绘制UI 的形式还是基于dom操作方式。在QuickJs中实现document相关接口。如createElement,insertBefore,appendChild,createTextNode,remoteChild。后期也将模拟事件传递过程完成事件监听触发能力。
在布局样式上,本文这里采用了React Native 背后的Yoga Layout 作为布局引擎,所以这里也只支持目前流行弹性布局和盒模型的相关样式。

工程相关

我们这里选用Skia作为我们的渲染引擎,被chrome&flutter依赖,其完备渲染能力和社区支持对于本文的实践都具有非常可观的支持。

skia 的编译

1. 源码拉取

1
2
3
4
5
6
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="${PWD}/depot_tools:${PATH}"

git clone https://skia.googlesource.com/skia.git
cd skia
python2 tools/git-sync-deps

2. skia的编译

Skia 编译主要有三个参数控制
is_official_build 决定是否有符号优化 是否使用系统三方库 ,如果选择true,则编译的时候需要去系统寻找一些三方库,如果libjpeg 等等,如果不存在或者版本不对都会报错
is_component_build 决定是否编译成动态链接版本
skia_use_system_xxx=false
如skia_use_system_libjpeg_turbo = false 就是不使用系统库路径搜索到的代码,使用仓库内的third_party内的源码进行编译。可以使用这个参数避免去寻找一些系统上没有带的三方库。
但是有一些三方依赖库还是需要安装一下,因为仓库内的一些gn也是引用系统库路径的代码进行编译
在linux的系统上可以执行tools/install_dependencies.sh 进行安装
其他系统可以查看文件内容和报错进行安装

最简单的编译方式

1
2
bin/gn gen out/Release
ninja -C out/Release

———-这样子打出来的静态库大小有一百多M

3. 在cmake中引用skia

1
2
3
4
set(SKIADIR /Users/disheng/sources/skia)
include_directories(${SKIADIR})
include_directories(/usr/local/include)
link_directories(${SKIADIR}/out/Release)

如果在编译skia使用is_official_build 中使用了true,则需要把一些三方库也给引入进来
如link_libraries(SDL2 SDL2main pthread fontconfig freetype)
这样子
在mac上还需要引入一些library

1
2
3
4
5
6
7
8
find_library(CoreServices CoreServices)
find_library(CoreGraphics CoreGraphics)
find_library(CoreText CoreText)
find_library(CoreFoundation CoreFoundation)
find_library(OpenGL_LIBRARY OpenGL)

add_executable(skiademo main.cpp)
target_link_libraries(skiademo ${OpenGL_LIBRARY} ${CMAKE_DL_LIBS} ${CoreServices} ${CoreGraphics} ${CoreText} ${CoreFoundation} ${COCOA_LIBRARY} m )

yoga layout编译

yoga给我们提供了cmakelist

1
2
3
mkdir cmake-build
cmake ../
// yogalayout 布局提供一写debug能力,如果需要可以添加宏定义DEBUG,ADD_DEFINITIONS(-DDEBUG)

quickjs 编译和引用

QuickJs 和其描述的一样,没有外部依赖

1
2
3
4
5
6
make && make install 
// 由于我们是一个cpp项目,quickjs 是一个c语言的项目,我们需要利用extern "C"关键词来做一下处理
extern "C" {
#include "quickjs-libc.h"
#include "quickjs.h"
}

Dom接口设计

我们的目标是让开发者在使用dom接口时可以尽量符合w3c标准,而不用利用module的形式,
所以我们需要向执行环境注入document 对象并在内部维护一个tree和一个全局NodeMap.与此同时这颗Dom Tree 还要和 Layout Tree 进行关联,让其可以实现动态布局的能力。
iot_render.png

全局document对象

QuickJs 在Gloabl Object上添加的方法

1
JS_SetPropertyStr(this->context, JS_GetGlobalObject(this->context), globalName, globalValue);
1
2
3
4
5
6
7
8
9
10
/**
* 准备quickjs runtime
*/
QuickRuntime *runtime = new QuickRuntime();
runtime->init();
runtime->createContext();
// 创建根节点
DocumentNode* rootDocumentNode = new DocumentNode(runtime->getContext(),"root");
JSValue *document = rootDocumentNode->getJSValue();
runtime->addGloablValue("document",*document);

方法设计

这是 document.createElement 的入口函数

1
2
3
4
5
6
static JSValue _createElement(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
DocumentNode *thisNode = CacheNode::getCacheNode(ctx,&this_val);
auto createNode = thisNode->createElement(JS_ToCString(ctx,argv[0]));
return *createNode->getJSValue();
}

CacheNode 在这里的主要原因是为了帮助 JSValue 和 我们内置Dom Tree上的Node关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DocumentNode::DocumentNode(JSContext *context, string _elementType) {
this->elementType = _elementType;
this->layout = new Layout(500, 500);
this->context = context;
this->nodeContext = new NodeContext();
this->nodeContext->setLayout(this->layout);
this->parentNode = nullptr;
this->node = new YGNode();
int nodeID = NodeId::getNodeId();
this->nodeId = nodeID;
this->node->setContext(this->nodeContext);
CacheNode::insert(nodeID, this);
this->toJSValue();
}

绘制过程

绘制过程往往是渲染容器核心,但是本文主要探究quickjs在iot ui上的应用,
在dom tree 进行变动后,我们会把根节点关联的YGNode 进行 布局计算。将此项核心工作交给yoga来进行。

1
rootNode->calculateLayout();

拿到布局树的根节点,获取到yoga的计算的位置信息,进行绘制。

实践结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const rect = document.createElement('div');
rect.style = {
height:400,
width:400,
justifyContent:'flex-start',
alignItems:'flex-start'
}
const circle = document.createElement('div');
circle.style={
height:300,
width:300,
borderRadius:50,// 百分比
justifyContent:'center',
alignItems:'center',
}
rect.appendChild(circle);


circle.appendChild(document.createTextNode("IOT RENDER HELLO WORLD"));
document.appendChild(rect)
document.style = {
height:500,
width:500,
justifyContent:'center',
alignItems:'center',
}

image.png


QuickJS 在IOT上的一些初步尝试
https://disheng.site/2023/04/07/quickjs-on-iot/
作者
笛声
发布于
2023年4月7日
许可协议