quickjs 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e13c4664328e497bb8c2179807e8c110c4709b6df84a9a60f888e5a822075de2
4
- data.tar.gz: c43d43f3c0b928798545ecf57bf9956df1912e3867220915dd5b13c8aeb0031f
3
+ metadata.gz: 54fe50175028245be99548bdb43a1884ceb956f77a999fca6c4cdf9ea9085a13
4
+ data.tar.gz: ddbf8d46cfec89db687899ec083e7ef2859ebd4ff9d711c9a18801d15c8d4ad0
5
5
  SHA512:
6
- metadata.gz: 581463e6df8d5559f9aa1f91e4512b68f745942d771b2246202d3137a378876c6662db800381abd4dc065b4a6a8acb03f247feda4b78aa9ce1fc31cdd6ed6836
7
- data.tar.gz: cd4f64a7d98a75c5e388b99e1cdbd44c6ad4cfc4cd96f8e23259f12aa8cbe18f8c580ad41633cb65c9f77a6f9732fbb6e042b6805cc034a0c4c060f4ecdb3635
6
+ metadata.gz: ce86220556d6e2bbf06f2da42227876f972a991e4beca36ecce89efcfc84f11ee6d62172813442368af11632f33f970db97535eab435f3fa6046be955c98529d
7
+ data.tar.gz: 255c23efa72230afbac3760e1cd8efd8ba29038263371ed4a4bc591dfbc58da9e48e2524bde82151cb8b10d14cda87255170fe0c46377efec8cb114ea8e9a855
data/README.md CHANGED
@@ -40,6 +40,13 @@ Quickjs.eval_code(code,
40
40
  )
41
41
  ```
42
42
 
43
+ #### Filename
44
+
45
+ ```rb
46
+ # Label shown in JS stack traces (default: "<code>")
47
+ Quickjs.eval_code(code, filename: 'my_script.js')
48
+ ```
49
+
43
50
  #### Timeout
44
51
 
45
52
  ```rb
@@ -133,6 +140,23 @@ end
133
140
  vm.eval_code("greetingTo('Rick')") #=> 'Hello! Rick'
134
141
  ```
135
142
 
143
+ Pass an `Array` as the name to register the function on an existing JS object (the last element is the method name; preceding elements are the object path):
144
+
145
+ ```rb
146
+ vm = Quickjs::VM.new
147
+ vm.eval_code("const myLib = {}")
148
+ vm.define_function(["myLib", "greetingTo"]) { |name| "Hello, #{name}!" }
149
+
150
+ vm.eval_code("myLib.greetingTo('Rick')") #=> 'Hello! Rick'
151
+
152
+ # Deeply nested
153
+ vm.eval_code("const a = { b: { c: {} } }")
154
+ vm.define_function(["a", "b", "c", "double"]) { |x| x * 2 }
155
+ vm.eval_code("a.b.c.double(21)") #=> 42
156
+ ```
157
+
158
+ `define_function` returns the registered name as a `Symbol` (or an `Array` of `Symbol`s for array paths).
159
+
136
160
  A Ruby exception raised inside the block is catchable in JS as an `Error`, and propagates back to Ruby as the original exception type if uncaught in JS.
137
161
 
138
162
  ```rb
@@ -177,14 +201,20 @@ vm.eval_code('console.log("hello", 42)')
177
201
  | `string` | ↔ | `String` | |
178
202
  | `true` / `false` | ↔ | `true` / `false` | |
179
203
  | `null` | ↔ | `nil` | |
180
- | `Array` | ↔ | `Array` | via JSON |
181
- | `Object` | ↔ | `Hash` | via JSON |
204
+ | `Array` | ↔ | `Array` | recursively converted |
205
+ | `Object` | ↔ | `Hash` | recursively converted; keys are always `String` |
206
+ | `function` | → | `Quickjs::Function` — `.source`, `.call(*args, on:)` | |
182
207
  | `undefined` | → | `Quickjs::Value::UNDEFINED` | |
183
208
  | `NaN` | → | `Quickjs::Value::NAN` | |
