mini_racer 0.17.0.pre5 → 0.17.0.pre7

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.
@@ -0,0 +1,380 @@
1
+ # This code used to be shared in lib/mini_racer.rb
2
+ # but was moved to the extension with https://github.com/rubyjs/mini_racer/pull/325.
3
+ # So now this is effectively duplicate logic with C/C++ code.
4
+ # Maybe one day it can be actually shared again between both backends.
5
+
6
+ module MiniRacer
7
+
8
+ MARSHAL_STACKDEPTH_DEFAULT = 2**9-2
9
+ MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2
10
+
11
+ class FailedV8Conversion
12
+ attr_reader :info
13
+ def initialize(info)
14
+ @info = info
15
+ end
16
+ end
17
+
18
+ # helper class returned when we have a JavaScript function
19
+ class JavaScriptFunction
20
+ def to_s
21
+ "JavaScript Function"
22
+ end
23
+ end
24
+
25
+ class Isolate
26
+ def initialize(snapshot = nil)
27
+ unless snapshot.nil? || snapshot.is_a?(Snapshot)
28
+ raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}"
29
+ end
30
+
31
+ # defined in the C class
32
+ init_with_snapshot(snapshot)
33
+ end
34
+ end
35
+
36
+ class Platform
37
+ class << self
38
+ def set_flags!(*args, **kwargs)
39
+ flags_to_strings([args, kwargs]).each do |flag|
40
+ # defined in the C class
41
+ set_flag_as_str!(flag)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def flags_to_strings(flags)
48
+ flags.flatten.map { |flag| flag_to_string(flag) }.flatten
49
+ end
50
+
51
+ # normalize flags to strings, and adds leading dashes if needed
52
+ def flag_to_string(flag)
53
+ if flag.is_a?(Hash)
54
+ flag.map do |key, value|
55
+ "#{flag_to_string(key)} #{value}"
56
+ end
57
+ else
58
+ str = flag.to_s
59
+ str = "--#{str}" unless str.start_with?('--')
60
+ str
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # eval is defined in the C class
67
+ class Context
68
+
69
+ class ExternalFunction
70
+ def initialize(name, callback, parent)
71
+ unless String === name
72
+ raise ArgumentError, "parent_object must be a String"
73
+ end
74
+ parent_object, _ , @name = name.rpartition(".")
75
+ @callback = callback
76
+ @parent = parent
77
+ @parent_object_eval = nil
78
+ @parent_object = nil
79
+
80
+ unless parent_object.empty?
81
+ @parent_object = parent_object
82
+
83
+ @parent_object_eval = ""
84
+ prev = ""
85
+ first = true
86
+ parent_object.split(".").each do |obj|
87
+ prev << obj
88
+ if first
89
+ @parent_object_eval << "if (typeof #{prev} !== 'object' || typeof #{prev} !== 'function') { #{prev} = {} };\n"
90
+ else
91
+ @parent_object_eval << "#{prev} = #{prev} || {};\n"
92
+ end
93
+ prev << "."
94
+ first = false
95
+ end
96
+ @parent_object_eval << "#{parent_object};"
97
+ end
98
+ notify_v8
99
+ end
100
+ end
101
+
102
+ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil)
103
+ options ||= {}
104
+
105
+ check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, marshal_stack_depth: marshal_stack_depth, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout)
106
+
107
+ @functions = {}
108
+ @timeout = nil
109
+ @max_memory = nil
110
+ @current_exception = nil
111
+ @timeout = timeout
112
+ @max_memory = max_memory
113
+ @marshal_stack_depth = marshal_stack_depth
114
+
115
+ # false signals it should be fetched if requested
116
+ @isolate = isolate || false
117
+
118
+ @ensure_gc_after_idle = ensure_gc_after_idle
119
+
120
+ if @ensure_gc_after_idle
121
+ @last_eval = nil
122
+ @ensure_gc_thread = nil
123
+ @ensure_gc_mutex = Mutex.new
124
+ end
125
+
126
+ @disposed = false
127
+
128
+ @callback_mutex = Mutex.new
129
+ @callback_running = false
130
+ @thread_raise_called = false
131
+ @eval_thread = nil
132
+
133
+ # defined in the C class
134
+ init_unsafe(isolate, snapshot)
135
+ end
136
+
137
+ def isolate
138
+ return @isolate if @isolate != false
139
+ # defined in the C class
140
+ @isolate = create_isolate_value
141
+ end
142
+
143
+ def eval(str, options=nil)
144
+ raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
145
+
146
+ filename = options && options[:filename].to_s
147
+
148
+ @eval_thread = Thread.current
149
+ isolate_mutex.synchronize do
150
+ @current_exception = nil
151
+ timeout do
152
+ eval_unsafe(str, filename)
153
+ end
154
+ end
155
+ ensure
156
+ @eval_thread = nil
157
+ ensure_gc_thread if @ensure_gc_after_idle
158
+ end
159
+
160
+ def call(function_name, *arguments)
161
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
162
+
163
+ @eval_thread = Thread.current
164
+ isolate_mutex.synchronize do
165
+ timeout do
166
+ call_unsafe(function_name, *arguments)
167
+ end
168
+ end
169
+ ensure
170
+ @eval_thread = nil
171
+ ensure_gc_thread if @ensure_gc_after_idle
172
+ end
173
+
174
+ def dispose
175
+ return if @disposed
176
+ isolate_mutex.synchronize do
177
+ return if @disposed
178
+ dispose_unsafe
179
+ @disposed = true
180
+ @isolate = nil # allow it to be garbage collected, if set
181
+ end
182
+ end
183
+
184
+
185
+ def attach(name, callback)
186
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
187
+
188
+ wrapped = lambda do |*args|
189
+ begin
190
+
191
+ r = nil
192
+
193
+ begin
194
+ @callback_mutex.synchronize{
195
+ @callback_running = true
196
+ }
197
+ r = callback.call(*args)
198
+ ensure
199
+ @callback_mutex.synchronize{
200
+ @callback_running = false
201
+ }
202
+ end
203
+
204
+ # wait up to 2 seconds for this to be interrupted
205
+ # will very rarely be called cause #raise is called
206
+ # in another mutex
207
+ @callback_mutex.synchronize {
208
+ if @thread_raise_called
209
+ sleep 2
210
+ end
211
+ }
212
+
213
+ r
214
+
215
+ ensure
216
+ @callback_mutex.synchronize {
217
+ @thread_raise_called = false
218
+ }
219
+ end
220
+ end
221
+
222
+ isolate_mutex.synchronize do
223
+ external = ExternalFunction.new(name, wrapped, self)
224
+ @functions["#{name}"] = external
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ def ensure_gc_thread
231
+ @last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
232
+ @ensure_gc_mutex.synchronize do
233
+ @ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
234
+ return if !Thread.main.alive? # avoid "can't alloc thread" exception
235
+ @ensure_gc_thread ||= Thread.new do
236
+ ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0
237
+ done = false
238
+ while !done
239
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
240
+
241
+ if @disposed
242
+ @ensure_gc_thread = nil
243
+ break
244
+ end
245
+
246
+ if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval
247
+ @ensure_gc_mutex.synchronize do
248
+ isolate_mutex.synchronize do
249
+ if !@eval_thread
250
+ low_memory_notification if !@disposed
251
+ @ensure_gc_thread = nil
252
+ done = true
253
+ end
254
+ end
255
+ end
256
+ end
257
+ sleep ensure_gc_after_idle_seconds if !done
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ def stop_attached
264
+ @callback_mutex.synchronize{
265
+ if @callback_running
266
+ @eval_thread.raise ScriptTerminatedError, "Terminated during callback"
267
+ @thread_raise_called = true
268
+ end
269
+ }
270
+ end
271
+
272
+ def timeout(&blk)
273
+ return blk.call unless @timeout
274
+
275
+ mutex = Mutex.new
276
+ done = false
277
+
278
+ rp,wp = IO.pipe
279
+
280
+ t = Thread.new do
281
+ begin
282
+ result = rp.wait_readable(@timeout/1000.0)
283
+ if !result
284
+ mutex.synchronize do
285
+ stop unless done
286
+ end
287
+ end
288
+ rescue => e
289
+ STDERR.puts e
290
+ STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
291
+ end
292
+ end
293
+
294
+ rval = blk.call
295
+ mutex.synchronize do
296
+ done = true
297
+ end
298
+
299
+ wp.close
300
+
301
+ # ensure we do not leak a thread in state
302
+ t.join
303
+ t = nil
304
+
305
+ rval
306
+ ensure
307
+ # exceptions need to be handled
308
+ wp&.close
309
+ t&.join
310
+ rp&.close
311
+ end
312
+
313
+ def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)
314
+ assert_option_is_nil_or_a('isolate', isolate, Isolate)
315
+ assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
316
+
317
+ assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1)
318
+ assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE)
319
+ assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
320
+ assert_numeric_or_nil('timeout', timeout, min_value: 1)
321
+
322
+ if isolate && snapshot
323
+ raise ArgumentError, 'can only pass one of isolate and snapshot options'
324
+ end
325
+ end
326
+
327
+ def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil)
328
+ if max_value && object.is_a?(Numeric) && object > max_value
329
+ raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}"
330
+ end
331
+
332
+ if object.is_a?(Numeric) && object < min_value
333
+ raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}"
334
+ end
335
+
336
+ if !object.nil? && !object.is_a?(Numeric)
337
+ raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}"
338
+ end
339
+ end
340
+
341
+ def assert_option_is_nil_or_a(option_name, object, klass)
342
+ unless object.nil? || object.is_a?(klass)
343
+ raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}"
344
+ end
345
+ end
346
+ end
347
+
348
+ # `size` and `warmup!` public methods are defined in the C class
349
+ class Snapshot
350
+ def initialize(str = '')
351
+ # ensure it first can load
352
+ begin
353
+ ctx = MiniRacer::Context.new
354
+ ctx.eval(str)
355
+ rescue MiniRacer::RuntimeError => e
356
+ raise MiniRacer::SnapshotError, e.message, e.backtrace
357
+ end
358
+
359
+ @source = str
360
+
361
+ # defined in the C class
362
+ load(str)
363
+ end
364
+
365
+ def warmup!(src)
366
+ # we have to do something here
367
+ # we are bloating memory a bit but it is more correct
368
+ # than hitting an exception when attempty to compile invalid source
369
+ begin
370
+ ctx = MiniRacer::Context.new
371
+ ctx.eval(@source)
372
+ ctx.eval(src)
373
+ rescue MiniRacer::RuntimeError => e
374
+ raise MiniRacer::SnapshotError, e.message, e.backtrace
375
+ end
376
+
377
+ warmup_unsafe!(src)
378
+ end
379
+ end
380
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'shared'
4
+
3
5
  module MiniRacer
