├── .gitignore ├── CMakeLists.txt ├── README.md ├── build └── .gitkeep └── src ├── fib.c ├── main.c ├── test.js └── timer.c /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | build 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(runtime) 3 | add_executable(runtime 4 | src/main.c 5 | src/timer.c 6 | src/fib.c) 7 | 8 | # libuv 9 | include_directories(/usr/local/include) 10 | add_library(libuv STATIC IMPORTED) 11 | set_target_properties(libuv 12 | PROPERTIES IMPORTED_LOCATION 13 | "/usr/local/lib/libuv.a") 14 | 15 | # quickjs 16 | add_library(quickjs STATIC IMPORTED) 17 | set_target_properties(quickjs 18 | PROPERTIES IMPORTED_LOCATION 19 | "/usr/local/lib/quickjs/libquickjs.a") 20 | 21 | target_link_libraries(runtime 22 | libuv 23 | quickjs) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal JS Runtime 2 | A toy JavaScript runtime based on QuickJS and libuv. 3 | 4 | ## Introduction 5 | This project demonstrates how to build a simplest JavaScript runtime with QuickJS and libuv. Takeaway points include: 6 | 7 | * Native `fib` function implementation. 8 | * `setTimeout` implementation. 9 | * Support for running microtasks and macrotasks. 10 | 11 | See my Chinese blog post for technical details: 12 | 13 | * [From JavaScript Engine to JavaScript Runtime - 1](https://ewind.us/2020/js-engine-to-js-runtime-1/) 14 | * [From JavaScript Engine to JavaScript Runtime - 2](https://ewind.us/2020/js-engine-to-js-runtime-2/) 15 | 16 | ## Getting Started 17 | Please make sure [CMake](https://cmake.org/), [QuickJS](https://bellard.org/quickjs/) and [libuv](https://libuv.org/) are globally installed. 18 | 19 | Build the runtime: 20 | 21 | ``` bash 22 | cd build 23 | cmake .. && make 24 | ``` 25 | 26 | Start the runtime: 27 | 28 | ``` bash 29 | ./runtime 30 | ``` 31 | 32 | ## Misc 33 | To find out how to port the original event loop shipped with QuickJS, checkout commit [fce26e](https://github.com/doodlewind/minimal-js-runtime/commit/fce26ed2641ca9ff5ee3bad7dd1dc76caa679aa8). 34 | 35 | ## License 36 | MIT 37 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doodlewind/minimal-js-runtime/95643a0b7367dd06ea3f0933704d58293feba817/build/.gitkeep -------------------------------------------------------------------------------- /src/fib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #define countof(x) (sizeof(x) / sizeof((x)[0])) 3 | 4 | static int fib(int n) { 5 | if (n <= 0) return 0; 6 | else if (n == 1) return 1; 7 | else return fib(n - 1) + fib(n - 2); 8 | } 9 | 10 | static JSValue js_fib(JSContext *ctx, JSValueConst this_val, 11 | int argc, JSValueConst *argv) { 12 | int n, res; 13 | if (JS_ToInt32(ctx, &n, argv[0])) return JS_EXCEPTION; 14 | res = fib(n); 15 | return JS_NewInt32(ctx, res); 16 | } 17 | 18 | static const JSCFunctionListEntry js_fib_funcs[] = { 19 | JS_CFUNC_DEF("fib", 1, js_fib ), 20 | }; 21 | 22 | static int js_fib_init(JSContext *ctx, JSModuleDef *m) { 23 | return JS_SetModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs)); 24 | } 25 | 26 | JSModuleDef *js_init_module_fib(JSContext *ctx, const char *module_name) { 27 | JSModuleDef *m; 28 | m = JS_NewCModule(ctx, module_name, js_fib_init); 29 | if (!m) return NULL; 30 | JS_AddModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs)); 31 | return m; 32 | } 33 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | static int eval_buf(JSContext *ctx, const void *buf, int buf_len, 6 | const char *filename, int eval_flags) { 7 | JSValue val; 8 | int ret; 9 | 10 | if ((eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE) { 11 | /* for the modules, we compile then run to be able to set 12 | import.meta */ 13 | val = JS_Eval(ctx, buf, buf_len, filename, 14 | eval_flags | JS_EVAL_FLAG_COMPILE_ONLY); 15 | if (!JS_IsException(val)) { 16 | js_module_set_import_meta(ctx, val, TRUE, TRUE); 17 | val = JS_EvalFunction(ctx, val); 18 | } 19 | } else { 20 | val = JS_Eval(ctx, buf, buf_len, filename, eval_flags); 21 | } 22 | if (JS_IsException(val)) { 23 | js_std_dump_error(ctx); 24 | ret = -1; 25 | } else { 26 | ret = 0; 27 | } 28 | JS_FreeValue(ctx, val); 29 | return ret; 30 | } 31 | 32 | typedef struct qjs_engine { 33 | JSContext *ctx; 34 | JSRuntime *rt; 35 | } qjs_engine; 36 | 37 | 38 | static void check_callback(uv_check_t *handle) { 39 | qjs_engine *engine = handle->data; 40 | JSContext *ctx = engine->ctx; 41 | 42 | JSContext *ctx1; 43 | int err; 44 | 45 | for (;;) { 46 | err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); 47 | if (err <= 0) { 48 | if (err < 0) 49 | js_std_dump_error(ctx1); 50 | break; 51 | } 52 | } 53 | } 54 | 55 | int main(int argc, char **argv) { 56 | JSRuntime *rt; 57 | JSContext *ctx; 58 | rt = JS_NewRuntime(); 59 | ctx = JS_NewContextRaw(rt); 60 | JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); 61 | JS_AddIntrinsicBaseObjects(ctx); 62 | JS_AddIntrinsicDate(ctx); 63 | JS_AddIntrinsicEval(ctx); 64 | JS_AddIntrinsicStringNormalize(ctx); 65 | JS_AddIntrinsicRegExp(ctx); 66 | JS_AddIntrinsicJSON(ctx); 67 | JS_AddIntrinsicProxy(ctx); 68 | JS_AddIntrinsicMapSet(ctx); 69 | JS_AddIntrinsicTypedArrays(ctx); 70 | JS_AddIntrinsicPromise(ctx); 71 | JS_AddIntrinsicBigInt(ctx); 72 | js_std_add_helpers(ctx, argc, argv); 73 | 74 | uv_loop_t *loop = calloc(1, sizeof(*loop)); 75 | uv_loop_init(loop); 76 | JS_SetContextOpaque(ctx, loop); 77 | 78 | uv_check_t *check = calloc(1, sizeof(*check)); 79 | 80 | qjs_engine *engine = calloc(1, sizeof(*engine)); 81 | engine->ctx = ctx; 82 | engine->rt = rt; 83 | uv_check_init(loop, check); 84 | check->data = engine; 85 | 86 | { 87 | extern JSModuleDef *js_init_module_fib(JSContext *ctx, const char *name); 88 | js_init_module_fib(ctx, "fib.so"); 89 | 90 | extern JSModuleDef *js_init_module_uv(JSContext *ctx, const char *name); 91 | js_init_module_uv(ctx, "uv.so"); 92 | } 93 | 94 | uint8_t *buf; 95 | size_t buf_len; 96 | const char *filename = "../src/test.js"; 97 | buf = js_load_file(ctx, &buf_len, filename); 98 | 99 | uv_check_start(check, check_callback); 100 | uv_unref((uv_handle_t *) check); 101 | 102 | eval_buf(ctx, buf, buf_len, filename, JS_EVAL_TYPE_MODULE); 103 | 104 | uv_run(loop, UV_RUN_DEFAULT); 105 | JS_FreeContext(ctx); 106 | JS_FreeRuntime(rt); 107 | return 0; 108 | } 109 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import { fib } from 'fib.so' 2 | import { setTimeout } from 'uv.so' 3 | 4 | console.log(`fib(10) = ${fib(10)}`) 5 | setTimeout(() => console.log('B'), 0) 6 | Promise.resolve().then(() => console.log('A')) 7 | 8 | setTimeout(() => { 9 | setTimeout(() => console.log('D'), 0) 10 | Promise.resolve().then(() => console.log('C')) 11 | }, 1000) 12 | -------------------------------------------------------------------------------- /src/timer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define countof(x) (sizeof(x) / sizeof((x)[0])) 5 | 6 | typedef struct { 7 | JSContext *ctx; 8 | uv_timer_t handle; 9 | int interval; 10 | JSValue obj; 11 | JSValue func; 12 | int argc; 13 | JSValue argv[]; 14 | } UVTimer; 15 | 16 | static JSClassID uv_timer_class_id; 17 | 18 | static void clearTimer(UVTimer *th) { 19 | JSContext *ctx = th->ctx; 20 | 21 | JS_FreeValue(ctx, th->func); 22 | th->func = JS_UNDEFINED; 23 | 24 | for (int i = 0; i < th->argc; i++) { 25 | JS_FreeValue(ctx, th->argv[i]); 26 | th->argv[i] = JS_UNDEFINED; 27 | } 28 | th->argc = 0; 29 | 30 | JSValue obj = th->obj; 31 | th->obj = JS_UNDEFINED; 32 | JS_FreeValue(ctx, obj); 33 | } 34 | 35 | static void callTimer(UVTimer *th) { 36 | JSContext *ctx = th->ctx; 37 | JSValue ret, func1; 38 | /* 'func' might be destroyed when calling itself (if it frees the handler), so must take extra care */ 39 | func1 = JS_DupValue(ctx, th->func); 40 | ret = JS_Call(ctx, func1, JS_UNDEFINED, th->argc, (JSValueConst *) th->argv); 41 | JS_FreeValue(ctx, func1); 42 | // FIXME 43 | // if (JS_IsException(ret)) 44 | // dump_error(ctx); 45 | JS_FreeValue(ctx, ret); 46 | } 47 | 48 | static void timerCallback(uv_timer_t *handle) { 49 | UVTimer *th = handle->data; 50 | JSContext *ctx = th->ctx; 51 | 52 | JSContext *ctx1; 53 | int err; 54 | 55 | for (;;) { 56 | err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); 57 | if (err <= 0) { 58 | if (err < 0) 59 | js_std_dump_error(ctx1); 60 | break; 61 | } 62 | } 63 | 64 | callTimer(th); 65 | if (!th->interval) 66 | clearTimer(th); 67 | } 68 | 69 | static JSValue js_uv_setTimeout(JSContext *ctx, JSValueConst this_val, 70 | int argc, JSValueConst *argv) { 71 | int64_t delay; 72 | JSValueConst func; 73 | UVTimer *th; 74 | JSValue obj; 75 | 76 | func = argv[0]; 77 | if (!JS_IsFunction(ctx, func)) 78 | return JS_ThrowTypeError(ctx, "not a function"); 79 | if (JS_ToInt64(ctx, &delay, argv[1])) 80 | return JS_EXCEPTION; 81 | 82 | obj = JS_NewObjectClass(ctx, uv_timer_class_id); 83 | if (JS_IsException(obj)) 84 | return obj; 85 | 86 | int nargs = argc - 2; 87 | 88 | th = calloc(1, sizeof(*th) + nargs * sizeof(JSValue)); 89 | if (!th) { 90 | JS_FreeValue(ctx, obj); 91 | return JS_EXCEPTION; 92 | } 93 | 94 | th->ctx = ctx; 95 | uv_loop_t *loop = JS_GetContextOpaque(ctx); 96 | uv_timer_init(loop, &th->handle); 97 | th->handle.data = th; 98 | th->interval = 0; 99 | th->obj = JS_DupValue(ctx, obj); 100 | th->func = JS_DupValue(ctx, func); 101 | th->argc = nargs; 102 | for (int i = 0; i < nargs; i++) 103 | th->argv[i] = JS_DupValue(ctx, argv[i + 2]); 104 | 105 | uv_timer_start(&th->handle, timerCallback, delay, 0); 106 | 107 | JS_SetOpaque(obj, th); 108 | return obj; 109 | } 110 | 111 | static const JSCFunctionListEntry js_os_funcs[] = { 112 | JS_CFUNC_DEF("setTimeout", 2, js_uv_setTimeout ), 113 | }; 114 | 115 | static int js_uv_init(JSContext *ctx, JSModuleDef *m) { 116 | return JS_SetModuleExportList(ctx, m, js_os_funcs, 117 | countof(js_os_funcs)); 118 | } 119 | 120 | JSModuleDef *js_init_module_uv(JSContext *ctx, const char *module_name) { 121 | JSModuleDef *m; 122 | m = JS_NewCModule(ctx, module_name, js_uv_init); 123 | if (!m) 124 | return NULL; 125 | JS_AddModuleExportList(ctx, m, js_os_funcs, countof(js_os_funcs)); 126 | return m; 127 | } 128 | --------------------------------------------------------------------------------