184
209
  | `Blob` | → | `Quickjs::Blob` — `.size`, `.type`, `.content` | requires `POLYFILL_FILE` |
185
210
  | `File` | → | `Quickjs::File` — `.name`, `.last_modified` + Blob attrs | requires `POLYFILL_FILE` |
186
211
  | `File` proxy | ← | `::File` | requires `POLYFILL_FILE`; applies to `define_function` return values |
187
212
 
213
+ ## Acknowledgements
214
+
215
+ - [@ursm](https://github.com/ursm) — for continuous contributions improving performance and developer experience
216
+ - [@persona-id](https://github.com/persona-id) — for providing real-world use cases that shape the direction of this project
217
+
188
218
  ## License
189
219
 
190
220
  - `ext/quickjsrb/quickjs`
@@ -57,8 +57,21 @@ typedef struct
57
57
  static int rb_hash_entry_to_js(VALUE r_key, VALUE r_val, VALUE extra)
58
58
  {
59
59
  RbHashToJsArg *arg = (RbHashToJsArg *)extra;
60
- VALUE r_key_str = rb_funcall(r_key, rb_intern("to_s"), 0);
61
- JS_SetPropertyStr(arg->ctx, arg->j_obj, StringValueCStr(r_key_str), to_js_value(arg->ctx, r_val));
60
+ const char *key_cstr;
61
+ if (SYMBOL_P(r_key))
62
+ {
63
+ key_cstr = rb_id2name(SYM2ID(r_key));
64
+ }
65
+ else if (RB_TYPE_P(r_key, T_STRING))
66
+ {
67
+ key_cstr = StringValueCStr(r_key);
68
+ }
69
+ else
70
+ {
71
+ VALUE r_key_str = rb_funcall(r_key, rb_intern("to_s"), 0);
72
+ key_cstr = StringValueCStr(r_key_str);
73
+ }
74
+ JS_SetPropertyStr(arg->ctx, arg->j_obj, key_cstr, to_js_value(arg->ctx, r_val));
62
75
  return ST_CONTINUE;
63
76
  }
64
77
 
@@ -69,26 +82,23 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
69
82
  case T_NIL:
70
83
  return JS_NULL;
71
84
  case T_FIXNUM:
85
+ return JS_NewInt64(ctx, NUM2LL(r_value));
72
86
  case T_FLOAT:
87
+ return JS_NewFloat64(ctx, NUM2DBL(r_value));
88
+ case T_BIGNUM:
73
89
  {
74
90
  VALUE r_str = rb_funcall(r_value, rb_intern("to_s"), 0);
75
- char *str = StringValueCStr(r_str);
91
+ JSValue j_str = JS_NewStringLen(ctx, RSTRING_PTR(r_str), RSTRING_LEN(r_str));
76
92
  JSValue j_global = JS_GetGlobalObject(ctx);
77
93
  JSValue j_numberClass = JS_GetPropertyStr(ctx, j_global, "Number");
78
- JSValue j_str = JS_NewString(ctx, str);
79
- JSValue j_stringified = JS_Call(ctx, j_numberClass, JS_UNDEFINED, 1, (JSValueConst *)&j_str);
80
- JS_FreeValue(ctx, j_global);
81
- JS_FreeValue(ctx, j_numberClass);
94
+ JSValue j_num = JS_Call(ctx, j_numberClass, JS_UNDEFINED, 1, (JSValueConst *)&j_str);
82
95
  JS_FreeValue(ctx, j_str);
83
-
84
- return j_stringified;
96
+ JS_FreeValue(ctx, j_numberClass);
97
+ JS_FreeValue(ctx, j_global);
98
+ return j_num;
85
99
  }
86
100
  case T_STRING:
87
- {
88
- char *str = StringValueCStr(r_value);
89
-
90
- return JS_NewString(ctx, str);
91
- }
101
+ return JS_NewStringLen(ctx, RSTRING_PTR(r_value), RSTRING_LEN(r_value));
92
102
  case T_SYMBOL:
93
103
  {
94
104
  if (r_value == QUICKJSRB_SYM(undefinedId))
@@ -100,10 +110,8 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
100
110
  JS_FreeValue(ctx, j_global);
101
111
  return j_nan;
102
112
  }
103
- VALUE r_str = rb_funcall(r_value, rb_intern("to_s"), 0);
104
- char *str = StringValueCStr(r_str);
105
-
106
- return JS_NewString(ctx, str);
113
+ const char *name = rb_id2name(SYM2ID(r_value));
114
+ return JS_NewString(ctx, name);
107
115
  }
108
116
  case T_TRUE:
109
117
  return JS_TRUE;
@@ -134,10 +142,7 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
134
142
  if (!JS_IsUndefined(data->j_file_proxy_creator))
135
143
  return quickjsrb_file_to_js(ctx, r_value);
136
144
  }
