Отношения между V8 и Node.js — это то, о чем говорят многие студенты, изучающие интерфейс — язык в браузере совместим со средой за пределами браузера, и эти два счастья пересекаются. И эти две радости приносят еще большую радость... Но задумывались ли вы когда-нибудь, как эти две радости пересекаются? Ниже мы возьмем встроенный JS-движок QuickJS в качестве примера, чтобы показать, как JS-движок постепенно настраивается в качестве новой среды выполнения JS.
Эта статья будет разделена на две части, одна за другой охватывающие (или реализующие с помощью максимально простого кода) следующее содержание:
- Встроенный встроенный JS-движок
- Расширьте собственные возможности для JS-движков
- Перенос цикла событий по умолчанию
- Поддержка цикла событий libuv
- Поддержка макрозадач и микрозадач
Первая часть в основном включает в себя первые три раздела, в основном знакомит с базовым использованием QuickJS, самого встроенного механизма JS и трансплантирует собственный пример цикла событий. В следующих двух разделах, соответствующих следующей главе, мы представим libuv, чтобы объяснить, как реализовать более масштабируемый цикл обработки событий на основе libuv и поддерживать макро- и микрозадачи.
Прекратите сплетничать и выходите на сцену Baixue :)
Встроенный встроенный JS-движок
В моем понимании «встроенность» движка JS можно понимать с двух уровней, один означает, что он ориентирован на бюджетные встраиваемые устройства, а другой означает, что его легкоВстроить в нативный проект. Среда выполнения JS (Runtime) на самом деле является нативным проектом, который использует движок JS в качестве выделенного интерпретатора для предоставления ему сетевых, процессных, файловых и других возможностей операционной системы. Поэтому, если вы хотите реализовать среду выполнения JS самостоятельно, первое, что вы должны рассмотреть, это «как встроить движок JS в нативный проект».
Содержание этого раздела предназначено для студентов, имеющих опыт работы с интерфейсом, таких как я (без серьезных проектов на C/C++), и знакомые друзья могут его пропустить.
Как это считается встраиванием JS-движка? Мы знаем, что простейшая программа на C — это функция main. Если бы мы могли вызвать движок для выполнения фрагмента JS-кода в основной функции, разве он не был бы успешно «встроен» — точно так же, как если положить кусок хлеба на каждый конец земли, мы можем превратить землю в бутерброд. .
Итак, как вы называете движок в своем собственном коде C? С точки зрения разработчиков C, движок JS можно использовать и как стороннюю библиотеку, его способ интеграции ничем не отличается от обычных сторонних библиотек, вкратце он включает в себя следующие шаги:
- Скомпилируйте исходный код движка в файл библиотеки, который может быть
.a
Статическая библиотека в формате, который также может быть.so
или.dll
Динамическая библиотека форматов. - Включите заголовочный файл движка в свой собственный исходный код C и вызовите предоставляемый им API.
- Составьте свой исходный код C и файлы библиотеки ссылок на двигателе, чтобы генерировать исполняемый файл.
Для QuickJS всего одна строкаmake && sudo make install
Компиляцию и установку можно завершить (опять же, так называемая установка собственных программных пакетов на самом деле просто копирует файлы заголовков, файлы скомпилированных библиотек и исполняемые файлы в каталог, соответствующий стандарту Unix), а затем вы можете использовать это в нашем используется в исходном коде C .
После завершения компиляции и установки QuickJS нам даже не нужно вручную писать C. Мы можем полениться и позволить QuickJS сгенерировать его для вас, потому что он поддерживает компиляцию 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;
}
Разве этот пример основной функции мы хотим? Этот Hello World был превращен в Bytecode в массиве, встроенном в прошедший CH.
Обратите внимание, что это только JS, составленные в байтовом коде, а затем прикрепите только вводную запись основного кода клей, не на самом деле не помещают его в C Commiter JS.
Конечно, этот исходный код C должен быть снова скомпилирован с помощью компилятора C. Как и при настройке при использовании Babel и Webpack, нативные проекты также нуждаются в настройке сборки. Для инструментов сборки здесь выбраны почти стандартные в современном инжинирингеCMake. И это соответствие исходного кода CCMakeLists.txt
Конфигурация сборки выглядит так:
cmake_minimum_required(VERSION 3.10)
# 约定 runtime 为最终生成的可执行文件
project(runtime)
add_executable(runtime
# 若拆分了多个 C 文件,逐行在此添加即可
src/main.c)
# 导入 QuickJS 的头文件和库文件
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 очень просто и не будет здесь повторяться. Короче говоря, приведенная выше конфигурация может скомпилироватьсяruntime
Двоичный файл, он может вывести Hello World, запустив его напрямую, зная, что этого достаточно.
Расширьте собственные возможности для JS-движков
После последнего шага мы уже установили движок JS в случае программы на C. Однако это всего лишь «чистая версия» движка, а значит, он не поддерживает языковые стандарты, какие-либо возможности, предоставляемые платформой. В браузереdocument.getElementById
и в Node.jsfs.readFile
, все относятся к этой способности. Поэтому перед реализацией более сложного Event Loop мы должны как минимум иметь возможность вызывать написанную нами нативную функцию C в движке JS, как это принято в консоли браузера:
> document.getElementById
ƒ getElementById() { [native code] }
Итак, как вы инкапсулируете код C в такую функцию? Как и другие движки JS, QuickJS предоставляет стандартизированный API, который позволяет вам реализовывать функции и классы JS на C. Давайте возьмем рекурсивную функцию Фибоначчи, которая вычисляет числа Фибоначчи, в качестве примера, чтобы продемонстрировать, как изменить функции JS с интенсивными вычислениями на C, чтобы значительно повысить производительность.
JS-версия исходной функции fib выглядит так:
function fib(n) {
if (n <= 0) return 0;
else if (n === 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
И версия функции fib для C выглядит так, как она выглядит?
int fib(int n) {
if (n <= 0) return 0;
else if (n == 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
Чтобы использовать приведенную выше функцию C в движке QuickJS, сделайте примерно следующее:
- Оберните функцию C в слой и обработайте преобразование типов между ней и движком JS.
- Смонтируйте упакованную функцию под модулем JS.
- Предоставьте весь собственный модуль внешнему миру.
Всего это всего около 30 строк связующего кода, и соответствующийfib.c
Исходный код выглядит следующим образом:
#include <quickjs/quickjs.h>
#define countof(x) (sizeof(x) / sizeof((x)[0]))
// 原始的 C 函数
static int fib(int n) {
if (n <= 0) return 0;
else if (n == 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
// 包一层,处理类型转换
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);
}
// 将包好的函数定义为 JS 模块下的 fib 方法
static const JSCFunctionListEntry js_fib_funcs[] = {
JS_CFUNC_DEF("fib", 1, js_fib ),
};
// 模块初始化时的回调
static int js_fib_init(JSContext *ctx, JSModuleDef *m) {
return JS_SetModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs));
}
// 最终对外的 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;
}
выше этогоfib.c
файл просто добавьтеCMakeLists.txt
серединаadd_executable
item, его можно скомпилировать и использовать. так в оригиналеmain.c
В записи просто добавьте еще две строки кода инициализации перед eval JS-кодом, чтобы подготовить среду JS-движка с нативными модулями:
// ...
int main(int argc, char **argv)
{
// ...
// 在 eval 前注册上名为 fib.so 的原生模块
extern JSModuleDef *js_init_module_fib(JSContext *ctx, const char *name);
js_init_module_fib(ctx, "fib.so");
// eval JS 字节码
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
// ...
}
Таким образом, мы можем использовать модули C в JS следующим образом:
import { fib } from "fib.so";
fib(42);
Как встроенный механизм JS, производительность QuickJS по умолчанию, естественно, уступает V8 с JIT. Измерено в QuickJSfib(42)
Это занимает около 30 секунд, в то время как V8 занимает всего около 3,5 секунд. Но как только появится нативный модуль C, QuickJS сможет одним махом превзойти V8 и выполнить расчет менее чем за 2 секунды.Легко в 15 раз быстрее!
Можно обнаружить, что современные движки JS имеют сильную JIT для задач с интенсивными вычислениями, поэтому, если вы замените JS в браузере на WASM, эффект ускорения может быть недостаточно идеальным. Подробности смотрите в моей статье:WebAssembly глазами белого ученого.
Перенос цикла событий по умолчанию
К этому моменту мы должны были понять, как встроить движок JS и расширить его с помощью модулей C. Тем не менее, вышеизложенноеfib
Функция — это просто синхронная функция, а не асинхронная. Как различные асинхронные возможности, поддерживающие обратные вызовы, поддерживаются средой выполнения? Для этого требуется легендарный цикл событий.
В настоящее время в сообществе интерфейсных разработчиков слишком много концептуальных представлений о цикле событий, но, к сожалению, лишь немногие на самом деле предлагают полезные реализации с кратким кодом. К счастью, QuickJS поставляется с хорошим примером с движком, рассказывающим вам, как реализовать собственный цикл обработки событий с нуля, что и будет рассмотрено в этом разделе.
Самым простым применением цикла событий, вероятно, является setTimeout. В соответствии со спецификацией языка QuickJS не поддерживает асинхронные API, такие как setTimeout, для которых по умолчанию требуются возможности среды выполнения. Однако, когда движок скомпилирован, он встроен по умолчанию.std
а такжеos
Два встроенных модуля, которые могут использовать setTimeout для поддержки асинхронности, например:
import { setTimeout } from "os";
setTimeout(() => { /* ... */ }, 0);
Небольшой осмотр исходного кода покажет, что этоos
модуля там нетquickjs.c
в самом двигателе, но с переднейfib.c
Точно так же нативные модули монтируются через стандартизированный API QuickJS. Как реализована эта родная функция setTimeout? Его исходный код на самом деле очень мал, например:
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;
}
Видно, что в реализации этого setTimeout нет ни многопоточности, ни операции опроса, просто передается структура, хранящая информацию о таймере.JS_SetOpaque
Таким образом, он связан только с объектом JS, возвращаемым в конце, что является очень простой синхронной операцией. Таким образом, точно так же, как при вызове встроенной функции fib,Когда eval выполняет код JS, после встречи с setTimeout он также синхронно выполняет немного кода C и немедленно возвращается, в этом нет ничего особенного..
Но почему setTimeout может быть асинхронным? Суть в том, что после eval мы собираемся запустить Event Loop. И загадка здесь на самом деле четко прописана в коде, сгенерированном компилятором QuickJS, я этого не ожидал:
// ...
int main(int argc, char **argv)
{
// ...
// eval JS 字节码
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
// 启动 Event Loop
js_std_loop(ctx);
// ...
}
Итак, после оценкиjs_std_loop
Это настоящий цикл событий, и его исходный код прост, как псевдокод:
/* 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;
}
}
Не так ли цикл смерти в первом исполнении двойной всю работу, а затем настроитьсяos_poll_func
? Но не будет ли цикл for потреблять процессор? Это место, где продвинутых студентов легко неправильно понять:В нативной разработке, даже если в процессе прописан бесконечный цикл, он может не всегда работать на переднем плане, можно приостановить себя через системные вызовы.
Например, процесс, который постоянно бездействует в течение одной секунды из-за системного вызова sleep в бесконечном цикле, будет выполняться системой только один тик в секунду и не будет занимать ресурсы в другое время. А вотos_poll_func
Инкапсуляция — это системный вызов опроса с аналогичным принципом (точнее, фактически используется выбор), так что возможности операционной системы могут быть использованы для выполнения системного вызова только при таких событиях, как [триггер таймера, чтение дескриптора файла и write].Процесс возвращается на передний план для выполнения тика, запуска JS-коллбэка, который должен быть запущен в это время, и висит в фоне все остальное время. Продолжая идти по этому пути, вся среда выполнения может быть реализована классическим асинхронным неблокирующим способом.
То, чего хотят добиться poll и select, одно и то же, но принцип разный: у первого лучше производительность, а у второго проще.
ввидуos_poll_func
Код длиннее, здесь только обзор его работы, связанной с таймером:
- Если в контексте есть таймер, будет выполнен обратный вызов, соответствующий истекшему таймеру.
- Найдите наименьшую задержку среди всех таймеров и используйте системный вызов select, чтобы приостановить себя на этот период времени.
Таким образом, поток setTimeout имеет смысл:Во-первых, просто установите структуру таймера на этапе eval, а затем используйте параметр таймера в цикле событий для вызова опроса операционной системы, чтобы выполнить обратный вызов JS, соответствующий таймеру с истекшим сроком действия, в следующем пробужденном тике. ..
Поэтому, разобравшись с механизмом этого Event Loop, нетрудно обнаружить, что если вас волнует только runtime API setTimeout, то метод копирования, ах, а не портирования на самом деле не сложен:
- Буду
os
Соответствующая часть setTimeout в нативном модуле копируется в виде fib. - Буду
js_std_loop
и его зависимости скопированы.
На самом деле это то, что можно сделать шаг за шагом, и фактический пример кода будет приведен вместе со следующей статьей.
Может ли анализ QuickJS позволить вам обнаружить, что многие концепции высокого уровня, о которых часто можно услышать, на самом деле не так уж сложны в реализации? Не забывайте, что QuickJS создан легендарным программистом Фабрисом Белларом. Ощущение от чтения его кода такое же, как при чтении справочных ответов на школьные упражнения, оно не упускает из виду все ключевые моменты знаний и очень вдохновляет. Он сам, как и Ван Чунян, создавший «подлинные боевые искусства в мире» в романах Цзинь Юна, очень впечатляет. Чтение высокоуровневого кода с вопросами почти всегда полезно.
Хорошо, это все для последней статьи. В следующей части, на основе знакомства с QuickJS и циклом событий, мы изменим цикл событий, чтобы он был реализован более расширяемой libuv, а также будут приведены примеры кода, задействованные в полном тексте. Если вы заинтересованы, пожалуйста, обратите внимание~