Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
#include "serde.c"
#include "mini_racer_v8.h"

// for debugging
#define RB_PUTS(v) \
do { \
fflush(stdout); \
rb_funcall(rb_mKernel, rb_intern("puts"), 1, v); \
} while (0)

#if RUBY_API_VERSION_CODE < 3*10000+4*100 // 3.4.0
static inline void rb_thread_lock_native_thread(void)
{
Expand Down Expand Up @@ -154,6 +161,7 @@ static VALUE platform_init_error;
static VALUE context_disposed_error;
static VALUE parse_error;
static VALUE memory_error;
static VALUE script_error;
static VALUE runtime_error;
static VALUE internal_error;
static VALUE snapshot_error;
Expand Down Expand Up @@ -482,12 +490,36 @@ static void des_object_ref(void *arg, uint32_t id)

static void des_error_begin(void *arg)
{
push(arg, rb_class_new_instance(0, NULL, rb_eRuntimeError));
push(arg, rb_ary_new());
}

static void des_error_end(void *arg)
{
pop(arg);
VALUE *a, h, message, stack, cause, newline;
DesCtx *c;

c = arg;
if (*c->err)
return;
if (c->tos == c->stack) {
snprintf(c->err, sizeof(c->err), "stack underflow");
return;
}
a = &c->tos->a;
h = rb_ary_pop(*a);
message = rb_hash_aref(h, rb_str_new_cstr("message"));
stack = rb_hash_aref(h, rb_str_new_cstr("stack"));
cause = rb_hash_aref(h, rb_str_new_cstr("cause"));
if (NIL_P(message))
message = rb_str_new_cstr("JS exception");
if (!NIL_P(stack)) {
newline = rb_str_new_cstr("\n");
message = rb_funcall(message, rb_intern("concat"), 2, newline, stack);
}
*a = rb_class_new_instance(1, &message, script_error);
if (!NIL_P(cause))
rb_iv_set(*a, "@cause", cause);
pop(c);
}

static int collect(VALUE k, VALUE v, VALUE a)
Expand Down Expand Up @@ -894,6 +926,7 @@ static VALUE rendezvous_callback_do(VALUE arg)
static void *rendezvous_callback(void *arg)
{
struct rendezvous_nogvl *a;
const char *err;
Context *c;
int exc;
VALUE r;
Expand All @@ -917,7 +950,12 @@ static void *rendezvous_callback(void *arg)
buf_move(&s.b, a->req);
return NULL;
fail:
ser_init1(&s, 'e'); // exception pending
ser_init0(&s); // ruby exception pending
w_byte(&s, 'e'); // send ruby error message to v8 thread
r = rb_funcall(c->exception, rb_intern("to_s"), 0);
err = StringValueCStr(r);
if (err)
w(&s, err, strlen(err));
goto out;
}

Expand Down Expand Up @@ -975,18 +1013,20 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
int exc;

rendezvous_no_des(c, req, &res); // takes ownership of |req|
r = c->exception;
c->exception = Qnil;
// if js land didn't handle exception from ruby callback, re-raise it now
if (res.len == 1 && *res.buf == 'e') {
assert(!NIL_P(r));
rb_exc_raise(r);
}
r = rb_protect(deserialize, (VALUE)&(struct rendezvous_des){d, &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;
}

Expand Down Expand Up @@ -1634,6 +1674,11 @@ static VALUE snapshot_size0(VALUE self)
return LONG2FIX(RSTRING_LENINT(ss->blob));
}

static VALUE script_error_cause(VALUE self)
{
return rb_iv_get(self, "@cause");
}

__attribute__((visibility("default")))
void Init_mini_racer_extension(void)
{
Expand All @@ -1648,10 +1693,13 @@ void Init_mini_racer_extension(void)
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);
script_error = rb_define_class_under(m, "ScriptError", 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);

rb_define_method(script_error, "cause", script_error_cause, 0);

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);
Expand Down
31 changes: 29 additions & 2 deletions ext/mini_racer_extension/mini_racer_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ struct State
v8::Persistent<v8::Context> persistent_context; // single-thread mode only
v8::Persistent<v8::Context> persistent_safe_context; // single-thread mode only
v8::Persistent<v8::Function> persistent_safe_context_function; // single-thread mode only
v8::Persistent<v8::Value> ruby_exception;
Context *ruby_context;
int64_t max_memory;
int err_reason;
Expand Down Expand Up @@ -122,6 +123,20 @@ struct Serialized
}
};

