编译嵌入式引擎QuickJS

7,410 阅读7分钟

背景

我是通过这篇文章了解到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代码打印的内容,基本目的已经达到了,后面便可以深入学习了。