一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

從 JS 引擎到 JS 運(yùn)行時(shí)(上)

 黃爸爸好 2020-03-11

編者按:本文轉(zhuǎn)載自知乎專欄《前端隨想錄》,快來一起學(xué)習(xí)吧!

V8 和 Node.js 的關(guān)系,是許多前端同學(xué)們所津津樂道的——瀏覽器里的語言,又兼容了瀏覽器外的環(huán)境,兩份快樂重疊在一起。而這兩份快樂,又帶來了更多的快樂……但你有沒有想過,這兩份快樂到底是如何重疊在一起的呢?下面我們將以嵌入式 JS 引擎 QuickJS 為例,介紹一個(gè) JS 引擎是如何被逐步定制為一個(gè)新的 JS 運(yùn)行時(shí)的。

本文將分上下兩篇,逐一覆蓋(或者說,用盡可能簡(jiǎn)單的代碼實(shí)現(xiàn))這些內(nèi)容:

  • 集成嵌入式 JS 引擎

  • 為 JS 引擎擴(kuò)展原生能力

  • 移植默認(rèn) Event Loop

  • 支持 libuv Event Loop

  • 支持宏任務(wù)與微任務(wù)

上篇主要涉及前三節(jié),主要介紹 QuickJS 這一嵌入式 JS 引擎自身的基本使用,并移植其自帶的 Event Loop 示例。而下篇所對(duì)應(yīng)的后兩節(jié)中,我們將引入 libuv,講解如何基于 libuv 實(shí)現(xiàn)擴(kuò)展性更好的 Event Loop,并支持宏任務(wù)與微任務(wù)。

閑話少說,進(jìn)入白學(xué)現(xiàn)場(chǎng)吧 :)

集成嵌入式 JS 引擎

在我的理解里,JS 引擎的「嵌入式」可以從兩種層面來理解,一種意味著它面向低端的嵌入式設(shè)備,另一種則說明它很易于嵌入到原生項(xiàng)目中。而 JS 運(yùn)行時(shí) (Runtime) 其實(shí)也是一種原生項(xiàng)目,它將 JS 引擎作為專用的解釋器,為其提供操作系統(tǒng)的網(wǎng)絡(luò)、進(jìn)程、文件系統(tǒng)等平臺(tái)能力。因此,要想自己實(shí)現(xiàn)一個(gè) JS 運(yùn)行時(shí),首先應(yīng)該考慮的自然是「如何將 JS 引擎嵌入到原生項(xiàng)目中」了。

本節(jié)內(nèi)容是面向我這樣前端背景(沒有正經(jīng)做過 C / C++ 項(xiàng)目)的同學(xué)的,熟悉的小伙伴可以跳過。

怎樣才算將 JS 引擎嵌入了呢?我們知道,最簡(jiǎn)單的 C 程序就是個(gè) main 函數(shù)。如果我們能在 main 函數(shù)里調(diào)用引擎執(zhí)行一段 JS 代碼,那不就成功「嵌入」了嗎——就好像只要在地球兩頭各放一片面包,就能把地球做成三明治一樣。

所以,又該怎樣在自己寫的 C 代碼中調(diào)用引擎呢?從 C 開發(fā)者的視角看,JS 引擎也可以被當(dāng)作一個(gè)第三方庫(kù)來使用,它的集成方式和普通的第三方庫(kù)并沒有什么不同,簡(jiǎn)單說包括這幾步:

  1. 將引擎源碼編譯為庫(kù)文件,這既可以是 .a 格式的靜態(tài)庫(kù),也可以是 .so.dll 格式的動(dòng)態(tài)庫(kù)。

  2. 在自己的 C 源碼中 include 引擎的頭文件,調(diào)用它提供的 API。

  3. 編譯自己的 C 源碼,并鏈接上引擎的庫(kù)文件,生成最終的可執(zhí)行文件。

對(duì) QuickJS 來說,只要一行 make && sudo make install 就能完成編譯和安裝(再啰嗦一句,原生軟件包的所謂安裝,其實(shí)就是把頭文件與編譯出來的庫(kù)文件、可執(zhí)行文件,分別復(fù)制到符合 Unix 標(biāo)準(zhǔn)的目錄下而已),然后就可以在我們的 C 源碼里使用它了。

完成 QuickJS 的編譯安裝后,我們甚至不用親自動(dòng)手寫 C,可以偷懶讓 QuickJS 幫你生成,因?yàn)樗С职?JS 編譯到 C 噢。像這樣的一行 JS:

`console.log('Hello World');`