bool bubble_up_ruby_exception(State& st, v8::TryCatch *try_catch)
{
auto exception = try_catch->Exception();
if (exception.IsEmpty()) return false;
auto ruby_exception = v8::Local<v8::Value>::New(st.isolate, st.ruby_exception);
if (ruby_exception.IsEmpty()) return false;
if (!ruby_exception->SameValue(exception)) return false;
// signal that the ruby thread should reraise the exception
// that it caught earlier when executing a js->ruby callback
uint8_t c = 'e';
v8_reply(st.ruby_context, &c, 1);
return true;
}

// throws JS exception on serialization error
bool reply(State& st, v8::Local<v8::Value> v)
{
Expand Down Expand Up @@ -388,8 +403,17 @@ void v8_api_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
v8_roundtrip(st.ruby_context, &p, &n);
if (*p == 'c') // callback reply
break;
if (*p == 'e') // ruby exception pending
return st.isolate->TerminateExecution();
if (*p == 'e') { // ruby exception pending
v8::Local<v8::String> message;
auto type = v8::NewStringType::kNormal;
if (!v8::String::NewFromOneByte(st.isolate, p+1, type, n-1).ToLocal(&message)) {
message = v8::String::NewFromUtf8Literal(st.isolate, "Ruby exception");
}
auto exception = v8::Exception::Error(message);
st.ruby_exception.Reset(st.isolate, exception);
st.isolate->ThrowException(exception);
return;
}
v8_dispatch(st.ruby_context);
}
v8::ValueDeserializer des(st.isolate, p+1, n-1);
Expand Down Expand Up @@ -523,6 +547,7 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
st.err_reason = NO_ERROR;
}
if (bubble_up_ruby_exception(st, &try_catch)) return;
if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
if (cause) result = v8::Undefined(st.isolate);
auto err = to_error(st, &try_catch, cause);
Expand Down Expand Up @@ -571,6 +596,7 @@ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
st.err_reason = NO_ERROR;
}
if (bubble_up_ruby_exception(st, &try_catch)) return;
if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
if (cause) result = v8::Undefined(st.isolate);
auto err = to_error(st, &try_catch, cause);
Expand Down Expand Up @@ -895,6 +921,7 @@ State::~State()
v8::Isolate::Scope isolate_scope(isolate);
persistent_safe_context.Reset();
persistent_context.Reset();
ruby_exception.Reset();
}
isolate->Dispose();
for (Callback *cb : callbacks)
Expand Down
10 changes: 7 additions & 3 deletions ext/mini_racer_extension/serde.c
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,21 @@ static inline int r_zigzag(const uint8_t **p, const uint8_t *pe, int64_t *r)
return 0;
}

static inline void ser_init(Ser *s)
static void ser_init0(Ser *s)
{
memset(s, 0, sizeof(*s));
buf_init(&s->b);
}

static inline void ser_init(Ser *s)
{
ser_init0(s);
w(s, "\xFF\x0F", 2);
}

static void ser_init1(Ser *s, uint8_t c)
{
memset(s, 0, sizeof(*s));
buf_init(&s->b);
ser_init0(s);
w_byte(s, c);
w(s, "\xFF\x0F", 2);
}
Expand Down
13 changes: 13 additions & 0 deletions lib/mini_racer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ def backtrace
end
end

class ScriptError < EvalError
def initialize(message)
message, *@frames = message.split("\n")
@frames.map! { "JavaScript #{_1.strip}" }
super(message)
end

def backtrace
frames = super || []
@frames + frames
end
end

class SnapshotError < Error
def initialize(message)
message, *@frames = message.split("\n")
Expand Down
9 changes: 9 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,15 @@ def test_termination_exception
b.kill
end

def test_ruby_exception
context = MiniRacer::Context.new
context.attach("test", proc { raise "boom" })
actual = context.eval("try { test() } catch (e) { e }")
assert_equal(actual.class, MiniRacer::ScriptError)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tweaked it so the JS exception is deserialized to a MiniRacer::ScriptError Ruby exception now.

That should be implementable easily in truffleruby too, I think?

assert_equal(actual.message, "boom")
assert_equal(actual.backtrace, ["JavaScript Error: boom", "JavaScript at <eval>:1:7"])
end

def test_large_integer
[10_000_000_001, -2**63, 2**63-1].each { |big_int|
context = MiniRacer::Context.new
Expand Down
Loading