diff options
author | Dmitry Volyntsev <xeioex@nginx.com> | 2024-06-14 20:54:28 -0700 |
---|---|---|
committer | Dmitry Volyntsev <xeioexception@gmail.com> | 2024-09-17 18:05:57 -0700 |
commit | 201b127679e27fe63eff5c1b4356ec4ed9ec4611 (patch) | |
tree | 2477939f07f646ee6137a7d30ccc8e807206da61 /nginx/ngx_js.c | |
parent | 9b6744129ce4c18e74b5e41cd1da66f2cbadcf36 (diff) | |
download | njs-201b127679e27fe63eff5c1b4356ec4ed9ec4611.tar.gz njs-201b127679e27fe63eff5c1b4356ec4ed9ec4611.zip |
Modules: introduced QuickJS engine.
"js_engine" directive is introduced which sets JavaScript engine.
When the module is built with QuickJS library "js_engine qjs;" sets
QuickJS engine for the current block. By default njs engine is used.
For example,
nginx.conf:
location /a {
js_engine qjs;
# will be handled by QuickJS
js_content main.handler;
}
location /b {
# will be handled by njs
js_content main.handler;
}
QuickJS engine implements drop-in replacement for nginx/njs objects
with the following exceptions:
* nginx module API to be added later: ngx.fetch(), ngx.shared.dict.
* Built-in modules to be added later: fs, crypto, WebCrypto, xml.
* NJS specific API: njs.dump(), njs.on(), console.dump().
* js_preload_object directive.
Diffstat (limited to 'nginx/ngx_js.c')
-rw-r--r-- | nginx/ngx_js.c | 1562 |
1 files changed, 1562 insertions, 0 deletions
diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index ce4988f9..5fe3dc84 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -8,6 +8,7 @@ #include <ngx_config.h> #include <ngx_core.h> +#include <math.h> #include "ngx_js.h" @@ -57,6 +58,53 @@ static ngx_int_t ngx_engine_njs_string(ngx_engine_t *e, static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *conf); +#if (NJS_HAVE_QUICKJS) +static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine, + ngx_engine_opts_t *opts); +static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, + u_char *start, size_t size); +static ngx_int_t ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, + njs_opaque_value_t *args, njs_uint_t nargs); +static void *ngx_engine_qjs_external(ngx_engine_t *engine); +static ngx_int_t ngx_engine_qjs_pending(ngx_engine_t *engine); +static ngx_int_t ngx_engine_qjs_string(ngx_engine_t *e, + njs_opaque_value_t *value, ngx_str_t *str); + +static JSValue ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int immediate); +static JSValue ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); + +static JSValue ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_constant_integer(JSContext *cx, + JSValueConst this_val, int magic); +static JSValue ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int level); +static JSValue ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_console_time_end(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val); + +static void ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val); + +static JSModuleDef *ngx_qjs_module_loader(JSContext *ctx, + const char *module_name, void *opaque); +static int ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx); +static void ngx_qjs_rejection_tracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, JS_BOOL is_handled, void *opaque); + +static JSValue ngx_qjs_value(JSContext *cx, const ngx_str_t *path); +static ngx_int_t ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, + ngx_str_t *dst); + +static JSModuleDef *ngx_qjs_core_init(JSContext *cx, const char *name); +#endif + static njs_int_t ngx_js_ext_build(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static njs_int_t ngx_js_ext_conf_file_path(njs_vm_t *vm, @@ -377,6 +425,57 @@ njs_module_t *njs_js_addon_modules_shared[] = { static njs_int_t ngx_js_console_proto_id; +#if (NJS_HAVE_QUICKJS) + +static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = { + JS_CGETSET_DEF("build", ngx_qjs_ext_build, NULL), + JS_CGETSET_DEF("conf_prefix", ngx_qjs_ext_conf_prefix, NULL), + JS_CGETSET_DEF("conf_file_path", ngx_qjs_ext_conf_file_path, NULL), + JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_ERR), + JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL), + JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_INFO), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0), + JS_CGETSET_DEF("prefix", ngx_qjs_ext_prefix, NULL), + JS_PROP_STRING_DEF("version", NGINX_VERSION, JS_PROP_C_W_E), + JS_PROP_INT32_DEF("version_number", nginx_version, JS_PROP_C_W_E), + JS_CGETSET_MAGIC_DEF("WARN", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_WARN), + JS_CGETSET_DEF("worker_id", ngx_qjs_ext_worker_id, NULL), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_console[] = { + JS_CFUNC_MAGIC_DEF("error", 1, ngx_qjs_ext_log, NGX_LOG_ERR), + JS_CFUNC_MAGIC_DEF("info", 1, ngx_qjs_ext_log, NGX_LOG_INFO), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, NGX_LOG_INFO), + JS_CFUNC_DEF("time", 1, ngx_qjs_ext_console_time), + JS_CFUNC_DEF("timeEnd", 1, ngx_qjs_ext_console_time_end), + JS_CFUNC_MAGIC_DEF("warn", 1, ngx_qjs_ext_log, NGX_LOG_WARN), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_global[] = { + JS_CFUNC_MAGIC_DEF("setTimeout", 1, ngx_qjs_ext_set_timeout, 0), + JS_CFUNC_MAGIC_DEF("setImmediate", 1, ngx_qjs_ext_set_timeout, 1), + JS_CFUNC_DEF("clearTimeout", 1, ngx_qjs_ext_clear_timeout), +}; + + +static JSClassDef ngx_qjs_console_class = { + "Console", + .finalizer = ngx_qjs_console_finalizer, +}; + + +qjs_module_t ngx_qjs_ngx_module = { + .name = "ngx", + .init = ngx_qjs_core_init, +}; + +#endif + static ngx_engine_t * ngx_create_engine(ngx_engine_opts_t *opts) { @@ -415,6 +514,25 @@ ngx_create_engine(ngx_engine_opts_t *opts) : ngx_engine_njs_destroy; break; +#if (NJS_HAVE_QUICKJS) + case NGX_ENGINE_QJS: + rc = ngx_engine_qjs_init(engine, opts); + if (rc != NGX_OK) { + return NULL; + } + + engine->name = "QuickJS"; + engine->type = NGX_ENGINE_QJS; + engine->compile = ngx_engine_qjs_compile; + engine->call = ngx_engine_qjs_call; + engine->external = ngx_engine_qjs_external; + engine->pending = ngx_engine_qjs_pending; + engine->string = ngx_engine_qjs_string; + engine->destroy = opts->destroy ? opts->destroy + : ngx_engine_qjs_destroy; + break; +#endif + default: return NULL; } @@ -700,6 +818,1386 @@ ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, } +#if (NJS_HAVE_QUICKJS) + +static ngx_int_t +ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts) +{ + JSRuntime *rt; + + rt = JS_NewRuntime(); + if (rt == NULL) { + return NGX_ERROR; + } + + engine->u.qjs.ctx = qjs_new_context(rt, opts->u.qjs.addons); + if (engine->u.qjs.ctx == NULL) { + return NGX_ERROR; + } + + JS_SetRuntimeOpaque(rt, opts->u.qjs.metas); + JS_SetContextOpaque(engine->u.qjs.ctx, opts->u.qjs.addons); + + JS_SetModuleLoaderFunc(rt, NULL, ngx_qjs_module_loader, opts->conf); + + return NGX_OK; +} + + +static ngx_int_t +ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, + size_t size) +{ + JSValue code; + ngx_str_t text; + JSContext *cx; + ngx_engine_t *engine; + ngx_js_code_entry_t *pc; + + engine = conf->engine; + cx = engine->u.qjs.ctx; + + code = JS_Eval(cx, (char *) start, size, "<main>", + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + + if (JS_IsException(code)) { + ngx_qjs_exception(engine, &text); + ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile %V", &text); + return NGX_ERROR; + } + + pc = njs_arr_add(engine->precompiled); + if (pc == NULL) { + JS_FreeValue(cx, code); + ngx_log_error(NGX_LOG_EMERG, log, 0, "njs_arr_add() failed"); + return NGX_ERROR; + } + + pc->code = JS_WriteObject(cx, &pc->code_size, code, JS_WRITE_OBJ_BYTECODE); + if (pc->code == NULL) { + JS_FreeValue(cx, code); + ngx_log_error(NGX_LOG_EMERG, log, 0, "JS_WriteObject() failed"); + return NGX_ERROR; + } + + JS_FreeValue(cx, code); + + return NGX_OK; +} + + +static JSValue +js_std_await(JSContext *ctx, JSValue obj) +{ + int state, err; + JSValue ret; + JSContext *ctx1; + + for (;;) { + state = JS_PromiseState(ctx, obj); + if (state == JS_PROMISE_FULFILLED) { + ret = JS_PromiseResult(ctx, obj); + JS_FreeValue(ctx, obj); + break; + + } else if (state == JS_PROMISE_REJECTED) { + ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj)); + JS_FreeValue(ctx, obj); + break; + + } else if (state == JS_PROMISE_PENDING) { + err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + if (err < 0) { + /* js_std_dump_error(ctx1); */ + } + + } else { + /* not a promise */ + ret = obj; + break; + } + } + + return ret; +} + + +ngx_engine_t * +ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) +{ + JSValue rv; + njs_mp_t *mp; + uint32_t i, length; + JSRuntime *rt; + ngx_str_t exception; + JSContext *cx; + ngx_engine_t *engine; + ngx_js_code_entry_t *pc; + + mp = njs_mp_fast_create(2 * getpagesize(), 128, 512, 16); + if (mp == NULL) { + return NULL; + } + + engine = njs_mp_alloc(mp, sizeof(ngx_engine_t)); + if (engine == NULL) { + return NULL; + } + + memcpy(engine, cf->engine, sizeof(ngx_engine_t)); + engine->pool = mp; + + if (cf->reuse_queue != NULL) { + engine->u.qjs.ctx = ngx_js_queue_pop(cf->reuse_queue); + if (engine->u.qjs.ctx != NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ctx->log, 0, + "js reused context: %p", engine->u.qjs.ctx); + JS_SetContextOpaque(engine->u.qjs.ctx, external); + return engine; + } + } + + rt = JS_NewRuntime(); + if (rt == NULL) { + return NULL; + } + + JS_SetRuntimeOpaque(rt, JS_GetRuntimeOpaque( + JS_GetRuntime(cf->engine->u.qjs.ctx))); + + cx = qjs_new_context(rt, JS_GetContextOpaque(cf->engine->u.qjs.ctx)); + if (cx == NULL) { + JS_FreeRuntime(rt); + return NULL; + } + + engine->u.qjs.ctx = cx; + JS_SetContextOpaque(cx, external); + + JS_SetHostPromiseRejectionTracker(rt, ngx_qjs_rejection_tracker, ctx); + + + /* TODO: bind objects from preload vm */ + + rv = JS_UNDEFINED; + pc = engine->precompiled->start; + length = engine->precompiled->items; + + for (i = 0; i < length; i++) { + rv = JS_ReadObject(cx, pc[i].code, pc[i].code_size, + JS_READ_OBJ_BYTECODE); + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js load module exception: %V", &exception); + goto destroy; + } + } + + if (JS_ResolveModule(cx, rv) < 0) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js resolve module failed"); + goto destroy; + } + + rv = JS_EvalFunction(cx, rv); + + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V", + &exception); + goto destroy; + } + + rv = js_std_await(cx, rv); + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V", + &exception); + goto destroy; + } + + JS_FreeValue(cx, rv); + + return engine; + +destroy: + + JS_FreeContext(cx); + JS_FreeRuntime(rt); + njs_mp_destroy(mp); + + return NULL; +} + + +static ngx_int_t +ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, + njs_opaque_value_t *args, njs_uint_t nargs) +{ + int rc; + JSValue fn, val; + ngx_str_t exception; + JSRuntime *rt; + JSContext *cx, *cx1; + + cx = ctx->engine->u.qjs.ctx; + + fn = ngx_qjs_value(cx, fname); + if (!JS_IsFunction(cx, fn)) { + JS_FreeValue(cx, fn); + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js function \"%V\" not found", + fname); + + return NGX_ERROR; + } + + val = JS_Call(cx, fn, JS_UNDEFINED, nargs, &ngx_qjs_arg(args[0])); + JS_FreeValue(cx, fn); + if (JS_IsException(val)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js call exception: %V", &exception); + + return NGX_ERROR; + } + + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + ngx_qjs_arg(ctx->retval) = val; + + rt = JS_GetRuntime(cx); + + for ( ;; ) { + rc = JS_ExecutePendingJob(rt, &cx1); + if (rc <= 0) { + if (rc == -1) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js job exception: %V", &exception); + + return NGX_ERROR; + } + + break; + } + } + + if (ngx_qjs_unhandled_rejection(ctx)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); + return NGX_ERROR; + } + + return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; +} + + +static void * +ngx_engine_qjs_external(ngx_engine_t *e) +{ + return JS_GetContextOpaque(e->u.qjs.ctx); +} + + +static ngx_int_t +ngx_engine_qjs_pending(ngx_engine_t *e) +{ + return JS_IsJobPending(JS_GetRuntime(e->u.qjs.ctx)); +} + + +static ngx_int_t +ngx_engine_qjs_string(ngx_engine_t *e, njs_opaque_value_t *value, + ngx_str_t *str) +{ + return ngx_qjs_dump_obj(e, ngx_qjs_arg(*value), str); +} + + +static void +ngx_js_cleanup_reuse_ctx(void *data) +{ + JSRuntime *rt; + JSContext *cx; + + ngx_js_queue_t *reuse = data; + + for ( ;; ) { + cx = ngx_js_queue_pop(reuse); + if (cx == NULL) { + break; + } + + rt = JS_GetRuntime(cx); + JS_FreeContext(cx); + JS_FreeRuntime(rt); + } +} + + +void +ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, + ngx_js_loc_conf_t *conf) +{ + uint32_t i, length; + JSRuntime *rt; + JSContext *cx; + JSClassID class_id; + ngx_qjs_event_t *event; + ngx_js_opaque_t *opaque; + njs_rbtree_node_t *node; + ngx_pool_cleanup_t *cln; + ngx_js_code_entry_t *pc; + ngx_js_rejected_promise_t *rejected_promise; + + cx = e->u.qjs.ctx; + + if (ctx != NULL) { + node = njs_rbtree_min(&ctx->waiting_events); + + while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) { + event = (ngx_qjs_event_t *) ((u_char *) node + - offsetof(ngx_qjs_event_t, node)); + + if (event->destructor != NULL) { + event->destructor(event); + } + + node = njs_rbtree_node_successor(&ctx->waiting_events, node); + } + + if (ctx->rejected_promises != NULL) { + rejected_promise = ctx->rejected_promises->start; + + for (i = 0; i < ctx->rejected_promises->items; i++) { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); + } + } + + class_id = JS_GetClassID(ngx_qjs_arg(ctx->args[0])); + opaque = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), class_id); + opaque->external = NULL; + + JS_FreeValue(cx, ngx_qjs_arg(ctx->args[0])); + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + + } else if (e->precompiled != NULL) { + pc = e->precompiled->start; + length = e->precompiled->items; + + for (i = 0; i < length; i++) { + js_free(cx, pc[i].code); + } + } + + njs_mp_destroy(e->pool); + + if (conf != NULL && conf->reuse != 0) { + if (conf->reuse_queue == NULL) { + conf->reuse_queue = ngx_js_queue_create(ngx_cycle->pool, + conf->reuse); + if (conf->reuse_queue == NULL) { + goto free_ctx; + } + + cln = ngx_pool_cleanup_add(ngx_cycle->pool, 0); + if (cln == NULL) { + goto free_ctx; + } + + cln->handler = ngx_js_cleanup_reuse_ctx; + cln->data = conf->reuse_queue; + } + + if (ngx_js_queue_push(conf->reuse_queue, cx) != NGX_OK) { + goto free_ctx; + } + + return; + } + +free_ctx: + + rt = JS_GetRuntime(cx); + JS_FreeContext(cx); + JS_FreeRuntime(rt); +} + + +static JSValue +ngx_qjs_value(JSContext *cx, const ngx_str_t *path) +{ + u_char *start, *p, *end; + JSAtom key; + size_t size; + JSValue value, rv; + + start = path->data; + end = start + path->len; + + value = JS_GetGlobalObject(cx); + + for ( ;; ) { + p = njs_strlchr(start, end, '.'); + + size = ((p != NULL) ? p : end) - start; + if (size == 0) { + JS_FreeValue(cx, value); + return JS_ThrowTypeError(cx, "empty path element"); + } + + key = JS_NewAtomLen(cx, (char *) start, size); + if (key == JS_ATOM_NULL) { + JS_FreeValue(cx, value); + return JS_ThrowInternalError(cx, "could not create atom"); + } + + rv = JS_GetProperty(cx, value, key); + JS_FreeAtom(cx, key); + if (JS_IsException(rv)) { + JS_FreeValue(cx, value); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, value); + + if (p == NULL) { + break; + } + + start = p + 1; + value = rv; + } + + return rv; +} + + +static ngx_int_t +ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst) +{ + size_t len, byte_offset, byte_length; + u_char *start, *p; + JSValue buffer, stack; + ngx_str_t str, stack_str; + JSContext *cx; + + if (JS_IsNullOrUndefined(val)) { + dst->data = NULL; + dst->len = 0; + return NGX_OK; + } + + cx = e->u.qjs.ctx; + + buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL); + if (!JS_IsException(buffer)) { + start = JS_GetArrayBuffer(cx, &dst->len, buffer); + + JS_FreeValue(cx, buffer); + + if (start != NULL) { + start += byte_offset; + dst->len = byte_length; + + dst->data = njs_mp_alloc(e->pool, dst->len); + if (dst->data == NULL) { + return NGX_ERROR; + } + + memcpy(dst->data, start, dst->len); + return NGX_OK; + } + } + + str.data = (u_char *) JS_ToCString(cx, val); + if (str.data != NULL) { + str.len = ngx_strlen(str.data); + + stack = JS_GetPropertyStr(cx, val, "stack"); + + stack_str.len = 0; + stack_str.data = NULL; + + if (!JS_IsException(stack) && !JS_IsUndefined(stack)) { + stack_str.data = (u_char *) JS_ToCString(cx, stack); + if (stack_str.data != NULL) { + stack_str.len = ngx_strlen(stack_str.data); + } + } + + len = str.len; + + if (stack_str.len != 0) { + len += stack_str.len + njs_length("\n"); + } + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + JS_FreeCString(cx, (char *) str.data); + JS_FreeValue(cx, stack); + return NGX_ERROR; + } + + p = ngx_cpymem(start, str.data, str.len); + + if (stack_str.len != 0) { + *p++ = '\n'; + (void) ngx_cpymem(p, stack_str.data, stack_str.len); + JS_FreeCString(cx, (char *) stack_str.data); + } + + JS_FreeCString(cx, (char *) str.data); + JS_FreeValue(cx, stack); + + } else { + len = njs_length("[exception]"); + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + return NGX_ERROR; + } + + memcpy(start, "[exception]", len); + } + + dst->data = start; + dst->len = len; + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_call(ngx_js_ctx_t *ctx, JSValue fn, JSValue *argv, int argc) +{ + int rc; + JSValue ret; + ngx_str_t exception; + JSRuntime *rt; + JSContext *cx, *cx1; + + cx = ctx->engine->u.qjs.ctx; + + ret = JS_Call(cx, fn, JS_UNDEFINED, argc, argv); + if (JS_IsException(ret)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js call exception: %V", &exception); + + return NGX_ERROR; + } + + JS_FreeValue(cx, ret); + + rt = JS_GetRuntime(cx); + + for ( ;; ) { + rc = JS_ExecutePendingJob(rt, &cx1); + if (rc <= 0) { + if (rc == -1) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js job exception: %V", &exception); + + return NGX_ERROR; + } + + break; + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s) +{ + JSValue exception; + + exception = JS_GetException(e->u.qjs.ctx); + if (ngx_qjs_dump_obj(e, exception, s) != NGX_OK) { + return NGX_ERROR; + } + + JS_FreeValue(e->u.qjs.ctx, exception); + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n) +{ + double num; + + if (JS_ToFloat64(cx, &num, val)) { + return NGX_ERROR; + } + + if (isinf(num) || isnan(num)) { + (void) JS_ThrowTypeError(cx, "invalid number"); + return NGX_ERROR; + } + + *n = num; + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst) +{ + size_t len, byte_offset, byte_length; + u_char *start; + JSValue buffer; + JSContext *cx; + const char *str; + + if (JS_IsNullOrUndefined(val)) { + dst->data = NULL; + dst->len = 0; + return NGX_OK; + } + + cx = e->u.qjs.ctx; + + buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL); + if (!JS_IsException(buffer)) { + start = JS_GetArrayBuffer(cx, &dst->len, buffer); + + JS_FreeValue(cx, buffer); + + if (start != NULL) { + start += byte_offset; + dst->len = byte_length; + + dst->data = njs_mp_alloc(e->pool, dst->len); + if (dst->data == NULL) { + return NGX_ERROR; + } + + memcpy(dst->data, start, dst->len); + return NGX_OK; + } + } + + str = JS_ToCString(cx, val); + if (str == NULL) { + return NGX_ERROR; + } + + len = strlen(str); + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + JS_FreeCString(cx, str); + return NGX_ERROR; + } + + memcpy(start, str, len); + + JS_FreeCString(cx, str); + + dst->data = start; + dst->len = len; + + return NGX_OK; +} + + +static void +ngx_qjs_timer_handler(ngx_event_t *ev) +{ + void *external; + JSContext *cx; + ngx_int_t rc; + ngx_js_ctx_t *ctx; + ngx_qjs_event_t *event; + + event = (ngx_qjs_event_t *) ((u_char *) ev - offsetof(ngx_qjs_event_t, ev)); + + cx = event->ctx; + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + + rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, event->args, + event->nargs); + + ngx_js_del_event(ctx, event); + + ngx_qjs_external_event_finalize(cx)(external, rc); +} + + +static void +ngx_qjs_clear_timer(ngx_qjs_event_t *event) +{ + int i; + JSContext *cx; + + cx = event->ctx; + + if (event->ev.timer_set) { + ngx_del_timer(&event->ev); + } + + JS_FreeValue(cx, event->function); + + for (i = 0; i < (int) event->nargs; i++) { + JS_FreeValue(cx, event->args[i]); + } +} + + +static JSValue +ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int immediate) +{ + int i, n; + void *external; + uint32_t delay; + ngx_js_ctx_t *ctx; + ngx_qjs_event_t *event; + ngx_connection_t *c; + + if (!JS_IsFunction(cx, argv[0])) { + return JS_ThrowTypeError(cx, "first arg must be a function"); + } + + delay = 0; + + if (!immediate && argc >= 2) { + if (JS_ToUint32(cx, &delay, argv[1]) < 0) { + return JS_EXCEPTION; + } + } + + n = immediate ? 1 : 2; + argc = (argc >= n) ? argc - n : 0; + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + + event = ngx_pcalloc(ngx_qjs_external_pool(cx, external), + sizeof(ngx_qjs_event_t) + sizeof(JSValue) * argc); + if (event == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + event->ctx = cx; + event->function = JS_DupValue(cx, argv[0]); + event->nargs = argc; + event->args = (JSValue *) &event[1]; + event->destructor = ngx_qjs_clear_timer; + event->fd = ctx->event_id++; + + c = ngx_qjs_external_connection(cx, external); + + event->ev.log = c->log; + event->ev.data = event; + event->ev.handler = ngx_qjs_timer_handler; + + if (event->nargs != 0) { + for (i = 0; i < argc; i++) { + event->args[i] = JS_DupValue(cx, argv[n + i]); + } + } + + ngx_js_add_event(ctx, event); + + ngx_add_timer(&event->ev, delay); + + return JS_NewInt32(cx, event->fd); +} + + +static JSValue +ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + uint32_t id; + ngx_js_ctx_t *ctx; + ngx_qjs_event_t event_lookup, *event; + njs_rbtree_node_t *rb; + + if (JS_ToUint32(cx, &id, argv[0]) < 0) { + return JS_EXCEPTION; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + event_lookup.fd = id; + + rb = njs_rbtree_find(&ctx->waiting_events, &event_lookup.node); + if (rb == NULL) { + return JS_ThrowReferenceError(cx, "failed to find timer"); + } + + event = (ngx_qjs_event_t *) ((u_char *) rb + - offsetof(ngx_qjs_event_t, node)); + + ngx_js_del_event(ctx, event); + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, +#ifdef NGX_BUILD + (char *) NGX_BUILD, + njs_strlen(NGX_BUILD) +#else + (char *) "", + 0 +#endif + ); +} + + +static JSValue +ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data, + ngx_cycle->prefix.len); +} + + +static JSValue +ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->conf_file.data, + ngx_cycle->conf_file.len); +} + + +static JSValue +ngx_qjs_ext_constant_integer(JSContext *cx, JSValueConst this_val, int magic) +{ + return JS_NewInt32(cx, magic); +} + + +static JSValue +ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->error_log.data, + ngx_cycle->error_log.len); +} + + +static JSValue +ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data, + ngx_cycle->prefix.len); +} + + +static JSValue +ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val) +{ + return JS_NewInt32(cx, ngx_worker); +} + + +static void +ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_queue_t *labels, *q, *next; + ngx_js_console_t *console; + ngx_js_timelabel_t *label; + + console = JS_GetOpaque(val, NGX_QJS_CLASS_ID_CONSOLE); + if (console == (void *) 1) { + return; + } + + labels = &console->labels; + q = ngx_queue_head(labels); + + for ( ;; ) { + if (q == ngx_queue_sentinel(labels)) { + break; + } + + next = ngx_queue_next(q); + + label = ngx_queue_data(q, ngx_js_timelabel_t, queue); + ngx_queue_remove(&label->queue); + js_free_rt(rt, label); + + q = next; + } + + js_free_rt(rt, console); +} + + +static JSValue +ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int magic) +{ + char *p; + uint32_t level; + ngx_str_t msg; + ngx_js_ctx_t *ctx; + ngx_connection_t *c; + + p = JS_GetContextOpaque(cx); + if (p == NULL) { + return JS_ThrowInternalError(cx, "external is not set"); + } + + level = magic & NGX_JS_LOG_MASK; + + if (level == 0) { + if (JS_ToUint32(cx, &level, argv[0]) < 0) { + return JS_EXCEPTION; + } + + argc--; + argv++; + } + + ctx = ngx_qjs_external_ctx(cx, p); + c = ngx_qjs_external_connection(cx, p); + + for ( ; argc > 0; argc--, argv++) { + if (ngx_qjs_dump_obj(ctx->engine, argv[0], &msg) != NGX_OK) { + return JS_EXCEPTION; + } + + ngx_js_logger(c, level, (u_char *) msg.data, msg.len); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + ngx_str_t name; + ngx_queue_t *labels, *q; + ngx_js_console_t *console; + ngx_connection_t *c; + ngx_js_timelabel_t *label; + + static const ngx_str_t default_label = ngx_string("default"); + + console = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_CONSOLE); + if (console == NULL) { + return JS_ThrowInternalError(cx, "this is not a console object"); + } + + if (console == (void *) 1) { + console = js_malloc(cx, sizeof(ngx_js_console_t)); + if (console == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ngx_queue_init(&console->labels); + + JS_SetOpaque(this_val, console); + } + + if (!JS_IsUndefined(argv[0])) { + name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]); + if (name.data == NULL) { + return JS_EXCEPTION; + } + + } else { + name = default_label; + } + + labels = &console->labels; + + for (q = ngx_queue_head(labels); + q != ngx_queue_sentinel(labels); + q = ngx_queue_next(q)) + { + label = ngx_queue_data(q, ngx_js_timelabel_t, queue); + + if (name.len == label->name.length + && ngx_strncmp(name.data, label->name.start, name.len) == 0) + { + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" already" + " exists", &name); + + goto done; + } + } + + label = js_malloc(cx, sizeof(ngx_js_timelabel_t) + name.len); + if (label == NULL) { + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + + return JS_ThrowOutOfMemory(cx); + } + + label->name.length = name.len; + label->name.start = (u_char *) label + sizeof(ngx_js_timelabel_t); + memcpy(label->name.start, name.data, name.len); + + label->time = ngx_js_monotonic_time(); + + ngx_queue_insert_tail(&console->labels, &label->queue); + +done: + + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_console_time_end(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + uint64_t ns, ms; + ngx_str_t name; + ngx_queue_t *labels, *q; + ngx_js_console_t *console; + ngx_connection_t *c; + ngx_js_timelabel_t *label; + + static const ngx_str_t default_label = ngx_string("default"); + + ns = ngx_js_monotonic_time(); + + console = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_CONSOLE); + if (console == NULL) { + return JS_ThrowInternalError(cx, "this is not a console object"); + } + + if (!JS_IsUndefined(argv[0])) { + name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]); + if (name.data == NULL) { + return JS_EXCEPTION; + } + + } else { + name = default_label; + } + + if (console == (void *) 1) { + goto not_found; + } + + labels = &console->labels; + q = ngx_queue_head(labels); + + for ( ;; ) { + if (q == ngx_queue_sentinel(labels)) { + goto not_found; + } + + label = ngx_queue_data(q, ngx_js_timelabel_t, queue); + + if (name.len == label->name.length + && ngx_strncmp(name.data, label->name.start, name.len) == 0) + { + ngx_queue_remove(&label->queue); + break; + } + + q = ngx_queue_next(q); + } + + ns = ns - label->time; + + js_free(cx, label); + + ms = ns / 1000000; + ns = ns % 1000000; + + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: %V: %uL.%06uLms", + &name, ms, ns); + + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + + return JS_UNDEFINED; + +not_found: + + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" doesn't exist", + &name); + + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + + return JS_UNDEFINED; +} + + +static JSModuleDef * +ngx_qjs_module_loader(JSContext *cx, const char *module_name, void *opaque) +{ + JSValue func_val; + njs_int_t ret; + njs_str_t text; + JSModuleDef *m; + njs_module_info_t info; + ngx_js_loc_conf_t *conf; + ngx_js_code_entry_t *pc; + + conf = opaque; + + njs_memzero(&info, sizeof(njs_module_info_t)); + + info.name.start = (u_char *) module_name; + info.name.length = njs_strlen(module_name); + + ret = ngx_js_module_lookup(conf, &info); + if (ret != NJS_OK) { + JS_ThrowReferenceError(cx, "could not load module filename '%s'", + module_name); + return NULL; + } + + ret = ngx_js_module_read(conf->engine->pool, info.fd, &text); + + (void) close(info.fd); + + if (ret != NJS_OK) { + JS_ThrowInternalError(cx, "while reading \"%*s\" module", + (int) info.file.length, info.file.start); + return NULL; + } + + func_val = JS_Eval(cx, (char *) text.start, text.length, module_name, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + + njs_mp_free(conf->engine->pool, text.start); + + if (JS_IsException(func_val)) { + return NULL; + } + + if (conf->engine->precompiled == NULL) { + conf->engine->precompiled = njs_arr_create(conf->engine->pool, 4, + sizeof(ngx_js_code_entry_t)); + if (conf->engine->precompiled == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowOutOfMemory(cx); + return NULL; + } + } + + pc = njs_arr_add(conf->engine->precompiled); + if (pc == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowOutOfMemory(cx); + return NULL; + } + + pc->code = JS_WriteObject(cx, &pc->code_size, func_val, + JS_WRITE_OBJ_BYTECODE); + if (pc->code == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowInternalError(cx, "could not write module bytecode"); + return NULL; + } + + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(cx, func_val); + + return m; +} + + +static int +ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx) +{ + size_t len; + uint32_t i; + JSContext *cx; + const char *str; + ngx_js_rejected_promise_t *rejected_promise; + + if (ctx->rejected_promises == NULL + || ctx->rejected_promises->items == 0) + { + return 0; + } + + cx = ctx->engine->u.qjs.ctx; + rejected_promise = ctx->rejected_promises->start; + + str = JS_ToCStringLen(cx, &len, ngx_qjs_arg(rejected_promise->message)); + if (njs_slow_path(str == NULL)) { + return -1; + } + + JS_ThrowTypeError(cx, "unhandled promise rejection: %*s", (int) len, str); + JS_FreeCString(cx, str); + + for (i = 0; i < ctx->rejected_promises->items; i++) { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); + } + + njs_arr_destroy(ctx->rejected_promises); + ctx->rejected_promises = NULL; + + return 1; +} + + +static void +ngx_qjs_rejection_tracker(JSContext *cx, JSValueConst promise, + JSValueConst reason, JS_BOOL is_handled, void *opaque) +{ + void *promise_obj; + uint32_t i, length; + ngx_js_ctx_t *ctx; + ngx_js_rejected_promise_t *rejected_promise; + + ctx = opaque; + + if (is_handled && ctx->rejected_promises != NULL) { + rejected_promise = ctx->rejected_promises->start; + length = ctx->rejected_promises->items; + + promise_obj = JS_VALUE_GET_PTR(promise); + + for (i = 0; i < length; i++) { + if (JS_VALUE_GET_PTR(ngx_qjs_arg(rejected_promise[i].promise)) + == promise_obj) + { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); + njs_arr_remove(ctx->rejected_promises, &rejected_promise[i]); + + break; + } + } + + return; + } + + if (ctx->rejected_promises == NULL) { + if (ctx->engine == NULL) { + /* Do not track rejections during eval stage. The exception + * is lifted by the ngx_qjs_clone() function manually. */ + return; + } + + ctx->rejected_promises = njs_arr_create(ctx->engine->pool, 4, + sizeof(ngx_js_rejected_promise_t)); + if (ctx->rejected_promises == NULL) { + return; + } + } + + rejected_promise = njs_arr_add(ctx->rejected_promises); + if (rejected_promise == NULL) { + return; + } + + ngx_qjs_arg(rejected_promise->promise) = JS_DupValue(cx, promise); + ngx_qjs_arg(rejected_promise->message) = JS_DupValue(cx, reason); +} + + +static JSModuleDef * +ngx_qjs_core_init(JSContext *cx, const char *name) +{ + int ret; + JSValue global_obj, proto, obj; + JSModuleDef *m; + + if (!JS_IsRegisteredClass(JS_GetRuntime(cx), + NGX_QJS_CLASS_ID_CONSOLE)) + { + if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_CONSOLE, + &ngx_qjs_console_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_console, + njs_nitems(ngx_qjs_ext_console)); + + JS_SetClassProto(cx, NGX_QJS_CLASS_ID_CONSOLE, proto); + } + + obj = JS_NewObject(cx); + if (JS_IsException(obj)) { + return NULL; + } + + JS_SetPropertyFunctionList(cx, obj, ngx_qjs_ext_ngx, + njs_nitems(ngx_qjs_ext_ngx)); + + global_obj = JS_GetGlobalObject(cx); + + JS_SetPropertyFunctionList(cx, global_obj, ngx_qjs_ext_global, + njs_nitems(ngx_qjs_ext_global)); + + ret = JS_SetPropertyStr(cx, global_obj, "ngx", obj); + if (ret < 0) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_CONSOLE); + if (JS_IsException(obj)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetOpaque(obj, (void *) 1); + + ret = JS_SetPropertyStr(cx, global_obj, "console", obj); + if (ret < 0) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_FreeValue(cx, global_obj); + + m = JS_NewCModule(cx, name, NULL); + if (m == NULL) { + return NULL; + } + + return m; +} + +#endif + + ngx_int_t ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args, njs_uint_t nargs) @@ -2439,11 +3937,18 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) return NULL; } + /* + * set by ngx_pcalloc(): + * + * conf->reuse_queue = NULL; + */ + conf->paths = NGX_CONF_UNSET_PTR; conf->type = NGX_CONF_UNSET_UINT; conf->imports = NGX_CONF_UNSET_PTR; conf->preload_objects = NGX_CONF_UNSET_PTR; + conf->reuse = NGX_CONF_UNSET_SIZE; conf->buffer_size = NGX_CONF_UNSET_SIZE; conf->max_response_body_size = NGX_CONF_UNSET_SIZE; conf->timeout = NGX_CONF_UNSET_MSEC; @@ -2507,6 +4012,7 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); + ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128); ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 16384); ngx_conf_merge_size_value(conf->max_response_body_size, prev->max_response_body_size, 1048576); @@ -2562,3 +4068,59 @@ ngx_js_monotonic_time(void) return (uint64_t) tv.tv_sec * 1000000000 + tv.tv_usec * 1000; #endif } + + +ngx_js_queue_t * +ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity) +{ + ngx_js_queue_t *queue; + + queue = ngx_pcalloc(pool, sizeof(ngx_js_queue_t)); + if (queue == NULL) { + return NULL; + } + + queue->data = ngx_pcalloc(pool, sizeof(void *) * capacity); + if (queue->data == NULL) { + return NULL; + } + + queue->head = 0; + queue->tail = 0; + queue->size = 0; + queue->capacity = capacity; + + return queue; +} + + +ngx_int_t +ngx_js_queue_push(ngx_js_queue_t *queue, void *item) +{ + if (queue->size >= queue->capacity) { + return NGX_ERROR; + } + + queue->data[queue->tail] = item; + queue->tail = (queue->tail + 1) % queue->capacity; + queue->size++; + + return NGX_OK; +} + + +void * +ngx_js_queue_pop(ngx_js_queue_t *queue) +{ + void *item; + + if (queue->size == 0) { + return NULL; + } + + item = queue->data[queue->head]; + queue->head = (queue->head + 1) % queue->capacity; + queue->size--; + + return item; +} |