就可以用 qjsc -e 命令編譯成這樣的 C 源碼:

#include <quickjs/quickjs-libc.h>

const uint32_t qjsc_hello_size = 87;

const uint8_t qjsc_hello[87] = {

 0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,

 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48,

 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72,

 0x6c, 0x64, 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70,

 0x6c, 0x65, 0x73, 0x2f, 0x68, 0x65, 0x6c, 0x6c,

 0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00,

 0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00,

 0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x39,

 0xf1, 0x00, 0x00, 0x00, 0x43, 0xf2, 0x00, 0x00,

 0x00, 0x04, 0xf3, 0x00, 0x00, 0x00, 0x24, 0x01,

 0x00, 0xd1, 0x28, 0xe8, 0x03, 0x01, 0x00,

};

int main(int argc, char **argv)

{

  JSRuntime *rt;

  JSContext *ctx;

  rt = JS_NewRuntime();

  ctx = JS_NewContextRaw(rt);

  JS_AddIntrinsicBaseObjects(ctx);

  js_std_add_helpers(ctx, argc, argv);

  js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);

  js_std_loop(ctx);

  JS_FreeContext(ctx);

  JS_FreeRuntime(rt);

  return 0;

}

這不就是我們要的 main 函數(shù)示例嗎?這個(gè) Hello World 已經(jīng)變成了數(shù)組里的字節(jié)碼,嵌入到最簡(jiǎn)單的 C 項(xiàng)目中了。

注意這其實(shí)只是把 JS 編譯成字節(jié)碼,再附上個(gè) main 膠水代碼入口而已,不是真的把 JS 編譯成 C 啦。

當(dāng)然,這份 C 源碼還要再用 C 編譯器編譯一次才行。就像使用 Babel 和 Webpack 時(shí)的配置那樣,原生工程也需要構(gòu)建配置。對(duì)于構(gòu)建工具,這里選擇了現(xiàn)代工程中幾乎標(biāo)配的 CMake。和這份 C 源碼相配套的 CMakeLists.txt 構(gòu)建配置,則是這樣的:

cmake_minimum_required(VERSION 3.10)

# 約定 runtime 為最終生成的可執(zhí)行文件

project(runtime)

add_executable(runtime

        # 若拆分了多個(gè) C 文件,逐行在此添加即可

        src/main.c)

# 導(dǎo)入 QuickJS 的頭文件和庫(kù)文件

include_directories(/usr/local/include)

add_library(quickjs STATIC IMPORTED)

set_target_properties(quickjs

        PROPERTIES IMPORTED_LOCATION

        '/usr/local/lib/quickjs/libquickjs.a')

# 將 QuickJS 鏈接到 runtime

target_link_libraries(runtime

        quickjs)

CMake 的使用很簡(jiǎn)單,在此不再贅述??傊?,上面的配置能編譯出 runtime 二進(jìn)制文件,直接運(yùn)行它能輸出 Hello World,知道這些就夠啦。

為 JS 引擎擴(kuò)展原生能力

上一步走通后,我們其實(shí)已經(jīng)將 JS 引擎套在了一個(gè) C 程序的殼里了。然而,這只是個(gè)「純凈版」的引擎,也就意味著它并不支持語言標(biāo)準(zhǔn)之外,任何由平臺(tái)提供的能力。像瀏覽器里的 document.getElementById 和 Node.js 里的 fs.readFile,就都屬于這樣的能力。因此,在實(shí)現(xiàn)更復(fù)雜的 Event Loop 之前,我們至少應(yīng)該能在 JS 引擎里調(diào)用到自己寫的 C 原生函數(shù),就像瀏覽器控制臺(tái)里司空見慣的這樣:

> document.getElementById

getElementById() { [native code] }

所以,該怎樣將 C 代碼封裝為這樣的函數(shù)呢?和其它 JS 引擎一樣地,QuickJS 提供了標(biāo)準(zhǔn)化的 API,方便你用 C 來實(shí)現(xiàn) JS 中的函數(shù)和類。下面我們以計(jì)算斐波那契數(shù)的遞歸 fib 函數(shù)為例,演示如何將 JS 的計(jì)算密集型函數(shù)改由 C 實(shí)現(xiàn),從而大幅提升性能。

JS 版的原始 fib 函數(shù)是這樣的:

function fib(n) {

  if (<= 0) return 0;

  else if (=== 1) return 1;

  else return fib(- 1) + fib(- 2);

}

而 C 版本的 fib 函數(shù)則是這樣的,怎么看起來這么像呢?

