Skip to content

Commit 180c6d0

Browse files
Add JS::Error class to catch JS exception
1 parent efb24d8 commit 180c6d0

File tree

9 files changed

+191
-28
lines changed

9 files changed

+191
-28
lines changed

ext/js/bindgen/rb-js-abi-host.c

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ void rb_js_abi_host_string_free(rb_js_abi_host_string_t *ret) {
4747
ret->ptr = NULL;
4848
ret->len = 0;
4949
}
50+
void rb_js_abi_host_js_abi_result_free(rb_js_abi_host_js_abi_result_t *ptr) {
51+
switch ((int32_t) ptr->tag) {
52+
case 0: {
53+
rb_js_abi_host_js_abi_value_free(&ptr->val.success);
54+
break;
55+
}
56+
case 1: {
57+
rb_js_abi_host_js_abi_value_free(&ptr->val.failure);
58+
break;
59+
}
60+
}
61+
}
5062
void rb_js_abi_host_raw_integer_free(rb_js_abi_host_raw_integer_t *ptr) {
5163
switch ((int32_t) ptr->tag) {
5264
case 1: {
@@ -63,11 +75,27 @@ void rb_js_abi_host_list_js_abi_value_free(rb_js_abi_host_list_js_abi_value_t *p
6375
free(ptr->ptr);
6476
}
6577
}
66-
__attribute__((import_module("rb-js-abi-host"), import_name("eval-js: func(code: string) -> handle<js-abi-value>")))
67-
int32_t __wasm_import_rb_js_abi_host_eval_js(int32_t, int32_t);
68-
rb_js_abi_host_js_abi_value_t rb_js_abi_host_eval_js(rb_js_abi_host_string_t *code) {
69-
int32_t ret = __wasm_import_rb_js_abi_host_eval_js((int32_t) (*code).ptr, (int32_t) (*code).len);
70-
return (rb_js_abi_host_js_abi_value_t){ ret };
78+
__attribute__((import_module("rb-js-abi-host"), import_name("eval-js: func(code: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }")))
79+
void __wasm_import_rb_js_abi_host_eval_js(int32_t, int32_t, int32_t);
80+
void rb_js_abi_host_eval_js(rb_js_abi_host_string_t *code, rb_js_abi_host_js_abi_result_t *ret0) {
81+
82+
__attribute__((aligned(4)))
83+
uint8_t ret_area[8];
84+
int32_t ptr = (int32_t) &ret_area;
85+
__wasm_import_rb_js_abi_host_eval_js((int32_t) (*code).ptr, (int32_t) (*code).len, ptr);
86+
rb_js_abi_host_js_abi_result_t variant;
87+
variant.tag = (int32_t) (*((uint8_t*) (ptr + 0)));
88+
switch ((int32_t) variant.tag) {
89+
case 0: {
90+
variant.val.success = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
91+
break;
92+
}
93+
case 1: {
94+
variant.val.failure = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
95+
break;
96+
}
97+
}
98+
*ret0 = variant;
7199
}
72100
__attribute__((import_module("rb-js-abi-host"), import_name("is-js: func(value: handle<js-abi-value>) -> bool")))
73101
int32_t __wasm_import_rb_js_abi_host_is_js(int32_t);
@@ -182,11 +210,27 @@ bool rb_js_abi_host_js_value_strictly_equal(rb_js_abi_host_js_abi_value_t lhs, r
182210
int32_t ret = __wasm_import_rb_js_abi_host_js_value_strictly_equal((lhs).idx, (rhs).idx);
183211
return ret;
184212
}
185-
__attribute__((import_module("rb-js-abi-host"), import_name("reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> handle<js-abi-value>")))
186-
int32_t __wasm_import_rb_js_abi_host_reflect_apply(int32_t, int32_t, int32_t, int32_t);
187-
rb_js_abi_host_js_abi_value_t rb_js_abi_host_reflect_apply(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_js_abi_value_t this_argument, rb_js_abi_host_list_js_abi_value_t *arguments) {
188-
int32_t ret = __wasm_import_rb_js_abi_host_reflect_apply((target).idx, (this_argument).idx, (int32_t) (*arguments).ptr, (int32_t) (*arguments).len);
189-
return (rb_js_abi_host_js_abi_value_t){ ret };
213+
__attribute__((import_module("rb-js-abi-host"), import_name("reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }")))
214+
void __wasm_import_rb_js_abi_host_reflect_apply(int32_t, int32_t, int32_t, int32_t, int32_t);
215+
void rb_js_abi_host_reflect_apply(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_js_abi_value_t this_argument, rb_js_abi_host_list_js_abi_value_t *arguments, rb_js_abi_host_js_abi_result_t *ret0) {
216+
217+
__attribute__((aligned(4)))
218+
uint8_t ret_area[8];
219+
int32_t ptr = (int32_t) &ret_area;
220+
__wasm_import_rb_js_abi_host_reflect_apply((target).idx, (this_argument).idx, (int32_t) (*arguments).ptr, (int32_t) (*arguments).len, ptr);
221+
rb_js_abi_host_js_abi_result_t variant;
222+
variant.tag = (int32_t) (*((uint8_t*) (ptr + 0)));
223+
switch ((int32_t) variant.tag) {
224+
case 0: {
225+
variant.val.success = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
226+
break;
227+
}
228+
case 1: {
229+
variant.val.failure = (rb_js_abi_host_js_abi_value_t){ *((int32_t*) (ptr + 4)) };
230+
break;
231+
}
232+
}
233+
*ret0 = variant;
190234
}
191235
__attribute__((import_module("rb-js-abi-host"), import_name("reflect-construct: func(target: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> handle<js-abi-value>")))
192236
int32_t __wasm_import_rb_js_abi_host_reflect_construct(int32_t, int32_t, int32_t);

ext/js/bindgen/rb-js-abi-host.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ extern "C"
2222
void rb_js_abi_host_string_set(rb_js_abi_host_string_t *ret, const char *s);
2323
void rb_js_abi_host_string_dup(rb_js_abi_host_string_t *ret, const char *s);
2424
void rb_js_abi_host_string_free(rb_js_abi_host_string_t *ret);
25+
typedef struct {
26+
uint8_t tag;
27+
union {
28+
rb_js_abi_host_js_abi_value_t success;
29+
rb_js_abi_host_js_abi_value_t failure;
30+
} val;
31+
} rb_js_abi_host_js_abi_result_t;
32+
#define RB_JS_ABI_HOST_JS_ABI_RESULT_SUCCESS 0
33+
#define RB_JS_ABI_HOST_JS_ABI_RESULT_FAILURE 1
34+
void rb_js_abi_host_js_abi_result_free(rb_js_abi_host_js_abi_result_t *ptr);
2535
typedef struct {
2636
uint8_t tag;
2737
union {
@@ -37,7 +47,7 @@ extern "C"
3747
size_t len;
3848
} rb_js_abi_host_list_js_abi_value_t;
3949
void rb_js_abi_host_list_js_abi_value_free(rb_js_abi_host_list_js_abi_value_t *ptr);
40-
rb_js_abi_host_js_abi_value_t rb_js_abi_host_eval_js(rb_js_abi_host_string_t *code);
50+
void rb_js_abi_host_eval_js(rb_js_abi_host_string_t *code, rb_js_abi_host_js_abi_result_t *ret0);
4151
bool rb_js_abi_host_is_js(rb_js_abi_host_js_abi_value_t value);
4252
bool rb_js_abi_host_instance_of(rb_js_abi_host_js_abi_value_t value, rb_js_abi_host_js_abi_value_t klass);
4353
rb_js_abi_host_js_abi_value_t rb_js_abi_host_global_this(void);
@@ -53,7 +63,7 @@ extern "C"
5363
void rb_js_abi_host_js_value_typeof(rb_js_abi_host_js_abi_value_t value, rb_js_abi_host_string_t *ret0);
5464
bool rb_js_abi_host_js_value_equal(rb_js_abi_host_js_abi_value_t lhs, rb_js_abi_host_js_abi_value_t rhs);
5565
bool rb_js_abi_host_js_value_strictly_equal(rb_js_abi_host_js_abi_value_t lhs, rb_js_abi_host_js_abi_value_t rhs);
56-
rb_js_abi_host_js_abi_value_t rb_js_abi_host_reflect_apply(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_js_abi_value_t this_argument, rb_js_abi_host_list_js_abi_value_t *arguments);
66+
void rb_js_abi_host_reflect_apply(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_js_abi_value_t this_argument, rb_js_abi_host_list_js_abi_value_t *arguments, rb_js_abi_host_js_abi_result_t *ret0);
5767
rb_js_abi_host_js_abi_value_t rb_js_abi_host_reflect_construct(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_list_js_abi_value_t *arguments);
5868
bool rb_js_abi_host_reflect_delete_property(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_string_t *property_key);
5969
rb_js_abi_host_js_abi_value_t rb_js_abi_host_reflect_get(rb_js_abi_host_js_abi_value_t target, rb_js_abi_host_string_t *property_key);

ext/js/bindgen/rb-js-abi-host.wit

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
resource js-abi-value
22

3-
eval-js: func(code: string) -> js-abi-value
3+
variant js-abi-result {
4+
success(js-abi-value),
5+
failure(js-abi-value),
6+
}
7+
8+
eval-js: func(code: string) -> js-abi-result
49
is-js: func(value: js-abi-value) -> bool
510
instance-of: func(value: js-abi-value, klass: js-abi-value) -> bool
611
global-this: func() -> js-abi-value
@@ -27,7 +32,7 @@ js-value-typeof: func(value: js-abi-value) -> string
2732
js-value-equal: func(lhs: js-abi-value, rhs: js-abi-value) -> bool
2833
js-value-strictly-equal: func(lhs: js-abi-value, rhs: js-abi-value) -> bool
2934

30-
reflect-apply: func(target: js-abi-value, this-argument: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
35+
reflect-apply: func(target: js-abi-value, this-argument: js-abi-value, arguments: list<js-abi-value>) -> js-abi-result
3136
reflect-construct: func(target: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
3237
reflect-delete-property: func(target: js-abi-value, property-key: string) -> bool
3338
reflect-get: func(target: js-abi-value, property-key: string) -> js-abi-value

ext/js/js-core.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ void rb_abi_lend_object(VALUE obj);
2222

2323
static VALUE rb_mJS;
2424
static VALUE rb_cJS_Object;
25+
static VALUE rb_cJS_Error;
2526

2627
static ID i_to_js;
2728

@@ -76,6 +77,14 @@ static inline void rstring_to_abi_string(VALUE rstr,
7677
memcpy(abi_str->ptr, RSTRING_PTR(rstr), abi_str->len);
7778
}
7879

80+
static inline void raise_js_error_if_failure(const rb_js_abi_host_js_abi_result_t *result) {
81+
if (result->tag == RB_JS_ABI_HOST_JS_ABI_RESULT_FAILURE) {
82+
VALUE js_err = jsvalue_s_new(result->val.failure);
83+
VALUE rb_err = rb_class_new_instance(1, &js_err, rb_cJS_Error);
84+
rb_exc_raise(rb_err);
85+
}
86+
}
87+
7988
/*
8089
* call-seq:
8190
* JS.eval(code) -> JS::Object
@@ -88,7 +97,10 @@ static inline void rstring_to_abi_string(VALUE rstr,
8897
static VALUE _rb_js_eval_js(VALUE _, VALUE code_str) {
8998
rb_js_abi_host_string_t abi_str;
9099
rstring_to_abi_string(code_str, &abi_str);
91-
return jsvalue_s_new(rb_js_abi_host_eval_js(&abi_str));
100+
rb_js_abi_host_js_abi_result_t ret;
101+
rb_js_abi_host_eval_js(&abi_str, &ret);
102+
raise_js_error_if_failure(&ret);
103+
return jsvalue_s_new(ret.val.success);
92104
}
93105

94106
static VALUE _rb_js_is_js(VALUE _, VALUE obj) {
@@ -276,8 +288,10 @@ static VALUE _rb_js_obj_call(int argc, VALUE *argv, VALUE obj) {
276288
rb_ary_push(rv_args, rb_proc);
277289
}
278290

279-
VALUE result = jsvalue_s_new(
280-
rb_js_abi_host_reflect_apply(abi_method->abi, p->abi, &abi_args));
291+
rb_js_abi_host_js_abi_result_t ret;
292+
rb_js_abi_host_reflect_apply(abi_method->abi, p->abi, &abi_args, &ret);
293+
raise_js_error_if_failure(&ret);
294+
VALUE result = jsvalue_s_new(ret.val.success);
281295
RB_GC_GUARD(rv_args);
282296
RB_GC_GUARD(method);
283297
return result;
@@ -539,4 +553,6 @@ void Init_js() {
539553
rb_define_method(rb_cTrueClass, "to_js", _rb_js_true_to_js, 0);
540554
rb_define_method(rb_cFalseClass, "to_js", _rb_js_false_to_js, 0);
541555
rb_define_method(rb_cProc, "to_js", _rb_js_proc_to_js, 0);
556+
557+
rb_cJS_Error = rb_define_class_under(rb_mJS, "Error", rb_eStandardError);
542558
}

ext/js/lib/js.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,21 @@ def respond_to_missing?(sym, include_private)
3131
self[sym].typeof == "function"
3232
end
3333
end
34+
35+
# A wrapper class for JavaScript Error to allow the Error to be thrown in Ruby.
36+
class JS::Error
37+
def initialize(exception)
38+
@exception = exception
39+
super
40+
end
41+
42+
def message
43+
stack = @exception[:stack]
44+
if stack.typeof == "string"
45+
# Error.stack contains the error message also
46+
stack.to_s
47+
else
48+
@exception.to_s
49+
end
50+
end
51+
end

packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.d.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
export type JsAbiResult = JsAbiResultSuccess | JsAbiResultFailure;
2+
export interface JsAbiResultSuccess {
3+
tag: "success",
4+
val: JsAbiValue,
5+
}
6+
export interface JsAbiResultFailure {
7+
tag: "failure",
8+
val: JsAbiValue,
9+
}
110
export type RawInteger = RawIntegerF64 | RawIntegerBignum;
211
export interface RawIntegerF64 {
312
tag: "f64",
@@ -9,7 +18,7 @@ export interface RawIntegerBignum {
918
}
1019
export function addRbJsAbiHostToImports(imports: any, obj: RbJsAbiHost, get_export: (name: string) => WebAssembly.ExportValue): void;
1120
export interface RbJsAbiHost {
12-
evalJs(code: string): JsAbiValue;
21+
evalJs(code: string): JsAbiResult;
1322
isJs(value: JsAbiValue): boolean;
1423
instanceOf(value: JsAbiValue, klass: JsAbiValue): boolean;
1524
globalThis(): JsAbiValue;
@@ -25,7 +34,7 @@ export interface RbJsAbiHost {
2534
jsValueTypeof(value: JsAbiValue): string;
2635
jsValueEqual(lhs: JsAbiValue, rhs: JsAbiValue): boolean;
2736
jsValueStrictlyEqual(lhs: JsAbiValue, rhs: JsAbiValue): boolean;
28-
reflectApply(target: JsAbiValue, thisArgument: JsAbiValue, arguments: JsAbiValue[]): JsAbiValue;
37+
reflectApply(target: JsAbiValue, thisArgument: JsAbiValue, arguments: JsAbiValue[]): JsAbiResult;
2938
reflectConstruct(target: JsAbiValue, arguments: JsAbiValue[]): JsAbiValue;
3039
reflectDeleteProperty(target: JsAbiValue, propertyKey: string): boolean;
3140
reflectGet(target: JsAbiValue, propertyKey: string): JsAbiValue;

packages/npm-packages/ruby-wasm-wasi/src/bindgen/rb-js-abi-host.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { data_view, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab, throw_invalid_bool } from './intrinsics.js';
22
export function addRbJsAbiHostToImports(imports, obj, get_export) {
33
if (!("rb-js-abi-host" in imports)) imports["rb-js-abi-host"] = {};
4-
imports["rb-js-abi-host"]["eval-js: func(code: string) -> handle<js-abi-value>"] = function(arg0, arg1) {
4+
imports["rb-js-abi-host"]["eval-js: func(code: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }"] = function(arg0, arg1, arg2) {
55
const memory = get_export("memory");
66
const ptr0 = arg0;
77
const len0 = arg1;
88
const result0 = UTF8_DECODER.decode(new Uint8Array(memory.buffer, ptr0, len0));
99
const ret0 = obj.evalJs(result0);
10-
return resources0.insert(ret0);
10+
const variant1 = ret0;
11+
switch (variant1.tag) {
12+
case "success": {
13+
const e = variant1.val;
14+
data_view(memory).setInt8(arg2 + 0, 0, true);
15+
data_view(memory).setInt32(arg2 + 4, resources0.insert(e), true);
16+
break;
17+
}
18+
case "failure": {
19+
const e = variant1.val;
20+
data_view(memory).setInt8(arg2 + 0, 1, true);
21+
data_view(memory).setInt32(arg2 + 4, resources0.insert(e), true);
22+
break;
23+
}
24+
default:
25+
throw new RangeError("invalid variant specified for JsAbiResult");
26+
}
1127
};
1228
imports["rb-js-abi-host"]["is-js: func(value: handle<js-abi-value>) -> bool"] = function(arg0) {
1329
const ret0 = obj.isJs(resources0.get(arg0));
@@ -104,7 +120,7 @@ export function addRbJsAbiHostToImports(imports, obj, get_export) {
104120
const ret0 = obj.jsValueStrictlyEqual(resources0.get(arg0), resources0.get(arg1));
105121
return ret0 ? 1 : 0;
106122
};
107-
imports["rb-js-abi-host"]["reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> handle<js-abi-value>"] = function(arg0, arg1, arg2, arg3) {
123+
imports["rb-js-abi-host"]["reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }"] = function(arg0, arg1, arg2, arg3, arg4) {
108124
const memory = get_export("memory");
109125
const len0 = arg3;
110126
const base0 = arg2;
@@ -114,7 +130,23 @@ export function addRbJsAbiHostToImports(imports, obj, get_export) {
114130
result0.push(resources0.get(data_view(memory).getInt32(base + 0, true)));
115131
}
116132
const ret0 = obj.reflectApply(resources0.get(arg0), resources0.get(arg1), result0);
117-
return resources0.insert(ret0);
133+
const variant1 = ret0;
134+
switch (variant1.tag) {
135+
case "success": {
136+
const e = variant1.val;
137+
data_view(memory).setInt8(arg4 + 0, 0, true);
138+
data_view(memory).setInt32(arg4 + 4, resources0.insert(e), true);
139+
break;
140+
}
141+
case "failure": {
142+
const e = variant1.val;
143+
data_view(memory).setInt8(arg4 + 0, 1, true);
144+
data_view(memory).setInt32(arg4 + 4, resources0.insert(e), true);
145+
break;
146+
}
147+
default:
148+
throw new RangeError("invalid variant specified for JsAbiResult");
149+
}
118150
};
119151
imports["rb-js-abi-host"]["reflect-construct: func(target: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> handle<js-abi-value>"] = function(arg0, arg1, arg2) {
120152
const memory = get_export("memory");

packages/npm-packages/ruby-wasm-wasi/src/index.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as RbAbi from "./bindgen/rb-abi-guest";
2-
import { addRbJsAbiHostToImports, JsAbiValue } from "./bindgen/rb-js-abi-host";
2+
import { addRbJsAbiHostToImports, JsAbiResult, JsAbiValue } from "./bindgen/rb-js-abi-host";
33

44
/**
55
* A Ruby VM instance
@@ -65,12 +65,21 @@ export class RubyVM {
6565
*/
6666
addToImports(imports: WebAssembly.Imports) {
6767
this.guest.addToImports(imports);
68+
function wrapTry(f: (...args: any[]) => JsAbiValue): () => JsAbiResult {
69+
return (...args) => {
70+
try {
71+
return { tag: "success", val: f(...args) };
72+
} catch (e) {
73+
return { tag: "failure", val: e }
74+
}
75+
}
76+
};
6877
addRbJsAbiHostToImports(
6978
imports,
7079
{
71-
evalJs: (code) => {
80+
evalJs: wrapTry((code) => {
7281
return Function(code)();
73-
},
82+
}),
7483
isJs: (value) => {
7584
// Just for compatibility with the old JS API
7685
return true;
@@ -144,9 +153,9 @@ export class RubyVM {
144153
jsValueStrictlyEqual(lhs, rhs) {
145154
return lhs === rhs;
146155
},
147-
reflectApply: function (target, thisArgument, args) {
156+
reflectApply: wrapTry((target, thisArgument, args) => {
148157
return Reflect.apply(target as any, thisArgument, args);
149-
},
158+
}),
150159
reflectConstruct: function (target, args) {
151160
throw new Error("Function not implemented.");
152161
},
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require "test-unit"
2+
require "js"
3+
4+
class JS::TestError < Test::Unit::TestCase
5+
def test_throw_error
6+
e = assert_raise(JS::Error) do
7+
JS.eval("throw new Error('foo')")
8+
end
9+
assert_match /^Error: foo/, e.message
10+
assert_equal "#<JS::Error: Error: foo>", e.inspect
11+
end
12+
13+
def test_throw_non_error
14+
e = assert_raise(JS::Error) do
15+
JS.eval("throw 'foo'")
16+
end
17+
assert_equal "foo", e.message
18+
assert_equal "#<JS::Error: foo>", e.inspect
19+
end
20+
end

0 commit comments

Comments
 (0)