zhengrenzhe's blog   About

Microsoft Chakra 嵌入使用指南

选择一个引擎

Chakra 是 Microsoft 开发的开源 Javascript引擎,Edge 就是由它驱动。虽然是由微软开发的,但已经不能带着有色眼镜看他了,毕竟他跟以「浏览器下载工具」著称的 IE 系列相比,差距还是很大的。

目前现代主流 JavaScript 引擎有:

还有一个值得一提的是 Duktape,这是一个专攻小型化,可嵌入的JS引擎,有多种语言绑定,勉强算一个次级主流的 JS引擎。

如果你有将 JS引擎嵌入自己的程序的需求,那么现在就需要从上面选择一个了。

首先看下 Duktape,如果你预期运行的 JS 都使用 ES5 甚至更早版本,或是将 JS 作为 DSL 使用,那它是可以胜任的。但如果需要运行 ES6, React, Vue 这类现代大型 JS库,Duktape 则不是一个正确的选择,它对 ES6 及更新的现代特性支持有限,详情可以看此表

接着是 JavaScriptCore,虽然这是开源的,但文档资料更多的是关于在 Apple 系上的,可以看下这篇知乎文章有个基本了解,如果是想要嵌入到非 Apple 系平台上,这个基本上也可以不考虑了。

SpiderMonkey,资料比较丰富,提供 JSAPI 以便将它嵌入你的程序,但目前貌似很少有使用它作为嵌入式引擎或提供其他语言绑定的。如果要将 SpiderMonkey 嵌入到 你的程序里,还是有些不便,原因是它比较大,源代码在 3.8 GB 左右,需要自己 build,所以暂时也可以不考虑。

到了让人又爱又恨的V8了,作为地球上 JS 引擎的霸主,它的性能,新标准跟进速度自是不必说,Github 上也有多种 V8 的其他语言绑定,比如历史上大名鼎鼎的 PyV8(虽然已经凉透了)。目前做的最好的是 Sony 的 v8eval,它一口气提供了三种语言的绑定,不得不说 Sony 大法好!但 V8 对于开发者用户而言有个大问题,它太大了,下载源代码,编译都是大问题,而且在中国显得更为蛋疼,反正我是没成功装上过 v8eval 🌝。

最后出场的 Chakra 是目前来说最佳也是唯一选择,它支持 96% 的 ES6 特性(V8也才98%),文档丰富,最重要的是,它小巧,并且官方直接提供 MacOS, Linux, Windows 的预编译动态链接库!!!省去了你的编译步骤,并且二进制文件也才20多MB,简直是业界良心!!!

Chakra 使用简单,官方也提供了非常丰富的示例,这里需要注意下,Chakra 是 Edge 的 JS引擎,我们把它嵌入进我们的程序时,实际使用的是 ChakraCore,它是 Chakra 的核心部分,并提供了 JavaScript Runtime (JSRT) APIS 与引擎交互,如果你的语言支持与 C/C++ 交互,那写 binding 也很方便。

ChakraCore 的架构

ChakraCore 架构图

可以看到 ChakraCore 只包含了 JIT, GC, Parser 等执行 ECMAScript 的部分,所以它内部并没有 DOM 与 BOM。

Hello, World

由于 ChakraCore 官方提供了预编译二进制文件,所以我们可以直接从它的 releases 页面下载使用。

接着我们来一步步跑通一个 Hello,World 示例。

系统环境:MacOS 10.13.4

mkdir chakra && cd chakra
wget https://aka.ms/chakracore/cc_osx_x64_1_8_3 # 依据你的操作系统自行选择
tar -zxvf cc_osx_x64_1_8_3 && rm cc_osx_x64_1_8_3

上一步我们已经下载好了 ChakraCore 的二进制文件,现在编写源代码来调用它吧!

touch hello_world.cc
/**
 * hello_world.cc
 *
 */

#include "ChakraCore.h"
#include <iostream>

std::unique_ptr<char[]> JsValueRefToStr(JsValueRef JsValue);

class ChakraHandle {
private:
    JsRuntimeHandle runtime;
    JsContextRef context;
    int callTimes = 0;

public:
    ChakraHandle()
    {
        /**
         * 创建一个 Runtime,Runtime 是一个完整的 JavaScript 执行环境,包含自己的 JIT, GC,
         * 一个特定的线程中只能有一个活跃的 Runtime
         */
        JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &runtime);