137
- if (TYPE(r_value) == T_OBJECT && RTEST(rb_funcall(
138
- r_value,
139
- rb_intern("is_a?"),
140
- 1, rb_const_get(rb_cClass, rb_intern("Exception")))))
145
+ if (rb_obj_is_kind_of(r_value, rb_eException))
141
146
  {
142
147
  return j_error_from_ruby_error(ctx, r_value);
143
148
  }
@@ -266,16 +271,13 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
266
271
  }
267
272
  case JS_TAG_STRING:
268
273
  {
269
- int couldntParse;
270
- VALUE r_result = rb_protect(r_try_json_parse, to_r_json(ctx, j_val), &couldntParse);
271
- if (couldntParse)
272
- {
274
+ size_t len;
275
+ const char *str = JS_ToCStringLen(ctx, &len, j_val);
276
+ if (str == NULL)
273
277
  return Qnil;
274
- }
275
- else
276
- {
277
- return r_result;
278
- }
278
+ VALUE r_str = rb_utf8_str_new(str, (long)len);
279
+ JS_FreeCString(ctx, str);
280
+ return r_str;
279
281
  }
280
282
  case JS_TAG_OBJECT:
281
283
  {
@@ -482,16 +484,19 @@ static VALUE r_try_call_proc(VALUE r_try_args)
482
484
 
483
485
  static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int argc, JSValueConst *argv, int _magic, JSValue *func_data)
484
486
  {
485
- const char *funcName = JS_ToCString(ctx, func_data[0]);
487
+ // func_data[0] holds the Ruby Symbol ID for the defined function (stored by
488
+ // vm_m_defineGlobalFunction). Looking up by ID avoids a JS_ToCString +
489
+ // rb_intern round-trip on every call.
490
+ int64_t key_id;
491
+ JS_ToInt64(ctx, &key_id, func_data[0]);
486
492
 
487
493
  VMData *data = JS_GetContextOpaque(ctx);
488
- VALUE r_proc = rb_hash_aref(data->defined_functions, ID2SYM(rb_intern(funcName)));
494
+ VALUE r_proc = rb_hash_aref(data->defined_functions, ID2SYM((ID)key_id));
489
495
  // Shouldn't happen
490
496
  if (r_proc == Qnil)
491
497
  {
492
- return JS_ThrowReferenceError(ctx, "Proc `%s` is not defined", funcName); // TODO: Free funcnName
498
+ return JS_ThrowReferenceError(ctx, "Proc is not defined");
493
499
  }
494
- JS_FreeCString(ctx, funcName);
495
500
 
496
501
  VALUE r_call_args = rb_ary_new();
497
502
  rb_ary_push(r_call_args, r_proc);
@@ -842,22 +847,36 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
842
847
  return result;
843
848
  }
844
849
 
