quickjs 0.13.0.pre → 0.14.0

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: f27d3afc594731cc4df53661889a9787626121923ece35c544b581ef3f52722c
4
- data.tar.gz: d3869683f9134339d1d2e44617845f8ece614e1503926841fb9d4ad6de3dc7ba
3
+ metadata.gz: 69b86d7c5040dba161c1d6f67cb3eab3fd8a3dc51b30eb6d355c2580dbc29602
4
+ data.tar.gz: 5e174baed18d79e0ac18a496f97cd580fd20830a98fb240351eeea46abe57683
5
5
  SHA512:
6
- metadata.gz: b5cfcad98315e8821644aa233ea86213f855900506767ccce01cb0c1a653326456e3d00c453d0821117e31eee543ec440ed1e4583dd6b420a62e49ffcb53e080
7
- data.tar.gz: 7df6cc20b91fce4c14e2a2b96b31bb447ee50eba03498a99e336c8b35d62164a05874ed7753509a4d0c1958761f96c5c85f4de66c6301b79eecbe208dbcf8212
6
+ metadata.gz: 9f7abcc1733fcfe9c51aacf81c9e34b05e7e44142a9db79a8bd4525ba67570f897cd7c1fb200428507876dad35a67b9a3f383711a3b0cfc9ff1b46728cd1c125
7
+ data.tar.gz: a15abae6b482f0df7efc89e9b0a3b9d3ca3bab8bb8d04ce7844f60b195a0314d8e12d2b3bbdf8cf01dae763e75945d4e84cf3c1efbe2a99d4cfa525e2279625e
data/README.md CHANGED
@@ -23,17 +23,9 @@ gem 'quickjs'
23
23
  require 'quickjs'
24
24
 
25
25
  Quickjs.eval_code('const fn = (n, pow) => n ** pow; fn(2,8);') # => 256
26
- Quickjs.eval_code('const fn = (name) => `Hi, ${name}!`; fn("Itadori");') # => "Hi, Itadori!
27
- Quickjs.eval_code("const isOne = (n) => 1 === n; func(1);") #=> true (TrueClass)
28
- Quickjs.eval_code("const isOne = (n) => 1 === n; func(3);") #=> false (FalseClass)
29
-
30
- # When code returns 'object' for `typeof`, the result is converted via JSON.stringify (JS) -> JSON.parse (Ruby)
31
- Quickjs.eval_code("[1,2,3]") #=> [1, 2, 3] (Array)
32
- Quickjs.eval_code("({ a: '1', b: 1 })") #=> { 'a' => '1', 'b' => 1 } (Hash)
33
-
34
- Quickjs.eval_code("null") #=> nil
35
- Quickjs.eval_code('const obj = {}; obj.missingKey;') # => :undefined (Quickjs::Value::Undefined)
36
- Quickjs.eval_code("Number('whatever')") #=> :NaN (Quickjs::Value::NAN)
26
+ Quickjs.eval_code('const fn = (name) => `Hi, ${name}!`; fn("Itadori");') # => "Hi, Itadori!"
27
+ Quickjs.eval_code("[1,2,3]") #=> [1, 2, 3]
28
+ Quickjs.eval_code("({ a: '1', b: 1 })") #=> { 'a' => '1', 'b' => 1 }
37
29
  ```
38
30
 
39
31
  <details>
@@ -42,36 +34,42 @@ Quickjs.eval_code("Number('whatever')") #=> :NaN (Quickjs::Value::NAN)
42
34
  #### Resources
43
35
 
44
36
  ```rb
45
- # 1GB memory limit
46
- Quickjs.eval_code(code, { memory_limit: 1024 ** 3 })
47
-
48
- # 1MB max stack size
49
- Quickjs.eval_code(code, { max_stack_size: 1024 ** 2 })
37
+ Quickjs.eval_code(code,
38
+ memory_limit: 1024 ** 3, # 1GB memory limit
39
+ max_stack_size: 1024 ** 2, # 1MB max stack size
40
+ )
50
41
  ```
51
42
 
52
- #### Toggle features
43
+ #### Timeout
53
44
 
54
45
  ```rb
