diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..feac882 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,93 @@ +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 +=============================== + +- `MiniRacer::Platform.set_flags! :single_threaded` still runs everything on + the same thread but is prone to crashes in Ruby < 3.4.0 due to a Ruby runtime + bug that clobbers thread-local variables. + +- 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 `ensure_gc_after_idle` argument is a no-op in `:single_threaded` mode. + +- 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..9fe0b48 --- /dev/null +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -0,0 +1,1564 @@ +#include +#include +#include +#include +#include + +#include "ruby.h" +#include "ruby/encoding.h" +#include "ruby/version.h" +#include "ruby/thread.h" +#include "serde.c" +#include "mini_racer_v8.h" + +#if RUBY_API_VERSION_CODE < 3*10000+4*100 // 3.4.0 +static inline void rb_thread_lock_native_thread(void) +{ + // Without rb_thread_lock_native_thread, V8 in single-threaded mode is + // prone to crash with debug checks like this... + // + // # Fatal error in ../deps/v8/src/base/platform/platform-posix.cc, line 1350 + // # Debug check failed: MainThreadIsCurrentThread(). + // + // ...because the Ruby runtime clobbers thread-local variables when it + // context-switches threads. You have been warned. +} +#endif + +#define countof(x) (sizeof(x) / sizeof(*(x))) +#define endof(x) ((x) + countof(x)) + +// mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|, +// from then on read-only and accessible without holding locks +int single_threaded; + +// 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 +{ + 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; + struct State *pst; // used by v8 thread + VALUE procs; // array of js -> ruby callbacks + VALUE exception; // pending exception or Qnil + Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv| + Buf snapshot; + // |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; + struct { + pthread_mutex_t mtx; + pthread_cond_t cv; + int cancel; + } wd; // watchdog + Barrier early_init, late_init; +} Context; + +typedef struct Snapshot { + VALUE blob; +} Snapshot; + +static void context_destroy(Context *c); +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| + +struct rendezvous_nogvl +{ + Context *context; + Buf *req, *res; +}; + +// arg == &(struct rendezvous_nogvl){...} +static void *rendezvous_callback(void *arg); + +// 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; + unsigned long *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) +{ + unsigned long 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_LENINT(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; + +#ifdef __APPLE__ + clock_gettime(CLOCK_REALTIME, &t); +#else + clock_gettime(CLOCK_MONOTONIC, &t); +#endif + 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 (;;) { + if (c->wd.cancel) + break; + pthread_cond_timedwait(&c->wd.cv, &c->wd.mtx, &deadline); + if (c->wd.cancel) + break; + if (deadline_exceeded(deadline)) { + v8_terminate_execution(c->pst); + break; + } + } + pthread_mutex_unlock(&c->wd.mtx); + return NULL; +} + +static void v8_timedwait(Context *c, const uint8_t *p, size_t n, + void (*func)(struct State *pst, 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(c->pst, 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(c->pst, 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(c->pst); + case 'I': return v8_idle_notification(c->pst, p+1, n-1); + case 'P': return v8_pump_message_loop(c->pst); + case 'S': return v8_heap_stats(c->pst); + case 'T': return v8_snapshot(c->pst, p+1, n-1); + case 'W': return v8_warmup(c->pst, 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(c->pst); + } + fprintf(stderr, "mini_racer: bad request %02x\n", *p); + fflush(stderr); +} + +static void dispatch(Context *c) +{ + buf_reset(&c->res); + dispatch1(c, c->req.buf, c->req.len); + buf_reset(&c->req); +} + +// called by v8_isolate_and_context +void v8_thread_main(Context *c, struct State *pst) +{ + struct timespec deadline; + + c->pst = pst; + 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(c->pst); + } else { + pthread_cond_wait(&c->cv, &c->mtx); + } + } + if (!c->req.len) + continue; // spurious wakeup or quit signal from other thread + dispatch(c); + 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) +{ + struct rendezvous_nogvl *args; + + buf_reset(&c->req); + if (single_threaded) { + assert(*c->res.buf == 'c'); // js -> ruby callback + args = &(struct rendezvous_nogvl){c, &c->req, &c->res}; + rb_thread_call_with_gvl(rendezvous_callback, args); + } else { + 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); + context_destroy(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); +} + +// 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_LENINT(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 + if (single_threaded) { + v8_single_threaded_enter(c->pst, c, dispatch); + } else { + 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; + 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->exception = Qnil; + c->procs = rb_ary_new(); + buf_init(&c->snapshot); + buf_init(&c->req); + buf_init(&c->res); + cause = "pthread_condattr_init"; + if ((r = pthread_condattr_init(&cattr))) + goto fail0; +#ifndef __APPLE__ + pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC); +#endif + 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; + pthread_condattr_destroy(&cattr); + return TypedData_Wrap_Struct(klass, &context_type, c); +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_thread_do(void *arg) +{ + Context *c; + + c = arg; + v8_single_threaded_dispose(c->pst); + context_destroy(c); + return NULL; +} + +static void context_free_thread(Context *c) +{ + pthread_t thr; + int r; + + // dispose on another thread so we don't block when trying to + // enter an isolate that's in a stuck state; that *should* be + // impossible but apparently it happened regularly before the + // rewrite and I'm carrying it over out of an abundance of caution + if ((r = pthread_create(&thr, NULL, context_free_thread_do, c))) { + fprintf(stderr, "mini_racer: pthread_create: %s", strerror(r)); + fflush(stderr); + context_free_thread_do(c); + } else { + pthread_detach(thr); + } +} + +static void context_free(void *arg) +{ + Context *c; + + c = arg; + if (single_threaded) { + context_free_thread(c); + } else { + 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_destroy(Context *c) +{ + 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); +} + +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_LENINT(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; + if (single_threaded) { + atomic_store(&c->quit, 1); // disposed + // intentionally a no-op for now + } else { + 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->pst); + 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_LENINT(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, *q, 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 + // strip dashes and underscores to reduce the number of variant + // spellings (--no-single-threaded, --nosingle-threaded, + // --no_single_threaded, etc.) + p = q = buf; + for (;;) { + if (*p != '-') + if (*p != '_') + *q++ = *p; + if (!*p++) + break; + } + if (!strcmp(buf, "singlethreaded")) { + single_threaded = 1; + } else if (!strcmp(buf, "nosinglethreaded")) { + single_threaded = 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_LENINT(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_LENINT(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; caller must free |*p| with free() +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); + if (single_threaded) + rb_thread_lock_native_thread(); +} + +static VALUE context_initialize(int argc, VALUE *argv, VALUE self) +{ + VALUE kwargs, a, k, v; + pthread_attr_t attr; + const char *cause; + pthread_t thr; + Snapshot *ss; + Context *c; + char *s; + int r; + + 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_LENINT(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 > INT32_MAX) + rb_raise(rb_eArgError, "bad ensure_gc_after_idle"); + } 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"); + } 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); + if (c->timeout < 0 || c->timeout > INT32_MAX) + rb_raise(rb_eArgError, "bad timeout"); + } 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: + if (single_threaded) { + v8_once_init(); + c->pst = v8_thread_init(c, c->snapshot.buf, c->snapshot.len, c->max_memory, c->verbose_exceptions); + } else { + cause = "pthread_attr_init"; + if ((r = pthread_attr_init(&attr))) + goto fail; + pthread_attr_setstacksize(&attr, 2<<20); // 2 MiB + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + // v8 thread takes ownership of |c| + cause = "pthread_create"; + r = pthread_create(&thr, &attr, v8_thread_start, c); + pthread_attr_destroy(&attr); + if (r) + goto fail; + barrier_wait(&c->early_init); + barrier_wait(&c->late_init); + } + return Qnil; +fail: + rb_raise(runtime_error, "Context.initialize: %s: %s", cause, strerror(r)); + return Qnil; // pacify compiler +} + +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