如何在 Flutter 中使用 QuickJS
1 Nov 2023
1. QuickJS 介绍
以下内容来自 QuickJS 官方网站。
QuickJS 是一个小型且可嵌入的JavaScript引擎。它支持 ES2020 规范,包括模块、异步生成器、代理和 BigInt。它还可选择性地支持数学扩展,如大十进制浮点数(BigDecimal)、大二进制浮点数(BigFloat)和运算符重载。
主要特点:
- 小巧且易于嵌入:只需几个 C 文件,没有外部依赖,对于一个简单的"Hello World"程序,x86 代码仅占 210 KiB;
- 快速的解释器,启动时间非常短:在桌面 PC 的单核心上,可以在大约 100 秒内运行 ECMAScript 测试套件的 75000 个测试。运行时实例的完整生命周期不到300微秒;
- 几乎完整支持 ES2020,包括模块、异步生成器和完整的附录B支持(用于遗留 Web 兼容性);
- 当选择 ES2020 功能时,通过了接近 100% 的 ECMAScript 测试套件测试。测试概要可在 Test262 报告中找到;
- 可以将 JavaScript 源代码编译为可执行文件,无需外部依赖;
- 使用引用计数进行垃圾回收(以减少内存使用并具有确定性行为),并带有循环删除功能;
- 数学扩展:BigDecimal、BigFloat、运算符重载、bigint 模式、math 模式;
- 具有基本的C库包装的小型内置标准库;
- 带有 JavaScript 实现的命令行解释器,带有上下文着色功能。
2. 了解 Dart 如何与 C 交互
Flutter 应用使用 Dart 开发,与 C 库交互,就得使用 dart:ffi
库。
dart:ffi
是专门用来与原生 C APIs 进行交互的库,FFI 代表 foreign function interface,即外部函数接口。该库的详细使用方式可以参阅 官方文档。
3. 为不同平台编译 QuickJS
Flutter 是跨平台的 UI 框架,要在不同平台使用 QuickJS,就需要为不同平台编译 QuickJS 的动态库。在 Windows 上,需要编译出 .dll
文件;在 Linux 和 Android 上,需要编译出 .so
文件。
编译不同平台的动态库是在 Flutter 中使用 QuickJS 的前期准备,这里主要介绍 Windows 和 Android 平台的编译步骤,编译 Linux 平台的动态库较简单所以省略。
3.1 为 Windows 平台编译 QuickJS 动态库
安装 MYSYS2,编译 QuickJS 需要使用 MYSYS2 中的 MINGW。
安装完成后,运行 MYSYS2 中的 MINGW64(32 位运行 MINGW32),执行下面的命令安装编译所需工具链:
- 如果想要编译 64 位的 QuickJS,则安装
x86_64-toolchain
:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make mingw-w64-x86_64-dlfcn
echo "#! /bin/sh" > /mingw64/bin/make
echo "\"mingw32-make\" \"\$@\"" >> /mingw64/bin/make
- 如果想要编译 32 位的 QuickJS,则安装
i686-toolchain
:
pacman -S mingw-w64-i686-gcc mingw-w64-i686-make mingw-w64-i686-dlfcn
echo "#! /bin/sh" > /mingw32/bin/make
echo "\"mingw32-make\" \"\$@\"" >> /mingw32/bin/make
之后,继续使用 MYSYS2 打开的 MINGW 终端 clone QuickJS 的仓库:
git clone https://github.com/bellard/quickjs.git
切换到 QuickJS 仓库下,执行命令进行编译:
cd quickjs && make
运行完 make
命令后可以得到 libquickjs.a
,此时再运行下面的命令即可得到 libquickjs.dll
:
gcc -shared -o libquickjs.dll -static -s -Wl,--whole-archive libquickjs.a -lm -Wl,--no-whole-archive
3.2 为 Android 平台编译 QuickJS 动态库
Android 中使用 C/C++ 库需要编写一个 CMakeLists.txt
:
cmake_minimum_required(VERSION 3.4.1)
project(quickjs LANGUAGES C)
include_directories(quickjs)
set(QUICK_JS_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../quickjs)
set(
SOURCE_DIR
${QUICK_JS_DIR}/cutils.c
${QUICK_JS_DIR}/libbf.c
${QUICK_JS_DIR}/libregexp.c
${QUICK_JS_DIR}/libunicode.c
${QUICK_JS_DIR}/quickjs.c
${QUICK_JS_DIR}/quickjs-libc.c
)
file(STRINGS "${QUICK_JS_DIR}/VERSION" CONFIG_VERSION)
add_definitions(-DCONFIG_VERSION="${CONFIG_VERSION}")
add_definitions(-DCONFIG_BIGNUM)
add_definitions(-D_GNU_SOURCE)
add_definitions(-DCONFIG_CC="gcc")
add_definitions(-DCONFIG_PREFIX="/usr/local")
add_library(
${PROJECT_NAME}
SHARED
${SOURCE_DIR}
)
target_include_directories(${PROJECT_NAME} PUBLIC .)
这个 CMakeLists.txt
位于 Flutter 项目目录的 android/src/main/cpp
文件夹下,将 QuickJS 仓库放置于 Flutter 项目根目录,当 Flutter 编译 Android 平台应用时,会自动生成一个 libquickjs.so
并打包进安装包中。
关于 Android 平台集成 C/C++ 的详细介绍,请参阅官方文档。
4. 使用 ffigen 生成函数绑定
要调用 C/C++ 库中的函数,首先要在 Dart 侧进行“声明”,例如在 C 中有这样一个函数:
int add(int a, int b) {
return a + b;
}
那么在 Dart 中,我们就要有一个对应的函数声明,以供 Dart 代码调用这个函数:
import 'dart:ffi' as ffi;
final nativAddFunc = dynamicLibrary.lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>('add');
这里的 dynamicLibrary.lookup 方法会通过函数名、返回类型、参数类型去查找对应的 C 函数。
在一个编程语言中对另一个编程语言的函数/变量进行声明,专业术语称之为 语言绑定(language bindings)。
那么问题来了,QuickJS 里有那么多函数,每一个都要在 Dart 侧声明一遍吗?
答案是确实如此,虽然我们大多数时候用不到所有函数和变量,但我们也要编写相当多的代码来使用 QuickJS。
然而 Dart 的官方开发人员非常给力,开发了 ffigen
这个库,该库可以通过头文件自动生成 bindings,大大提高了开发效率!
要生成 QuickJS 的 bindings 只需要:
i. 在 Flutter 项目中安装 ffigen
:
flutter pub add ffigen
ii. 配置 pubspecs.yaml
:
...
## 增加下面的配置
ffigen:
name: 'QuickJSBindings'
description: 'generate bindings for quick js'
output: 'lib/bindings.dart'
headers:
entry-points:
- 'quickjs/quickjs.h'
- 'quickjs/quickjs-libc.h'
iii. 运行 dart run ffigen
。
仅需三步,即可生成完整的 bindings(生成前记得将 QuickJS 的仓库放置于项目根目录)。
5. 使用 QuickJS
首先,打开 libquickjs 动态库:
final _lib = DynamicLibrary.open(libquickjs);
final _ = QuickJSBindings(_lib);
然后,创建 JSContext 和 JSRuntime:
final _runtime = _.JS_NewRuntime();
final _context = _.JS_NewContext(_runtime);
最后,调用 JS_Eval 方法执行 JS 代码:
const flag = JS_EVAL_FLAG_STRICT;
final input = code.toNativeUtf8().cast<Char>();
final name = filename.toNativeUtf8().cast<Char>();
final inputLen = _getPtrCharLen(input);
final jsValue = _.JS_Eval(_context, input, inputLen, name, flag);
calloc.free(input);
calloc.free(name);
final result = _js2Dart(_context, jsValue);
_jsStdLoop(_context);
_jsFreeValue(_context, jsValue);
if (result is Exception) {
throw ret;
}
以上便是使用 QuickJS 的方法,当然,还有一些细节问题需要处理,例如如何处理 Promise 类型的返回值,如何创建事件循环等等。但这篇文章主要介绍如何接入 QuickJS,所以在此不再展开叙述(主要还是懒)。