55
- # Enable `std` module by quickjs: https://bellard.org/quickjs/quickjs.html#std-module
56
- vm = Quickjs.eval_code(features: [::Quickjs::MODULE_STD])
57
-
58
- # Enable `os` module by quickjs: https://bellard.org/quickjs/quickjs.html#os-module
59
- vm = Quickjs.eval_code(features: [::Quickjs::MODULE_OS])
60
-
61
- # Provide `setTimeout` managed by CRuby
62
- vm = Quickjs.eval_code(features: [::Quickjs::FEATURE_TIMEOUT])
46
+ # eval_code will be interrupted after 1 sec (default: 100 msec)
47
+ Quickjs.eval_code(code, timeout_msec: 1_000)
48
+ ```
63
49
 
64
- # Inject the polyfill of Intl
65
- vm = Quickjs.eval_code(features: [::Quickjs::POLYFILL_INTL])
50
+ #### Features
66
51
 
67
- # Inject the polyfill of Blob and File (W3C File API)
68
- vm = Quickjs.eval_code(features: [::Quickjs::POLYFILL_FILE])
52
+ ```rb
53
+ Quickjs.eval_code(code, features: [::Quickjs::MODULE_STD, ::Quickjs::POLYFILL_FILE])
69
54
  ```
70
55
 
56
+ | Constant | Description |
57
+ |---|---|
58
+ | `MODULE_STD` | QuickJS [`std` module](https://bellard.org/quickjs/quickjs.html#std-module) |
59
+ | `MODULE_OS` | QuickJS [`os` module](https://bellard.org/quickjs/quickjs.html#os-module) |
60
+ | `FEATURE_TIMEOUT` | `setTimeout` / `setInterval` managed by CRuby |
61
+ | `POLYFILL_INTL` | Intl API (DateTimeFormat, NumberFormat, PluralRules, Locale) |
62
+ | `POLYFILL_FILE` | W3C File API (Blob and File) |
63
+ | `POLYFILL_ENCODING` | Encoding API (TextEncoder and TextDecoder) |
64
+ | `POLYFILL_URL` | URL API (URL and URLSearchParams) |
65
+ | `POLYFILL_CRYPTO` | Web Crypto API (`crypto.getRandomValues`, `crypto.randomUUID`, `crypto.subtle`); combine with `POLYFILL_ENCODING` for string↔buffer conversion |
66
+
71
67
  </details>
72
68
 
73
69
  ### `Quickjs::VM`: Maintain a consistent VM/runtime
74
70
 
71
+ Accepts the same [options](#quickjseval_code-evaluate-javascript-code-instantly) as `Quickjs.eval_code`.
72
+
75
73
  ```rb
76
74
  vm = Quickjs::VM.new
77
75
  vm.eval_code('const a = { b: "c" };')
@@ -80,44 +78,28 @@ vm.eval_code('a.b = "d";')
80
78
  vm.eval_code('a.b;') #=> "d"
81
79
  ```
82
80
 
83
- <details>
84
- <summary>Config VM/runtime</summary>
85
-
86
- #### Resources
81
+ #### `Quickjs::VM#call`: ⚡ Call a JS function directly with Ruby arguments
87
82
 
88
83
  ```rb
89
- vm = Quickjs::VM.new(
90
- memory_limit: 1024 ** 3,
91
- max_stack_size: 1024 ** 2,
92
- )
93
- ```
94
-
95
- #### Toggle features
96
-
97
- ```rb
98
- # Enable `std` module by quickjs: https://bellard.org/quickjs/quickjs.html#std-module
99
- vm = Quickjs::VM.new(features: [::Quickjs::MODULE_STD])
100
-
101
- # Enable `os` module by quickjs: https://bellard.org/quickjs/quickjs.html#os-module
102
- vm = Quickjs::VM.new(features: [::Quickjs::MODULE_OS])
84
+ vm = Quickjs::VM.new
85
+ vm.eval_code('function add(a, b) { return a + b; }')
103
86
 
104
- # Provide `setTimeout` managed by CRuby
105
- vm = Quickjs::VM.new(features: [::Quickjs::FEATURE_TIMEOUT])
87
+ vm.call('add', 1, 2) #=> 3
88
+ vm.call(:add, 1, 2) #=> 3 (Symbol also works)
106
89
 
107
- # Inject the polyfill of Intl
108
- vm = Quickjs::VM.new(features: [::Quickjs::POLYFILL_INTL])
90
+ # Nested functions preserves `this` binding
91
+ vm.eval_code('const counter = { n: 0, inc() { return ++this.n; } }')
92
+ vm.call('counter.inc') #=> 1
93
+ vm.call('counter.inc') #=> 2
109
94
 
110
- # Inject the polyfill of Blob and File (W3C File API)
111
- vm = Quickjs::VM.new(features: [::Quickjs::POLYFILL_FILE])
112
- ```
95
+ # Keys with special characters via bracket notation
96
+ vm.eval_code("const obj = {}; obj['my-fn'] = x => x * 2;")
97
+ vm.call('obj["my-fn"]', 21) #=> 42
113
98
 
