quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

commit c9c4c204176a07c1a67d2bad27e20f5853c104d7
parent 0aafb2206fcbc8039b6c8130bad288309cd5f9cc
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 19:53:11 +0200

quickjs-tart: port local changes to 2026-06-04

Restore the Taler HTTP, host messaging, ArrayBuffer, qjsc, Apple, and
profiling customizations on the new upstream release. Update the Meson
build for dtoa and adapt module loading and event polling to the new
APIs.

Diffstat:
Mmeson.build | 17++++++++---------
Mqtart.c | 3++-
Mquickjs/Makefile | 6+++---
Mquickjs/qjsc.c | 8+++++++-
Aquickjs/quickjs-http.c | 522+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aquickjs/quickjs-http.h | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mquickjs/quickjs-libc.c | 1002++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mquickjs/quickjs-libc.h | 12++++++++++++
Mquickjs/quickjs.c | 339++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mquickjs/quickjs.h | 8+++++++-
Mquickjs/repl.js | 1+
11 files changed, 2060 insertions(+), 69 deletions(-)

diff --git a/meson.build b/meson.build @@ -12,7 +12,6 @@ flags = [ '-D_LARGEFILE_SOURCE', '-D_FILE_OFFSET_BITS=64', '-DCONFIG_VERSION="0.0.1"', - '-DCONFIG_BIGNUM', '-fno-omit-frame-pointer', ] @@ -54,8 +53,8 @@ endif sodium_proj = subproject('libsodium', required : true) sodium_dep = sodium_proj.get_variable('sodium_dep') -# quickjs math library (big float) -libbf = static_library('bf', 'quickjs/libbf.c') +# quickjs floating-point conversion library +dtoa = static_library('dtoa', 'quickjs/dtoa.c') # regular expression library libregexp = static_library('regexp', 'quickjs/libregexp.c') # unicode @@ -118,13 +117,13 @@ avoid_cross_warning = true # native version of quickjs used by the qjsc (.js -> .c compiler), # compiled for the build platform. if avoid_cross_warning or meson.is_cross_build() - libbf_native = static_library('bf_native', 'quickjs/libbf.c', native : true) + dtoa_native = static_library('dtoa_native', 'quickjs/dtoa.c', native : true) libregexp_native = static_library('regexp_native', 'quickjs/libregexp.c', native : true) libunicode_native = static_library('unicode_native', 'quickjs/libunicode.c', native : true) cutils_native = static_library('cutils_native', 'quickjs/cutils.c', native : true) quickjs_native = static_library('quickjs_native', 'quickjs/quickjs.c', native : true) else - libbf_native = libbf + dtoa_native = dtoa libregexp_native = libregexp libunicode_native = libunicode cutils_native = cutils @@ -140,7 +139,7 @@ qjsc_exe = executable('qjsc', [ # Just the compiler, no HTTP support required c_args : ['-DNO_HTTP'], link_with: [ - libbf_native, + dtoa_native, libregexp_native, libunicode_native, cutils_native, @@ -228,7 +227,7 @@ if not meson.is_cross_build() 'qtart.c', ], link_with: [ - libbf, + dtoa, libregexp, libunicode , cutils, @@ -247,7 +246,7 @@ endif if host_machine.system() == 'ios' talerwalletcore_lib = static_library('talerwalletcore', 'taler_wallet_core_lib.c', link_with : [ - libbf, + dtoa, libregexp, libunicode, cutils, @@ -272,7 +271,7 @@ else endif talerwalletcore_lib = shared_library('talerwalletcore', 'taler_wallet_core_lib.c', link_with : [ - libbf, + dtoa, libregexp, libunicode, cutils, diff --git a/qtart.c b/qtart.c @@ -474,7 +474,8 @@ int main(int argc, char **argv) js_os_set_http_impl(rt, http_impl); /* loader for ES6 modules */ - JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); + JS_SetModuleLoaderFunc2(rt, NULL, js_module_loader, + js_module_check_attributes, NULL); JS_SetHostPromiseRejectionTracker(rt, js_std_promise_rejection_tracker, NULL); diff --git a/quickjs/Makefile b/quickjs/Makefile @@ -133,7 +133,7 @@ else ifdef CONFIG_COSMO else HOST_CC=gcc CC=$(CROSS_PREFIX)gcc - CFLAGS+=-g -Wall -MMD -MF $(OBJDIR)/$(@F).d + CFLAGS=-ggdb -fno-omit-frame-pointer -Wall -MMD -MF $(OBJDIR)/$(@F).d CFLAGS += -Wno-array-bounds -Wno-format-truncation -Wno-infinite-recursion ifdef CONFIG_LTO AR=$(CROSS_PREFIX)gcc-ar @@ -253,8 +253,8 @@ QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) -HOST_LIBS=-lm -ldl -lpthread -LIBS=-lm -lpthread +HOST_LIBS=-lm -ldl -lpthread -lcurl -lsodium -lmbedcrypto +LIBS=-lm -lpthread -lcurl -lsodium -lmbedcrypto ifndef CONFIG_WIN32 LIBS+=-ldl endif diff --git a/quickjs/qjsc.c b/quickjs/qjsc.c @@ -352,7 +352,9 @@ static void compile_file(JSContext *ctx, FILE *fo, eval_flags |= JS_EVAL_TYPE_MODULE; else eval_flags |= JS_EVAL_TYPE_GLOBAL; - obj = JS_Eval(ctx, (const char *)buf, buf_len, filename, eval_flags); +// obj = JS_Eval(ctx, (const char *)buf, buf_len, filename, eval_flags); // filename contains full path + obj = JS_Eval(ctx, (const char *)buf, buf_len, "<compiled_js>", eval_flags); // which we don't want to expose + // TODO: In the future we should to make this behavior configurable with a flag to qjsc if (JS_IsException(obj)) { js_std_dump_error(ctx); exit(1); @@ -504,6 +506,10 @@ static int output_executable(const char *out_filename, const char *cfilename, *arg++ = "-lm"; *arg++ = "-ldl"; *arg++ = "-lpthread"; + // FIXME: Make conditional + *arg++ = "-lcurl"; + *arg++ = "-lsodium"; + *arg++ = "-lmbedcrypto"; *arg = NULL; if (verbose) { diff --git a/quickjs/quickjs-http.c b/quickjs/quickjs-http.c @@ -0,0 +1,522 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +#include <stdlib.h> +#include <pthread.h> +#include <stdio.h> +#include <curl/curl.h> +#include <arpa/inet.h> +#include <strings.h> +#include <string.h> +#include <assert.h> + +#include "curl/multi.h" +#include "cutils.h" +#include "quickjs-http.h" +#include "list.h" + +struct CurlClientState { + pthread_t thread; + pthread_mutex_t mutex; + BOOL started; + BOOL stopped; + CURLSH *curlsh; + CURLM *curlm; + int last_request_id; + struct list_head request_list; /* list of CurlRequestState.link */ + struct list_head add_queue; /* multi_add_handle queue */ + struct list_head cancel_queue; /* multi_remove_handle queue */ +}; + +struct CurlRequestState { + struct CurlClientState *ccs; + struct list_head link_req; /* for request_list */ + struct list_head link_add; /* for add_queue */ + struct list_head link_cancel; /* for cancel_queue */ + DynBuf response_data; + BOOL cancelled; + CURL *curl; + int request_id; + enum JSHttpRedirectFlag redirect; + JSHttpResponseCb response_cb; + void *response_cb_cls; + // Request headers + struct curl_slist *req_headers; + struct curl_slist *resp_headers; + char *errbuf; +}; + +// Must only be called with locked client mutex +static void destroy_curl_request_state(struct CurlRequestState *crs) +{ + if (!crs) { + return; + } + + list_del(&crs->link_req); + curl_slist_free_all(crs->req_headers); + curl_slist_free_all(crs->resp_headers); + dbuf_free(&crs->response_data); + if (crs->curl) { + curl_easy_cleanup(crs->curl); + crs->curl = NULL; + } + free(crs->errbuf); + free(crs); +} + +static void * +handle_done(CURL *curl, CURLcode res) +{ + struct CurlRequestState *crs = NULL; + struct CurlClientState *ccs = NULL; + struct JSHttpResponseInfo hri = { 0 }; + long resp_code; + char **headers = NULL; + BOOL cancelled; + + curl_easy_getinfo(curl, CURLINFO_PRIVATE, &crs); + ccs = crs->ccs; + + hri.request_id = crs->request_id; + + if (CURLE_OK == res) { + int num_headers = 0; + int i; + struct curl_slist *sl = crs->resp_headers; + char *url = NULL; + + curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &url); + + if (crs->redirect == JS_HTTP_REDIRECT_ERROR && NULL != url) { + hri.status = 0; + hri.errmsg = crs->errbuf; + strncpy(crs->errbuf, "Got redirect status, but redirects are not allowed for this request", CURL_ERROR_SIZE); + goto done; + } + + while (sl != NULL) { + if (NULL != strchr(sl->data, ':')) { + num_headers++; + } + sl = sl->next; + } + + headers = malloc((num_headers + 1) * sizeof(char *)); + if (!headers) { + hri.status = 0; + goto done; + } + memset(headers, 0, (num_headers + 1) * sizeof (char *)); + sl = crs->resp_headers; + i = 0; + while (sl != NULL) { + if (NULL != strchr(sl->data, ':')) { + headers[i] = sl->data; + i++; + } + sl = sl->next; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp_code); + hri.status = resp_code; + hri.body = crs->response_data.buf; + hri.body_len = crs->response_data.size; + hri.response_headers = headers; + hri.num_response_headers = num_headers; + } else { + hri.status = 0; + hri.errmsg = crs->errbuf; + } + +done: + + pthread_mutex_lock(&ccs->mutex); + cancelled = crs->cancelled; + pthread_mutex_unlock(&ccs->mutex); + + if (cancelled == FALSE) { + // FIXME: What if this CB somehow destroys the client? + crs->response_cb(crs->response_cb_cls, &hri); + } + + free(headers); + pthread_mutex_lock(&ccs->mutex); + destroy_curl_request_state(crs); + pthread_mutex_unlock(&ccs->mutex); + return NULL; +} + +static size_t curl_header_callback(char *buffer, size_t size, + size_t nitems, void *userdata) +{ + struct CurlRequestState *crs = userdata; + size_t sz = size * nitems; + char *hval; + + hval = strndup(buffer, sz); + if (!hval) { + return 0; + } + crs->resp_headers = curl_slist_append(crs->resp_headers, hval); + free(hval); + return sz; +} + + +static size_t curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + struct CurlRequestState *rctx = userp; + + if (0 != dbuf_put(&rctx->response_data, data, realsize)) { + return 0; + } + + return realsize; +} + + +static int +create_impl(void *cls, struct JSHttpRequestInfo *req_info) +{ + struct CurlClientState *ccs = cls; + struct CurlRequestState *crs; + CURL *curl; + BOOL debug = req_info->debug > 0; + const char *method = req_info->method; + + crs = malloc(sizeof *crs); + if (!crs) { + return -1; + } + memset(crs, 0, sizeof *crs); + crs->request_id = ++ccs->last_request_id; + crs->ccs = ccs; + crs->response_cb = req_info->response_cb; + crs->response_cb_cls = req_info->response_cb_cls; + crs->errbuf = malloc(CURL_ERROR_SIZE); + if (!crs->errbuf) { + goto error; + } + memset(crs->errbuf, 0, CURL_ERROR_SIZE); + dbuf_init(&crs->response_data); + + curl = curl_easy_init(); + crs->curl = curl; + curl_easy_setopt(curl, CURLOPT_PRIVATE, crs); + curl_easy_setopt(curl, CURLOPT_SHARE, ccs->curlsh); + curl_easy_setopt(curl, CURLOPT_URL, req_info->url); + curl_easy_setopt(curl, CURLOPT_DNS_SERVERS, "9.9.9.9"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "qtart"); + curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt"); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, crs); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, crs); + + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, crs->errbuf); + +#ifdef QTART_INSECURE_SKIP_TLS_VERIFICATION + // This is only a temporary hack to use the libcurl HTTP client implementation + // on platforms (like iOS) where we can't easily access the root store. + // Outside of testing, such platforms should supply a native HTTP client + // implementation and not use the libcurl implementation compiled + // into qtart. + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); +#endif + + if (req_info->timeout_ms < 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 0L); + } else if (0 == req_info->timeout_ms) { + // Default timeout of 5 minutes. + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5L * 60000L); + } else { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long) req_info->timeout_ms); + } + + if (debug == TRUE) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + + crs->redirect = req_info->redirect; + + switch (req_info->redirect) { + case JS_HTTP_REDIRECT_TRANSPARENT: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + break; + case JS_HTTP_REDIRECT_MANUAL: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + break; + case JS_HTTP_REDIRECT_ERROR: + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + break; + default: + assert(0); + } + + if (0 == strcasecmp(req_info->method, "get")) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } else if (0 == strcasecmp(method, "delete")) { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + } else if (0 == strcasecmp(method, "head")) { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } else if ((0 == strcasecmp(method, "post")) || + (0 == strcasecmp(method, "put"))) { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + if (0 == strcasecmp(method, "put")) { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + } + if (req_info->req_body_len > 0) { + curl_off_t len = req_info->req_body_len; + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, len); + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, req_info->req_body); + } + } else { + goto error; + } + + if (req_info->request_headers != NULL) { + char **h = req_info->request_headers; + while (*h) { + crs->req_headers = curl_slist_append(crs->req_headers, *h); + h++; + } + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, crs->req_headers); + + pthread_mutex_lock(&ccs->mutex); + list_add_tail(&crs->link_add, &ccs->add_queue); + list_add_tail(&crs->link_req, &ccs->request_list); + pthread_mutex_unlock(&ccs->mutex); + + curl_multi_wakeup(ccs->curlm); + + return crs->request_id; +error: + if (crs) { + dbuf_free(&crs->response_data); + if (crs->errbuf) { + free(crs->errbuf); + } + if (crs->curl) { + curl_easy_cleanup(crs->curl); + } + free(crs); + } + return -1; +} + +static int +destroy_impl(void *cls, int request_id) +{ + struct list_head *el; + struct CurlClientState *ccs = cls; + + pthread_mutex_lock(&ccs->mutex); + + list_for_each(el, &ccs->request_list) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); + if (crs->request_id == request_id && !crs->cancelled) { + list_add_tail(&crs->link_cancel, &ccs->cancel_queue); + } + } + + pthread_mutex_unlock(&ccs->mutex); + + curl_multi_wakeup(ccs->curlm); + + return 0; +} + +/** + * Entry point for the thread that processes HTTP requests with libcurl. + */ +static void * +curl_multi_thread_run(void *cls) +{ + struct CurlClientState *ccs = cls; + struct list_head *el, *el1; + int still_running; + struct CURLMsg *m; + BOOL stopped; + + while (1) { + CURLMcode mc; + + mc = curl_multi_perform(ccs->curlm, &still_running); + + if (CURLM_OK != mc) { + fprintf(stderr, "curl_multi_perform failed\n"); + break; + } + + mc = curl_multi_poll(ccs->curlm, NULL, 0, 1000, NULL); + if (CURLM_OK != mc) { + fprintf(stderr, "curl_multi_poll failed\n"); + break; + } + + pthread_mutex_lock(&ccs->mutex); + stopped = ccs->stopped; + pthread_mutex_unlock(&ccs->mutex); + + if (stopped) { + break; + } + + do { + + // Add new requests in queue + pthread_mutex_lock(&ccs->mutex); + list_for_each_safe(el, el1, &ccs->add_queue) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_add); + curl_multi_add_handle(ccs->curlm, crs->curl); + list_del(el); + } + pthread_mutex_unlock(&ccs->mutex); + + // Cancel requests in queue + pthread_mutex_lock(&ccs->mutex); + list_for_each_safe(el, el1, &ccs->cancel_queue) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_cancel); + curl_multi_remove_handle(ccs->curlm, crs->curl); + crs->cancelled = TRUE; + list_del(el); + } + pthread_mutex_unlock(&ccs->mutex); + + // Process finished request + int msgq = 0; + m = curl_multi_info_read(ccs->curlm, &msgq); + if (m && (m->msg == CURLMSG_DONE)) { + CURL *e = m->easy_handle; + curl_multi_remove_handle(ccs->curlm, e); + handle_done(e, m->data.result); + } + } while(m); + } + if (CURLM_OK != curl_multi_cleanup(ccs->curlm)) { + fprintf(stderr, "warning: curl_multi_cleanup failed\n"); + } + if (CURLSHE_OK != curl_share_cleanup(ccs->curlsh)) { + fprintf(stderr, "warning: curl_share_cleanup failed\n"); + } + return NULL; +} + +struct JSHttpClientImplementation * +js_curl_http_client_create() +{ + struct JSHttpClientImplementation *impl = NULL; + struct CurlClientState *ccs = NULL; + int res; + + ccs = malloc(sizeof *ccs); + if (!ccs) { + goto error; + } + + pthread_mutex_init(&ccs->mutex, NULL); + ccs->started = FALSE; + ccs->stopped = FALSE; + ccs->last_request_id = 0; + ccs->curlsh = curl_share_init(); + if (!ccs->curlsh) { + goto error; + } + ccs->curlm = curl_multi_init(); + if (!ccs->curlm) { + goto error; + } + init_list_head(&ccs->request_list); + init_list_head(&ccs->add_queue); + init_list_head(&ccs->cancel_queue); + + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + curl_share_setopt(ccs->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); + + impl = malloc(sizeof *impl); + if (!impl) { + goto error; + } + impl->req_create = &create_impl; + impl->req_cancel = &destroy_impl; + impl->cls = ccs; + + res = pthread_create(&ccs->thread, NULL, &curl_multi_thread_run, ccs); + ccs->started = TRUE; + + if (0 != res) { + goto error; + } + + return impl; +error: + if (ccs) { + curl_share_cleanup(ccs->curlsh); + curl_multi_cleanup(ccs->curlm); + free(ccs); + } + if (impl) { + free(impl); + } + return NULL; +} + +static void +destroy_client_state(struct CurlClientState *ccs) +{ + struct list_head *el, *el1; + if (!ccs) { + return; + } + if (ccs->started == TRUE) { + void *retval; + int res; + + pthread_mutex_lock(&ccs->mutex); + ccs->stopped = TRUE; + pthread_mutex_unlock(&ccs->mutex); + curl_multi_wakeup(ccs->curlm); + res = pthread_join(ccs->thread, &retval); + if (0 != res) { + fprintf(stderr, "warning: could not join with curl thread\n"); + } + ccs->started = FALSE; + } + pthread_mutex_lock(&ccs->mutex); + list_for_each_safe(el, el1, &ccs->request_list) { + struct CurlRequestState *crs = list_entry(el, struct CurlRequestState, link_req); + destroy_curl_request_state(crs); + } + pthread_mutex_unlock(&ccs->mutex); + pthread_mutex_destroy(&ccs->mutex); + free(ccs); +} + +void +js_curl_http_client_destroy(struct JSHttpClientImplementation *impl) +{ + if (!impl) { + return; + } + destroy_client_state(impl->cls); + impl->cls = NULL; + free(impl); +} diff --git a/quickjs/quickjs-http.h b/quickjs/quickjs-http.h @@ -0,0 +1,211 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024 Taler Systems SA + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + + +// ## Native HTTP client library support. + +// Considerations: +// - the API is designed for the HTTP client implementation +// to run in its own thread and *not* be integrated with the +// application's main event loop. +// - focus on small API +// - not a generic HTTP client, only supposed to serve the needs +// of a JS runtime +// - only very tiny subset of HTTP supported +// - no request/response streaming +// - should be appropriate to implement a JS HTTP fetch function +// in the style of WHATWG fetch +// - no focus on ABI compatibility whatsoever + + +#ifndef _QUICKJS_HTTP_H +#define _QUICKJS_HTTP_H + +#include <stdint.h> +#include <limits.h> +#include <stddef.h> + +// Forward declaration; +struct JSHttpResponseInfo; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +enum JSHttpRedirectFlag { + /** + * Handle redirects transparently. + */ + JS_HTTP_REDIRECT_TRANSPARENT = 0, + /** + * Redirect status codes are returned to the client. + * The client can choose to follow them manually (or not). + */ + JS_HTTP_REDIRECT_MANUAL = 1, + /** + * All redirect status codes result in an error. + */ + JS_HTTP_REDIRECT_ERROR = 2, +}; + +/** + * Info needed to start a new HTTP request. + */ +struct JSHttpRequestInfo { + /** + * Callback called with the response for the request. + */ + JSHttpResponseCb response_cb; + + /** + * Closure for response_cb. + */ + void *response_cb_cls; + + /** + * Request URL. + */ + const char *url; + + /** + * Request method. + */ + const char *method; + + /** + * NULL-terminated array of request headers. + */ + char **request_headers; + + /** + * 0: Handle redirects transparently. + * 1: Handle redirects manually. + * 2: Redirects result in an error. + */ + enum JSHttpRedirectFlag redirect; + + /** + * Request timeout in milliseconds. + * + * When 0 is specified, the timeout is the default request + * timeout for the platform. + * + * When -1 is specified, there is no timeout. This might not be + * supported on all platforms. + */ + int timeout_ms; + + /** + * Enable debug output for this request. + */ + int debug; + + /** + * Request body or NULL. + */ + void *req_body; + + /** + * Length or request body or 0. + */ + uint32_t req_body_len; +}; + +/** + * Contents of an HTTP response. + */ +struct JSHttpResponseInfo { + + /** + * Request that this is a response to. + * + * (Think of the request ID like a file descriptor number). + */ + int request_id; + + /** + * HTTP response status code or 0 on error. + */ + int status; + + /** + * When status is 0, error message. + */ + char *errmsg; + + /** + * Array of `num_response_headers` response headers. + */ + char **response_headers; + + /** + * Number of response headers. + */ + int num_response_headers; + + /** + * Response body or NULL. + */ + void *body; + + /** + * Length of the response body or 0. + */ + uint32_t body_len; +}; + +/** + * Callback called when an HTTP response has arrived. + * + * IMPORTANT: May be called from an arbitrary thread. + */ +typedef void (*JSHttpResponseCb)(void *cls, struct JSHttpResponseInfo *resp); + +/** + * Function to create a new HTTP fetch request. + * The request can still be configured until it is started. + * An identifier for the request will be written to @a handle. + * + * @return negative number on error, positive request_id on success + */ +typedef int (*JSHttpReqCreateFn)(void *cls, struct JSHttpRequestInfo *req_info); + +/** + * Cancel a request. The request_id will become invalid + * and the callback won't be called with request_id. + */ +typedef int (*JSHttpReqCancelFn)(void *cls, int request_id); + +struct JSHttpClientImplementation { + /** + * Opaque closure passed to client functions. + */ + void *cls; + JSHttpReqCreateFn req_create; + JSHttpReqCancelFn req_cancel; +}; + + +struct JSHttpClientImplementation * +js_curl_http_client_create(void); + +void +js_curl_http_client_destroy(struct JSHttpClientImplementation *impl); + +#endif /* _QUICKJS_HTTP_H */ diff --git a/quickjs/quickjs-libc.c b/quickjs/quickjs-libc.c @@ -22,11 +22,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +#include "quickjs.h" #include <stdlib.h> #include <stdio.h> #include <stdarg.h> #include <inttypes.h> #include <string.h> +#include <ctype.h> #include <assert.h> #include <unistd.h> #include <errno.h> @@ -34,9 +36,11 @@ #include <sys/time.h> #include <time.h> #include <signal.h> +#include <sys/fcntl.h> #include <limits.h> #include <sys/stat.h> #include <dirent.h> +#include <string.h> #if defined(_WIN32) #include <windows.h> #include <conio.h> @@ -85,6 +89,12 @@ typedef sig_t sighandler_t; - add socket calls */ +#ifndef NO_HTTP +#include <arpa/inet.h> +#include "quickjs-http.h" +#endif + + typedef struct { struct list_head link; int fd; @@ -134,6 +144,35 @@ typedef struct { typedef struct { struct list_head link; + char *msg_data; +} JSHostMessage; + +typedef struct { + pthread_mutex_t mutex; + struct list_head msg_queue; /* list of JSHostMessage.link */ + int read_fd; + int write_fd; +} JSHostMessagePipe; + +typedef struct { + struct list_head link; + int request_id; + int status; + char *errmsg; + char **response_headers; + void *body; + size_t body_len; +} JSHttpMessage; + +typedef struct { + pthread_mutex_t mutex; + struct list_head msg_queue; /* list of JSHttpMessage.link */ + int read_fd; + int write_fd; +} JSHttpMessagePipe; + +typedef struct { + struct list_head link; JSWorkerMessagePipe *recv_pipe; JSValue on_message_func; int poll_fd_index; /* temporary use in js_os_poll() */ @@ -155,10 +194,32 @@ typedef struct JSThreadState { int next_timer_id; /* for setTimeout() */ /* not used in the main thread */ JSWorkerMessagePipe *recv_pipe, *send_pipe; + // send/receive message to/from the host in the main thread + JSHostMessagePipe *host_pipe; + + // receive messages from the HTTP client thread + JSHttpMessagePipe *http_pipe; + + JSValue on_host_message_func; + + JSHostMessageHandlerFn host_message_handler_f; + void *host_message_handler_cls; + + int is_worker_thread; + + // used to provided fairer scheduling of event reactions + unsigned int poll_iteration_count; + + struct list_head http_requests; + +#ifndef NO_HTTP + struct JSHttpClientImplementation *http_client_impl; +#endif + #if !defined(_WIN32) struct pollfd *poll_fds; int poll_fds_size; -#endif +#endif } JSThreadState; static uint64_t os_pending_signals; @@ -483,6 +544,55 @@ static JSValue js_std_loadFile(JSContext *ctx, JSValueConst this_val, return ret; } +static JSValue js_std_writeFile(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + const char *filename_buf = NULL; + const char *data_buf = NULL; + size_t data_len; + FILE *file = NULL; + size_t bytes_written = 0; + JSValue ret = JS_UNDEFINED; + + filename_buf = JS_ToCString(ctx, argv[0]); + if (!filename_buf) { + ret = JS_EXCEPTION; + goto done; + } + + data_buf = JS_ToCStringLen(ctx, &data_len, argv[1]); + if (!data_buf) { + ret = JS_EXCEPTION; + goto done; + } + file = fopen(filename_buf, "w"); + if (!file) { + ret = JS_ThrowReferenceError(ctx, "could not open '%s'", filename_buf); + goto done; + } + + while (bytes_written < data_len) { + size_t sret; + sret = fwrite(data_buf + bytes_written, 1, data_len - bytes_written, file); + if (0 == sret) { + break; + } + bytes_written += sret; + } + + if (bytes_written != data_len) { + JS_ThrowReferenceError(ctx, "could not write all bytes"); + goto done; + } +done: + JS_FreeCString(ctx, filename_buf); + JS_FreeCString(ctx, data_buf); + if (file) { + fclose(file); + } + return ret; +} + typedef JSModuleDef *(JSInitModuleFunc)(JSContext *ctx, const char *module_name); @@ -903,7 +1013,7 @@ static JSValue js_evalScript(JSContext *ctx, JSValueConst this_val, str = JS_ToCStringLen(ctx, &len, argv[0]); if (!str) return JS_EXCEPTION; - if (!ts->recv_pipe && ++ts->eval_script_recurse == 1) { + if (!ts->is_worker_thread && ++ts->eval_script_recurse == 1) { /* install the interrupt handler */ JS_SetInterruptHandler(JS_GetRuntime(ctx), interrupt_handler, NULL); } @@ -914,7 +1024,7 @@ static JSValue js_evalScript(JSContext *ctx, JSValueConst this_val, flags |= JS_EVAL_FLAG_ASYNC; ret = JS_Eval(ctx, str, len, "<evalScript>", flags); JS_FreeCString(ctx, str); - if (!ts->recv_pipe && --ts->eval_script_recurse == 0) { + if (!ts->is_worker_thread && --ts->eval_script_recurse == 0) { /* remove the interrupt handler */ JS_SetInterruptHandler(JS_GetRuntime(ctx), NULL, NULL); os_pending_signals &= ~((uint64_t)1 << SIGINT); @@ -1655,6 +1765,7 @@ static const JSCFunctionListEntry js_std_funcs[] = { JS_CFUNC_DEF("getenviron", 1, js_std_getenviron ), JS_CFUNC_DEF("urlGet", 1, js_std_urlGet ), JS_CFUNC_DEF("loadFile", 1, js_std_loadFile ), + JS_CFUNC_DEF("writeFile", 2, js_std_writeFile ), JS_CFUNC_DEF("strerror", 1, js_std_strerror ), JS_CFUNC_DEF("parseExtJSON", 1, js_std_parseExtJSON ), @@ -1985,7 +2096,7 @@ static JSValue js_os_rename(JSContext *ctx, JSValueConst this_val, static BOOL is_main_thread(JSRuntime *rt) { JSThreadState *ts = JS_GetRuntimeOpaque(rt); - return !ts->recv_pipe; + return !ts->is_worker_thread; } static JSOSRWHandler *find_rh(JSThreadState *ts, int fd) @@ -2172,6 +2283,405 @@ static void free_timer(JSRuntime *rt, JSOSTimer *th) js_free_rt(rt, th); } +#ifndef NO_HTTP + +typedef struct { + // linked list of all requests + struct list_head link; + + int request_id; + + JSValue resolve_func; + JSValue reject_func; + + JSContext* ctx; +} HttpRequestContext; + + +void js_os_set_http_impl(JSRuntime *rt, struct JSHttpClientImplementation *impl) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + + ts->http_client_impl = impl; +} + +int expect_property_str_bool(JSContext *ctx, JSValueConst this_val, const char *prop_name) +{ + JSValue prop_val; + BOOL bool_val; + + prop_val = JS_GetPropertyStr(ctx, this_val, prop_name); + if (JS_IsException(prop_val)) { + return -1; + } + bool_val = JS_ToBool(ctx, prop_val); + JS_FreeValue(ctx, prop_val); + return bool_val; +} + +static void free_http_request_context(HttpRequestContext *req_context) +{ + JSContext *ctx; + JSThreadState *ts; + + if (!req_context) { + return; + } + ctx = req_context->ctx; + ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + ts->http_client_impl->req_cancel(ts->http_client_impl->cls, req_context->request_id); + req_context->ctx = NULL; + JS_FreeValue(ctx, req_context->resolve_func); + JS_FreeValue(ctx, req_context->reject_func); + if (NULL != req_context->link.prev) { + list_del(&req_context->link); + } + js_free(ctx, req_context); +} + +static void js_free_http_message(JSHttpMessage *msg) +{ + if (msg->body) { + free(msg->body); + msg->body = NULL; + } + if (msg->errmsg) { + free(msg->errmsg); + msg->errmsg = NULL; + } + if (msg->response_headers) { + char **h; + for (h = msg->response_headers; *h; h++) { + free(*h); + } + free(msg->response_headers); + msg->response_headers = NULL; + } + free(msg); +} + +static void handle_http_resp(void *cls, struct JSHttpResponseInfo *resp_info) +{ +// printf("received response for request %i from native client\n", resp_info->request_id); + + // Called from a different thread. + // We must enqueue something that the message loop will process + // + HttpRequestContext *req_context = cls; + JSContext *ctx = req_context->ctx; + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHttpMessage *msg; + JSHttpMessagePipe *hp; + + msg = malloc(sizeof (*msg)); + if (!msg) { + goto fail; + } + memset(msg, 0, sizeof (*msg)); + + msg->status = resp_info->status; + msg->request_id = resp_info->request_id; + + if (resp_info->response_headers) { + int num_headers; + + num_headers = resp_info->num_response_headers; + + msg->response_headers = malloc((num_headers + 1) * sizeof (char *)); + if (!msg->response_headers) { + goto fail; + } + + memset(msg->response_headers, 0, (num_headers + 1) * sizeof (char *)); + for (int i = 0; i < num_headers; i++) { + msg->response_headers[i] = strdup(resp_info->response_headers[i]); + if (!msg->response_headers[i]) { + goto fail; + } + } + } else { + msg->response_headers = NULL; + } + + if (resp_info->errmsg != NULL) { + msg->errmsg = strdup(resp_info->errmsg); + if (!msg->errmsg) { + goto fail; + } + } + + if (resp_info->body_len > 0) { + msg->body = malloc(resp_info->body_len); + if (!msg->body) { + goto fail; + } + msg->body_len = resp_info->body_len; + memcpy(msg->body, resp_info->body, resp_info->body_len); + } + + hp = ts->http_pipe; + pthread_mutex_lock(&hp->mutex); + /* indicate that data is present */ + if (list_empty(&hp->msg_queue)) { + uint8_t ch = '\0'; + int ret; + for(;;) { + ret = write(hp->write_fd, &ch, 1); + if (ret == 1) + break; + if (ret < 0 && (errno != EAGAIN || errno != EINTR)) + break; + } + } + list_add_tail(&msg->link, &hp->msg_queue); + pthread_mutex_unlock(&hp->mutex); +// printf("finished handling http response for request %i\n", resp_info->request_id); + return; + fail: + printf("error handling http response for request %i\n", resp_info->request_id); + js_free_http_message(msg); + return; +} + +static JSValue cancel_http_req(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic, + JSValue *func_data) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + int req_id; + int ret; + + JS_ToInt32(ctx, &req_id, func_data[0]); + + // cancel HTTP request + ret = ts->http_client_impl->req_cancel(ts->http_client_impl->cls, req_id); + + return JS_NewInt32(ctx, ret); +} + +static void +free_http_headers(JSContext *ctx, char **headers) +{ + if (!headers) { + return; + } + for (char **h = headers; *h != NULL; h++) { + js_free(ctx, *h); + } + js_free(ctx, headers); +} + +static char **gather_http_headers(JSContext *ctx, JSValueConst js_headers) +{ + JSValue length_prop; + uint32_t length; + char **headers = NULL; + + length_prop = JS_GetPropertyStr(ctx, js_headers, "length"); + if (JS_IsException(length_prop)) { + return NULL; + } + if (0 != JS_ToUint32(ctx, &length, length_prop)) { + return NULL; + } + JS_FreeValue(ctx, length_prop); + + headers = js_mallocz(ctx, (length + 1) * sizeof (char *)); + if (!headers) { + goto exception; + } + + for (uint32_t i = 0; i < length; i++) { + char *hval; + JSValue item = JS_GetPropertyUint32(ctx, js_headers, i); + if (JS_IsException(item)) { + goto exception; + } + const char *cstr = JS_ToCString(ctx, item); + if (!cstr) { + JS_FreeValue(ctx, item); + goto exception; + } + hval = js_strdup(ctx, cstr); + if (!hval) { + goto exception; + } + JS_FreeCString(ctx, cstr); + JS_FreeValue(ctx, item); + headers[i] = hval; + } + return headers; +exception: + free_http_headers(ctx, headers); + return NULL; +} + + +/** + * fetchHttp(url, { method, headers, body }): { + * response: Promise<Response>, + * cancelFn: () => void, + * } + */ +static JSValue js_os_fetchHttp(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue ret_val = JS_UNINITIALIZED; + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + JSValue resolving_funs[2]; + JSValue options = JS_UNINITIALIZED; + JSValue method = JS_UNINITIALIZED; + const char *method_str = NULL; + const char *req_url = NULL; + struct JSHttpRequestInfo req = { 0 }; + HttpRequestContext *req_context = NULL; + BOOL debug = FALSE; + int redirect = 0; + int ret; + + if (NULL == ts->http_client_impl) { + JS_ThrowInternalError(ctx, "no HTTP client implementation available"); + goto exception; + } + + req_context = js_mallocz(ctx, sizeof *req_context); + req_context->ctx = ctx; + + req_url = JS_ToCString(ctx, argv[0]); + if (!req_url) { + goto exception; + } + + options = argv[1]; + if (JS_VALUE_GET_TAG(options) == JS_TAG_UNDEFINED) { + method = JS_NewString(ctx, "get"); + } else if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { + int has_prop_redirect; + + method = JS_GetPropertyStr(ctx, options, "method"); + debug = expect_property_str_bool(ctx, options, "debug"); + + has_prop_redirect = JS_HasPropertyStr(ctx, options, "redirect"); + if (has_prop_redirect < 0) { + goto exception; + } + if (has_prop_redirect) { + int32_t redir_num; + JSValue redir_val = JS_GetPropertyStr(ctx, options, "redirect"); + if (JS_IsException(redir_val)) { + goto exception; + } + if (JS_ToInt32(ctx, &redir_num, redir_val)) { + goto exception; + } + if (redir_num < 0 || redir_num > JS_HTTP_REDIRECT_ERROR) { + JS_ThrowTypeError(ctx, "redirect option out of range"); + goto exception; + } + redirect = redir_num; + } + } else { + JS_ThrowTypeError(ctx, "invalid options"); + goto exception; + } + + if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { + JSValue header_item = JS_GetPropertyStr(ctx, options, "headers"); + if (JS_IsException(header_item)) { + goto exception; + } + if (JS_VALUE_GET_TAG(header_item) == JS_TAG_OBJECT) { + char **headers = gather_http_headers(ctx, header_item); + if (NULL == headers) { + JS_FreeValue(ctx, header_item); + goto exception; + } + req.request_headers = headers; + } + JS_FreeValue(ctx, header_item); + } + if (JS_VALUE_GET_TAG(options) == JS_TAG_OBJECT) { + JSValue data; + uint8_t *data_ptr = NULL; + size_t data_len = 0; + int has_prop; + + has_prop = JS_HasPropertyStr(ctx, options, "data"); + + if (-1 == has_prop) { + goto exception; + } + + if (has_prop) { + data = JS_GetPropertyStr(ctx, options, "data"); + if (JS_IsException(data)) { + goto exception; + } + if (!(JS_IsNull(data) || JS_IsUndefined(data))) { + data_ptr = JS_GetArrayBuffer(ctx, &data_len, data); + if (!data_ptr) { + goto exception; + } + } + req.req_body = data_ptr; + req.req_body_len = data_len; + } + } + + method_str = JS_ToCString(ctx, method); + + req.method = method_str; + req.url = req_url; + req.debug = debug; + req.redirect = redirect; + req.response_cb = &handle_http_resp; + req.response_cb_cls = req_context; + ret = ts->http_client_impl->req_create(ts->http_client_impl->cls, &req); + + if (ret < 0) { + JS_ThrowInternalError(ctx, "failed to create request"); + goto exception; + } + + list_add_tail(&req_context->link, &ts->http_requests); + + // requestId: number + JSValue requestId = JS_NewInt32(ctx, ret); + + // promise: Promise<Response> + JSValue promise = JS_NewPromiseCapability(ctx, resolving_funs); + if (JS_IsException(promise)) { + goto exception; + } + req_context->request_id = ret; + req_context->resolve_func = resolving_funs[0]; + req_context->reject_func = resolving_funs[1]; + + // cancelFn: () => void + JSValue cancelCls = JS_NewInt32(ctx, ret); + JSValue cancelFn = JS_NewCFunctionData(ctx, &cancel_http_req, 0, 0, 1, &cancelCls); + + ret_val = JS_NewObject(ctx); + JS_SetPropertyStr(ctx, ret_val, "requestId", requestId); + JS_SetPropertyStr(ctx, ret_val, "promise", promise); + JS_SetPropertyStr(ctx, ret_val, "cancelFn", cancelFn); + +done: + free_http_headers(ctx, req.request_headers); + JS_FreeValue(ctx, method); + JS_FreeCString(ctx, req_url); + JS_FreeCString(ctx, method_str); + return ret_val; +exception: + ret_val = JS_EXCEPTION; + goto done; + +} + +#endif + static JSValue js_os_setTimeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -2270,8 +2780,10 @@ static void call_handler(JSContext *ctx, JSValueConst func) func1 = JS_DupValue(ctx, func); ret = JS_Call(ctx, func1, JS_UNDEFINED, 0, NULL); JS_FreeValue(ctx, func1); - if (JS_IsException(ret)) + if (JS_IsException(ret)) { + fprintf(stderr, "exception in handler\n"); js_std_dump_error(ctx); + } JS_FreeValue(ctx, ret); } @@ -2544,6 +3056,153 @@ static int js_poll_add_poll_fd(JSThreadState *ts, int *pnfds, int fd, int events return 0; } +/* return 1 if a message was handled, 0 if no message */ +static int handle_host_message(JSRuntime *rt, JSContext *ctx) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHostMessagePipe *hp = ts->host_pipe; + int ret; + struct list_head *el; + JSHostMessage *msg; + JSValue obj, func, retval; + + pthread_mutex_lock(&hp->mutex); + if (!list_empty(&hp->msg_queue)) { + el = hp->msg_queue.next; + msg = list_entry(el, JSHostMessage, link); + + /* remove the message from the queue */ + list_del(&msg->link); + + if (list_empty(&hp->msg_queue)) { + uint8_t buf[16]; + int ret; + for(;;) { + ret = read(hp->read_fd, buf, sizeof(buf)); + if (ret >= 0) + break; + if (errno != EAGAIN && errno != EINTR) + break; + } + } + + obj = JS_NewString(ctx, msg->msg_data); + + free(msg->msg_data); + free(msg); + + pthread_mutex_unlock(&hp->mutex); + + /* 'func' might be destroyed when calling itself (if it frees the + handler), so must take extra care */ + func = JS_DupValue(ctx, ts->on_host_message_func); + retval = JS_Call(ctx, func, JS_UNDEFINED, 1, (JSValueConst *)&obj); + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, func); + if (JS_IsException(retval)) { + js_std_dump_error(ctx); + } else { + JS_FreeValue(ctx, retval); + } + ret = 1; + } else { + pthread_mutex_unlock(&hp->mutex); + ret = 0; + } + return ret; +} + +/* return 1 if a message was handled, 0 if no message */ +#ifndef NO_HTTP +static int handle_http_message(JSRuntime *rt, JSContext *ctx) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHttpMessagePipe *hp = ts->http_pipe; + int ret; + struct list_head *el; + struct list_head *req_el; + JSHttpMessage *msg; + JSValue obj, func, retval; + HttpRequestContext *request_ctx; + + pthread_mutex_lock(&hp->mutex); + if (!list_empty(&hp->msg_queue)) { + el = hp->msg_queue.next; + msg = list_entry(el, JSHttpMessage, link); + + /* remove the message from the queue */ + list_del(&msg->link); + + if (list_empty(&hp->msg_queue)) { + uint8_t buf[16]; + int ret; + for(;;) { + ret = read(hp->read_fd, buf, sizeof(buf)); + if (ret >= 0) + break; + if (errno != EAGAIN && errno != EINTR) + break; + } + } + + pthread_mutex_unlock(&hp->mutex); + + list_for_each(req_el, &ts->http_requests) { + request_ctx = list_entry(req_el, HttpRequestContext, link); + if (request_ctx->request_id == msg->request_id) { + if (msg->status != 0) { + JSValue headers_list = JS_NewArray(ctx); + + obj = JS_NewObject(ctx); + + if (msg->response_headers) { + char **h = msg->response_headers; + while (*h) { + qjs_array_append_new(ctx, headers_list, JS_NewString(ctx, *h)); + h++; + } + } + JS_SetPropertyStr(ctx, obj, "headers", headers_list); + + //JS_SetPropertyStr(ctx, obj, "data", JS_NewTypedArray(ctx, JS_NewArrayBufferCopy(ctx, msg->body, msg->body_len), 1)); + JS_SetPropertyStr(ctx, obj, "data", JS_NewArrayBufferCopy(ctx, msg->body, msg->body_len)); + + JS_SetPropertyStr(ctx, obj, "status", JS_NewInt32(ctx, msg->status)); + func = JS_DupValue(ctx, request_ctx->resolve_func); + retval = JS_Call(ctx, func, JS_UNDEFINED, 1, (JSValueConst *)&obj); + JS_FreeValue(ctx, obj); + JS_FreeValue(ctx, func); + if (JS_IsException(retval)) { + js_std_dump_error(ctx); + } else { + JS_FreeValue(ctx, retval); + } + } else { + JSAtom atom_message; + + atom_message = JS_NewAtom(ctx, "message"); + obj = JS_NewError(ctx); + JS_DefinePropertyValue(ctx, obj, atom_message, + JS_NewString(ctx, msg->errmsg), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + retval = JS_Call(ctx, request_ctx->reject_func, JS_UNDEFINED, 1, &obj); + JS_FreeAtom(ctx, atom_message); + JS_FreeValue(ctx, retval); + } + break; + } + } + + js_free_http_message(msg); + ret = 1; + } else { + pthread_mutex_unlock(&hp->mutex); + ret = 0; + } + return ret; +} +#endif /* NO_HTTP */ + static int js_os_poll(JSContext *ctx) { JSRuntime *rt = JS_GetRuntime(ctx); @@ -2552,9 +3211,14 @@ static int js_os_poll(JSContext *ctx) int64_t cur_time, delay; JSOSRWHandler *rh; struct list_head *el; + BOOL have_http_requests = FALSE; + int host_poll_fd_index; +#ifndef NO_HTTP + int http_poll_fd_index; +#endif /* only check signals in the main thread */ - if (!ts->recv_pipe && + if (!ts->is_worker_thread && unlikely(os_pending_signals != 0)) { JSOSSignalHandler *sh; uint64_t mask; @@ -2570,10 +3234,20 @@ static int js_os_poll(JSContext *ctx) } } - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && - list_empty(&ts->port_list)) +#ifndef NO_HTTP + have_http_requests = !list_empty(&ts->http_requests); +#endif + + if ((!have_http_requests) && list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && + list_empty(&ts->port_list) && JS_IsNull(ts->on_host_message_func)) { return -1; /* no more events */ + } + /* Handle host messages here so we don't get starved by timers. */ + if (handle_host_message(rt, ctx)) { + goto done; + } + if (!list_empty(&ts->os_timers)) { cur_time = get_time_ms(); min_delay = 10000; @@ -2624,35 +3298,67 @@ static int js_os_poll(JSContext *ctx) } } + host_poll_fd_index = nfds; + if (js_poll_add_poll_fd(ts, &nfds, ts->host_pipe->read_fd, POLLIN)) + return -1; +#ifndef NO_HTTP + http_poll_fd_index = nfds; + if (js_poll_add_poll_fd(ts, &nfds, ts->http_pipe->read_fd, POLLIN)) + return -1; +#endif + nfds = poll(ts->poll_fds, nfds, min_delay); if (nfds > 0) { - list_for_each(el, &ts->os_rw_handlers) { - rh = list_entry(el, JSOSRWHandler, link); - if (!JS_IsNull(rh->rw_func[0]) && - (ts->poll_fds[rh->poll_fd_index].revents & (POLLERR | POLLHUP | POLLNVAL | POLLIN))) { - call_handler(ctx, rh->rw_func[0]); - /* must stop because the list may have been modified */ - goto done; + /* Start with a different event type on every iteration for fairness. */ + switch (ts->poll_iteration_count % 4) { + case 0: + list_for_each(el, &ts->os_rw_handlers) { + rh = list_entry(el, JSOSRWHandler, link); + if (!JS_IsNull(rh->rw_func[0]) && + (ts->poll_fds[rh->poll_fd_index].revents & + (POLLERR | POLLHUP | POLLNVAL | POLLIN))) { + call_handler(ctx, rh->rw_func[0]); + /* must stop because the list may have been modified */ + goto done; + } + if (!JS_IsNull(rh->rw_func[1]) && + (ts->poll_fds[rh->poll_fd_index].revents & + (POLLERR | POLLHUP | POLLNVAL | POLLOUT))) { + call_handler(ctx, rh->rw_func[1]); + /* must stop because the list may have been modified */ + goto done; + } } - if (!JS_IsNull(rh->rw_func[1]) && - (ts->poll_fds[rh->poll_fd_index].revents & (POLLERR | POLLHUP | POLLNVAL | POLLOUT))) { - call_handler(ctx, rh->rw_func[1]); - /* must stop because the list may have been modified */ + /* fallthrough */ + case 1: + list_for_each(el, &ts->port_list) { + JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); + if (!JS_IsNull(port->on_message_func) && + ts->poll_fds[port->poll_fd_index].revents != 0 && + handle_posted_message(rt, ctx, port)) { + goto done; + } + } + /* fallthrough */ + case 2: + if (ts->poll_fds[host_poll_fd_index].revents != 0 && + handle_host_message(rt, ctx)) { goto done; } - } - - list_for_each(el, &ts->port_list) { - JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); - if (!JS_IsNull(port->on_message_func)) { - if (ts->poll_fds[port->poll_fd_index].revents != 0) { - if (handle_posted_message(rt, ctx, port)) - goto done; - } + /* fallthrough */ + case 3: +#ifndef NO_HTTP + if (ts->poll_fds[http_poll_fd_index].revents != 0 && + handle_http_message(rt, ctx)) { + goto done; } +#endif + break; } } + done: + ts->poll_iteration_count++; return 0; } #endif /* !_WIN32 */ @@ -3535,6 +4241,50 @@ static JSWorkerMessagePipe *js_new_message_pipe(void) return ps; } +static JSHostMessagePipe *js_new_host_message_pipe(void) +{ + JSHostMessagePipe *ps; + int pipe_fds[2]; + + if (pipe(pipe_fds) < 0) + return NULL; + + ps = malloc(sizeof(*ps)); + if (!ps) { + close(pipe_fds[0]); + close(pipe_fds[1]); + return NULL; + } + init_list_head(&ps->msg_queue); + pthread_mutex_init(&ps->mutex, NULL); + ps->read_fd = pipe_fds[0]; + ps->write_fd = pipe_fds[1]; + return ps; +} + +#ifndef NO_HTTP +static JSHttpMessagePipe *js_new_http_message_pipe(void) +{ + JSHttpMessagePipe *ps; + int pipe_fds[2]; + + if (pipe(pipe_fds) < 0) + return NULL; + + ps = malloc(sizeof(*ps)); + if (!ps) { + close(pipe_fds[0]); + close(pipe_fds[1]); + return NULL; + } + init_list_head(&ps->msg_queue); + pthread_mutex_init(&ps->mutex, NULL); + ps->read_fd = pipe_fds[0]; + ps->write_fd = pipe_fds[1]; + return ps; +} +#endif + static JSWorkerMessagePipe *js_dup_message_pipe(JSWorkerMessagePipe *ps) { atomic_add_int(&ps->ref_count, 1); @@ -3575,6 +4325,52 @@ static void js_free_message_pipe(JSWorkerMessagePipe *ps) } } +static void js_free_host_message(JSHostMessage *msg) +{ + free(msg->msg_data); + free(msg); +} + +static void js_free_host_message_pipe(JSHostMessagePipe *ps) +{ + struct list_head *el, *el1; + JSHostMessage *msg; + + if (!ps) + return; + + list_for_each_safe(el, el1, &ps->msg_queue) { + msg = list_entry(el, JSHostMessage, link); + js_free_host_message(msg); + } + pthread_mutex_destroy(&ps->mutex); + close(ps->read_fd); + close(ps->write_fd); + free(ps); +} + +#ifndef NO_HTTP + +static void js_free_http_message_pipe(JSHttpMessagePipe *ps) +{ + struct list_head *el, *el1; + JSHttpMessage *msg; + + if (!ps) + return; + + list_for_each_safe(el, el1, &ps->msg_queue) { + msg = list_entry(el, JSHttpMessage, link); + js_free_http_message(msg); + } + pthread_mutex_destroy(&ps->mutex); + close(ps->read_fd); + close(ps->write_fd); + free(ps); +} + +#endif + static void js_free_port(JSRuntime *rt, JSWorkerMessageHandler *port) { if (port) { @@ -3637,7 +4433,8 @@ static void *worker_func(void *opaque) ts = JS_GetRuntimeOpaque(rt); ts->recv_pipe = args->recv_pipe; ts->send_pipe = args->send_pipe; - + ts->is_worker_thread = TRUE; + /* function pointer to avoid linking the whole JS_NewContext() if not needed */ ctx = js_worker_new_context_func(rt); @@ -3846,7 +4643,6 @@ static JSValue js_worker_postMessage(JSContext *ctx, JSValueConst this_val, js_free(ctx, data); js_free(ctx, sab_tab); return JS_EXCEPTION; - } static JSValue js_worker_set_onmessage(JSContext *ctx, JSValueConst this_val, @@ -3905,6 +4701,110 @@ static const JSCFunctionListEntry js_worker_proto_funcs[] = { #endif /* USE_WORKER */ +int +js_os_post_message_from_host(JSContext *ctx, const char *msg_str) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSHostMessage *msg; + JSHostMessagePipe *hp; + + msg = malloc(sizeof (*msg)); + if (!msg) { + goto fail; + } + msg->msg_data = strdup(msg_str); + if (!msg->msg_data) { + goto fail; + } + + hp = ts->host_pipe; + pthread_mutex_lock(&hp->mutex); + /* indicate that data is present */ + if (list_empty(&hp->msg_queue)) { + uint8_t ch = '\0'; + int ret; + for(;;) { + ret = write(hp->write_fd, &ch, 1); + if (ret == 1) + break; + if (ret < 0 && (errno != EAGAIN || errno != EINTR)) + break; + } + } + list_add_tail(&msg->link, &hp->msg_queue); + pthread_mutex_unlock(&hp->mutex); + return 0; + fail: + if (msg) { + free(msg->msg_data); + free(msg); + } + return -1; +} + +static JSValue js_os_simulateHostMessage(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + const char *s; + + s = JS_ToCString(ctx, argv[0]); + + if (!s) { + return JS_EXCEPTION; + } + + js_os_post_message_from_host(ctx, s); + + return JS_UNDEFINED; +} + +void js_os_set_host_message_handler(JSContext *ctx, JSHostMessageHandlerFn f, void *cls) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + ts->host_message_handler_f = f; + ts->host_message_handler_cls = cls; +} + +static JSValue js_os_postHostMessage(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + const char *s; + + s = JS_ToCString(ctx, argv[0]); + + if (!s) { + return JS_EXCEPTION; + } + + if (NULL != ts->host_message_handler_f) { + ts->host_message_handler_f(ts->host_message_handler_cls, s); + } + + JS_FreeCString(ctx, s); + + return JS_UNDEFINED; +} + +static JSValue js_os_setMessageFromHostHandler(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = JS_GetRuntimeOpaque(rt); + JSValue func = argv[0]; + + if (JS_IsNull(func)) { + JS_FreeValue(ctx, ts->on_host_message_func); + ts->on_host_message_func = JS_NULL; + } else { + if (!JS_IsFunction(ctx, func)) + return JS_ThrowTypeError(ctx, "not a function"); + JS_FreeValue(ctx, ts->on_host_message_func); + ts->on_host_message_func = JS_DupValue(ctx, func); + } + return JS_UNDEFINED; +} + void js_std_set_worker_new_context_func(JSContext *(*func)(JSRuntime *rt)) { #ifdef USE_WORKER @@ -3970,6 +4870,9 @@ static const JSCFunctionListEntry js_os_funcs[] = { #endif JS_CFUNC_DEF("now", 0, js_os_now ), JS_CFUNC_DEF("setTimeout", 2, js_os_setTimeout ), +#ifndef NO_HTTP + JS_CFUNC_DEF("fetchHttp", 2, js_os_fetchHttp ), +#endif JS_CFUNC_DEF("clearTimeout", 1, js_os_clearTimeout ), JS_CFUNC_DEF("sleepAsync", 1, js_os_sleepAsync ), JS_PROP_STRING_DEF("platform", OS_PLATFORM, 0 ), @@ -4007,6 +4910,9 @@ static const JSCFunctionListEntry js_os_funcs[] = { JS_CFUNC_DEF("dup", 1, js_os_dup ), JS_CFUNC_DEF("dup2", 2, js_os_dup2 ), #endif + JS_CFUNC_DEF("postMessageToHost", 1, js_os_postHostMessage ), + JS_CFUNC_DEF("simulateHostMessageFromHost", 1, js_os_simulateHostMessage ), + JS_CFUNC_DEF("setMessageFromHostHandler", 1, js_os_setMessageFromHostHandler ), }; static int js_os_init(JSContext *ctx, JSModuleDef *m) @@ -4136,6 +5042,7 @@ void js_std_init_handlers(JSRuntime *rt) ts = malloc(sizeof(*ts)); if (!ts) { +oom_fail: fprintf(stderr, "Could not allocate memory for the worker"); exit(1); } @@ -4144,11 +5051,26 @@ void js_std_init_handlers(JSRuntime *rt) init_list_head(&ts->os_signal_handlers); init_list_head(&ts->os_timers); init_list_head(&ts->port_list); + ts->on_host_message_func = JS_NULL; + ts->host_pipe = js_new_host_message_pipe(); + if (!ts->host_pipe) { + goto oom_fail; + } init_list_head(&ts->rejected_promise_list); +#ifndef NO_HTTP + ts->http_pipe = js_new_http_message_pipe(); + if (!ts->http_pipe) { + goto oom_fail; + } +#endif ts->next_timer_id = 1; JS_SetRuntimeOpaque(rt, ts); +#ifndef NO_HTTP + init_list_head(&ts->http_requests); +#endif + #ifdef USE_WORKER /* set the SharedArrayBuffer memory handlers */ { @@ -4182,6 +5104,14 @@ void js_std_free_handlers(JSRuntime *rt) free_timer(rt, th); } +#ifndef NO_HTTP + list_for_each_safe(el, el1, &ts->http_requests) { + HttpRequestContext *request_ctx = list_entry(el, HttpRequestContext, link); + free_http_request_context(request_ctx); + } +#endif + + JS_FreeValueRT(rt, ts->on_host_message_func); list_for_each_safe(el, el1, &ts->rejected_promise_list) { JSRejectedPromiseEntry *rp = list_entry(el, JSRejectedPromiseEntry, link); JS_FreeValueRT(rt, rp->promise); @@ -4205,6 +5135,12 @@ void js_std_free_handlers(JSRuntime *rt) free(ts->poll_fds); #endif + js_free_host_message_pipe(ts->host_pipe); + +#ifndef NO_HTTP + js_free_http_message_pipe(ts->http_pipe); +#endif + free(ts); JS_SetRuntimeOpaque(rt, NULL); /* fail safe */ } @@ -4305,9 +5241,10 @@ void js_std_loop(JSContext *ctx) } js_std_promise_rejection_check(ctx); - - if (!os_poll_func || os_poll_func(ctx)) + + if (!os_poll_func || os_poll_func(ctx)) { break; + } } } @@ -4400,4 +5337,3 @@ void js_std_eval_binary_json_module(JSContext *ctx, exit(1); } } - diff --git a/quickjs/quickjs-libc.h b/quickjs/quickjs-libc.h @@ -29,10 +29,16 @@ #include "quickjs.h" +#ifndef NO_HTTP +#include "quickjs-http.h" +#endif + #ifdef __cplusplus extern "C" { #endif +typedef void (*JSHostMessageHandlerFn)(void *cls, const char *msg); + JSModuleDef *js_init_module_std(JSContext *ctx, const char *module_name); JSModuleDef *js_init_module_os(JSContext *ctx, const char *module_name); void js_std_add_helpers(JSContext *ctx, int argc, char **argv); @@ -58,7 +64,13 @@ void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, JSValueConst reason, JS_BOOL is_handled, void *opaque); void js_std_set_worker_new_context_func(JSContext *(*func)(JSRuntime *rt)); +void js_os_set_host_message_handler(JSContext *ctx, JSHostMessageHandlerFn f, void *cls); +int js_os_post_message_from_host(JSContext *ctx, const char *msg_str); +#ifndef NO_HTTP +void js_os_set_http_impl(JSRuntime *rt, struct JSHttpClientImplementation *impl); +#endif + #ifdef __cplusplus } /* extern "C" { */ #endif diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c @@ -72,7 +72,7 @@ #define CONFIG_ATOMICS #endif -#if !defined(__EMSCRIPTEN__) +#if !defined(__EMSCRIPTEN__) && !defined(__APPLE__) /* enable stack limitation */ #define CONFIG_STACK_CHECK #endif @@ -113,6 +113,9 @@ /* test the GC by forcing it before each object allocation */ //#define FORCE_GC_AT_MALLOC +/* use function call trampolines for better output in profiling tools */ +//#define PERF_TRAMPOLINE + #ifdef CONFIG_ATOMICS #include <pthread.h> #include <stdatomic.h> @@ -713,6 +716,7 @@ typedef struct JSFunctionBytecode { JSValue *cpool; /* constant pool (self pointer) */ int cpool_count; int closure_var_count; + void *perf_trampoline; struct { /* debug info, move to separate structure to save memory? */ JSAtom filename; @@ -8950,6 +8954,17 @@ int JS_PreventExtensions(JSContext *ctx, JSValueConst obj) } /* return -1 if exception otherwise TRUE or FALSE */ +int JS_HasPropertyStr(JSContext *ctx, JSValueConst obj, const char *propname) +{ + JSAtom atom; + int ret; + atom = JS_NewAtom(ctx, propname); + ret = JS_HasProperty(ctx, obj, atom); + JS_FreeAtom(ctx, atom); + return ret; +} + +/* return -1 if exception otherwise TRUE or FALSE */ int JS_HasProperty(JSContext *ctx, JSValueConst obj, JSAtom prop) { JSObject *p; @@ -17769,7 +17784,7 @@ typedef enum { #endif /* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */ -static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, +static JSValue __JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JSValueConst this_obj, JSValueConst new_target, int argc, JSValue *argv, int flags) { @@ -20593,6 +20608,179 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, return ret_val; } +#ifdef PERF_TRAMPOLINE + +#include <sys/mman.h> +#include <fcntl.h> +#include <unistd.h> + +static FILE * +perf_map_get_file(void) +{ + static FILE *perf_map_file = NULL; + if (perf_map_file) { + return perf_map_file; + } + char filename[100]; + pid_t pid = getpid(); + // Location and file name of perf map is hard-coded in perf tool. + // Use exclusive create flag wit nofollow to prevent symlink attacks. + int flags = O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC; + snprintf(filename, sizeof(filename) - 1, "/tmp/perf-%jd.map", + (intmax_t)pid); + int fd = open(filename, flags, 0600); + if (fd == -1) { + return NULL; + } + perf_map_file = fdopen(fd, "w"); + if (!perf_map_file) { + close(fd); + return NULL; + } + return perf_map_file; +} + +static void +perf_map_write_entry(JSContext *ctx, const void *code_addr, unsigned int code_size, JSFunctionBytecode *b) +{ + FILE *method_file = perf_map_get_file(); + const char *atom_entry = NULL; + const char *atom_filename = NULL; + const char *filename = NULL; + int line = 0; + if (b->has_debug) { + int col; + line = find_line_num(ctx, b, -1, &col); + } + if (b->func_name != JS_ATOM_NULL) { + atom_entry = JS_AtomToCString(ctx, b->func_name); + } + if (b->has_debug && b->debug.filename != JS_ATOM_NULL) { + atom_filename = JS_AtomToCString(ctx, b->debug.filename); + } + if (NULL == atom_filename) { + filename = "<unknown>"; + } else { + filename = atom_filename; + } + if (NULL == atom_entry) { + fprintf(method_file, "%p %x js@%s:%u\n", code_addr, code_size, filename, line); + } else { + fprintf(method_file, "%p %x js::%s@%s:%u\n", code_addr, code_size, atom_entry, filename, line); + } + fflush(method_file); + JS_FreeCString(ctx, atom_entry); + JS_FreeCString(ctx, atom_filename); +} + +typedef struct { + JSContext *caller_ctx; + JSValueConst func_obj; + JSValueConst this_obj; + JSValueConst new_target; + int argc; + JSValue *argv; + int flags; +} CallInternalArgs; + +typedef JSValue CallFn(CallInternalArgs *args); +typedef JSValue TrampolineFn(CallInternalArgs *args, CallFn fn); + +/** + * For x86-64: + * push %rbp; mov %rsp,%rbp; call *%rsi; pop %rbp; ret; +*/ +char perf_trampoline_code[] = {0x55, 0x48, 0x89, 0xe5, 0xff, 0xd6, 0x5d, 0xc3}; + +void *compile_trampoline() +{ + size_t mem_size = 4096 * 16; + char *memory = + mmap(NULL, // address + mem_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, + -1, // fd (not used here) + 0); // offset (not used here) + memcpy(memory, perf_trampoline_code, 8); + mprotect(memory, mem_size, PROT_READ | PROT_EXEC); + return memory; +} + +static JSValue fallback_trampoline(CallInternalArgs *ci_args, CallFn fn) +{ + JSValue value; + value = fn(ci_args); + return value; +} + +static JSValue JS_CallInternalStruct(CallInternalArgs *ci_args) +{ + return __JS_CallInternal(ci_args->caller_ctx, ci_args->func_obj, + ci_args->this_obj, ci_args->new_target, + ci_args->argc, ci_args->argv, ci_args->flags); +} + +static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, + JSValueConst this_obj, JSValueConst new_target, + int argc, JSValue *argv, int flags) +{ + CallInternalArgs ci_args; + JSObject *p; + JSFunctionBytecode *b = NULL; + + ci_args.caller_ctx = caller_ctx; + ci_args.func_obj = func_obj; + ci_args.this_obj = this_obj; + ci_args.new_target = new_target; + ci_args.argc = argc; + ci_args.argv = argv; + ci_args.flags = flags; + + if (unlikely(JS_VALUE_GET_TAG(func_obj) != JS_TAG_OBJECT)) { + if (flags & JS_CALL_FLAG_GENERATOR) { + JSAsyncFunctionState *s = JS_VALUE_GET_PTR(func_obj); + JSStackFrame *sf; + /* func_obj get contains a pointer to JSFuncAsyncState */ + /* the stack frame is already allocated */ + sf = &s->frame; + p = JS_VALUE_GET_OBJ(sf->cur_func); + b = p->u.func.function_bytecode; + } + } else { + p = JS_VALUE_GET_OBJ(func_obj); + if (p->class_id == JS_CLASS_BYTECODE_FUNCTION) { + b = p->u.func.function_bytecode; + } + } + + if (b) { + TrampolineFn *fn; + if (!b->perf_trampoline) { + b->perf_trampoline = compile_trampoline(); + if (b->perf_trampoline) { + perf_map_write_entry(caller_ctx, b->perf_trampoline, 8, b); + } + } + fn = b->perf_trampoline; + if (fn) { + return fn(&ci_args, JS_CallInternalStruct); + } + } + + return fallback_trampoline(&ci_args, JS_CallInternalStruct); + //return __JS_CallInternal(caller_ctx, func_obj, this_obj, new_target, argc, argv, flags); +} + +#else + +static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, + JSValueConst this_obj, JSValueConst new_target, + int argc, JSValue *argv, int flags) +{ + return __JS_CallInternal(caller_ctx, func_obj, this_obj, new_target, argc, argv, flags); +} + +#endif /* PERF_TRAMPOLINE */ + #ifdef OPCODE_ASM_LABEL #pragma GCC diagnostic pop #endif @@ -36083,6 +36271,8 @@ static JSValue js_create_function(JSContext *ctx, JSFunctionDef *fd) b->stack_size = stack_size; + b->perf_trampoline = NULL; + if (fd->strip_debug) { JS_FreeAtom(ctx, fd->filename); dbuf_free(&fd->pc2line); // probably useless @@ -37134,8 +37324,9 @@ static JSValue __JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, fd->js_mode = js_mode; fd->func_name = JS_DupAtom(ctx, JS_ATOM__eval_); if (b) { - if (add_closure_variables(ctx, fd, b, scope_idx)) + if (add_closure_variables(ctx, fd, b, scope_idx)) { goto fail; + } } fd->module = m; if (m != NULL || (flags & JS_EVAL_FLAG_ASYNC)) { @@ -42612,6 +42803,33 @@ static JSValue js_array_pop(JSContext *ctx, JSValueConst this_val, return JS_EXCEPTION; } +int qjs_array_append_new(JSContext *ctx, JSValue this_val, JSValue item) +{ + JSValue obj; + int64_t len, from, newLen; + + obj = JS_ToObject(ctx, this_val); + if (js_get_length64(ctx, &len, obj)) + goto exception; + newLen = len + 1; + if (newLen > MAX_SAFE_INTEGER) { + JS_ThrowTypeError(ctx, "Array loo long"); + goto exception; + } + from = len; + if (JS_SetPropertyInt64(ctx, obj, from, item) < 0) + goto exception; + if (JS_SetProperty(ctx, obj, JS_ATOM_length, JS_NewInt64(ctx, newLen)) < 0) + goto exception; + + JS_FreeValue(ctx, obj); + return 0; + + exception: + JS_FreeValue(ctx, obj); + return -1; +} + static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int unshift) { @@ -56848,6 +57066,20 @@ void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj) js_array_buffer_update_typed_arrays(abuf); } +/* check if obj is ArrayBuffer or SharedArrayBuffer */ +static BOOL js_is_array_buffer(JSContext *ctx, JSValueConst obj) +{ + JSObject *p; + if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT) + return FALSE; + p = JS_VALUE_GET_OBJ(obj); + if (p->class_id != JS_CLASS_ARRAY_BUFFER && + p->class_id != JS_CLASS_SHARED_ARRAY_BUFFER) { + return FALSE; + } + return TRUE; +} + /* get an ArrayBuffer or SharedArrayBuffer */ static JSArrayBuffer *js_get_array_buffer(JSContext *ctx, JSValueConst obj) { @@ -56864,24 +57096,6 @@ static JSArrayBuffer *js_get_array_buffer(JSContext *ctx, JSValueConst obj) return p->u.array_buffer; } -/* return NULL if exception. WARNING: any JS call can detach the - buffer and render the returned pointer invalid */ -uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj) -{ - JSArrayBuffer *abuf = js_get_array_buffer(ctx, obj); - if (!abuf) - goto fail; - if (abuf->detached) { - JS_ThrowTypeErrorDetachedArrayBuffer(ctx); - goto fail; - } - *psize = abuf->byte_length; - return abuf->data; - fail: - *psize = 0; - return NULL; -} - static BOOL array_buffer_is_resizable(const JSArrayBuffer *abuf) { return abuf->max_byte_length >= 0; @@ -57297,6 +57511,68 @@ JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj, return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, ta->buffer)); } +BOOL JS_IsArrayBuffer(JSValueConst obj) +{ + JSObject *p; + + if (JS_VALUE_GET_TAG(obj) != JS_TAG_OBJECT) + return FALSE; + p = JS_VALUE_GET_OBJ(obj); + if (p->class_id == JS_CLASS_ARRAY_BUFFER || + p->class_id == JS_CLASS_SHARED_ARRAY_BUFFER) { + return TRUE; + } + if (p->class_id >= JS_CLASS_UINT8C_ARRAY && + p->class_id <= JS_CLASS_FLOAT64_ARRAY) { + return TRUE; + } + return FALSE; +} + +/* return NULL if exception. WARNING: any JS call can detach the + buffer and render the returned pointer invalid */ +uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj) +{ + if (js_is_array_buffer(ctx, obj)) { + JSArrayBuffer* abuf = js_get_array_buffer(ctx, obj); + if (!abuf) + goto fail; + if (abuf->detached) { + JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + goto fail; + } + *psize = abuf->byte_length; + return abuf->data; + } else { + JSObject* p; + JSTypedArray* ta; + JSArrayBuffer* abuf; + p = get_typed_array(ctx, obj); + if (!p) { + goto fail; + } + if (typed_array_is_oob(p)) { + JS_ThrowTypeErrorArrayBufferOOB(ctx); + goto fail; + } + ta = p->u.typed_array; + abuf = ta->buffer->u.array_buffer; + if (!abuf) + goto fail; + if (abuf->detached) { + JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + goto fail; + } + *psize = ta->length; + return abuf->data + ta->offset; + } + JS_ThrowTypeError(ctx, "expected ArrayBuffer or ArrayBufferView"); +fail: + *psize = 0; + return NULL; +} + + static JSValue js_typed_array_get_toStringTag(JSContext *ctx, JSValueConst this_val) { @@ -59634,6 +59910,27 @@ static int typed_array_init(JSContext *ctx, JSValueConst obj, return 0; } +JSValue JS_NewTypedArraySimple(JSContext *ctx, JSValue array_buf, size_t bytes_per_element) +{ + JSValue obj; + JSObject *p = JS_VALUE_GET_OBJ(array_buf); + JSArrayBuffer *abuf = NULL; + + if (p->class_id != JS_CLASS_ARRAY_BUFFER) { + return JS_ThrowTypeError(ctx, "expected array buffer"); + } + abuf = p->u.array_buffer; + if (abuf->detached) { + return JS_ThrowTypeErrorDetachedArrayBuffer(ctx); + } + obj = JS_NewObjectClass(ctx, JS_CLASS_UINT8_ARRAY); + if (typed_array_init(ctx, obj, array_buf, 0, abuf->byte_length, FALSE)) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + return obj; +} + static JSValue js_array_from_iterator(JSContext *ctx, uint32_t *plen, JSValueConst obj, JSValueConst method) diff --git a/quickjs/quickjs.h b/quickjs/quickjs.h @@ -759,6 +759,9 @@ static inline const char *JS_ToCString(JSContext *ctx, JSValueConst val1) } void JS_FreeCString(JSContext *ctx, const char *ptr); + +int qjs_array_append_new (JSContext *ctx, JSValue array, JSValue item); + JSValue JS_NewObjectProtoClass(JSContext *ctx, JSValueConst proto, JSClassID class_id); JSValue JS_NewObjectClass(JSContext *ctx, int class_id); JSValue JS_NewObjectProto(JSContext *ctx, JSValueConst proto); @@ -801,6 +804,7 @@ int JS_SetPropertyInt64(JSContext *ctx, JSValueConst this_obj, int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *prop, JSValue val); int JS_HasProperty(JSContext *ctx, JSValueConst this_obj, JSAtom prop); +int JS_HasPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *propname); int JS_IsExtensible(JSContext *ctx, JSValueConst obj); int JS_PreventExtensions(JSContext *ctx, JSValueConst obj); int JS_DeleteProperty(JSContext *ctx, JSValueConst obj, JSAtom prop, int flags); @@ -871,8 +875,10 @@ typedef void JSFreeArrayBufferDataFunc(JSRuntime *rt, void *opaque, void *ptr); JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len, JSFreeArrayBufferDataFunc *free_func, void *opaque, JS_BOOL is_shared); +JSValue JS_NewTypedArraySimple(JSContext *ctx, JSValue array_buf, size_t bytes_per_element); JSValue JS_NewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len); void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj); +JS_BOOL JS_IsArrayBuffer(JSValueConst obj); uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj); typedef enum JSTypedArrayEnum { @@ -891,7 +897,7 @@ typedef enum JSTypedArrayEnum { } JSTypedArrayEnum; JSValue JS_NewTypedArray(JSContext *ctx, int argc, JSValueConst *argv, - JSTypedArrayEnum array_type); + JSTypedArrayEnum array_type); JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj, size_t *pbyte_offset, size_t *pbyte_length, diff --git a/quickjs/repl.js b/quickjs/repl.js @@ -545,6 +545,7 @@ import * as os from "os"; std.exit(0); } else { std.puts("\n(Press Ctrl-C again to quit)\n"); + reset(); readline_print_prompt(); } }