背景
我是通过这篇文章了解到QuickJS的。相信了解JS底层引擎运行机制对学习这门语言会有很大帮助。
QuickJS引擎使用C语言编写,作者提供了Linux下的构建文件MakeFile。看了下公司测试服务器上没有gcc环境,网上搜了下安装新一点的gcc有点费时。贫穷的我只有一台Windows电脑和一部红米安卓机,后来一阵搜罗,不知道在哪找到了Windows下编译QuickJS项目的方法。
因为是Android出身,我想在Android上尝试运行QuickJS,但几乎没有开发过JNI项目,只能硬着头皮花了周末两天成功让QuickJS在Android项目上编译成功。
其实使用CMake可以在多种平台(linux/windows/macos/android等)下构建C项目,只是需要编写CMakeLists.txt,前提是要像懂CMakeLists.txt的语法。
写到这里就觉得做个符合招聘要求的Android开发真的好难:精通各种语言及其框架(Java、Kotlin、JS、HTML、CSS、Dart...)、精通Android系统(应用层、应用框架层、JNI、核心C++库、ART、硬件层、Linux内核...)、精通各种应用的IDE及构建工具(AS、Gradle、CMake、Webpack、Node...)、精通漂亮的UI开发(流畅精美的Android页面、丝滑震撼的H5页面、靓瞎狗眼的动画特效...)、精通撸轮大法(通俗易懂又不失精心设计的高雅、简单易用又不失便捷扩展的丰满...)、精通各种底层实现(JVM实现、GC算法、Android系统启动过程、Android应用启动过程、Android多进程通信、多线程并发、数据结构、算法、浏览器内核、JS引擎、排版引擎、渲染引擎)、精通各种轮子的源码实现、精通以上所有内容的各种使用场景和各种优化及其更好的使用方法...
下面分享下Windows、Android环境下构建QuickJS的过程。
Windows下构建QuickJS项目
进入这里,根据指引安装msys2及mingw工具链后构建即可
这里温馨提示下:如果构建后报错信息中和libdl这个库有关的(找不到dl这个库),请尝试执行pacman -S mingw-w64-x86_64-dlfcn后再进行构建操作,初步分析错误源头自MakeFile中的这里
HOST_LIBS=-lm -ldl -lpthread
LIBS=-lm
ifndef CONFIG_WIN32
LIBS+=-ldl -lpthread
编译后会生成一些可执行文件便可按照作者的指引玩耍了
Android下构建QuickJS项目
目的很简单,通过JNI调用QuickJS提供的方法执行一段JS代码
于此,我需要疯狂脑补,从这里我学会了使用两种方法来注册Native方法:静态注册、动态注册。实际项目中倾向于动态注册方式,这里提一个facebook的轮子fbjni,用于轻松畅享JNI;从这里我学会了如何在VSCode上使用CMake构建C项目,从这里、这里、还有这里fbjni学到了一点编写CMakeLists.txt的皮毛。
文章首段提到的那篇文章中有提到有个C++的QuickJS项目,还有个Android的QuickJS项目,菜鸡的我现在还不会调用QuickJS提供的API也看不懂这个Android项目中的各种JNI操作。经对比C++那个项目更清爽,所以选择它作为巨人的肩膀,下面介绍这个项目的具体内容
quickjs目录下的CMakeLists.txt
project(quickjs LANGUAGES C)
file(STRINGS VERSION version)
set(quickjs_src quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c)
set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE CONFIG_BIGNUM)
add_library(quickjs ${quickjs_src})
target_compile_definitions(quickjs PRIVATE ${quickjs_def} )
add_library(quickjs-dumpleaks ${quickjs_src})
target_compile_definitions(quickjs-dumpleaks PRIVATE ${quickjs_def} DUMP_LEAKS=1)
if(UNIX)
find_package(Threads)
target_link_libraries(quickjs ${CMAKE_DL_LIBS} m Threads::Threads)
target_link_libraries(quickjs-dumpleaks ${CMAKE_DL_LIBS} m Threads::Threads)
endif()
我太菜了,好不容易才看懂了这个文件:声明了项目名为quickjs的C(不是C++)项目、读取VERSION文件中的内容作为变量version的值(笔者编写时,里头的内容为2020-09-06),接着设置源码集变量quickjs_src(包含的文件有quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c)、设置构建参数quickjs_def(看源码是用来定义宏的CONFIG_VERSION _GNU_SOURCE CONFIG_BIGNUM,果然target_compile_definitions就是这个作用) 、为这个项目生成静态库叫quickjs、再生成一个叫quickjs-dumpleaks的静态库、最后就是UNIX环境下吧啦吧啦...
一句话概括最有用的信息是:构建QuickJS的运行时静态库,quickjs-libc.c就是依赖了引擎quickjs.c的运行时,至于引擎、运行时、VM这种高大上的名词含义,可以自行百度一下。
项目根目录下的CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(quickjspp)
set(CMAKE_CXX_STANDARD 17)
#set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
if(CMAKE_COMPILER_IS_GNUCC)
add_compile_options(-Wall -Wno-unused-parameter)
endif()
add_subdirectory(quickjs)
add_executable(qjs qjs.cpp)
target_link_libraries(qjs quickjs)
enable_testing()
add_subdirectory(test)
主工程名为quickjscpp,使用C++17标准编译,添加子工程quickjs和test,以qjs.cpp为入口生成二进制文件且依赖库quickjs
qjs.cpp
#include "quickjspp.hpp"
#include <iostream>
int main(int argc, char ** argv)
{
JSRuntime * rt;
JSContext * ctx;
using namespace qjs;
Runtime runtime;
rt = runtime.rt;
Context context(runtime);
ctx = context.ctx;
js_std_init_handlers(rt);
/* loader for ES6 modules */
JS_SetModuleLoaderFunc(rt, nullptr, js_module_loader, nullptr);
js_std_add_helpers(ctx, argc - 1, argv + 1);
/* system modules */
js_init_module_std(ctx, "std");
js_init_module_os(ctx, "os");
/* make 'std' and 'os' visible to non module code */
const char * str = "import * as std from 'std';\n"
"import * as os from 'os';\n"
"globalThis.std = std;\n"
"globalThis.os = os;\n";
context.eval(str, "<input>", JS_EVAL_TYPE_MODULE);
try
{
if(argv[1])
context.evalFile(argv[1], JS_EVAL_TYPE_MODULE);
}
catch(exception)
{
//js_std_dump_error(ctx);
auto exc = context.getException();
std::cerr << (exc.isError() ? "Error: " : "Throw: ") << (std::string)exc << std::endl;
if((bool)exc["stack"])
std::cerr << (std::string)exc["stack"] << std::endl;
js_std_free_handlers(rt);
return 1;
}
js_std_loop(ctx);
js_std_free_handlers(rt);
return 0;
}
就是main方法中创建运行时JSRuntime和上下文JSContext,利用上下文调用QuickJS提供的eval方法执行一段JS代码。
我们需要修改下这两个CMakeLists.txt,让它更符合Android JNI的胃口。
修改quickjs目录下的CMakeLists.txt
project(quickjs LANGUAGES C)
file(STRINGS VERSION version)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
set(quickjs_src quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c)
set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE CONFIG_BIGNUM)
add_library(quickjs ${quickjs_src})
target_link_libraries(quickjs
${log-lib})
target_compile_definitions(quickjs PRIVATE ${quickjs_def} )
就是添加了NDK中Android Log库,目的是为了实现JS中的console.log打印的日志能出现在Android控制台
修改项目根目录下的CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(qjspp)
set(CMAKE_CXX_STANDARD 17)
file(GLOB libfbjni_link_DIRS "${build_DIR}/fbjni*.aar/jni/${ANDROID_ABI}")
file(GLOB libfbjni_include_DIRS "${build_DIR}/fbjni-*-headers.jar/")
find_library(FBJNI_LIBRARY fbjni PATHS ${libfbjni_link_DIRS}
NO_CMAKE_FIND_ROOT_PATH)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
add_subdirectory(quickjs)
add_library(qjs SHARED qjs.cpp)
target_include_directories(qjs PRIVATE
// Additional header directories here
${libfbjni_include_DIRS}
)
target_link_libraries(qjs quickjs jsi
${FBJNI_LIBRARY}
${log-lib})
除了调整为输出qjs共享库外,还参照facebook提供的方式添加了fbjni(maven库),另外还添加了本地编译的jsi(facebook提供的JSBridge抽象层,便于替换JS引擎),当然这里并没有使用fbjni、jsi的功能,仅仅学习下如何引用外部库的方式。
添加JNI功能
Java部分
public class MyDemo {
static {
System.loadLibrary("qjs");
}
//声明 native方法 注意签名中引用类型后面的分号
public native String stringJNI();
public String run() {
// 执行native方法
return stringJNI();
}
// 实例方法
public void print(String msg) {
Log.d("MyDemo", msg);
}
}
加载动态库qjs.so,定义native方法stringJNI,再就是很普通的两个实例方法run和print,run方法在MainActvity中调用,可以看到这里是在子线程中调用的JNI方法:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
Thread() {
MyDemo().run()
}.start()
}
}
C++部分
直接修改qjs.cpp,将main方法换成testQjs,再添加一些JNI的功能
//
// Created by flower on 2020/9/6.
//
#include "qjspp.hpp"
#include <iostream>
#include <jni.h>
#include <fbjni/fbjni.h>
#include <iostream>
#include <jsi/jsi.h>
#include <fbjni/detail/Hybrid.h>
// 实现头文件中的方法
void showMsg(char *msg);
void showMsg(char *msg) {
LOGI("message is %s", msg);
}
#ifdef __cplusplus
extern "C" {
#endif
//extern "C" JNIEXPORT jstring JNICALL
//Java_com_ndk_demo_MainActivity_stringFromJNI(
// JNIEnv *env,
// jobject /* this */) {
// std::string hello = "Hello from C++";
//
// return env->NewStringUTF(hello.c_str());
//}
static const char *className = "com/ndk/demo/MyDemo";
int testQjs(int argc, char **argv) {
JSRuntime *rt;
JSContext *ctx;
using namespace qjs;
Runtime runtime;
rt = runtime.rt;
Context context(runtime);
ctx = context.ctx;
js_std_init_handlers(rt);
/* loader for ES6 modules */
JS_SetModuleLoaderFunc(rt, nullptr, js_module_loader, nullptr);
js_std_add_helpers(ctx, argc - 1, argv + 1);
/* system modules */
js_init_module_std(ctx, "std");
js_init_module_os(ctx, "os");
/* make 'std' and 'os' visible to non module code */
const char *str = "let a = 0; a++; console.log(a);console.log('qjs well')";
try {
context.eval(str);
}
catch (exception) {
//js_std_dump_error(ctx);
auto exc = context.getException();
std::cerr << (exc.isError() ? "Error: " : "Throw: ") << (std::string) exc << std::endl;
if ((bool) exc["stack"])
std::cerr << (std::string) exc["stack"] << std::endl;
js_std_free_handlers(rt);
return 1;
}
js_std_loop(ctx);
js_std_free_handlers(rt);
return 0;
}
static jstring stringFromJNI(JNIEnv *env, jobject jobj) {
showMsg("welcome to jni");
jclass clazz = facebook::jni::detail::findClass(env, className);
LOGI("java class", clazz);
jmethodID mid = env->GetMethodID(clazz, "print", "(Ljava/lang/String;)V");
std::string msg = "hello, java";
env->CallVoidMethod(jobj, mid, env->NewStringUTF(msg.c_str()));
LOGI("run native method stringFromJNI with param handle");
std::string hello = "hello, this is from c++";
testQjs(0, NULL);
return env->NewStringUTF(hello.c_str());
}
static JNINativeMethod gJni_Methods_table[] = {
{"stringJNI", "()Ljava/lang/String;", reinterpret_cast<void *>(stringFromJNI)}
};
static int
jniRegisterNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *gMethods,
int numMethods) {
jclass clazz;
LOGI("register native methods for java class %s", className);
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGI("cannot find java class %s", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGI("register failed");
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGI("jni onload");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) /
sizeof(JNINativeMethod));
return JNI_VERSION_1_6;
}
#ifdef __cplusplus
}
#endif
理解qjs.cpp中的代码
简单描述下就是:以动态方式注册了native方法stringFromJNI,对应于java层MyDemo类中定义的stringJNI方法,可以看到两边的名称并不一样,但仍能互通,这足以表明存在一种关系Java——JNI(JVM提供)——C。
stringFromJNI中代码最后调用了testQjs方法,这个方法是依照原main方法修改的,主要变化在这里
const char *str = "let a = 0; a++; console.log(a);console.log('qjs well')";
context.eval(str);
以字符串形式执行了一段js代码,并打印了变量a的值和一段文字。
菜鸡的我此时还不知道eval方法具体内容,也不知道是如何实现console的log方法的。于是,我全局搜索console得到如下结果
并不聪明的的我看了下觉得最后两项比较符合,进去一看发现这两行代码在一块
JS_SetPropertyStr(ctx, console, "log",
JS_NewCFunction(ctx, js_print, "log", 1));
JS_SetPropertyStr(ctx, global_obj, "console", console);
愚钝的我大概知道了QuickJS中是如何实现全局对象console的log方法了:将console对象的log方法挂在js_print方法上。再看下js_print这个方法
static JSValue js_print(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
int i;
const char *str;
size_t len;
for(i = 0; i < argc; i++) {
if (i != 0)
putchar(' ');
str = JS_ToCStringLen(ctx, &len, argv[i]);
if (!str)
return JS_EXCEPTION;
fwrite(str, 1, len, stdout);
JS_FreeCString(ctx, str);
}
putchar('\n');
return JS_UNDEFINED;
}
理解并修改js_print方法
我看到这一句fwrite(str, 1, len, stdout);,也就是使用的标准输出库stdout打印字符串str,摸脑壳想了下,stdout输出的内容应该只能在shell环境下才能看到,在Android Studio的控制台是看不到的。
于是又搜索Android stdout,看到有如android 重定向stdout输出内容等方案,试了下不好用,后来脑壳又转了下,NDK不是赫然提供了一个叫log-lib的库吗,于是才有了前面quickjs目录下CMakeLists.txt中添加log-lib库的操作,我是在运行时quickjs-libc.c依赖的引擎quickjs.c的头文件quickjs.h中添加的打印方法
#include <android/log.h>
// 宏定义日志打印
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "ndk-demo",__VA_ARGS__)
然后将上面的fwrite(str, 1, len, stdout);替换成 LOGI("qjs-log :%s", str);,撸完之后再跑一次APP,宛如找到女朋友般看到了在Android Studio打印的日志
2020-09-10 10:16:54.887 26639-26639/com.ndk.demo W/com.ndk.demo: Accessing hidden method Landroid/view/View;->computeFitSystemWindows(Landroid/graphics/Rect;Landroid/graphics/Rect;)Z (light greylist, reflection)
2020-09-10 10:16:54.888 26639-26639/com.ndk.demo W/com.ndk.demo: Accessing hidden method Landroid/view/ViewGroup;->makeOptionalFitsSystemWindows()V (light greylist, reflection)
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo I/ndk-demo: jni onload
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo I/ndk-demo: register native methods for java class com/ndk/demo/MyDemo
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo I/ndk-demo: message is welcome to jni
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo I/ndk-demo: java class
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo D/MyDemo: hello, java
2020-09-10 10:16:54.929 26639-26667/com.ndk.demo I/ndk-demo: run native method stringFromJNI with param handle
2020-09-10 10:16:54.931 26639-26667/com.ndk.demo I/ndk-demo: qjs-log :1
2020-09-10 10:16:54.931 26639-26667/com.ndk.demo I/ndk-demo: qjs-log :qjs well
最后两行便是执行JS代码打印的内容,基本目的已经达到了,后面便可以深入学习了。