        /**
         * 创建一个 Context,一个 Runtime 可包含多个 Context,多个 Context 共享 JIT,GC
         */
        JsCreateContext(runtime, &context);
        JsSetCurrentContext(context);
    }

    ~ChakraHandle()
    {
        /**
         * 引擎运行结束后回收资源
         */
        JsSetCurrentContext(JS_INVALID_REFERENCE);
        JsDisposeRuntime(runtime);
    }

    /**
     * 执行一段 JavaScript 字符串
     */
    std::unique_ptr<char[]> Eval_JS(const char* script, const char* sourceUrl = "")
    {
        /**
         * JsValueRef 是一个 JavaScript 值的引用,外部资源与引擎内的 JavaScript 资源交互
         * 都需要通过它
         */

        /**
         * 运行一段 JS 代码时需要告知引擎这段代码的来源,通常为空字符串即可。
         * JsSourceUrl 即将 sourceUrl 转化为 JavaScript 值引用
         */
        JsValueRef JsSourceUrl;
        JsCreateString(sourceUrl, strlen(sourceUrl), &JsSourceUrl);

        /**
         * ChakraCore 运行一段 JS 代码时对脚本字符串数据类型要求为
         * JavascriptString 或 JavascriptExternalArrayBuffer,
         * 使用 JavascriptExternalArrayBuffer 有更好的性能及更小的内存消耗
         *
         * JsScript 保存将原始的 const char* 转换为 ArrayBuffer 后的值引用
         */
        JsValueRef JsScript;
        JsCreateExternalArrayBuffer((void*)script, strlen(script), nullptr, nullptr, &JsScript);

        /**
         * 终于要运行了!
         * JsParseScriptAttributeNone 表示我们不对 Parse 过程有任何设置
         * callTimes 是一个标识运行过程的cookie,仅调试用,这里我们给一个自增的int即可
         */
        JsValueRef result;
        JsRun(JsScript, callTimes++, JsSourceUrl, JsParseScriptAttributeNone, &result);

        /**
         * 返回脚本的运行结果,这时 result 还是 JavaScript 值引用,需要将其转换为字符串
         */
        return JsValueRefToStr(result);
    }
};

/**
 * JavaScript 值引用到 C++ 字符串的过程
 */
std::unique_ptr<char[]> JsValueRefToStr(JsValueRef JsValue)
{
    /**
     * 值引用在 ChakraCore 中并不一定就是字符串,所以首先要将其转换为字符串
     * 如果已经是字符串,那这一步什么都不会改变
     */
    JsValueRef JsValueString;
    JsConvertValueToString(JsValue, &JsValueString);

    /**
     * 由于我们并不知道上一步转换出的字符串长度是多少,所以需要两次调用
     * JsCopyString,第一次获取长度,第二次才是真实的复制内容到 C++ 里
     */
    size_t strLength;
    JsCopyString(JsValueString, nullptr, 0, &strLength);

    auto result = std::make_unique<char[]>(strLength + 1);

    JsCopyString(JsValueString, result.get(), strLength + 1, nullptr);

    result.get()[strLength] = 0;
    return result;
}

int main()
{
    auto chakra = ChakraHandle();
    auto result = chakra.Eval_JS("(()=>'hello, world!')()").get();
    std::cout << result << std::endl; // hello, world!
}

接着编写makefile

touch makefile
INPUT=hello_world.cc
OUTPUT=hello_world

export DYLD_LIBRARY_PATH=ChakraCoreFiles/lib

build:
	@ clang++ $(INPUT) -I ChakraCoreFiles/include -std=c++17 -L ChakraCoreFiles/lib/ -l ChakraCore -o $(OUTPUT)

run:
	@ make build
	@ ./$(OUTPUT)
	@ make clean

clean:
	@ rm $(OUTPUT)

终于可以开始运行了

$ make run
hello, world!

耶!我们已经成功的将 ChakraCore 嵌入到我们的程序里了。

异常处理

不过通常情况下,我们并不能保证 JS 运行一定是正确的,有可能你忘记提前运行一个函数,导致某个变量未定义,这时 ChakraCore 会进入一个异常状态,需要我们手动清除异常才能进行下一次执行。如果我们对前面执行的 JS 代码进行更改:

auto result = chakra.Eval_JS("(()=>a)()").get();

再次编译代码,会产生一个 Segmentation fault:

$ make run
make: *** [run] Segmentation fault: 11

这是因为 JS 代码里的变量 a 未定义导致了异常,JsRun 并没有运行出一个结果给 result,后面的代码试图读取了一个错误的地址导致了最终的错误,所以我们还需要一段代码来检测 JS 运行是否成功,如果不成功则获取异常作为结果返回出去。得益于 JsRun 的返回值是一个错误码,所以我们可以写一个宏函数来处理 JsRun 的返回值:

/**
 * JsGetAndClearException 负责获取异常结果并清除 Runtime 的异常状态
 * 获取的异常结果是一个对象,对象有一个 message 属性保存错误信息,
 * 所以需要创建一个 id 来读取这个属性值
 */
