quickjs 0.14.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: 69b86d7c5040dba161c1d6f67cb3eab3fd8a3dc51b30eb6d355c2580dbc29602
4
- data.tar.gz: 5e174baed18d79e0ac18a496f97cd580fd20830a98fb240351eeea46abe57683
3
+ metadata.gz: 54fe50175028245be99548bdb43a1884ceb956f77a999fca6c4cdf9ea9085a13
4
+ data.tar.gz: ddbf8d46cfec89db687899ec083e7ef2859ebd4ff9d711c9a18801d15c8d4ad0
5
5
  SHA512:
6
- metadata.gz: 9f7abcc1733fcfe9c51aacf81c9e34b05e7e44142a9db79a8bd4525ba67570f897cd7c1fb200428507876dad35a67b9a3f383711a3b0cfc9ff1b46728cd1c125
7
- data.tar.gz: a15abae6b482f0df7efc89e9b0a3b9d3ca3bab8bb8d04ce7844f60b195a0314d8e12d2b3bbdf8cf01dae763e75945d4e84cf3c1efbe2a99d4cfa525e2279625e
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`
@@ -3,6 +3,7 @@
3
3
  require 'mkmf'
4
4
 
5
5
  $VPATH << "$(srcdir)/quickjs"
6
+ $INCFLAGS << " -I$(srcdir)/quickjs"
6
7
 
7
8
  $srcs = [
8
9
  'dtoa.c',
@@ -21,8 +22,6 @@ $srcs = [
21
22
  'quickjsrb_crypto_subtle.c',
22
23
  ]
23
24
 
24
- append_cflags('-I$(srcdir)/quickjs')
25
-
26
25
  append_cflags('-g')
27
26
  append_cflags('-O2')
28
27
  append_cflags('-Wall')
@@ -26,6 +26,9 @@ const int num_native_errors = sizeof(native_errors) / sizeof(native_errors[0]);
26
26
 
27
27
  static int dispatch_log(VMData *data, const char *severity, VALUE r_row);
28
28
 
29
+ JSValue to_js_value(JSContext *ctx, VALUE r_value);
30
+ VALUE to_rb_value(JSContext *ctx, JSValue j_val);
31
+
29
32
  JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
30
33
  {
31
34
  JSValue j_error = JS_NewError(ctx); // may wanna have custom error class to determine in JS' end
@@ -45,6 +48,33 @@ JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
45
48
  return j_error;
46
49
  }
47
50
 
51
+ typedef struct
52
+ {
53
+ JSContext *ctx;
54
+ JSValue j_obj;
55
+ } RbHashToJsArg;
56
+
57
+ static int rb_hash_entry_to_js(VALUE r_key, VALUE r_val, VALUE extra)
58
+ {
59
+ RbHashToJsArg *arg = (RbHashToJsArg *)extra;
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));
75
+ return ST_CONTINUE;
76
+ }
77
+
48
78
  JSValue to_js_value(JSContext *ctx, VALUE r_value)
49
79
  {
50
80
  switch (TYPE(r_value))
@@ -52,45 +82,57 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
52
82
  case T_NIL:
53
83
  return JS_NULL;
54
84
  case T_FIXNUM:
85
+ return JS_NewInt64(ctx, NUM2LL(r_value));
55
86
  case T_FLOAT:
87
+ return JS_NewFloat64(ctx, NUM2DBL(r_value));
88
+ case T_BIGNUM:
56
89
  {
57
90
  VALUE r_str = rb_funcall(r_value, rb_intern("to_s"), 0);
58
- char *str = StringValueCStr(r_str);
91
+ JSValue j_str = JS_NewStringLen(ctx, RSTRING_PTR(r_str), RSTRING_LEN(r_str));
59
92
  JSValue j_global = JS_GetGlobalObject(ctx);
60
93
  JSValue j_numberClass = JS_GetPropertyStr(ctx, j_global, "Number");
61
- JSValue j_str = JS_NewString(ctx, str);
62
- JSValue j_stringified = JS_Call(ctx, j_numberClass, JS_UNDEFINED, 1, (JSValueConst *)&j_str);
63
- JS_FreeValue(ctx, j_global);
64
- JS_FreeValue(ctx, j_numberClass);
94
+ JSValue j_num = JS_Call(ctx, j_numberClass, JS_UNDEFINED, 1, (JSValueConst *)&j_str);
65
95
  JS_FreeValue(ctx, j_str);
66
-
67
- return j_stringified;
96
+ JS_FreeValue(ctx, j_numberClass);
97
+ JS_FreeValue(ctx, j_global);
98
+ return j_num;
68
99
  }
69
100
  case T_STRING:
70
- {
71
- char *str = StringValueCStr(r_value);
72
-
73
- return JS_NewString(ctx, str);
74
- }
101
+ return JS_NewStringLen(ctx, RSTRING_PTR(r_value), RSTRING_LEN(r_value));
75
102
  case T_SYMBOL:
76
103
  {
77
- VALUE r_str = rb_funcall(r_value, rb_intern("to_s"), 0);
78
- char *str = StringValueCStr(r_str);
79
-
80
- return JS_NewString(ctx, str);
104
+ if (r_value == QUICKJSRB_SYM(undefinedId))
105
+ return JS_UNDEFINED;
106
+ if (r_value == QUICKJSRB_SYM(nanId))
107
+ {
108
+ JSValue j_global = JS_GetGlobalObject(ctx);
109
+ JSValue j_nan = JS_GetPropertyStr(ctx, j_global, "NaN");
110
+ JS_FreeValue(ctx, j_global);
111
+ return j_nan;
112
+ }
113
+ const char *name = rb_id2name(SYM2ID(r_value));
114
+ return JS_NewString(ctx, name);
81
115
  }
82
116
  case T_TRUE:
83
117
  return JS_TRUE;
84
118
  case T_FALSE:
85
119
  return JS_FALSE;
86
- case T_HASH:
87
120
  case T_ARRAY:
88
121
  {
89
- VALUE r_json_str = rb_funcall(r_value, rb_intern("to_json"), 0);
90
- char *str = StringValueCStr(r_json_str);
91
- JSValue j_parsed = JS_ParseJSON(ctx, str, strlen(str), "<quickjsrb.c>");
92
-
93
- return j_parsed;
122
+ int len = RARRAY_LEN(r_value);
123
+ JSValue j_arr = JS_NewArray(ctx);
124
+ for (int i = 0; i < len; i++)
125
+ {
126
+ JS_SetPropertyUint32(ctx, j_arr, (uint32_t)i, to_js_value(ctx, RARRAY_AREF(r_value, i)));
127
+ }
128
+ return j_arr;
129
+ }
130
+ case T_HASH:
131
+ {
132
+ JSValue j_obj = JS_NewObject(ctx);
133
+ RbHashToJsArg arg = {ctx, j_obj};
134
+ rb_hash_foreach(r_value, rb_hash_entry_to_js, (VALUE)&arg);
135
+ return j_obj;
94
136
  }
95
137
  default:
96
138
  {
@@ -100,10 +142,7 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
100
142
  if (!JS_IsUndefined(data->j_file_proxy_creator))
101
143
  return quickjsrb_file_to_js(ctx, r_value);
102
144
  }
103
- if (TYPE(r_value) == T_OBJECT && RTEST(rb_funcall(
104
- r_value,
105
- rb_intern("is_a?"),
106
- 1, rb_const_get(rb_cClass, rb_intern("Exception")))))
145
+ if (rb_obj_is_kind_of(r_value, rb_eException))
107
146
  {
108
147
  return j_error_from_ruby_error(ctx, r_value);
109
148
  }
@@ -157,6 +196,59 @@ VALUE to_r_json(JSContext *ctx, JSValue j_val)
157
196
  return r_str;
158
197
  }
159
198
 
199
+ static int js_is_plain_object(JSContext *ctx, JSValue j_val)
200
+ {
201
+ JSValue j_proto = JS_GetPrototype(ctx, j_val);
202
+ if (JS_IsNull(j_proto))
203
+ return 1; // Object.create(null)
204
+ JSValue j_global = JS_GetGlobalObject(ctx);
205
+ JSValue j_Object = JS_GetPropertyStr(ctx, j_global, "Object");
206
+ JSValue j_Object_proto = JS_GetPropertyStr(ctx, j_Object, "prototype");
207
+ int result = JS_StrictEq(ctx, j_proto, j_Object_proto);
208
+ JS_FreeValue(ctx, j_proto);
209
+ JS_FreeValue(ctx, j_global);
210
+ JS_FreeValue(ctx, j_Object);
211
+ JS_FreeValue(ctx, j_Object_proto);
212
+ return result;
213
+ }
214
+
215
+ static VALUE js_array_to_rb(JSContext *ctx, JSValue j_val)
216
+ {
217
+ JSValue j_length = JS_GetPropertyStr(ctx, j_val, "length");
218
+ uint32_t length = 0;
219
+ JS_ToUint32(ctx, &length, j_length);
220
+ JS_FreeValue(ctx, j_length);
221
+
222
+ VALUE r_array = rb_ary_new_capa(length);
223
+ for (uint32_t i = 0; i < length; i++)
224
+ {
225
+ JSValue j_elem = JS_GetPropertyUint32(ctx, j_val, i);
226
+ rb_ary_push(r_array, to_rb_value(ctx, j_elem));
227
+ JS_FreeValue(ctx, j_elem);
228
+ }
229
+ return r_array;
230
+ }
231
+
232
+ static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val)
233
+ {
234
+ JSPropertyEnum *ptab;
235
+ uint32_t plen;
236
+ if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, j_val, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0)
237
+ return rb_hash_new();
238
+
239
+ VALUE r_hash = rb_hash_new();
240
+ for (uint32_t i = 0; i < plen; i++)
241
+ {
242
+ const char *key = JS_AtomToCString(ctx, ptab[i].atom);
243
+ JSValue j_prop = JS_GetProperty(ctx, j_val, ptab[i].atom);
244
+ rb_hash_aset(r_hash, rb_str_new2(key), to_rb_value(ctx, j_prop));
245
+ JS_FreeCString(ctx, key);
246
+ JS_FreeValue(ctx, j_prop);
247
+ }
248
+ JS_FreePropertyEnum(ctx, ptab, plen);
249
+ return r_hash;
250
+ }
251
+
160
252
  VALUE to_rb_value(JSContext *ctx, JSValue j_val)
161
253
  {
162
254
  switch (JS_VALUE_GET_NORM_TAG(j_val))
@@ -179,16 +271,13 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
179
271
  }
180
272
  case JS_TAG_STRING:
181
273
  {
182
- int couldntParse;
183
- VALUE r_result = rb_protect(r_try_json_parse, to_r_json(ctx, j_val), &couldntParse);
184
- if (couldntParse)
185
- {
274
+ size_t len;
275
+ const char *str = JS_ToCStringLen(ctx, &len, j_val);
276
+ if (str == NULL)
186
277
  return Qnil;
187
- }
188
- else
189
- {
190
- return r_result;
191
- }
278
+ VALUE r_str = rb_utf8_str_new(str, (long)len);
279
+ JS_FreeCString(ctx, str);
280
+ return r_str;
192
281
  }
193
282
  case JS_TAG_OBJECT:
194
283
  {
@@ -200,6 +289,18 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
200
289
  return Qnil;
201
290
  }
202
291
 
292
+ if (JS_IsFunction(ctx, j_val))
293
+ {
294
+ JSValue j_toStringFunc = JS_GetPropertyStr(ctx, j_val, "toString");
295
+ JSValue j_source = JS_Call(ctx, j_toStringFunc, j_val, 0, NULL);
296
+ JS_FreeValue(ctx, j_toStringFunc);
297
+ const char *source = JS_ToCString(ctx, j_source);
298
+ JS_FreeValue(ctx, j_source);
299
+ VALUE r_source = rb_str_new2(source);
300
+ JS_FreeCString(ctx, source);
301
+ return rb_funcall(rb_path2class("Quickjs::Function"), rb_intern("new"), 1, r_source);
302
+ }
303
+
203
304
  if (JS_IsError(ctx, j_val))
204
305
  {
205
306
  VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_val);
@@ -239,6 +340,13 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
239
340
  return r_maybe_file;
240
341
  }
241
342
 
343
+ if (JS_IsArray(ctx, j_val))
344
+ return js_array_to_rb(ctx, j_val);
345
+
346
+ if (js_is_plain_object(ctx, j_val))
347
+ return js_plain_object_to_rb(ctx, j_val);
348
+
349
+ // Fallback: non-plain objects (Date, RegExp, Map, class instances, functions, etc.)
242
350
  VALUE r_str = to_r_json(ctx, j_val);
243
351
 
244
352
  if (rb_funcall(r_str, rb_intern("=="), 1, rb_str_new2("undefined")))
@@ -376,16 +484,19 @@ static VALUE r_try_call_proc(VALUE r_try_args)
376
484
 
377
485
  static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int argc, JSValueConst *argv, int _magic, JSValue *func_data)
378
486
  {
379
- 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]);
380
492
 
381
493
  VMData *data = JS_GetContextOpaque(ctx);
382
- 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));
383
495
  // Shouldn't happen
384
496
  if (r_proc == Qnil)
385
497
  {
386
- return JS_ThrowReferenceError(ctx, "Proc `%s` is not defined", funcName); // TODO: Free funcnName
498
+ return JS_ThrowReferenceError(ctx, "Proc is not defined");
387
499
  }
388
- JS_FreeCString(ctx, funcName);
389
500
 
390
501
  VALUE r_call_args = rb_ary_new();
391
502
  rb_ary_push(r_call_args, r_proc);
@@ -736,22 +847,36 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
736
847
  return result;
737
848
  }
738
849
 
739
- static VALUE vm_m_evalCode(VALUE r_self, VALUE r_code)
850
+ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
740
851
  {
741
852
  VMData *data;
742
853
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
743
854
 
855
+ VALUE r_code, r_opts;
856
+ rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
857
+
744
858
  if (!RB_TYPE_P(r_code, T_STRING))
745
859
  {
746
860
  VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
747
861
  rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
748
862
  }
749
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
+
750
875
  clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
751
876
  JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
752
877
 
753
- char *code = StringValueCStr(r_code);
754
- 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);
755
880
  JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
756
881
  // JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
757
882
  // Free j_awaitedResult before to_rb_return_value because it may raise (longjmp), which would skip cleanup
@@ -769,33 +894,117 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
769
894
  VALUE r_block;
770
895
  rb_scan_args(argc, argv, "10*&", &r_name, &r_flags, &r_block);
771
896
 
772
- if (!(SYMBOL_P(r_name) || RB_TYPE_P(r_name, T_STRING)))
773
- {
774
- rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
775
- }
776
-
777
897
  VMData *data;
778
898
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
779
899
 
780
- VALUE r_name_sym = rb_funcall(r_name, rb_intern("to_sym"), 0);
900
+ if (RB_TYPE_P(r_name, T_ARRAY))
901
+ {
902
+ long path_len = RARRAY_LEN(r_name);
903
+ if (path_len < 1)
904
+ rb_raise(rb_eArgError, "function's path array must not be empty");
781
905
 
782
- rb_hash_aset(data->defined_functions, r_name_sym, r_block);
783
- VALUE r_name_str = rb_funcall(r_name, rb_intern("to_s"), 0);
784
- char *funcName = StringValueCStr(r_name_str);
906
+ for (long i = 0; i < path_len; i++)
907
+ {
908
+ VALUE r_seg = RARRAY_AREF(r_name, i);
909
+ if (!(SYMBOL_P(r_seg) || RB_TYPE_P(r_seg, T_STRING)))
910
+ rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
911
+ }
785
912
 
786
- JSValueConst ruby_data[2];
787
- ruby_data[0] = JS_NewString(data->context, funcName);
788
- ruby_data[1] = JS_NewBool(data->context, RTEST(rb_funcall(r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
913
+ // Build internal lookup key by joining path segments with "."
914
+ // e.g. ["myLib", "hello"] -> :"myLib.hello"
915
+ VALUE r_segs = rb_ary_new();
916
+ for (long i = 0; i < path_len; i++)
917
+ rb_ary_push(r_segs, rb_funcall(RARRAY_AREF(r_name, i), rb_intern("to_s"), 0));
918
+ VALUE r_key_str = rb_funcall(r_segs, rb_intern("join"), 1, rb_str_new2("."));
919
+ VALUE r_key_sym = rb_funcall(r_key_str, rb_intern("to_sym"), 0);
920
+ rb_hash_aset(data->defined_functions, r_key_sym, r_block);
921
+
922
+ VALUE r_func_seg_str = rb_funcall(RARRAY_AREF(r_name, path_len - 1), rb_intern("to_s"), 0);
923
+ char *funcName = StringValueCStr(r_func_seg_str);
924
+
925
+ JSValueConst ruby_data[2];
926
+ ruby_data[0] = JS_NewInt64(data->context, (int64_t)SYM2ID(r_key_sym));
927
+ ruby_data[1] = JS_NewBool(data->context, RTEST(rb_funcall(r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
928
+
929
+ // Resolve the parent object to attach the function to.
930
+ // For a single-element array, parent is the global object.
931
+ // For multi-element arrays, traverse path[0..n-2] using JS_Eval for the first
932
+ // segment (so lexical const/let bindings are resolved, not just global properties)
933
+ // and JS_GetPropertyStr for subsequent segments.
934
+ JSValue j_parent;
935
+ if (path_len == 1)
936
+ {
937
+ j_parent = JS_GetGlobalObject(data->context);
938
+ }
939
+ else
940
+ {
941
+ VALUE r_first_str = rb_funcall(RARRAY_AREF(r_name, 0), rb_intern("to_s"), 0);
942
+ const char *first_seg = StringValueCStr(r_first_str);
943
+ j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), "<vm>", JS_EVAL_TYPE_GLOBAL);
789
944
 
790
- JSValue j_global = JS_GetGlobalObject(data->context);
791
- JS_SetPropertyStr(
792
- data->context, j_global, funcName,
793
- JS_NewCFunctionData(data->context, js_quickjsrb_call_global, 1, 0, 2, ruby_data));
794
- JS_FreeValue(data->context, j_global);
795
- JS_FreeValue(data->context, ruby_data[0]);
796
- JS_FreeValue(data->context, ruby_data[1]);
945
+ if (JS_IsException(j_parent) || !JS_IsObject(j_parent))
946
+ {
947
+ JS_FreeValue(data->context, j_parent);
948
+ JS_FreeValue(data->context, ruby_data[0]);
949
+ JS_FreeValue(data->context, ruby_data[1]);
950
+ rb_raise(rb_eArgError, "cannot define function: '%s' is not an object", first_seg);
951
+ }
952
+
953
+ for (long i = 1; i < path_len - 1; i++)
954
+ {
955
+ VALUE r_seg_str = rb_funcall(RARRAY_AREF(r_name, i), rb_intern("to_s"), 0);
956
+ JSValue j_next = JS_GetPropertyStr(data->context, j_parent, StringValueCStr(r_seg_str));
957
+ JS_FreeValue(data->context, j_parent);
958
+
959
+ if (JS_IsException(j_next) || !JS_IsObject(j_next))
960
+ {
961
+ JS_FreeValue(data->context, j_next);
962
+ JS_FreeValue(data->context, ruby_data[0]);
963
+ JS_FreeValue(data->context, ruby_data[1]);
964
+ rb_raise(rb_eArgError, "cannot define function: '%s' is not an object", StringValueCStr(r_seg_str));
965
+ }
966
+ j_parent = j_next;
967
+ }
968
+ }
969
+
970
+ JS_SetPropertyStr(
971
+ data->context, j_parent, funcName,
972
+ JS_NewCFunctionData(data->context, js_quickjsrb_call_global, 1, 0, 2, ruby_data));
973
+ JS_FreeValue(data->context, j_parent);
974
+ JS_FreeValue(data->context, ruby_data[0]);
975
+ JS_FreeValue(data->context, ruby_data[1]);
976
+
977
+ VALUE r_result = rb_ary_new();
978
+ for (long i = 0; i < path_len; i++)
979
+ rb_ary_push(r_result, rb_funcall(RARRAY_AREF(r_name, i), rb_intern("to_sym"), 0));
980
+ return r_result;
981
+ }
982
+ else if (SYMBOL_P(r_name) || RB_TYPE_P(r_name, T_STRING))
983
+ {
984
+ VALUE r_name_sym = rb_funcall(r_name, rb_intern("to_sym"), 0);
797
985
 
798
- return r_name_sym;
986
+ rb_hash_aset(data->defined_functions, r_name_sym, r_block);
987
+ VALUE r_name_str = rb_funcall(r_name, rb_intern("to_s"), 0);
988
+ char *funcName = StringValueCStr(r_name_str);
989
+
990
+ JSValueConst ruby_data[2];
991
+ ruby_data[0] = JS_NewInt64(data->context, (int64_t)SYM2ID(r_name_sym));
992
+ ruby_data[1] = JS_NewBool(data->context, RTEST(rb_funcall(r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
993
+
994
+ JSValue j_global = JS_GetGlobalObject(data->context);
995
+ JS_SetPropertyStr(
996
+ data->context, j_global, funcName,
997
+ JS_NewCFunctionData(data->context, js_quickjsrb_call_global, 1, 0, 2, ruby_data));
998
+ JS_FreeValue(data->context, j_global);
999
+ JS_FreeValue(data->context, ruby_data[0]);
1000
+ JS_FreeValue(data->context, ruby_data[1]);
1001
+
1002
+ return r_name_sym;
1003
+ }
1004
+ else
1005
+ {
1006
+ rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
1007
+ }
799
1008
  }
800
1009
 
801
1010
  static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
@@ -1006,7 +1215,7 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1006
1215
  VALUE r_class_vm = rb_define_class_under(r_module_quickjs, "VM", rb_cObject);
1007
1216
  rb_define_alloc_func(r_class_vm, vm_alloc);
1008
1217
  rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
1009
- 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);
1010
1219
  rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
1011
1220
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
1012
1221
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Quickjs
6
+ class Function
7
+ def initialize(source)
8
+ @source = source
9
+ end
10
+
11
+ def source
12
+ @source
13
+ end
14
+
15
+ def call(*args, on: nil)
16
+ case on
17
+ when Quickjs::VM
18
+ _call_on(on, args)
19
+ when nil, Hash
20
+ vm = Quickjs::VM.new(**on || {})
21
+ res = _call_on(vm, args)
22
+ vm = nil
23
+ res
24
+ else
25
+ raise ArgumentError, 'on: must be a Quickjs::VM, a Hash of VM options, or nil'
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def _call_on(vm, args)
32
+ args_js = args.map { |a| JSON.generate(a) }.join(', ')
33
+ vm.eval_code("(#{@source})(#{args_js})")
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quickjs
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.1"
5
5
  end
data/lib/quickjs.rb CHANGED
@@ -5,6 +5,7 @@ require "timeout"
5
5
  require_relative "quickjs/version"
6
6
  require_relative "quickjs/subtle_crypto"
7
7
  require_relative "quickjs/crypto_key"
8
+ require_relative "quickjs/function"
8
9
  require_relative "quickjs/quickjsrb"
9
10
 
10
11
  module Quickjs
@@ -17,8 +18,10 @@ module Quickjs
17
18
  end
18
19
 
19
20
  def eval_code(code, overwrite_opts = {})
21
+ eval_opts = {}
22
+ eval_opts[:filename] = overwrite_opts.delete(:filename) if overwrite_opts.key?(:filename)
20
23
  vm = Quickjs::VM.new(**overwrite_opts)
21
- res = vm.eval_code(code)
24
+ res = vm.eval_code(code, **eval_opts)
22
25
  vm = nil
23
26
  res
24
27
  end
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "quickjs-rb-polyfills",
3
- "version": "0.14.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.14.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.14.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,11 +21,12 @@ 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
 
28
28
  def define_function: (String | Symbol name, *Symbol flags) { (*untyped) -> untyped } -> Symbol
29
+ | (Array[String | Symbol] path, *Symbol flags) { (*untyped) -> untyped } -> Array[Symbol]
29
30
 
30
31
  def import: (String | Array[String] | Hash[Symbol, String] imported, from: String, ?code_to_expose: String?) -> true
31
32
 
@@ -42,6 +43,14 @@ module Quickjs
42
43
  end
43
44
  end
44
45
 
46
+ class Function
47
+ def initialize: (String source) -> void
48
+
49
+ def source: () -> String
50
+
51
+ def call: (*untyped args, ?on: VM | Hash[Symbol, untyped] | nil) -> untyped
52
+ end
53
+
45
54
  class RuntimeError < ::RuntimeError
46
55
  def initialize: (String message, String? js_name) -> void
47
56
 
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.14.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmsk
@@ -90,6 +90,7 @@ files:
90
90
  - ext/quickjsrb/vendor/polyfill-url.min.js
91
91
  - lib/quickjs.rb
92
92
  - lib/quickjs/crypto_key.rb
93
+ - lib/quickjs/function.rb
93
94
  - lib/quickjs/subtle_crypto.rb
94
95
  - lib/quickjs/version.rb
95
96
  - polyfills/check-licenses.mjs