4
6
 
5
7
  class Context
@@ -43,6 +45,7 @@ module MiniRacer
43
45
  end
44
46
 
45
47
  def heap_stats
48
+ raise ContextDisposedError if @disposed
46
49
  {
47
50
  total_physical_size: 0,
48
51
  total_heap_size_executable: 0,
@@ -60,6 +63,14 @@ module MiniRacer
60
63
  end
61
64
  end
62
65
 
66
+ def low_memory_notification
67
+ GC.start
68
+ end
69
+
70
+ def idle_notification(idle_time)
71
+ true
72
+ end
73
+
63
74
  private
64
75
 
65
76
  @context_initialized = false
@@ -95,9 +106,11 @@ module MiniRacer
95
106
  else
96
107
  @snapshot = nil
97
108
  end
98
- @is_object_or_array_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
109
+ @is_object_or_array_func, @is_map_func, @is_map_iterator_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
99
110
  [
100
111
  (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
112
+ (x) => { return x instanceof Map },
113
+ (x) => { return x[Symbol.toStringTag] === 'Map Iterator' },
101
114
  (x) => { return x instanceof Date },
102
115
  (x) => { return x.getTime(x) },
103
116
  (x) => { return typeof x === 'symbol' },
@@ -233,6 +246,10 @@ module MiniRacer
233
246
  js_date_to_time(value)
234
247
  elsif symbol?(value)
235
248
  js_symbol_to_symbol(value)
249
+ elsif map?(value)
250
+ js_map_to_hash(value)
251
+ elsif map_iterator?(value)
252
+ value.map { |e| convert_js_to_ruby(e) }
236
253
  else
237
254
  object = value
238
255
  h = {}
@@ -251,6 +268,14 @@ module MiniRacer
251
268
  @is_object_or_array_func.call(val)
252
269
  end
253
270
 
271
+ def map?(value)
272
+ @is_map_func.call(value)
273
+ end
274
+
275
+ def map_iterator?(value)
276
+ @is_map_iterator_func.call(value)
277
+ end
278
+
254
279
  def time?(value)
255
280
  @is_time_func.call(value)
256
281
  end
@@ -268,6 +293,12 @@ module MiniRacer
268
293
  @js_symbol_to_symbol_func.call(value).to_s.to_sym
269
294
  end
270
295
 
296
+ def js_map_to_hash(map)
297
+ map.to_a.to_h do |key, value|
298
+ [convert_js_to_ruby(key), convert_js_to_ruby(value)]
299
+ end
300
+ end
301
+
271
302
  def js_new_date(value)
272
303
  @js_new_date_func.call(value)
273
304
  end
@@ -320,14 +351,6 @@ module MiniRacer
320
351
  # However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
321
352
  @snapshot = snapshot
322
353
  end
323
-
324
- def low_memory_notification
325
- GC.start
326
- end
327
-
328
- def idle_notification(idle_time)
329
- true
330
- end
331
354
  end
332
355
 
333
356
  class Platform
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.17.0.pre5"
4
+ VERSION = "0.17.0.pre7"
5
5
  LIBV8_NODE_VERSION = "~> 22.7.0.4"
6
6
  end