114
- #### VM timeout
115
-
116
- ```rb
117
- # `eval_code` will be interrupted after 1 sec (default: 100 msec)
118
- vm = Quickjs::VM.new(timeout_msec: 1_000)
99
+ # Async functions are automatically awaited
100
+ vm.eval_code('async function fetchVal() { return 42; }')
101
+ vm.call('fetchVal') #=> 42
119
102
  ```
120
- </details>
121
103
 
122
104
  #### `Quickjs::VM#import`: 🔌 Import ESM from a source code
123
105
 
@@ -151,32 +133,75 @@ end
151
133
  vm.eval_code("greetingTo('Rick')") #=> 'Hello! Rick'
152
134
  ```
153
135
 
154
- #### `Quickjs::VM#logs`: 💾 Capture console logs
136
+ 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
+
138
+ ```rb
139
+ vm.define_function("fail") { raise IOError, "something went wrong" }
140
+
141
+ vm.eval_code('try { fail() } catch (e) { e.message }') #=> "something went wrong"
142
+ vm.eval_code("fail()") #=> raise IOError transparently
143
+ ```
155
144
 
156
- All logs by `console.(log|info|debug|warn|error)` on VM are recorded and inspectable.
145
+ With `POLYFILL_FILE` enabled, a Ruby `::File` returned from the block becomes a JS `File`-compatible proxy. Passing it back to Ruby from JS returns the original `::File` object.
146
+
147
+ ```rb
148
+ vm = Quickjs::VM.new(features: [::Quickjs::POLYFILL_FILE])
149
+ vm.define_function(:get_file) { File.open('report.pdf') }
150
+
151
+ vm.eval_code("get_file().name") #=> "report.pdf"
152
+ vm.eval_code("get_file().size") #=> Integer (byte size)
153
+ vm.eval_code("await get_file().text()") #=> file content as String
154
+ ```
155
+
156
+ #### `Quickjs::VM#on_log`: 📡 Handle console logs in real time
157
+
158
+ Register a block to be called for each `console.(log|info|debug|warn|error)` call.
157
159
 
158
160
  ```rb
159
161
  vm = Quickjs::VM.new
160
- vm.eval_code('console.log("log me", null)')
162
+ vm.on_log { |log| puts "#{log.severity}: #{log.to_s}" }
161
163
 
162
- vm.logs #=> Array of Quickjs::VM::Log
163
- vm.logs.last.severity #=> :info
164
- vm.logs.last.to_s #=> 'log me null'
165
- vm.logs.last.raw #=> ['log me', nil]
164
+ vm.eval_code('console.log("hello", 42)')
165
+ # => prints: info: hello 42
166
+
167
+ # log.severity #=> :info / :verbose / :warning / :error
168
+ # log.to_s #=> space-joined string of all arguments
169
+ # log.raw #=> Array of raw Ruby values
166
170
  ```
167
171
 
172
+ ### Value Conversion
173
+
174
+ | JavaScript | | Ruby | Note |
175
+ |---|:---:|---|---|
176
+ | `number` (integer / float) | ↔ | `Integer` / `Float` | |
177
+ | `string` | ↔ | `String` | |
178
+ | `true` / `false` | ↔ | `true` / `false` | |
179
+ | `null` | ↔ | `nil` | |
180
+ | `Array` | ↔ | `Array` | via JSON |
181
+ | `Object` | ↔ | `Hash` | via JSON |
182
+ | `undefined` | → | `Quickjs::Value::UNDEFINED` | |
183
+ | `NaN` | → | `Quickjs::Value::NAN` | |
184
+ | `Blob` | → | `Quickjs::Blob` — `.size`, `.type`, `.content` | requires `POLYFILL_FILE` |
185
+ | `File` | → | `Quickjs::File` — `.name`, `.last_modified` + Blob attrs | requires `POLYFILL_FILE` |
186
+ | `File` proxy | ← | `::File` | requires `POLYFILL_FILE`; applies to `define_function` return values |
187
+
168
188
  ## License