int fib(int n) {

  if (<= 0) return 0;

  else if (== 1) return 1;

  else return fib(- 1) + fib(- 2);

}

要想在 QuickJS 引擎中使用上面這個(gè) C 函數(shù),大致要做這么幾件事:

  1. 把 C 函數(shù)包一層,處理它與 JS 引擎之間的類型轉(zhuǎn)換。

  2. 將包好的函數(shù)掛載到 JS 模塊下。

  3. 將整個(gè)原生模塊對(duì)外提供出來。

這一共只要約 30 行膠水代碼就夠了,相應(yīng)的 fib.c 源碼如下所示:

#include <quickjs/quickjs.h> #define countof(x) (sizeof(x) / sizeof((x)[0]))

// 原始的 C 函數(shù) static int fib(int n) {

    if (<= 0) return 0;

    else if (== 1) return 1;

    else return fib(- 1) + fib(- 2);

}

// 包一層,處理類型轉(zhuǎn)換 static JSValue js_fib(JSContext *ctx, JSValueConst this_val,

                      int argc, JSValueConst *argv) {

    int n, res;

    if (JS_ToInt32(ctx, &n, argv[0])) return JS_EXCEPTION;

    res = fib(n);

    return JS_NewInt32(ctx, res);

}

// 將包好的函數(shù)定義為 JS 模塊下的 fib 方法 static const JSCFunctionListEntry js_fib_funcs[] = {

    JS_CFUNC_DEF('fib', 1, js_fib ),

};

// 模塊初始化時(shí)的回調(diào) static int js_fib_init(JSContext *ctx, JSModuleDef *m) {

    return JS_SetModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs));

}

// 最終對(duì)外的 JS 模塊定義 JSModuleDef *js_init_module_fib(JSContext *ctx, const char *module_name) {

    JSModuleDef *m;

    m = JS_NewCModule(ctx, module_name, js_fib_init);

    if (!m) return NULL;

    JS_AddModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs));

    return m;

}

上面這個(gè) fib.c 文件只要加入 CMakeLists.txt 中的 add_executable 項(xiàng)中,就可以被編譯進(jìn)來使用了。這樣在原本的 main.c 入口里,只要在 eval JS 代碼前多加兩行初始化代碼,就能準(zhǔn)備好帶有原生模塊的 JS 引擎環(huán)境了:

// ... int main(int argc, char **argv)

