From 861d9fe93aad8266bc01ea46d7c8ce70d2bd2d75 Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Mon, 30 Dec 2024 11:15:05 +0100 Subject: [PATCH] Run V8 on separate thread Rationale, implementation and known bugs are documented in DESIGN.md but the elevator pitch is that Ruby and V8 don't like sharing the same system stack. mini_racer_extension.cc has been split into mini_racer_extension.c and mini_racer_v8.cc. The former deals with Ruby, the latter with JS. This work has been sponsored by Discourse. --- DESIGN.md | 91 + ext/mini_racer_extension/extconf.rb | 2 + .../mini_racer_extension.c | 1447 ++++++++++++ .../mini_racer_extension.cc | 1945 ----------------- ext/mini_racer_extension/mini_racer_v8.cc | 750 +++++++ ext/mini_racer_extension/mini_racer_v8.h | 50 + ext/mini_racer_extension/serde.c | 747 +++++++ lib/mini_racer.rb | 401 +--- test/mini_racer_test.rb | 317 +-- test/test_forking.rb | 30 - test/test_multithread.rb | 4 +- 11 files changed, 3193 insertions(+), 2591 deletions(-) create mode 100644 DESIGN.md create mode 100644 ext/mini_racer_extension/mini_racer_extension.c delete mode 100644 ext/mini_racer_extension/mini_racer_extension.cc create mode 100644 ext/mini_racer_extension/mini_racer_v8.cc create mode 100644 ext/mini_racer_extension/mini_racer_v8.h create mode 100644 ext/mini_racer_extension/serde.c delete mode 100644 test/test_forking.rb diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ae813a9 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,91 @@ +rationale +========= + +Before the commit that added this document, Ruby and V8 shared the same system +stack but it's been observed that they don't always co-exist peacefully there. + +Symptoms range from unexpected JS stack overflow exceptions and hitting debug +checks in V8, to outright segmentation faults. + +To mitigate that, V8 runs on separate threads now. + +implementation +============== + +Each `MiniRacer::Context` is paired with a native system thread that runs V8. + +Multiple Ruby threads can concurrently access the `MiniRacer::Context`. +MiniRacer ensures mutual exclusion. Ruby threads won't trample each other. + +Ruby threads communicate with the V8 thread through a mutex-and-condition-variable +protected request/response memory buffer. + +The wire format is V8's native (de)serialization format. An encoder/decoder +has been added to MiniRacer. + +Requests and (some) responses are prefixed with a single character +that indicates the desired action: `'C'` is `context.call(...)`, +`'E'` is `context.eval(...)`, and so on. + +A response from the V8 thread either starts with: + +- `'\xff'`, indicating a normal response that should be deserialized as-is + +- `'c'`, signaling an in-band request (not a response!) to call a Ruby function + registered with `context.attach(...)`. In turn, the Ruby thread replies with + a `'c'` response containing the return value from the Ruby function. + +Special care has been taken to ensure Ruby and JS functions can call each other +recursively without deadlocking. The Ruby thread uses a recursive mutex that +excludes other Ruby threads but still allows reentrancy from the same thread. + +The exact request and response payloads are documented in the source code but +they are almost universally: + +- either a single value (e.g. `true` or `false`), or + +- a two or three element array (ex. `[filename, source]` for `context.eval(...)`), or + +- for responses, an errback-style `[response, error]` array, where `error` + is a multi-line string that contains the error message on the first line, + and, optionally, the stack trace. If not empty, the error string is turned + into a Ruby exception and raised. + +deliberate changes & known bugs +=============================== + +- Forking is no longer supported. `fork(2)` and threads don't go well together. + After the fork, only the calling thread remains; the V8 thread is gone and + likely left mutexes and other resources in an undefined state. + +- The `Isolate` class is gone. Maintaining a one-to-many relationship between + isolates and contexts in a multi-threaded environment had a bad cost/benefit + ratio. `Isolate` methods like `isolate.low_memory_notification` have been + moved to `Context`, ex., `context.low_memory_notification`. + +- The `marshal_stack_depth` argument is still accepted but ignored; it's no + longer necessary. + +- The `timeout` argument no longer interrupts long-running Ruby code. Killing + or interrupting a Ruby thread executing arbitrary code is fraught with peril. + +- Returning an invalid JS `Date` object (think `new Date(NaN)`) now raises a + `RangeError` instead of silently returning a bogus `Time` object. + +- Not all JS objects map 1-to-1 to Ruby objects. Typed arrays and arraybuffers + are currently mapped to `Encoding::ASCII_8BIT`strings as the closest Ruby + equivalent to a byte buffer. + +- Not all JS objects are serializable/cloneable. Where possible, such objects + are substituted with a cloneable representation, else a `MiniRacer::RuntimeError` + is raised. + + Promises, argument objects, map and set iterators, etc., are substituted, + either with an empty object (promises, argument objects), or by turning them + into arrays (map/set iterators.) + + Function objects are substituted with a marker so they can be represented + as `MiniRacer::JavaScriptFunction` objects on the Ruby side. + + SharedArrayBuffers are not cloneable by design but aren't really usable in + `mini_racer` in the first place (no way to share them between isolates.) diff --git a/ext/mini_racer_extension/extconf.rb b/ext/mini_racer_extension/extconf.rb index 0b7b5ea..93ef03c 100644 --- a/ext/mini_racer_extension/extconf.rb +++ b/ext/mini_racer_extension/extconf.rb @@ -1,5 +1,7 @@ require 'mkmf' +$srcs = ["mini_racer_extension.c", "mini_racer_v8.cc"] + if RUBY_ENGINE == "truffleruby" File.write("Makefile", dummy_makefile($srcdir).join("")) return diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c new file mode 100644 index 0000000..fd8b8ba --- /dev/null +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -0,0 +1,1447 @@ +#include +#include +#include +#include +#include + +#include "ruby.h" +#include "ruby/encoding.h" +#include "ruby/thread.h" +#include "serde.c" +#include "mini_racer_v8.h" + +#define countof(x) (sizeof(x) / sizeof(*(x))) +#define endof(x) ((x) + countof(x)) + +// work around missing pthread_barrier_t on macOS +typedef struct Barrier +{ + pthread_mutex_t mtx; + pthread_cond_t cv; + int count, in, out; +} Barrier; + +static inline int barrier_init(Barrier *b, int count) +{ + int r; + + if ((r = pthread_mutex_init(&b->mtx, NULL))) + return r; + if ((r = pthread_cond_init(&b->cv, NULL))) { + pthread_mutex_destroy(&b->mtx); + return r; + } + b->count = count; + b->out = 0; + b->in = 0; + return 0; +} + +static inline void barrier_destroy(Barrier *b) +{ + pthread_mutex_destroy(&b->mtx); + pthread_cond_destroy(&b->cv); +} + +static inline int barrier_wait(Barrier *b) +{ + int last; + + pthread_mutex_lock(&b->mtx); + while (b->out) + pthread_cond_wait(&b->cv, &b->mtx); + if (++b->in == b->count) { + b->in = 0; + b->out = b->count; + pthread_cond_broadcast(&b->cv); + } else { + do + pthread_cond_wait(&b->cv, &b->mtx); + while (b->in); + } + last = (--b->out == 0); + if (last) + pthread_cond_broadcast(&b->cv); + pthread_mutex_unlock(&b->mtx); + return last; +} + +typedef struct Context +{ + Barrier early_init, late_init; + // |rr_mtx| stands for "recursive ruby mutex"; it's used to exclude + // other ruby threads but allow reentrancy from the same ruby thread + // (think ruby->js->ruby->js calls) + pthread_mutex_t rr_mtx; + pthread_mutex_t mtx; + pthread_cond_t cv; + pthread_t thr; // v8 thread handle + Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv| + int depth; // call depth, protected by |rr_mtx| + // protected by |mtx|; RW for ruby threads, RO for v8 thread; + // atomic because context_stop (which can be called from other ruby + // threads) writes it without holding |mtx|, to avoid deadlocking + // 1=shut down v8, 2=free memory; note that only the v8 thread + // frees the memory and it intentionally stays around until + // the ruby object is gc'd, otherwise lifecycle management + // gets too complicated + atomic_int quit; + int verbose_exceptions; + int64_t idle_gc, max_memory, timeout; + Buf snapshot; + uintptr_t isolate; // for v8_terminate_execution() + VALUE procs; // array of js -> ruby callbacks + VALUE exception; // pending exception or Qnil + struct { + pthread_mutex_t mtx; + pthread_cond_t cv; + int cancel; + } wd; // watchdog +} Context; + +typedef struct Snapshot { + VALUE blob; +} Snapshot; + +static void context_free(void *arg); +static void context_mark(void *arg); +static size_t context_size(const void *arg); + +static const rb_data_type_t context_type = { + .wrap_struct_name = "mini_racer/context", + .function = { + .dfree = context_free, + .dmark = context_mark, + .dsize = context_size, + }, +}; + +static void snapshot_free(void *arg); +static void snapshot_mark(void *arg); +static size_t snapshot_size(const void *arg); + +static const rb_data_type_t snapshot_type = { + .wrap_struct_name = "mini_racer/snapshot", + .function = { + .dfree = snapshot_free, + .dmark = snapshot_mark, + .dsize = snapshot_size, + }, +}; + +static VALUE platform_init_error; +static VALUE context_disposed_error; +static VALUE parse_error; +static VALUE memory_error; +static VALUE runtime_error; +static VALUE internal_error; +static VALUE snapshot_error; +static VALUE terminated_error; +static VALUE context_class; +static VALUE snapshot_class; +static VALUE date_time_class; +static VALUE js_function_class; + +static pthread_mutex_t flags_mtx = PTHREAD_MUTEX_INITIALIZER; +static Buf flags; // protected by |flags_mtx| + +// note: must be stack-allocated or VALUEs won't be visible to ruby's GC +typedef struct State +{ + VALUE a, b; +} State; + +// note: must be stack-allocated or VALUEs won't be visible to ruby's GC +typedef struct DesCtx +{ + State *tos; + VALUE refs; // array + char err[64]; + State stack[512]; +} DesCtx; + +static void DesCtx_init(DesCtx *c) +{ + c->tos = c->stack; + c->refs = rb_ary_new(); + *c->tos = (State){Qundef, Qundef}; + *c->err = '\0'; +} + +static void put(DesCtx *c, VALUE v) +{ + VALUE *a, *b; + + if (*c->err) + return; + a = &c->tos->a; + b = &c->tos->b; + switch (TYPE(*a)) { + case T_ARRAY: + rb_ary_push(*a, v); + break; + case T_HASH: + if (*b == Qundef) { + *b = v; + } else { + *b = rb_funcall(*b, rb_intern("to_s"), 0); + rb_hash_aset(*a, *b, v); + *b = Qundef; + } + break; + case T_UNDEF: + *a = v; + break; + default: + snprintf(c->err, sizeof(c->err), "bad state"); + return; + } +} + +static void push(DesCtx *c, VALUE v) +{ + if (*c->err) + return; + if (c->tos == endof(c->stack)) { + snprintf(c->err, sizeof(c->err), "stack overflow"); + return; + } + *++c->tos = (State){v, Qundef}; + rb_ary_push(c->refs, v); +} + +// see also des_named_props_end +static void pop(DesCtx *c) +{ + if (*c->err) + return; + if (c->tos == c->stack) { + snprintf(c->err, sizeof(c->err), "stack underflow"); + return; + } + put(c, (*c->tos--).a); +} + +static void des_null(void *arg) +{ + put(arg, Qnil); +} + +static void des_undefined(void *arg) +{ + put(arg, Qnil); +} + +static void des_bool(void *arg, int v) +{ + put(arg, v ? Qtrue : Qfalse); +} + +static void des_int(void *arg, int64_t v) +{ + put(arg, LONG2FIX(v)); +} + +static void des_num(void *arg, double v) +{ + put(arg, DBL2NUM(v)); +} + +static void des_date(void *arg, double v) +{ + double sec, usec; + + if (!isfinite(v)) + rb_raise(rb_eRangeError, "invalid Date"); + sec = v/1e3; + usec = 1e3 * fmod(v, 1e3); + put(arg, rb_time_new(sec, usec)); +} + +// note: v8 stores bigints in 1's complement, ruby in 2's complement, +// so we have to take additional steps to ensure correct conversion +static void des_bigint(void *arg, const void *p, size_t n, int sign) +{ + VALUE v; + size_t i; + DesCtx *c; + uint64_t *a, t, limbs[65]; // +1 to suppress sign extension + + c = arg; + if (*c->err) + return; + if (n > sizeof(limbs) - sizeof(*limbs)) { + snprintf(c->err, sizeof(c->err), "bigint too big"); + return; + } + a = limbs; + t = 0; + for (i = 0; i < n; a++, i += sizeof(*a)) { + memcpy(a, (char *)p + i, sizeof(*a)); + t = *a; + } + if (t >> 63) + *a++ = 0; // suppress sign extension + v = rb_big_unpack(limbs, a-limbs); + if (sign < 0) + v = rb_big_mul(v, LONG2FIX(-1)); + put(c, v); +} + +static void des_string(void *arg, const char *s, size_t n) +{ + put(arg, rb_utf8_str_new(s, n)); +} + +static void des_string8(void *arg, const uint8_t *s, size_t n) +{ + put(arg, rb_enc_str_new((char *)s, n, rb_ascii8bit_encoding())); +} + +// des_string16: |s| is not word aligned +// des_string16: |n| is in bytes, not code points +static void des_string16(void *arg, const void *s, size_t n) +{ + rb_encoding *e; + DesCtx *c; + + c = arg; + if (*c->err) + return; + // TODO(bnoordhuis) replace this hack with something more principled + if (n == sizeof(js_function_marker) && !memcmp(js_function_marker, s, n)) + return put(c, rb_funcall(js_function_class, rb_intern("new"), 0)); + e = rb_enc_find("UTF-16LE"); // TODO cache? + if (!e) { + snprintf(c->err, sizeof(c->err), "no UTF16-LE encoding"); + return; + } + put(c, rb_enc_str_new((char *)s, n, e)); +} + +// ruby doesn't really have a concept of a byte array so store it as +// an 8-bit string instead; it's either that or a regular array of +// numbers, but the latter is markedly less efficient, storage-wise +static void des_arraybuffer(void *arg, const void *s, size_t n) +{ + put(arg, rb_enc_str_new((char *)s, n, rb_ascii8bit_encoding())); +} + +static void des_array_begin(void *arg) +{ + push(arg, rb_ary_new()); +} + +static void des_array_end(void *arg) +{ + pop(arg); +} + +static void des_named_props_begin(void *arg) +{ + push(arg, rb_hash_new()); +} + +// see also pop +static void des_named_props_end(void *arg) +{ + DesCtx *c; + + c = arg; + if (*c->err) + return; + if (c->tos == c->stack) { + snprintf(c->err, sizeof(c->err), "stack underflow"); + return; + } + c->tos--; // dropped, no way to represent in ruby +} + +static void des_object_begin(void *arg) +{ + push(arg, rb_hash_new()); +} + +static void des_object_end(void *arg) +{ + pop(arg); +} + +static void des_object_ref(void *arg, uint32_t id) +{ + DesCtx *c; + VALUE v; + + c = arg; + v = rb_ary_entry(c->refs, id); + put(c, v); +} + +static void des_error_begin(void *arg) +{ + push(arg, rb_class_new_instance(0, NULL, rb_eRuntimeError)); +} + +static void des_error_end(void *arg) +{ + pop(arg); +} + +static int collect(VALUE k, VALUE v, VALUE a) +{ + rb_ary_push(a, k); + rb_ary_push(a, v); + return ST_CONTINUE; +} + +static int serialize1(Ser *s, VALUE refs, VALUE v) +{ + uint64_t limbs[64]; + VALUE a, t, id; + size_t i, n; + int sign; + + if (*s->err) + return -1; + switch (TYPE(v)) { + case T_ARRAY: + id = rb_hash_lookup(refs, v); + if (NIL_P(id)) { + n = RARRAY_LEN(v); + i = rb_hash_size_num(refs); + rb_hash_aset(refs, v, LONG2FIX(i)); + ser_array_begin(s, n); + for (i = 0; i < n; i++) + if (serialize1(s, refs, rb_ary_entry(v, i))) + return -1; + ser_array_end(s, n); + } else { + ser_object_ref(s, FIX2LONG(id)); + } + break; + case T_HASH: + id = rb_hash_lookup(refs, v); + if (NIL_P(id)) { + a = rb_ary_new(); + i = rb_hash_size_num(refs); + n = rb_hash_size_num(v); + rb_hash_aset(refs, v, LONG2FIX(i)); + rb_hash_foreach(v, collect, a); + for (i = 0; i < 2*n; i += 2) { + t = rb_ary_entry(a, i); + switch (TYPE(t)) { + case T_FIXNUM: + case T_STRING: + case T_SYMBOL: + continue; + } + break; + } + if (i == 2*n) { + ser_object_begin(s); + for (i = 0; i < 2*n; i += 2) { + if (serialize1(s, refs, rb_ary_entry(a, i+0))) + return -1; + if (serialize1(s, refs, rb_ary_entry(a, i+1))) + return -1; + } + ser_object_end(s, n); + } else { + return bail(&s->err, "TODO serialize as Map"); + } + } else { + ser_object_ref(s, FIX2LONG(id)); + } + break; + case T_DATA: + if (date_time_class == CLASS_OF(v)) { + v = rb_funcall(v, rb_intern("to_time"), 0); + } + if (rb_cTime == CLASS_OF(v)) { + struct timeval tv = rb_time_timeval(v); + ser_date(s, tv.tv_sec*1e3 + tv.tv_usec/1e3); + } else { + static const char undefined_conversion[] = "Undefined Conversion"; + ser_string(s, undefined_conversion, sizeof(undefined_conversion)-1); + } + break; + case T_NIL: + ser_null(s); + break; + case T_UNDEF: + ser_undefined(s); + break; + case T_TRUE: + ser_bool(s, 1); + break; + case T_FALSE: + ser_bool(s, 0); + break; + case T_BIGNUM: + // note: v8 stores bigints in 1's complement, ruby in 2's complement, + // so we have to take additional steps to ensure correct conversion + memset(limbs, 0, sizeof(limbs)); + sign = rb_big_sign(v) ? 1 : -1; + if (sign < 0) + v = rb_big_mul(v, LONG2FIX(-1)); + rb_big_pack(v, limbs, countof(limbs)); + ser_bigint(s, limbs, countof(limbs), sign); + break; + case T_FIXNUM: + ser_int(s, FIX2LONG(v)); + break; + case T_FLOAT: + ser_num(s, NUM2DBL(v)); + break; + case T_SYMBOL: + v = rb_sym2str(v); + // fallthru + case T_STRING: + ser_string(s, RSTRING_PTR(v), RSTRING_LENINT(v)); + break; + default: + snprintf(s->err, sizeof(s->err), "unsupported type %x", TYPE(v)); + return -1; + } + return 0; +} + +static struct timespec deadline_ms(int ms) +{ + static const int64_t ns_per_sec = 1000*1000*1000; + struct timespec t; + + //clock_gettime(CLOCK_MONOTONIC, &t); + clock_gettime(CLOCK_REALTIME, &t); + t.tv_sec += ms/1000; + t.tv_nsec += ms%1000 * ns_per_sec/1000; + while (t.tv_nsec >= ns_per_sec) { + t.tv_nsec -= ns_per_sec; + t.tv_sec++; + } + return t; +} + +static int timespec_le(struct timespec a, struct timespec b) +{ + if (a.tv_sec < b.tv_sec) return 1; + return a.tv_sec == b.tv_sec && a.tv_nsec <= b.tv_nsec; +} + +static int deadline_exceeded(struct timespec deadline) +{ + return timespec_le(deadline, deadline_ms(0)); +} + +static void *v8_watchdog(void *arg) +{ + struct timespec deadline; + Context *c; + + c = arg; + deadline = deadline_ms(c->timeout); + pthread_mutex_lock(&c->wd.mtx); + for (;;) { + pthread_cond_timedwait(&c->wd.cv, &c->wd.mtx, &deadline); + if (c->wd.cancel) + break; + if (deadline_exceeded(deadline)) { + v8_terminate_execution(c->isolate); + break; + } + } + pthread_mutex_unlock(&c->wd.mtx); + return NULL; +} + +static void v8_timedwait(Context *c, const uint8_t *p, size_t n, + void (*func)(const uint8_t *p, size_t n)) +{ + pthread_t thr; + int r; + + r = -1; + if (c->timeout > 0 && (r = pthread_create(&thr, NULL, v8_watchdog, c))) { + fprintf(stderr, "mini_racer: watchdog: pthread_create: %s\n", strerror(r)); + fflush(stderr); + } + func(p, n); + if (r) + return; + pthread_mutex_lock(&c->wd.mtx); + c->wd.cancel = 1; + pthread_cond_signal(&c->wd.cv); + pthread_mutex_unlock(&c->wd.mtx); + pthread_join(thr, NULL); + c->wd.cancel = 0; +} + +static void dispatch1(Context *c, const uint8_t *p, size_t n) +{ + uint8_t b; + + assert(n > 0); + switch (*p) { + case 'A': return v8_attach(p+1, n-1); + case 'C': return v8_timedwait(c, p+1, n-1, v8_call); + case 'E': return v8_timedwait(c, p+1, n-1, v8_eval); + case 'H': return v8_heap_snapshot(); + case 'I': return v8_idle_notification(p+1, n-1); + case 'P': return v8_pump_message_loop(); + case 'S': return v8_heap_stats(); + case 'T': return v8_snapshot(p+1, n-1); + case 'W': return v8_warmup(p+1, n-1); + case 'L': + b = 0; + v8_reply(c, &b, 1); // doesn't matter what as long as it's not empty + return v8_low_memory_notification(); + } + fprintf(stderr, "mini_racer: bad request %02x\n", *p); + fflush(stderr); +} + +// called by v8_isolate_and_context +void v8_thread_main(Context *c, uintptr_t isolate) +{ + struct timespec deadline; + + c->isolate = isolate; + barrier_wait(&c->late_init); + pthread_mutex_lock(&c->mtx); + while (!c->quit) { + if (!c->req.len) { + if (c->idle_gc > 0) { + deadline = deadline_ms(c->idle_gc); + pthread_cond_timedwait(&c->cv, &c->mtx, &deadline); + if (deadline_exceeded(deadline)) + v8_low_memory_notification(); + } else { + pthread_cond_wait(&c->cv, &c->mtx); + } + } + if (!c->req.len) + continue; // spurious wakeup or quit signal from other thread + buf_reset(&c->res); + dispatch1(c, c->req.buf, c->req.len); + buf_reset(&c->req); + pthread_cond_signal(&c->cv); + } +} + +// called by v8_thread_main and from mini_racer_v8.cc, +// in all cases with Context.mtx held +void v8_dispatch(Context *c) +{ + dispatch1(c, c->req.buf, c->req.len); + buf_reset(&c->req); +} + +// called from mini_racer_v8.cc with Context.mtx held +// only called when inside v8_call, v8_eval, or v8_pump_message_loop +void v8_roundtrip(Context *c, const uint8_t **p, size_t *n) +{ + buf_reset(&c->req); + pthread_cond_signal(&c->cv); + while (!c->req.len) + pthread_cond_wait(&c->cv, &c->mtx); + buf_reset(&c->res); + *p = c->req.buf; + *n = c->req.len; +} + +// called from mini_racer_v8.cc with Context.mtx held +void v8_reply(Context *c, const uint8_t *p, size_t n) +{ + buf_put(&c->res, p, n); +} + +static void v8_once_init(void) +{ + static pthread_once_t once = PTHREAD_ONCE_INIT; + pthread_once(&once, v8_global_init); +} + +static void *v8_thread_start(void *arg) +{ + Context *c; + + c = arg; + barrier_wait(&c->early_init); + v8_once_init(); + v8_thread_init(c, c->snapshot.buf, c->snapshot.len, c->max_memory, + c->verbose_exceptions); + while (c->quit < 2) + pthread_cond_wait(&c->cv, &c->mtx); + pthread_mutex_unlock(&c->mtx); + pthread_mutex_destroy(&c->mtx); + pthread_cond_destroy(&c->cv); + barrier_destroy(&c->early_init); + barrier_destroy(&c->late_init); + pthread_mutex_destroy(&c->wd.mtx); + pthread_cond_destroy(&c->wd.cv); + buf_reset(&c->snapshot); + buf_reset(&c->req); + buf_reset(&c->res); + ruby_xfree(c); + return NULL; +} + +static VALUE deserialize1(const uint8_t *p, size_t n) +{ + char err[64]; + DesCtx d; + + DesCtx_init(&d); + if (des(&err, p, n, &d)) + rb_raise(runtime_error, "%s", err); + if (d.tos != d.stack) // should not happen + rb_raise(runtime_error, "parse stack not empty"); + return d.tos->a; +} + +static VALUE deserialize(VALUE arg) +{ + Buf *b; + + b = (void *)arg; + return deserialize1(b->buf, b->len); +} + +struct rendezvous_nogvl +{ + Context *context; + Buf *req, *res; +}; + +// called with |rr_mtx| and GVL held; can raise exception +static VALUE rendezvous_callback_do(VALUE arg) +{ + struct rendezvous_nogvl *a; + VALUE func, args; + Context *c; + Buf *b; + + a = (void *)arg; + b = a->res; + c = a->context; + assert(b->len > 0); + assert(*b->buf == 'c'); + args = deserialize1(b->buf+1, b->len-1); // skip 'c' marker + func = rb_ary_pop(args); // callback id + func = rb_ary_entry(c->procs, FIX2LONG(func)); + return rb_funcall2(func, rb_intern("call"), RARRAY_LEN(args), RARRAY_PTR(args)); +} + +// called with |rr_mtx| and GVL held; |mtx| is unlocked +// callback data is in |a->res|, serialized result goes in |a->req| +static void *rendezvous_callback(void *arg) +{ + struct rendezvous_nogvl *a; + Context *c; + int exc; + VALUE r; + Ser s; + + a = arg; + c = a->context; + r = rb_protect(rendezvous_callback_do, (VALUE)a, &exc); + if (exc) { + c->exception = rb_errinfo(); + rb_set_errinfo(Qnil); + goto fail; + } + ser_init1(&s, 'c'); // callback reply + ser_array_begin(&s, 2); + // either [result, undefined] or [undefined, err] + if (exc) + ser_undefined(&s); + if (serialize1(&s, rb_hash_new(), r)) { // should not happen + c->exception = rb_exc_new_cstr(internal_error, s.err); + ser_reset(&s); + goto fail; + } + if (!exc) + ser_undefined(&s); + ser_array_end(&s, 2); +out: + buf_move(&s.b, a->req); + return NULL; +fail: + ser_init1(&s, 'e'); // exception pending + goto out; +} + +static inline void *rendezvous_nogvl(void *arg) +{ + struct rendezvous_nogvl *a; + Context *c; + + a = arg; + c = a->context; + pthread_mutex_lock(&c->rr_mtx); + if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion + fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth); + fflush(stderr); + } + c->depth++; +next: + pthread_mutex_lock(&c->mtx); + assert(c->req.len == 0); + assert(c->res.len == 0); + buf_move(a->req, &c->req); // v8 thread takes ownership of req + pthread_cond_signal(&c->cv); + do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len); + buf_move(&c->res, a->res); + pthread_mutex_unlock(&c->mtx); + if (*a->res->buf == 'c') { // js -> ruby callback? + rb_thread_call_with_gvl(rendezvous_callback, a); + goto next; + } + c->depth--; + pthread_mutex_unlock(&c->rr_mtx); + return NULL; +} + +static void rendezvous_no_des(Context *c, Buf *req, Buf *res) +{ + if (atomic_load(&c->quit)) { + buf_reset(req); + rb_raise(context_disposed_error, "disposed context"); + } + rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res}, + NULL, NULL, 0); +} + +// send request to & receive reply from v8 thread; takes ownership of |req| +// can raise exceptions and longjmp away but won't leak |req| +static VALUE rendezvous(Context *c, Buf *req) +{ + VALUE r; + Buf res; + int exc; + + rendezvous_no_des(c, req, &res); // takes ownership of |req| + r = rb_protect(deserialize, (VALUE)&res, &exc); + buf_reset(&res); + if (exc) { + r = rb_errinfo(); + rb_set_errinfo(Qnil); + rb_exc_raise(r); + } + if (!NIL_P(c->exception)) { + r = c->exception; + c->exception = Qnil; + rb_exc_raise(r); + } + return r; +} + +static void handle_exception(VALUE e) +{ + const char *s; + VALUE klass; + + if (NIL_P(e)) + return; + StringValue(e); + s = RSTRING_PTR(e); + switch (*s) { + case NO_ERROR: + return; + case INTERNAL_ERROR: + klass = internal_error; + break; + case MEMORY_ERROR: + klass = memory_error; + break; + case PARSE_ERROR: + klass = parse_error; + break; + case RUNTIME_ERROR: + klass = runtime_error; + break; + case TERMINATED_ERROR: + klass = terminated_error; + break; + default: + rb_raise(internal_error, "bad error class %02x", *s); + } + rb_raise(klass, "%s", s+1); +} + +static VALUE context_alloc(VALUE klass) +{ + pthread_mutexattr_t mattr; + pthread_condattr_t cattr; + pthread_attr_t tattr; + const char *cause; + Context *c; + VALUE f, a; + int r; + + // Safe to lazy init because we hold the GVL + if (NIL_P(date_time_class)) { + f = rb_intern("const_defined?"); + a = rb_str_new_cstr("DateTime"); + if (Qtrue == rb_funcall(rb_cObject, f, 1, a)) + date_time_class = rb_const_get(rb_cObject, rb_intern("DateTime")); + } + c = ruby_xmalloc(sizeof(*c)); + memset(c, 0, sizeof(*c)); + c->procs = rb_ary_new(); + c->exception = Qnil; + buf_init(&c->snapshot); + buf_init(&c->req); + buf_init(&c->res); + cause = "pthread_condattr_init"; + if ((r = pthread_condattr_init(&cattr))) + goto fail0; + //pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC); + cause = "pthread_mutexattr_init"; + if ((r = pthread_mutexattr_init(&mattr))) + goto fail1; + pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE); + cause = "pthread_mutex_init"; + r = pthread_mutex_init(&c->rr_mtx, &mattr); + pthread_mutexattr_destroy(&mattr); + if (r) + goto fail1; + if (pthread_mutex_init(&c->mtx, NULL)) + goto fail2; + cause = "pthread_cond_init"; + if ((r = pthread_cond_init(&c->cv, &cattr))) + goto fail3; + cause = "pthread_mutex_init"; + if ((r = pthread_mutex_init(&c->wd.mtx, NULL))) + goto fail4; + cause = "pthread_cond_init"; + if (pthread_cond_init(&c->wd.cv, &cattr)) + goto fail5; + cause = "barrier_init"; + if ((r = barrier_init(&c->early_init, 2))) + goto fail6; + cause = "barrier_init"; + if ((r = barrier_init(&c->late_init, 2))) + goto fail7; + cause = "pthread_attr_init"; + if ((r = pthread_attr_init(&tattr))) + goto fail8; + pthread_attr_setstacksize(&tattr, 2u<<20); // 2 MiB + pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED); + // v8 thread takes ownership of |c| + cause = "pthread_create"; + r = pthread_create(&c->thr, &tattr, v8_thread_start, c); + pthread_attr_destroy(&tattr); + if (r) + goto fail8; + pthread_condattr_destroy(&cattr); + return TypedData_Wrap_Struct(klass, &context_type, c); +fail8: + barrier_destroy(&c->late_init); +fail7: + barrier_destroy(&c->early_init); +fail6: + pthread_cond_destroy(&c->wd.cv); +fail5: + pthread_mutex_destroy(&c->wd.mtx); +fail4: + pthread_cond_destroy(&c->cv); +fail3: + pthread_mutex_destroy(&c->mtx); +fail2: + pthread_mutex_destroy(&c->rr_mtx); +fail1: + pthread_condattr_destroy(&cattr); +fail0: + ruby_xfree(c); + rb_raise(runtime_error, "%s: %s", cause, strerror(r)); + return Qnil; // pacify compiler +} + +static void context_free(void *arg) +{ + Context *c; + + c = arg; + pthread_mutex_lock(&c->mtx); + c->quit = 2; // 2 = v8 thread frees + pthread_cond_signal(&c->cv); + pthread_mutex_unlock(&c->mtx); +} + +static void context_mark(void *arg) +{ + Context *c; + + c = arg; + rb_gc_mark(c->procs); + rb_gc_mark(c->exception); +} + +static size_t context_size(const void *arg) +{ + const Context *c = arg; + return sizeof(*c); +} + +static VALUE context_attach(VALUE self, VALUE name, VALUE proc) +{ + Context *c; + VALUE e; + Ser s; + + TypedData_Get_Struct(self, Context, &context_type, c); + // request is (A)ttach, [name, id] array + ser_init1(&s, 'A'); + ser_array_begin(&s, 2); + ser_string(&s, RSTRING_PTR(name), RSTRING_LENINT(name)); + ser_int(&s, RARRAY_LEN(c->procs)); + ser_array_end(&s, 2); + rb_ary_push(c->procs, proc); + // response is an exception or undefined + e = rendezvous(c, &s.b); + handle_exception(e); + return Qnil; +} + +static void *context_dispose_do(void *arg) +{ + Context *c; + + c = arg; + pthread_mutex_lock(&c->mtx); + while (c->req.len || c->res.len) + pthread_cond_wait(&c->cv, &c->mtx); + atomic_store(&c->quit, 1); // disposed + pthread_cond_signal(&c->cv); // wake up v8 thread + pthread_mutex_unlock(&c->mtx); + return NULL; +} + +static VALUE context_dispose(VALUE self) +{ + Context *c; + + TypedData_Get_Struct(self, Context, &context_type, c); + rb_thread_call_without_gvl(context_dispose_do, c, NULL, NULL); + return Qnil; +} + +static VALUE context_stop(VALUE self) +{ + Context *c; + + // does not grab |mtx| because Context.stop can be called from another + // thread and then we deadlock if e.g. the V8 thread busy-loops in JS + TypedData_Get_Struct(self, Context, &context_type, c); + if (atomic_load(&c->quit)) + rb_raise(context_disposed_error, "disposed context"); + v8_terminate_execution(c->isolate); + return Qnil; +} + +static VALUE context_call(int argc, VALUE *argv, VALUE self) +{ + VALUE a, e, h; + Context *c; + int i; + Ser s; + + TypedData_Get_Struct(self, Context, &context_type, c); + rb_scan_args(argc, argv, "1*", &a, &e); + Check_Type(a, T_STRING); + // request is (C)all, [name, args...] array + ser_init1(&s, 'C'); + ser_array_begin(&s, argc); + h = rb_hash_new(); + for (i = 0; i < argc; i++) { + if (serialize1(&s, h, argv[i])) { + ser_reset(&s); + rb_raise(runtime_error, "Context.call: %s", s.err); + } + } + ser_array_end(&s, argc); + // response is [result, err] array + a = rendezvous(c, &s.b); // takes ownership of |s.b| + e = rb_ary_pop(a); + handle_exception(e); + return rb_ary_pop(a); +} + +static VALUE context_eval(int argc, VALUE *argv, VALUE self) +{ + VALUE a, e, source, filename, kwargs; + Context *c; + Ser s; + + TypedData_Get_Struct(self, Context, &context_type, c); + filename = Qnil; + rb_scan_args(argc, argv, "1:", &source, &kwargs); + Check_Type(source, T_STRING); + if (!NIL_P(kwargs)) + filename = rb_hash_aref(kwargs, rb_id2sym(rb_intern("filename"))); + if (NIL_P(filename)) + filename = rb_str_new_cstr(""); + Check_Type(filename, T_STRING); + // request is (E)val, [filename, source] array + ser_init1(&s, 'E'); + ser_array_begin(&s, 2); + ser_string(&s, RSTRING_PTR(filename), RSTRING_LENINT(filename)); + ser_string(&s, RSTRING_PTR(source), RSTRING_LENINT(source)); + ser_array_end(&s, 2); + // response is [result, errname] array + a = rendezvous(c, &s.b); // takes ownership of |s.b| + e = rb_ary_pop(a); + handle_exception(e); + return rb_ary_pop(a); +} + +static VALUE context_heap_stats(VALUE self) +{ + VALUE a, h, k, v; + Context *c; + int i, n; + Buf b; + + TypedData_Get_Struct(self, Context, &context_type, c); + buf_init(&b); + buf_putc(&b, 'S'); // (S)tats, returns object + h = rendezvous(c, &b); // takes ownership of |b| + a = rb_ary_new(); + rb_hash_foreach(h, collect, a); + for (i = 0, n = RARRAY_LEN(a); i < n; i += 2) { + k = rb_ary_entry(a, i+0); + v = rb_ary_entry(a, i+1); + rb_hash_delete(h, k); + rb_hash_aset(h, rb_str_intern(k), v); // turn "key" into :key + } + return h; +} + +static VALUE context_heap_snapshot(VALUE self) +{ + Buf req, res; + Context *c; + + TypedData_Get_Struct(self, Context, &context_type, c); + buf_init(&req); + buf_putc(&req, 'H'); // (H)eap snapshot, returns plain bytes + rendezvous_no_des(c, &req, &res); // takes ownership of |req| + return rb_utf8_str_new((char *)res.buf, res.len); +} + +static VALUE context_pump_message_loop(VALUE self) +{ + Context *c; + Buf b; + + TypedData_Get_Struct(self, Context, &context_type, c); + buf_init(&b); + buf_putc(&b, 'P'); // (P)ump, returns bool + return rendezvous(c, &b); // takes ownership of |b| +} + +static VALUE context_idle_notification(VALUE self, VALUE arg) +{ + Context *c; + Ser s; + + Check_Type(arg, T_FIXNUM); + TypedData_Get_Struct(self, Context, &context_type, c); + // request is (I)dle notification, idle_time_in_seconds + ser_init1(&s, 'I'); + ser_num(&s, LONG2FIX(arg) / 1e3); + // response is |undefined| + return rendezvous(c, &s.b); // takes ownership of |s.b| +} + +static VALUE context_low_memory_notification(VALUE self) +{ + Buf req, res; + Context *c; + + TypedData_Get_Struct(self, Context, &context_type, c); + buf_init(&req); + buf_putc(&req, 'L'); // (L)ow memory notification, returns nothing + rendezvous_no_des(c, &req, &res); // takes ownership of |req| + return Qnil; +} + +static int platform_set_flag1(VALUE k, VALUE v) +{ + char *p, buf[256]; + int ok; + + k = rb_funcall(k, rb_intern("to_s"), 0); + Check_Type(k, T_STRING); + if (!NIL_P(v)) { + v = rb_funcall(v, rb_intern("to_s"), 0); + Check_Type(v, T_STRING); + } + p = RSTRING_PTR(k); + if (!strncmp(p, "--", 2)) + p += 2; + if (NIL_P(v)) { + snprintf(buf, sizeof(buf), "--%s", p); + } else { + snprintf(buf, sizeof(buf), "--%s=%s", p, RSTRING_PTR(v)); + } + p = buf; + pthread_mutex_lock(&flags_mtx); + if (!flags.buf) + buf_init(&flags); + ok = (*flags.buf != 1); + if (ok) + buf_put(&flags, p, 1+strlen(p)); // include trailing \0 + pthread_mutex_unlock(&flags_mtx); + return ok; +} + +static VALUE platform_set_flags(int argc, VALUE *argv, VALUE klass) +{ + VALUE args, kwargs, k, v; + int i, n; + + (void)&klass; + rb_scan_args(argc, argv, "*:", &args, &kwargs); + Check_Type(args, T_ARRAY); + for (i = 0, n = RARRAY_LEN(args); i < n; i++) { + k = rb_ary_entry(args, i); + v = Qnil; + if (!platform_set_flag1(k, v)) + goto fail; + } + if (NIL_P(kwargs)) + return Qnil; + Check_Type(kwargs, T_HASH); + args = rb_ary_new(); + rb_hash_foreach(kwargs, collect, args); + for (i = 0, n = RARRAY_LEN(args); i < n; i += 2) { + k = rb_ary_entry(args, i+0); + v = rb_ary_entry(args, i+1); + if (!platform_set_flag1(k, v)) + goto fail; + } + return Qnil; +fail: + rb_raise(platform_init_error, "platform already initialized"); +} + +// called by v8_global_init +void v8_get_flags(char **p, size_t *n) +{ + *p = NULL; + *n = 0; + pthread_mutex_lock(&flags_mtx); + if (!flags.len) + goto out; + *p = malloc(flags.len); + if (!*p) + goto out; + *n = flags.len; + memcpy(*p, flags.buf, *n); + buf_reset(&flags); +out: + buf_init(&flags); + buf_putc(&flags, 1); // marker to indicate it's been cleared + pthread_mutex_unlock(&flags_mtx); +} + +static VALUE context_initialize(int argc, VALUE *argv, VALUE self) +{ + VALUE kwargs, a, k, v; + Snapshot *ss; + Context *c; + char *s; + + TypedData_Get_Struct(self, Context, &context_type, c); + rb_scan_args(argc, argv, ":", &kwargs); + if (NIL_P(kwargs)) + goto init; + a = rb_ary_new(); + rb_hash_foreach(kwargs, collect, a); + while (RARRAY_LEN(a)) { + v = rb_ary_pop(a); + k = rb_ary_pop(a); + k = rb_sym2str(k); + s = RSTRING_PTR(k); + if (!strcmp(s, "ensure_gc_after_idle")) { + Check_Type(v, T_FIXNUM); + c->idle_gc = FIX2LONG(v); + if (c->idle_gc < 0 || c->idle_gc >= UINT32_MAX) + rb_raise(rb_eArgError, "bad ensure_gc_after_idle: %ld", c->idle_gc); + } else if (!strcmp(s, "max_memory")) { + Check_Type(v, T_FIXNUM); + c->max_memory = FIX2LONG(v); + if (c->max_memory < 0 || c->max_memory >= UINT32_MAX) + rb_raise(rb_eArgError, "bad max_memory: %ld", c->max_memory); + } else if (!strcmp(s, "marshal_stack_depth")) { // backcompat, ignored + Check_Type(v, T_FIXNUM); + } else if (!strcmp(s, "timeout")) { + Check_Type(v, T_FIXNUM); + c->timeout = FIX2LONG(v); + } else if (!strcmp(s, "snapshot")) { + if (NIL_P(v)) + continue; + TypedData_Get_Struct(v, Snapshot, &snapshot_type, ss); + if (buf_put(&c->snapshot, RSTRING_PTR(ss->blob), RSTRING_LENINT(ss->blob))) + rb_raise(runtime_error, "out of memory"); + } else if (!strcmp(s, "verbose_exceptions")) { + c->verbose_exceptions = !(v == Qfalse || v == Qnil); + } else { + rb_raise(runtime_error, "bad keyword: %s", s); + } + } +init: + barrier_wait(&c->early_init); + barrier_wait(&c->late_init); + return Qnil; +} + +static VALUE snapshot_alloc(VALUE klass) +{ + Snapshot *ss; + + ss = ruby_xmalloc(sizeof(*ss)); + ss->blob = rb_enc_str_new("", 0, rb_ascii8bit_encoding()); + return TypedData_Wrap_Struct(klass, &snapshot_type, ss); +} + +static void snapshot_free(void *arg) +{ + ruby_xfree(arg); +} + +static void snapshot_mark(void *arg) +{ + Snapshot *ss; + + ss = arg; + rb_gc_mark(ss->blob); +} + +static size_t snapshot_size(const void *arg) +{ + const Snapshot *ss; + + ss = arg; + return sizeof(*ss) + RSTRING_LENINT(ss->blob); +} + +static VALUE snapshot_initialize(int argc, VALUE *argv, VALUE self) +{ + VALUE a, e, code, cv; + Snapshot *ss; + Context *c; + Ser s; + + TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); + rb_scan_args(argc, argv, "01", &code); + if (NIL_P(code)) + code = rb_str_new_cstr(""); + Check_Type(code, T_STRING); + cv = context_alloc(context_class); + context_initialize(0, NULL, cv); + TypedData_Get_Struct(cv, Context, &context_type, c); + // request is snapsho(T), "code" + ser_init1(&s, 'T'); + ser_string(&s, RSTRING_PTR(code), RSTRING_LENINT(code)); + // response is [arraybuffer, error] + a = rendezvous(c, &s.b); + e = rb_ary_pop(a); + context_dispose(cv); + if (*RSTRING_PTR(e)) + rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1); + ss->blob = rb_ary_pop(a); + return Qnil; +} + +static VALUE snapshot_warmup(VALUE self, VALUE arg) +{ + VALUE a, e, cv; + Snapshot *ss; + Context *c; + Ser s; + + TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); + Check_Type(arg, T_STRING); + cv = context_alloc(context_class); + context_initialize(0, NULL, cv); + TypedData_Get_Struct(cv, Context, &context_type, c); + // request is (W)armup, [snapshot, "warmup code"] + ser_init1(&s, 'W'); + ser_array_begin(&s, 2); + ser_string8(&s, (const uint8_t *)RSTRING_PTR(ss->blob), RSTRING_LENINT(ss->blob)); + ser_string(&s, RSTRING_PTR(arg), RSTRING_LENINT(arg)); + ser_array_end(&s, 2); + // response is [arraybuffer, error] + a = rendezvous(c, &s.b); + e = rb_ary_pop(a); + context_dispose(cv); + if (*RSTRING_PTR(e)) + rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1); + ss->blob = rb_ary_pop(a); + return self; +} + +static VALUE snapshot_dump(VALUE self) +{ + Snapshot *ss; + + TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); + return ss->blob; +} + +static VALUE snapshot_size0(VALUE self) +{ + Snapshot *ss; + + TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); + return LONG2FIX(RSTRING_LENINT(ss->blob)); +} + +__attribute__((visibility("default"))) +void Init_mini_racer_extension(void) +{ + VALUE c, m; + + m = rb_define_module("MiniRacer"); + c = rb_define_class_under(m, "Error", rb_eStandardError); + snapshot_error = rb_define_class_under(m, "SnapshotError", c); + platform_init_error = rb_define_class_under(m, "PlatformAlreadyInitialized", c); + context_disposed_error = rb_define_class_under(m, "ContextDisposedError", c); + + c = rb_define_class_under(m, "EvalError", c); + parse_error = rb_define_class_under(m, "ParseError", c); + memory_error = rb_define_class_under(m, "V8OutOfMemoryError", c); + runtime_error = rb_define_class_under(m, "RuntimeError", c); + internal_error = rb_define_class_under(m, "InternalError", c); + terminated_error = rb_define_class_under(m, "ScriptTerminatedError", c); + + c = context_class = rb_define_class_under(m, "Context", rb_cObject); + rb_define_method(c, "initialize", context_initialize, -1); + rb_define_method(c, "attach", context_attach, 2); + rb_define_method(c, "dispose", context_dispose, 0); + rb_define_method(c, "stop", context_stop, 0); + rb_define_method(c, "call", context_call, -1); + rb_define_method(c, "eval", context_eval, -1); + rb_define_method(c, "heap_stats", context_heap_stats, 0); + rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0); + rb_define_method(c, "pump_message_loop", context_pump_message_loop, 0); + rb_define_method(c, "idle_notification", context_idle_notification, 1); + rb_define_method(c, "low_memory_notification", context_low_memory_notification, 0); + rb_define_alloc_func(c, context_alloc); + + c = snapshot_class = rb_define_class_under(m, "Snapshot", rb_cObject); + rb_define_method(c, "initialize", snapshot_initialize, -1); + rb_define_method(c, "warmup!", snapshot_warmup, 1); + rb_define_method(c, "dump", snapshot_dump, 0); + rb_define_method(c, "size", snapshot_size0, 0); + rb_define_alloc_func(c, snapshot_alloc); + + c = rb_define_class_under(m, "Platform", rb_cObject); + rb_define_singleton_method(c, "set_flags!", platform_set_flags, -1); + + date_time_class = Qnil; // lazy init + js_function_class = rb_define_class_under(m, "JavaScriptFunction", rb_cObject); +} diff --git a/ext/mini_racer_extension/mini_racer_extension.cc b/ext/mini_racer_extension/mini_racer_extension.cc deleted file mode 100644 index d9bb469..0000000 --- a/ext/mini_racer_extension/mini_racer_extension.cc +++ /dev/null @@ -1,1945 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* workaround C Ruby <= 2.x problems w/ clang in C++ mode */ -#if defined(ENGINE_IS_CRUBY) && \ - RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR <= 6 -# define MR_METHOD_FUNC(fn) RUBY_METHOD_FUNC(fn) -#else -# define MR_METHOD_FUNC(fn) fn -#endif - -using namespace v8; - -typedef struct { - const char* data; - int raw_size; -} SnapshotInfo; - -class IsolateInfo { -public: - Isolate* isolate; - ArrayBuffer::Allocator* allocator; - StartupData* startup_data; - bool interrupted; - bool added_gc_cb; - pid_t pid; - VALUE mutex; - - class Lock { - VALUE &mutex; - - public: - Lock(VALUE &mutex) : mutex(mutex) { - rb_mutex_lock(mutex); - } - ~Lock() { - rb_mutex_unlock(mutex); - } - }; - - - IsolateInfo() : isolate(nullptr), allocator(nullptr), startup_data(nullptr), - interrupted(false), added_gc_cb(false), pid(getpid()), refs_count(0) { - VALUE cMutex = rb_const_get(rb_cThread, rb_intern("Mutex")); - mutex = rb_class_new_instance(0, nullptr, cMutex); - } - - ~IsolateInfo(); - - void init(SnapshotInfo* snapshot_info = nullptr); - - void mark() { - rb_gc_mark(mutex); - } - - Lock createLock() { - Lock lock(mutex); - return lock; - } - - void hold() { - refs_count++; - } - void release() { - if (--refs_count <= 0) { - delete this; - } - } - - int refs() { - return refs_count; - } - - static void* operator new(size_t size) { - return ruby_xmalloc(size); - } - - static void operator delete(void *block) { - xfree(block); - } -private: - // how many references to this isolate exist - // we can't rely on Ruby's GC for this, because Ruby could destroy the - // isolate before destroying the contexts that depend on them. We'd need to - // keep a list of linked contexts in the isolate to destroy those first when - // isolate destruction was requested. Keeping such a list would require - // notification from the context VALUEs when they are constructed and - // destroyed. With a ref count, those notifications are still needed, but - // we keep a simple int rather than a list of pointers. - std::atomic_int refs_count; -}; - -typedef struct { - IsolateInfo* isolate_info; - Persistent* context; -} ContextInfo; - -typedef struct { - bool parsed; - bool executed; - bool terminated; - bool json; - Persistent* value; - Persistent* message; - Persistent* backtrace; -} EvalResult; - -typedef struct { - ContextInfo* context_info; - Local* eval; - Local* filename; - useconds_t timeout; - EvalResult* result; - size_t max_memory; - size_t marshal_stackdepth; -} EvalParams; - -typedef struct { - ContextInfo *context_info; - char *function_name; - int argc; - bool error; - Local fun; - Local *argv; - EvalResult result; - size_t max_memory; - size_t marshal_stackdepth; -} FunctionCall; - -class IsolateData { -public: - enum Flag { - // first flags are bitfield - // max count: sizeof(uintptr_t) * 8 - IN_GVL, // whether we are inside of ruby gvl or not - DO_TERMINATE, // terminate as soon as possible - MEM_SOFTLIMIT_REACHED, // we've hit the memory soft limit - MEM_SOFTLIMIT_MAX, // maximum memory value - MARSHAL_STACKDEPTH_REACHED, // we've hit our max stack depth - MARSHAL_STACKDEPTH_VALUE, // current stackdepth - MARSHAL_STACKDEPTH_MAX, // maximum stack depth during marshal - }; - - static void Init(Isolate *isolate) { - // zero out all fields in the bitfield - isolate->SetData(0, 0); - } - - static uintptr_t Get(Isolate *isolate, Flag flag) { - Bitfield u = { reinterpret_cast(isolate->GetData(0)) }; - switch (flag) { - case IN_GVL: return u.IN_GVL; - case DO_TERMINATE: return u.DO_TERMINATE; - case MEM_SOFTLIMIT_REACHED: return u.MEM_SOFTLIMIT_REACHED; - case MEM_SOFTLIMIT_MAX: return static_cast(u.MEM_SOFTLIMIT_MAX) << 10; - case MARSHAL_STACKDEPTH_REACHED: return u.MARSHAL_STACKDEPTH_REACHED; - case MARSHAL_STACKDEPTH_VALUE: return u.MARSHAL_STACKDEPTH_VALUE; - case MARSHAL_STACKDEPTH_MAX: return u.MARSHAL_STACKDEPTH_MAX; - } - - // avoid compiler warning - return u.IN_GVL; - } - - static void Set(Isolate *isolate, Flag flag, uintptr_t value) { - Bitfield u = { reinterpret_cast(isolate->GetData(0)) }; - switch (flag) { - case IN_GVL: u.IN_GVL = value; break; - case DO_TERMINATE: u.DO_TERMINATE = value; break; - case MEM_SOFTLIMIT_REACHED: u.MEM_SOFTLIMIT_REACHED = value; break; - // drop least significant 10 bits 'store memory amount in kb' - case MEM_SOFTLIMIT_MAX: u.MEM_SOFTLIMIT_MAX = value >> 10; break; - case MARSHAL_STACKDEPTH_REACHED: u.MARSHAL_STACKDEPTH_REACHED = value; break; - case MARSHAL_STACKDEPTH_VALUE: u.MARSHAL_STACKDEPTH_VALUE = value; break; - case MARSHAL_STACKDEPTH_MAX: u.MARSHAL_STACKDEPTH_MAX = value; break; - } - isolate->SetData(0, reinterpret_cast(u.dataPtr)); - } - -private: - struct Bitfield { - // WARNING: this would explode on platforms below 64 bit ptrs - // compiler will fail here, making it clear for them. - // Additionally, using the other part of the union to reinterpret the - // memory is undefined behavior according to spec, but is / has been stable - // across major compilers for decades. - static_assert(sizeof(uintptr_t) >= sizeof(uint64_t), "mini_racer not supported on this platform. ptr size must be at least 64 bit."); - union { - uint64_t dataPtr: 64; - // order in this struct matters. For cpu performance keep larger subobjects - // aligned on their boundaries (8 16 32), try not to straddle - struct { - size_t MEM_SOFTLIMIT_MAX:22; - bool IN_GVL:1; - bool DO_TERMINATE:1; - bool MEM_SOFTLIMIT_REACHED:1; - bool MARSHAL_STACKDEPTH_REACHED:1; - uint8_t :0; // align to next 8bit bound - size_t MARSHAL_STACKDEPTH_VALUE:10; - uint8_t :0; // align to next 8bit bound - size_t MARSHAL_STACKDEPTH_MAX:10; - }; - }; - }; -}; - -struct StackCounter { - static void Reset(Isolate* isolate) { - if (IsolateData::Get(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX) > 0) { - IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, 0); - IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, false); - } - } - - static void SetMax(Isolate* isolate, size_t marshalMaxStackDepth) { - if (marshalMaxStackDepth > 0) { - IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX, marshalMaxStackDepth); - IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, 0); - IsolateData::Set(isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, false); - } - } - - StackCounter(Isolate* isolate) { - this->isActive = IsolateData::Get(isolate, IsolateData::MARSHAL_STACKDEPTH_MAX) > 0; - - if (this->isActive) { - this->isolate = isolate; - this->IncDepth(1); - } - } - - bool IsTooDeep() { - if (!this->IsActive()) { - return false; - } - - size_t depth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE); - size_t maxDepth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_MAX); - if (depth > maxDepth) { - IsolateData::Set(this->isolate, IsolateData::MARSHAL_STACKDEPTH_REACHED, true); - return true; - } - - return false; - } - - bool IsActive() { - return this->isActive && !IsolateData::Get(this->isolate, IsolateData::DO_TERMINATE); - } - - ~StackCounter() { - if (this->IsActive()) { - this->IncDepth(-1); - } - } - -private: - Isolate* isolate; - bool isActive; - - void IncDepth(int direction) { - int inc = direction > 0 ? 1 : -1; - - size_t depth = IsolateData::Get(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE); - - // don't decrement past 0 - if (inc > 0 || depth > 0) { - depth += inc; - } - - IsolateData::Set(this->isolate, IsolateData::MARSHAL_STACKDEPTH_VALUE, depth); - } -}; - -static VALUE rb_cContext; -static VALUE rb_cSnapshot; -static VALUE rb_cIsolate; - -static VALUE rb_eScriptTerminatedError; -static VALUE rb_eV8OutOfMemoryError; -static VALUE rb_eParseError; -static VALUE rb_eScriptRuntimeError; -static VALUE rb_cJavaScriptFunction; -static VALUE rb_eSnapshotError; -static VALUE rb_ePlatformAlreadyInitializedError; -static VALUE rb_mJSON; - -static VALUE rb_cFailedV8Conversion; -static VALUE rb_cDateTime = Qnil; - -// note: |current_platform| is deliberately leaked on program exit -// because it's not safe to destroy it after main() has exited; -// abnormal termination may have left V8 in an undefined state -static Platform *current_platform; -static std::mutex platform_lock; - -static pthread_attr_t *thread_attr_p; -static std::atomic_int ruby_exiting(0); -static bool single_threaded = false; - -static void mark_context(void *); -static void deallocate(void *); -static size_t context_memsize(const void *); -static const rb_data_type_t context_type = { - "mini_racer/context_info", - { mark_context, deallocate, context_memsize } -}; - -static void deallocate_snapshot(void *); -static size_t snapshot_memsize(const void *); -static const rb_data_type_t snapshot_type = { - "mini_racer/snapshot_info", - { NULL, deallocate_snapshot, snapshot_memsize } -}; - -static void mark_isolate(void *); -static void deallocate_isolate(void *); -static size_t isolate_memsize(const void *); -static const rb_data_type_t isolate_type = { - "mini_racer/isolate_info", - { mark_isolate, deallocate_isolate, isolate_memsize } -}; - -static VALUE rb_platform_set_flag_as_str(VALUE _klass, VALUE flag_as_str) { - bool platform_already_initialized = false; - - Check_Type(flag_as_str, T_STRING); - - platform_lock.lock(); - - if (current_platform == NULL) { - if (!strcmp(RSTRING_PTR(flag_as_str), "--single_threaded")) { - single_threaded = true; - } - V8::SetFlagsFromString(RSTRING_PTR(flag_as_str), RSTRING_LENINT(flag_as_str)); - } else { - platform_already_initialized = true; - } - - platform_lock.unlock(); - - // important to raise outside of the lock - if (platform_already_initialized) { - rb_raise(rb_ePlatformAlreadyInitializedError, "The V8 platform is already initialized"); - } - - return Qnil; -} - -static void init_v8() { - // no need to wait for the lock if already initialized - if (current_platform != NULL) return; - - platform_lock.lock(); - - if (current_platform == NULL) { - V8::InitializeICU(); - if (single_threaded) { - current_platform = platform::NewSingleThreadedDefaultPlatform().release(); - } else { - current_platform = platform::NewDefaultPlatform().release(); - } - V8::InitializePlatform(current_platform); - V8::Initialize(); - } - - platform_lock.unlock(); -} - -static void gc_callback(Isolate *isolate, GCType type, GCCallbackFlags flags) { - if (IsolateData::Get(isolate, IsolateData::MEM_SOFTLIMIT_REACHED)) { - return; - } - - size_t softlimit = IsolateData::Get(isolate, IsolateData::MEM_SOFTLIMIT_MAX); - - HeapStatistics stats; - isolate->GetHeapStatistics(&stats); - size_t used = stats.used_heap_size(); - - if(used > softlimit) { - IsolateData::Set(isolate, IsolateData::MEM_SOFTLIMIT_REACHED, true); - isolate->TerminateExecution(); - } -} - -// to be called with active lock and scope -static void prepare_result(MaybeLocal v8res, - TryCatch& trycatch, - Isolate* isolate, - Local context, - EvalResult& evalRes /* out */) { - - // just don't touch .parsed - evalRes.terminated = false; - evalRes.json = false; - evalRes.value = nullptr; - evalRes.message = nullptr; - evalRes.backtrace = nullptr; - evalRes.executed = !v8res.IsEmpty(); - - if (evalRes.executed) { - // arrays and objects get converted to json - Local local_value = v8res.ToLocalChecked(); - if ((local_value->IsObject() || local_value->IsArray()) && - !local_value->IsDate() && !local_value->IsFunction()) { - MaybeLocal ml = context->Global()->Get( - context, String::NewFromUtf8Literal(isolate, "JSON")); - - if (ml.IsEmpty()) { // exception - evalRes.executed = false; - } else { - Local JSON = ml.ToLocalChecked().As(); - - Local stringify = JSON->Get( - context, v8::String::NewFromUtf8Literal(isolate, "stringify")) - .ToLocalChecked().As(); - - Local object = local_value->ToObject(context).ToLocalChecked(); - const unsigned argc = 1; - Local argv[argc] = { object }; - MaybeLocal maybe_json = stringify->Call(context, JSON, argc, argv); - Local json; - - if (!maybe_json.ToLocal(&json)) { - evalRes.executed = false; - } else { - // JSON.stringify() returns undefined for inputs that - // are exotic objects, like WASM function or string refs - evalRes.json = !json->IsUndefined(); - Persistent* persistent = new Persistent(); - persistent->Reset(isolate, json); - evalRes.value = persistent; - } - } - } else { - Persistent* persistent = new Persistent(); - persistent->Reset(isolate, local_value); - evalRes.value = persistent; - } - } - - if (!evalRes.executed || !evalRes.parsed) { - if (trycatch.HasCaught()) { - if (!trycatch.Exception()->IsNull()) { - evalRes.message = new Persistent(); - Local message = trycatch.Message(); - char buf[1000]; - int len, line, column; - - if (!message->GetLineNumber(context).To(&line)) { - line = 0; - } - - if (!message->GetStartColumn(context).To(&column)) { - column = 0; - } - - len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()), - *String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()), - line, - column); - - if ((size_t) len >= sizeof(buf)) { - len = sizeof(buf) - 1; - buf[len] = '\0'; - } - - Local v8_message = String::NewFromUtf8(isolate, buf, NewStringType::kNormal, len).ToLocalChecked(); - evalRes.message->Reset(isolate, v8_message); - } else if(trycatch.HasTerminated()) { - evalRes.terminated = true; - evalRes.message = new Persistent(); - Local tmp = String::NewFromUtf8Literal(isolate, "JavaScript was terminated (either by timeout or explicitly)"); - evalRes.message->Reset(isolate, tmp); - } - if (!trycatch.StackTrace(context).IsEmpty()) { - evalRes.backtrace = new Persistent(); - evalRes.backtrace->Reset(isolate, - trycatch.StackTrace(context).ToLocalChecked()->ToString(context).ToLocalChecked()); - } - } - } -} - -static void* -nogvl_context_eval(void* arg) { - - EvalParams* eval_params = (EvalParams*)arg; - EvalResult* result = eval_params->result; - IsolateInfo* isolate_info = eval_params->context_info->isolate_info; - Isolate* isolate = isolate_info->isolate; - - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - TryCatch trycatch(isolate); - Local context = eval_params->context_info->context->Get(isolate); - Context::Scope context_scope(context); - v8::ScriptOrigin *origin = NULL; - - IsolateData::Init(isolate); - - if (eval_params->max_memory > 0) { - IsolateData::Set(isolate, IsolateData::MEM_SOFTLIMIT_MAX, eval_params->max_memory); - if (!isolate_info->added_gc_cb) { - isolate->AddGCEpilogueCallback(gc_callback); - isolate_info->added_gc_cb = true; - } - } - - MaybeLocal