169
189
 
170
190
  - `ext/quickjsrb/quickjs`
171
191
  - [MIT License Copyright (c) 2017-2021 by Fabrice Bellard and Charlie Gordon](https://github.com/bellard/quickjs/blob/6e2e68fd0896957f92eb6c242a2e048c1ef3cae0/LICENSE).
172
192
  - `ext/quickjsrb/vendor/polyfill-intl-en.min.js` ([bundled and minified from `polyfills/`](https://github.com/hmsk/quickjs.rb/tree/main/polyfills))
193
+ - MIT License Copyright (c) 2022 FormatJS
194
+ - [@formatjs/intl-supportedvaluesof](https://github.com/formatjs/formatjs/blob/main/packages/intl-supportedvaluesof/LICENSE.md)
173
195
  - MIT License Copyright (c) 2023 FormatJS
174
196
  - [@formatjs/intl-getcanonicallocales](https://github.com/formatjs/formatjs/blob/main/packages/intl-getcanonicallocales/LICENSE.md)
175
197
  - [@formatjs/intl-locale](https://github.com/formatjs/formatjs/blob/main/packages/intl-locale/LICENSE.md)
176
198
  - [@formatjs/intl-pluralrules](https://github.com/formatjs/formatjs/blob/main/packages/intl-pluralrules/LICENSE.md)
177
199
  - [@formatjs/intl-numberformat](https://github.com/formatjs/formatjs/blob/main/packages/intl-numberformat/LICENSE.md)
178
200
  - [@formatjs/intl-datetimeformat](https://github.com/formatjs/formatjs/blob/main/packages/intl-datetimeformat/LICENSE.md)
179
- - MIT License Copyright (c) 2025 Michael Mclaughlin
180
- - [decimal.js](https://www.npmjs.com/package/decimal.js)
201
+ - [@formatjs/ecma402-abstract](https://github.com/formatjs/formatjs/blob/main/packages/ecma402-abstract/LICENSE.md)
202
+ - [@formatjs/fast-memoize](https://github.com/formatjs/formatjs/blob/main/packages/fast-memoize/LICENSE.md)
203
+ - [@formatjs/intl-localematcher](https://github.com/formatjs/formatjs/blob/main/packages/intl-localematcher/LICENSE.md)
204
+ - MIT License Copyright (c) 2026 FormatJS
205
+ - [@formatjs/bigdecimal](https://github.com/formatjs/formatjs/blob/main/packages/bigdecimal/LICENSE.md)
181
206
 
182
207
  Otherwise, [the MIT License, Copyright 2024 by Kengo Hamasaki](/LICENSE).
data/Rakefile CHANGED
@@ -54,6 +54,13 @@ namespace :polyfills do
54
54
  Rake::Task[:compile].invoke
55
55
  end
56
56
 
57
+ desc 'Check licenses of bundled polyfill dependencies'
58
+ task :check_licenses do
59
+ Dir.chdir(File.expand_path('polyfills', __dir__)) do
60
+ sh 'npm run check-licenses'
61
+ end
62
+ end
63
+
57
64
  namespace :version do
58
65
  task :check do
59
66
  check_polyfill_version! do |pkg_v, gem_v|
@@ -14,8 +14,11 @@ $srcs = [
14
14
  'polyfill-intl-en.min.c',
15
15
  'polyfill-file.min.c',
16
16
  'polyfill-encoding.min.c',
17
+ 'polyfill-url.min.c',
17
18
  'quickjsrb.c',
18
19
  'quickjsrb_file.c',
20
+ 'quickjsrb_crypto.c',
21
+ 'quickjsrb_crypto_subtle.c',
19
22
  ]
20
23
 
21
24
  append_cflags('-I$(srcdir)/quickjs')
@@ -72,6 +75,10 @@ polyfill-encoding.min.js:
72
75
  $(COPY) $(srcdir)/vendor/$@ $@
73
76
  polyfill-encoding.min.c: ./qjsc polyfill-encoding.min.js
74
77
  ./qjsc -fno-string-normalize -fno-eval -fno-proxy -fno-module-loader -c -M polyfill/encoding.so,encoding -m -o $@ polyfill-encoding.min.js
78
+ polyfill-url.min.js:
79
+ $(COPY) $(srcdir)/vendor/$@ $@
80
+ polyfill-url.min.c: ./qjsc polyfill-url.min.js
81
+ ./qjsc -fno-string-normalize -fno-eval -fno-proxy -fno-module-loader -c -M polyfill/url.so,url -m -o $@ polyfill-url.min.js
75
82
  COMPILE_POLYFILL
76
83
  conf
77
84
  end
@@ -1,5 +1,6 @@
1
1
  #include "quickjsrb.h"
2
2
  #include "quickjsrb_file.h"
3
+ #include "quickjsrb_crypto.h"
3
4
 
4
5
  const char *featureStdId = "feature_std";
5
6
  const char *featureOsId = "feature_os";
@@ -7,6 +8,8 @@ const char *featureTimeoutId = "feature_timeout";
7
8
  const char *featurePolyfillIntlId = "feature_polyfill_intl";
8
9
  const char *featurePolyfillFileId = "feature_polyfill_file";
9
10
  const char *featurePolyfillEncodingId = "feature_polyfill_encoding";
11
+ const char *featurePolyfillUrlId = "feature_polyfill_url";
12
+ const char *featurePolyfillCryptoId = "feature_polyfill_crypto";
10
13
 
11
14
  const char *undefinedId = "undefined";
12
15
  const char *nanId = "NaN";
@@ -486,20 +489,14 @@ static VALUE r_try_call_listener(VALUE r_args)
486
489
 
487
490
  static int dispatch_log(VMData *data, const char *severity, VALUE r_row)
488
491
  {
492
+ if (NIL_P(data->log_listener))
493
+ return 0;
494
+
489
495
  VALUE r_log = r_log_new(severity, r_row);
490
- if (!NIL_P(data->log_listener))
491
- {
492
- VALUE r_args = rb_ary_new3(2, data->log_listener, r_log);
493
- int error;
494
- rb_protect(r_try_call_listener, r_args, &error);
495
- if (error)
496
- return error;
497
- }
498
- else
499
- {
500
- rb_ary_push(data->logs, r_log);
501
- }
502
- return 0;
496
+ VALUE r_args = rb_ary_new3(2, data->log_listener, r_log);
497
+ int error;
498
+ rb_protect(r_try_call_listener, r_args, &error);
499
+ return error;
503
500
  }
504
501
 
505
502
  static VALUE vm_m_on_log(VALUE r_self)
@@ -680,6 +677,18 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
680
677
  JS_FreeValue(data->context, j_polyfillEncodingResult);
681
678
  }
682
679
 
680
+ if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
681
+ {
682
+ JSValue j_polyfillUrlObject = JS_ReadObject(data->context, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, JS_READ_OBJ_BYTECODE);
683
+ JSValue j_polyfillUrlResult = JS_EvalFunction(data->context, j_polyfillUrlObject);
684
+ JS_FreeValue(data->context, j_polyfillUrlResult);
685
+ }
686
+
687
+ if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillCryptoId))))
688
+ {
689
+ quickjsrb_init_crypto(data->context, j_global);
690
+ }
691
+
683
692
  JSValue j_console = JS_NewObject(data->context);
684
693
  JS_SetPropertyStr(
685
694
  data->context, j_console, "log",
@@ -713,6 +722,20 @@ static int interrupt_handler(JSRuntime *runtime, void *opaque)
713
722
  return elapsed_ms >= eval_time->limit_ms ? 1 : 0;
714
723
  }
715
724
 
725
+ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
726
+ {
727
+ if (JS_VALUE_GET_NORM_TAG(j_val) == JS_TAG_OBJECT && JS_PromiseState(ctx, j_val) != -1)
728
+ {
729
+ JS_FreeValue(ctx, j_val);
730
+ VALUE r_error_message = rb_str_new2("An unawaited Promise was returned to the top-level");
731
+ rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_NO_AWAIT_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
732
+ return Qnil;
733
+ }
734
+ VALUE result = to_rb_value(ctx, j_val);
735
+ JS_FreeValue(ctx, j_val);
736
+ return result;
737
+ }
738
+
716
739
  static VALUE vm_m_evalCode(VALUE r_self, VALUE r_code)
717
740
  {
718
741
  VMData *data;
@@ -730,26 +753,11 @@ static VALUE vm_m_evalCode(VALUE r_self, VALUE r_code)
730
753
  char *code = StringValueCStr(r_code);
731
754
  JSValue j_codeResult = JS_Eval(data->context, code, strlen(code), "<code>", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC);
732
755
  JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
756
+ // JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
757
+ // Free j_awaitedResult before to_rb_return_value because it may raise (longjmp), which would skip cleanup
733
758
  JSValue j_returnedValue = JS_GetPropertyStr(data->context, j_awaitedResult, "value");
734
- // Do this by rescuing to_rb_value
735
- if (JS_VALUE_GET_NORM_TAG(j_returnedValue) == JS_TAG_OBJECT && JS_PromiseState(data->context, j_returnedValue) != -1)
736
- {
737
- JS_FreeValue(data->context, j_returnedValue);
738
- JS_FreeValue(data->context, j_awaitedResult);
739
- VALUE r_error_message = rb_str_new2("An unawaited Promise was returned to the top-level");
740
- rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_NO_AWAIT_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
741
- return Qnil;
742
- }
743
- else
744
- {
745
- // Free j_awaitedResult before to_rb_value because to_rb_value may
746
- // raise (longjmp) for JS exceptions, which would skip any cleanup
747
- // after it and leak JS GC objects.
748
- JS_FreeValue(data->context, j_awaitedResult);
749
- VALUE result = to_rb_value(data->context, j_returnedValue);
750
- JS_FreeValue(data->context, j_returnedValue);
751
- return result;
752
- }
759
+ JS_FreeValue(data->context, j_awaitedResult);
760
+ return to_rb_return_value(data->context, j_returnedValue);
753
761
  }
754
762
 
755
763
  static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
@@ -790,6 +798,142 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
790
798
  return r_name_sym;
791
799
  }
792
800
 
801
+ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
802
+ {
803
+ if (argc < 1)
804
+ rb_raise(rb_eArgError, "wrong number of arguments (given 0, expected 1+)");
805
+
806
+ VALUE r_name = argv[0];
807
+
808
+ VMData *data;
809
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
810
+
811
+ JSValue j_this = JS_UNDEFINED;
812
+ JSValue j_func;
813
+
814
+ VALUE r_path;
815
+ if (SYMBOL_P(r_name) || RB_TYPE_P(r_name, T_STRING))
816
+ {
817
+ VALUE r_name_str = rb_funcall(r_name, rb_intern("to_s"), 0);
818
+ const char *name_str = StringValueCStr(r_name_str);
819
+ size_t name_len = strlen(name_str);
820
+ const char *last_bracket = strrchr(name_str, '[');
821
+ const char *last_dot = strrchr(name_str, '.');
822
+
823
+ if (last_bracket != NULL && last_bracket != name_str && name_str[name_len - 1] == ']')
824
+ {
825
+ // Bracket notation: 'a["key"]' or 'a.b["key"]' or 'a[0]'
826
+ // Split into parent expression and the bracketed key
827
+ VALUE r_parent = rb_str_new(name_str, last_bracket - name_str);
828
+ const char *key_start = last_bracket + 1;
829
+ size_t key_len = name_len - (key_start - name_str) - 1; // exclude ']'
830
+ // Strip surrounding quotes for string keys: 'a["b"]' → key = b
831
+ if (key_len >= 2 &&
832
+ ((key_start[0] == '\'' && key_start[key_len - 1] == '\'') ||
833
+ (key_start[0] == '"' && key_start[key_len - 1] == '"')))
834
+ {
835
+ key_start++;
836
+ key_len -= 2;
837
+ }
838
+ VALUE r_key = rb_str_new(key_start, key_len);
839
+ r_path = rb_ary_new3(2, r_parent, r_key);
840
+ }
841
+ else if (last_dot != NULL && last_dot != name_str)
842
+ {
843
+ // Dot notation: 'a.b.c' → ['a.b', 'c'] so the parent becomes `this`
844
+ VALUE r_parent = rb_str_new(name_str, last_dot - name_str);
845
+ VALUE r_key = rb_str_new2(last_dot + 1);
846
+ r_path = rb_ary_new3(2, r_parent, r_key);
847
+ }
848
+ else
849
+ {
850
+ r_path = rb_ary_new3(1, r_name_str);
851
+ }
852
+ }
853
+ else
854
+ {
855
+ rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
856
+ }
857
+
858
+ {
859
+ long path_len = RARRAY_LEN(r_path);
860
+
861
+ VALUE r_first = RARRAY_AREF(r_path, 0);
862
+ if (!(SYMBOL_P(r_first) || RB_TYPE_P(r_first, T_STRING)))
863
+ rb_raise(rb_eTypeError, "function path elements should be Symbols or Strings");
864
+
865
+ VALUE r_first_str = rb_funcall(r_first, rb_intern("to_s"), 0);
866
+ const char *first_seg = StringValueCStr(r_first_str);
867
+
868
+ // JS_Eval accesses both global object properties and lexical (const/let) bindings
869
+ JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg), "<vm>", JS_EVAL_TYPE_GLOBAL);
870
+ if (JS_IsException(j_cur))
871
+ return to_rb_value(data->context, j_cur); // raises
872
+
873
+ for (long i = 1; i < path_len; i++)
874
+ {
875
+ VALUE r_seg = RARRAY_AREF(r_path, i);
876
+ if (!(SYMBOL_P(r_seg) || RB_TYPE_P(r_seg, T_STRING)))
877
+ {
878
+ JS_FreeValue(data->context, j_cur);
879
+ JS_FreeValue(data->context, j_this);
880
+ rb_raise(rb_eTypeError, "function path elements should be Symbols or Strings");
881
+ }
882
+ VALUE r_seg_str = rb_funcall(r_seg, rb_intern("to_s"), 0);
883
+ const char *seg = StringValueCStr(r_seg_str);
884
+
885
+ JSValue j_next = JS_GetPropertyStr(data->context, j_cur, seg);
886
+ if (JS_IsException(j_next))
887
+ {
888
+ JS_FreeValue(data->context, j_cur);
889
+ JS_FreeValue(data->context, j_this);
890
+ return to_rb_value(data->context, j_next); // raises
891
+ }
892
+
893
+ JS_FreeValue(data->context, j_this);
894
+ j_this = j_cur;
895
+ j_cur = j_next;
896
+ }
897
+
898
+ j_func = j_cur;
899
+ }
900
+
901
+ if (!JS_IsFunction(data->context, j_func))
902
+ {
903
+ JS_FreeValue(data->context, j_func);
904
+ JS_FreeValue(data->context, j_this);
905
+ VALUE r_error_message = rb_str_new2("given path is not a function");
906
+ rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
907
+ return Qnil;
908
+ }
909
+
910
+ int nargs = argc - 1;
911
+ JSValue *j_args = NULL;
912
+ if (nargs > 0)
913
+ {
914
+ j_args = (JSValue *)malloc(sizeof(JSValue) * nargs);
915
+ for (int i = 0; i < nargs; i++)
916
+ j_args[i] = to_js_value(data->context, argv[i + 1]);
917
+ }
918
+
919
+ clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
920
+ JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
921
+
922
+ JSValue j_result = JS_Call(data->context, j_func, j_this, nargs, (JSValueConst *)j_args);
923
+
924
+ JS_FreeValue(data->context, j_func);
925
+ JS_FreeValue(data->context, j_this);
926
+ if (j_args)
927
+ {
928
+ for (int i = 0; i < nargs; i++)
929
+ JS_FreeValue(data->context, j_args[i]);
930
+ free(j_args);
931
+ }
932
+
933
+ // js_std_await handles both async (promise) and sync results; frees j_result
934
+ return to_rb_return_value(data->context, js_std_await(data->context, j_result));
935
+ }
936
+
793
937
  static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
794
938
  {
795
939
  VALUE r_import_string, r_opts;
@@ -850,16 +994,6 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
850
994
  return Qtrue;
851
995
  }
852
996
 
853
- static VALUE vm_m_logs(VALUE r_self)
854
- {
855
- rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, "Quickjs::VM#logs is deprecated; use Quickjs::VM#on_log instead");
856
-
857
- VMData *data;
858
- TypedData_Get_Struct(r_self, VMData, &vm_type, data);
859
-
860
- return data->logs;
861
- }
862
-
863
997
  RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
864
998
  {
865
999
  rb_require("json");
@@ -873,9 +1007,9 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
873
1007
  rb_define_alloc_func(r_class_vm, vm_alloc);
874
1008
  rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
875
1009
  rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, 1);
1010
+ rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
876
1011
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
877
1012
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
878
- rb_define_method(r_class_vm, "logs", vm_m_logs, 0);
879
1013
  rb_define_method(r_class_vm, "on_log", vm_m_on_log, 0);
880
1014
  r_define_log_class(r_class_vm);
881
1015
  }
@@ -18,6 +18,8 @@ extern const uint32_t qjsc_polyfill_file_min_size;
18
18
  extern const uint8_t qjsc_polyfill_file_min;
19
19
  extern const uint32_t qjsc_polyfill_encoding_min_size;
20
20
  extern const uint8_t qjsc_polyfill_encoding_min;
21
+ extern const uint32_t qjsc_polyfill_url_min_size;
22
+ extern const uint8_t qjsc_polyfill_url_min;
21
23
 
22
24
  extern const char *featureStdId;
23
25
  extern const char *featureOsId;
@@ -25,6 +27,8 @@ extern const char *featureTimeoutId;
25
27
  extern const char *featurePolyfillIntlId;
26
28
  extern const char *featurePolyfillFileId;
27
29
  extern const char *featurePolyfillEncodingId;
30
+ extern const char *featurePolyfillUrlId;
31
+ extern const char *featurePolyfillCryptoId;
28
32
 
29
33
  extern const char *undefinedId;
30
34
  extern const char *nanId;
@@ -48,7 +52,6 @@ typedef struct VMData
48
52
  struct JSContext *context;
49
53
  VALUE defined_functions;
50
54
  struct EvalTime *eval_time;
51
- VALUE logs;
52
55
  VALUE log_listener;
53
56
  VALUE alive_objects;
54
57
  JSValue j_file_proxy_creator;
@@ -80,7 +83,6 @@ static void vm_mark(void *ptr)
80
83
  {
81
84
  VMData *data = (VMData *)ptr;
82
85
  rb_gc_mark_movable(data->defined_functions);
83
- rb_gc_mark_movable(data->logs);
84
86
  rb_gc_mark_movable(data->log_listener);
85
87
  rb_gc_mark_movable(data->alive_objects);
86
88
  }
@@ -89,7 +91,6 @@ static void vm_compact(void *ptr)
89
91
  {
90
92
  VMData *data = (VMData *)ptr;
91
93
  data->defined_functions = rb_gc_location(data->defined_functions);
92
- data->logs = rb_gc_location(data->logs);
93
94
  data->log_listener = rb_gc_location(data->log_listener);
94
95
  data->alive_objects = rb_gc_location(data->alive_objects);
95
96
  }
@@ -110,7 +111,6 @@ static VALUE vm_alloc(VALUE r_self)
110
111
  VMData *data;
111
112
  VALUE obj = TypedData_Make_Struct(r_self, VMData, &vm_type, data);
112
113
  data->defined_functions = rb_hash_new();
113
- data->logs = rb_ary_new();
114
114
  data->log_listener = Qnil;
115
115
  data->alive_objects = rb_hash_new();
116
116
  data->j_file_proxy_creator = JS_UNDEFINED;
@@ -156,6 +156,8 @@ static void r_define_constants(VALUE r_parent_class)
156
156
  rb_define_const(r_parent_class, "POLYFILL_INTL", QUICKJSRB_SYM(featurePolyfillIntlId));
157
157
  rb_define_const(r_parent_class, "POLYFILL_FILE", QUICKJSRB_SYM(featurePolyfillFileId));
158
158
  rb_define_const(r_parent_class, "POLYFILL_ENCODING", QUICKJSRB_SYM(featurePolyfillEncodingId));
159
+ rb_define_const(r_parent_class, "POLYFILL_URL", QUICKJSRB_SYM(featurePolyfillUrlId));
160
+ rb_define_const(r_parent_class, "POLYFILL_CRYPTO", QUICKJSRB_SYM(featurePolyfillCryptoId));
159
161
 
160
162
  VALUE rb_cQuickjsValue = rb_define_class_under(r_parent_class, "Value", rb_cObject);
161
163
  rb_define_const(rb_cQuickjsValue, "UNDEFINED", QUICKJSRB_SYM(undefinedId));