#define ErrorCheck(cmd)                                                  \
    do {                                                                 \
        JsErrorCode errCode = cmd;                                       \
        if (errCode != JsNoError && errCode == JsErrorScriptException) { \
            JsValueRef exception;                                        \
            JsGetAndClearException(&exception);                          \
                                                                         \
            JsPropertyIdRef id;                                          \
            JsCreatePropertyId("message", sizeof("message") - 1, &id);   \
                                                                         \
            JsValueRef value;                                            \
            JsGetProperty(exception, id, &value);                        \
                                                                         \
            return JsValueRefToStr(value);                               \
        }                                                                \
    } while (0)

我们把这段宏函数放在 hello_world.cc 中头文件的下面,并将 JsRun 放在宏调用中:

ErrorCheck(JsRun(JsScript, callTimes++, JsSourceUrl, JsParseScriptAttributeNone, &result));

再次编译运行

$ make run
'a' is not defined

欧耶!我们已经正确的处理了异常。实际上在 ChakraCore 运行过程中的每一步都是有可能出现错误,这不仅仅是由 JS 异常导致的,ChakraCore 的每个 API 的返回值都是错误码,详细的错误类型可见 JsErrorCode

注入外部资源

现在我们能正确跑通代码并能处理 JS 异常了,但我们的程序功能还是有点弱,引擎内的 JS 只能自己玩,没法与外部环境交互,例如没法使用 console.log,所以现在我们要注入外部资源到引擎内部,就写一个 console.log 试试吧,简单起见就叫做 echo,它接受一个参数,并把它打印出来,这样就能对 ChakraCore 执行的 JS 做一个简单的调试。

首先编写 echo 函数体:

/**
 * 这是一个 JsNativeFunction,也就是 JS 调用时实际执行的部分
 */
JsValueRef Echo(JsValueRef callee, bool isConstructCall, JsValueRef* arguments, unsigned short argumentCount, void* callbackState)
{
    /**
     * arguments 是一个 JsValueRef 型数组,表示用户传进来的参数,不过从第一个起才是用户传进来的
     * 当参数满足需求时,我们打印第一个
     */
    if (argumentCount >= 2) {
        std::cout << JsValueRefToStr(arguments[1]).get() << std::endl;
    }

    /**
     * JS_INVALID_REFERENCE 表示一个非法的引用,对于 JS 来说函数返回了个 undefined
     */
    return JS_INVALID_REFERENCE;
}

我们把这个函数可以放在 ChakraHandle 类定义的下面,不过需要在头文件下写一行函数声明:

JsValueRef Echo(JsValueRef callee, bool isConstructCall, JsValueRef* arguments, unsigned short argumentCount, void* callbackState);

接着需要把这个函数挂到 Runtime 全局对象下。这里需要特别说明一下,每个 Runtime 有自己的全局对象。你可以理解它为浏览器环境下的 window,当一个 JS 变量在当前作用域没找着时,会依次向上查找,直到 window 下,所以我们将 echo 函数挂在这个全局对象下,JS 就可以访问了。 来修改一下 ChakraHandle 类,为它添加一个 setEcho 方法,当类被初始化后调用它来挂载 echo 函数

class ChakraHandle {
    // ....

    ChakraHandle(){
        // ...

        setEcho();
    }

    // ...

    void setEcho()
    {
        /**
         * 创建要挂载到全局对象内的函数
         */
        JsValueRef function;
        JsCreateFunction(Echo, nullptr, &function);

        /**
         * 获取全局对象引用
         */
        JsValueRef globalObject;
        JsGetGlobalObject(&globalObject);

        /**
         * 创建函数名,因为是在全局对象下,所以实际上是一个属性名
         */
        JsPropertyIdRef func_name;
        JsCreatePropertyId("echo", sizeof("echo") - 1, &func_name);

        /**
         * 挂载!
         */
        JsSetProperty(globalObject, func_name, function, false);
    }
}

最后我们来修改一下 JS:

auto result = chakra.Eval_JS("echo('hello, world!!')").get();

编译运行一下:

$ make run
hello, world!!
undefined

耶,现在我们的 echo 函数已经正常工作了。这里返回 undefined 是因为在 echo 函数本体内我们返回了一个 JS_INVALID_REFERENCE

到这里本指南就基本结束了,所有示例代码放在了https://github.com/zhengrenzhe/Chakra-sample。至于 ChakraCore 的调试等更高级的功能暂时不在本篇内范围内

尽情的享受 ChakraCore 带来的快感吧!嘻嘻。

← Rust 所有权机制  选择与插入排序 →