{

  // ... // 在 eval 前注冊(cè)上名為 fib.so 的原生模塊 extern JSModuleDef *js_init_module_fib(JSContext *ctx, const char *name);

  js_init_module_fib(ctx, 'fib.so');

  // eval JS 字節(jié)碼 js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);

  // ... }

這樣,我們就能用這種方式在 JS 中使用 C 模塊了:

import { fib } from 'fib.so';

fib(42);

作為嵌入式 JS 引擎,QuickJS 的默認(rèn)性能自然比不過帶 JIT 的 V8。實(shí)測(cè) QuickJS 里 fib(42) 需要約 30 秒,而 V8 只要約 3.5 秒。但一旦引入 C 原生模塊,QuickJS 就能一舉超越 V8,在不到 2 秒內(nèi)完成計(jì)算,輕松提速 15 倍

可以發(fā)現(xiàn),現(xiàn)代 JS 引擎對(duì)計(jì)算密集任務(wù)的 JIT 已經(jīng)很強(qiáng),因此如果將瀏覽器里的 JS 替換為 WASM,加速效果未必足夠理想。詳見我的這篇文章:一個(gè)白學(xué)家眼里的 WebAssembly。

移植默認(rèn) Event Loop

到此為止,我們應(yīng)該已經(jīng)明白該如何嵌入 JS 引擎,并為其擴(kuò)展 C 模塊了。但是,上面的 fib 函數(shù)只是個(gè)同步函數(shù),并不是異步的。各類支持回調(diào)的異步能力,是如何被運(yùn)行時(shí)支持的呢?這就需要傳說中的 Event Loop 了。

目前,前端社區(qū)中已有太多關(guān)于 Event Loop 的概念性介紹,可惜仍然鮮有人真正用簡(jiǎn)潔的代碼給出可用的實(shí)現(xiàn)。好在 QuickJS 隨引擎附帶了個(gè)很好的例子,告訴大家如何化繁為簡(jiǎn)地從頭實(shí)現(xiàn)自己的 Event Loop,這也就是本節(jié)所希望覆蓋的內(nèi)容了。

Event Loop 最簡(jiǎn)單的應(yīng)用,可能就是 setTimeout 了。和語言規(guī)范一致地,QuickJS 默認(rèn)并沒有提供 setTimeout 這樣需要運(yùn)行時(shí)能力的異步 API 支持。但是,引擎編譯時(shí)默認(rèn)會(huì)內(nèi)置 stdos 兩個(gè)原生模塊,可以這樣使用 setTimeout 來支持異步:

import { setTimeout } from 'os';

setTimeout(() => { /* ... */ }, 0);

稍微檢查下源碼就能發(fā)現(xiàn),這個(gè) os 模塊并不在 quickjs.c 引擎本體里,而是和前面的 fib.c 如出一轍地,通過標(biāo)準(zhǔn)化的 QuickJS API 掛載上去的原生模塊。這個(gè)原生的 setTimeout 函數(shù)是怎么實(shí)現(xiàn)的呢?它的源碼其實(shí)很少,像這樣:

static JSValue js_os_setTimeout(JSContext *ctx, JSValueConst this_val,

                                int argc, JSValueConst *argv)

{

    int64_t delay;

    JSValueConst func;

    JSOSTimer *th;

    JSValue obj;

    func = argv[0];

    if (!JS_IsFunction(ctx, func))

        return JS_ThrowTypeError(ctx, 'not a function');

    if (JS_ToInt64(ctx, &delay, argv[1]))

        return JS_EXCEPTION;

    obj = JS_NewObjectClass(ctx, js_os_timer_class_id);

    if (JS_IsException(obj))

        return obj;

    th = js_mallocz(ctx, sizeof(*th));

    if (!th) {

        JS_FreeValue(ctx, obj);

        return JS_EXCEPTION;

    }

    th->has_object = TRUE;

    th->timeout = get_time_ms() + delay;

    th->func = JS_DupValue(ctx, func);

    list_add_tail(&th->link, &os_timers);

    JS_SetOpaque(obj, th);

    return obj;

}

可以看出,這個(gè) setTimeout 的實(shí)現(xiàn)中,并沒有任何多線程或 poll 的操作,只是把一個(gè)存儲(chǔ) timer 信息的結(jié)構(gòu)體通過 JS_SetOpaque 的方式,掛到了最后返回的 JS 對(duì)象上而已,是個(gè)非常簡(jiǎn)單的同步操作。因此,就和調(diào)用原生 fib 函數(shù)一樣地,在 eval 執(zhí)行 JS 代碼時(shí),遇到 setTimeout 后也是同步地執(zhí)行一點(diǎn) C 代碼后就立刻返回,沒有什么特別之處。

但為什么 setTimeout 能實(shí)現(xiàn)異步呢?關(guān)鍵在于 eval 之后,我們就要啟動(dòng) Event Loop 了。而這里的奧妙其實(shí)也在 QuickJS 編譯器生成的代碼里明確地寫出來了,沒想到吧:

// ... int main(int argc, char **argv)

{

  // ... // eval JS 字節(jié)碼 js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);

  // 啟動(dòng) Event Loop js_std_loop(ctx);

  // ... }

因此,eval 后的這個(gè) js_std_loop 就是真正的 Event Loop,而它的源碼則更是簡(jiǎn)單得像是偽代碼一樣:

/* main loop which calls the user JS callbacks */

void js_std_loop(JSContext *ctx)

{

    JSContext *ctx1;

    int err;

    for(;;) {

        /* execute the pending jobs */

        for(;;) {

            err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);

            if (err <= 0) {

                if (err < 0) {

                    js_std_dump_error(ctx1);

                }

                break;

            }

        }

        if (!os_poll_func || os_poll_func(ctx))

            break;

    }

}

這不就是在雙重的死循環(huán)里先執(zhí)行掉所有的 Job,然后調(diào) os_poll_func 嗎?可是,for 循環(huán)不會(huì)吃滿 CPU 嗎?這是個(gè)前端同學(xué)們?nèi)菀渍`解的地方:在原生開發(fā)中,進(jìn)程里即便寫著個(gè)死循環(huán),也未必始終在前臺(tái)運(yùn)行,可以通過系統(tǒng)調(diào)用將自己掛起。

例如,一個(gè)在死循環(huán)里通過 sleep 系統(tǒng)調(diào)用不停休眠一秒的進(jìn)程,就只會(huì)每秒被系統(tǒng)執(zhí)行一個(gè) tick,其它時(shí)間里都不占資源。而這里的 os_poll_func 封裝的,就是原理類似的 poll 系統(tǒng)調(diào)用(準(zhǔn)確地說,用的其實(shí)是 select),從而可以借助操作系統(tǒng)的能力,使得只在【定時(shí)器觸發(fā)、文件描述符讀寫】等事件發(fā)生時(shí),讓進(jìn)程回到前臺(tái)執(zhí)行一個(gè) tick,把此時(shí)應(yīng)該運(yùn)行的 JS 回調(diào)跑一遍,而其余時(shí)間都在后臺(tái)掛起。在這條路上繼續(xù)走下去,就能以經(jīng)典的異步非阻塞方式來實(shí)現(xiàn)整個(gè)運(yùn)行時(shí)啦。

poll 和 select 想實(shí)現(xiàn)的東西是一致的,只是原理不同,前者性能更好而后者更簡(jiǎn)單而已。

鑒于 os_poll_func 的代碼較長(zhǎng),這里只概括下它與 timer 相關(guān)的工作:

  • 如果上下文中存在 timer,將到期 timer 對(duì)應(yīng)的回調(diào)都執(zhí)行掉。

  • 找到所有 timer 中最小的時(shí)延,用 select 系統(tǒng)調(diào)用將自己掛起這段時(shí)間。

這樣,setTimeout 的流程就說得通了:先在 eval 階段簡(jiǎn)單設(shè)置一個(gè) timer 結(jié)構(gòu),然后在 Event Loop 里用這個(gè) timer 的參數(shù)去調(diào)用操作系統(tǒng)的 poll,從而在被喚醒的下一個(gè) tick 里把到期 timer 對(duì)應(yīng)的 JS 回調(diào)執(zhí)行掉就行。

所以,看明白這個(gè) Event Loop 的機(jī)制后,就不難發(fā)現(xiàn)如果只關(guān)心 setTimeout 這個(gè)運(yùn)行時(shí) API,那么照抄,啊不移植的方法其實(shí)并不復(fù)雜:

  • os 原生模塊里的 setTimeout 相關(guān)部分,仿照 fib 的形式抄進(jìn)來。

  • js_std_loop 及其依賴抄進(jìn)來。

這其實(shí)就是件按部就班就能完成的事,實(shí)際代碼示例會(huì)和下篇一起給出。

到現(xiàn)在為止這些對(duì) QuickJS 的分析,是否能讓大家發(fā)現(xiàn),許多經(jīng)常聽到的高大上概念,實(shí)現(xiàn)起來其實(shí)也沒有那么復(fù)雜呢?別忘了,QuickJS 出自傳奇程序員 Fabrice Bellard。讀他代碼的感受,就像讀高中習(xí)題的參考答案一樣,既不漏過每個(gè)關(guān)鍵的知識(shí)點(diǎn)又毫不拖泥帶水,非常有啟發(fā)性。他本人也像金庸小說里創(chuàng)造「天下武學(xué)正宗」的中神通王重陽(yáng)那樣,十分令人嘆服。帶著問題閱讀更高段位的代碼,也幾乎總能帶來豐富的收獲。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    欧美一区二区日韩一区二区| 亚洲最新的黄色录像在线| 久久精品蜜桃一区二区av| 日韩精品小视频在线观看| 日韩精品视频香蕉视频| 狠狠干狠狠操亚洲综合| 亚洲av熟女一区二区三区蜜桃| 东京热男人的天堂一二三区| 亚洲欧美精品伊人久久| 欧美一区二区三区喷汁尤物| 麻豆亚州无矿码专区视频| 免费在线观看欧美喷水黄片| 日韩蜜桃一区二区三区| 亚洲高清一区二区高清| 午夜直播免费福利平台| 日本成人中文字幕一区| 国产av一区二区三区麻豆| 久久精品国产一区久久久| 亚洲精品一区三区三区| 91免费一区二区三区| 亚洲欧美视频欧美视频| 日本深夜福利视频在线| 亚洲国产一级片在线观看| 日韩成人高清免费在线| 色婷婷成人精品综合一区| 精品推荐久久久国产av| 国产精品流白浆无遮挡| 免费人妻精品一区二区三区久久久| 99久免费精品视频在线观| 日韩精品少妇人妻一区二区| 97人妻精品免费一区二区| 免费观看一区二区三区黄片| 伊人久久青草地婷婷综合| 女同伦理国产精品久久久| 国产a天堂一区二区专区| 视频在线观看色一区二区| 日韩一级欧美一级久久| 99久久免费看国产精品| 噜噜中文字幕一区二区| 好吊视频一区二区在线| 福利一区二区视频在线|