845
- static VALUE vm_m_evalCode(VALUE r_self, VALUE r_code)
850
+ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
846
851
  {
847
852
  VMData *data;
848
853
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
849
854
 
855
+ VALUE r_code, r_opts;
856
+ rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
857
+
850
858
  if (!RB_TYPE_P(r_code, T_STRING))
851
859
  {
852
860
  VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
853
861
  rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
854
862
  }
855
863
 
864
+ const char *filename = "<code>";
865
+ if (!NIL_P(r_opts))
866
+ {
867
+ VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
868
+ if (!NIL_P(r_filename))
869
+ {
870
+ Check_Type(r_filename, T_STRING);
871
+ filename = StringValueCStr(r_filename);
872
+ }
873
+ }
874
+
856
875
  clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
857
876
  JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
858
877
 
859
- char *code = StringValueCStr(r_code);
860
- JSValue j_codeResult = JS_Eval(data->context, code, strlen(code), "<code>", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC);
878
+ StringValue(r_code);
879
+ JSValue j_codeResult = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), filename, JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC);
861
880
  JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
862
881
  // JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
863
882
  // Free j_awaitedResult before to_rb_return_value because it may raise (longjmp), which would skip cleanup
@@ -904,7 +923,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
904
923
  char *funcName = StringValueCStr(r_func_seg_str);
905
924
 
906
925
  JSValueConst ruby_data[2];
907
- ruby_data[0] = JS_NewString(data->context, StringValueCStr(r_key_str));
926
+ ruby_data[0] = JS_NewInt64(data->context, (int64_t)SYM2ID(r_key_sym));
908
927
  ruby_data[1] = JS_NewBool(data->context, RTEST(rb_funcall(r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
909
928
 
910
929
  // Resolve the parent object to attach the function to.
@@ -969,7 +988,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
969
988
  char *funcName = StringValueCStr(r_name_str);
970
989
 
971
990
  JSValueConst ruby_data[2];
972
- ruby_data[0] = JS_NewString(data->context, funcName);
991
+ ruby_data[0] = JS_NewInt64(data->context, (int64_t)SYM2ID(r_name_sym));
973
992
  ruby_data[1] = JS_NewBool(data->context, RTEST(rb_funcall(r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
974
993
 
975
994
  JSValue j_global = JS_GetGlobalObject(data->context);
@@ -1196,7 +1215,7 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1196
1215
  VALUE r_class_vm = rb_define_class_under(r_module_quickjs, "VM", rb_cObject);
1197
1216
  rb_define_alloc_func(r_class_vm, vm_alloc);
1198
1217
  rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
1199
- rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, 1);
1218
+ rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, -1);
1200
1219
  rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
1201
1220
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
1202
1221
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quickjs
4
- VERSION = "0.15.0"
4
+ VERSION = "0.15.1"
5
5
  end
data/lib/quickjs.rb CHANGED
@@ -18,8 +18,10 @@ module Quickjs
18
18
  end
19
19
 
20
20
  def eval_code(code, overwrite_opts = {})
21
+ eval_opts = {}
22
+ eval_opts[:filename] = overwrite_opts.delete(:filename) if overwrite_opts.key?(:filename)
21
23
  vm = Quickjs::VM.new(**overwrite_opts)
22
- res = vm.eval_code(code)
24
+ res = vm.eval_code(code, **eval_opts)
23
25
  vm = nil
24
26
  res
25
27
  end
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "quickjs-rb-polyfills",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "quickjs-rb-polyfills",
9
- "version": "0.15.0",
9
+ "version": "0.15.1",
10
10
  "dependencies": {
11
11
  "@formatjs/intl-datetimeformat": "^7.3.1",
12
12
  "@formatjs/intl-getcanonicallocales": "^3.2.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickjs-rb-polyfills",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "rolldown -c rolldown.config.mjs",
data/sig/quickjs.rbs CHANGED
@@ -21,7 +21,7 @@ module Quickjs
21
21
  class VM
22
22
  def initialize: (?features: Array[Symbol], ?memory_limit: Integer, ?max_stack_size: Integer, ?timeout_msec: Integer) -> void
23
23
 
24
- def eval_code: (String code) -> untyped
24
+ def eval_code: (String code, ?filename: String) -> untyped
25
25
 
26
26
  def call: (String | Symbol name, *untyped args) -> untyped
27
27
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quickjs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmsk