mini_racer 0.1.0 → 0.5.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.
@@ -0,0 +1,8 @@
1
+ require 'mkmf'
2
+
3
+ extension_name = 'mini_racer_loader'
4
+ dir_config extension_name
5
+
6
+ $CPPFLAGS += " -fvisibility=hidden "
7
+
8
+ create_makefile extension_name
@@ -0,0 +1,123 @@
1
+ #include <ruby.h>
2
+ #include <dlfcn.h>
3
+ #include <string.h>
4
+ #include <stdint.h>
5
+ #include <stdlib.h>
6
+
7
+ // Load a Ruby extension like Ruby does, only with flags that:
8
+ // a) hide symbols from other extensions (RTLD_LOCAL)
9
+ // b) bind symbols tightly (RTLD_DEEPBIND, when available)
10
+
11
+ void Init_mini_racer_loader(void);
12
+
13
+ static void *_dln_load(const char *file);
14
+
15
+ static VALUE _load_shared_lib(VALUE self, volatile VALUE fname)
16
+ {
17
+ (void) self;
18
+
19
+ // check that path is not tainted
20
+ SafeStringValue(fname);
21
+
22
+ FilePathValue(fname);
23
+ VALUE path = rb_str_encode_ospath(fname);
24
+
25
+ char *loc = StringValueCStr(path);
26
+ void *handle = _dln_load(loc);
27
+
28
+ return handle ? Qtrue : Qfalse;
29
+ }
30
+
31
+ // adapted from Ruby's dln.c
32
+ #define INIT_FUNC_PREFIX ((char[]) {'I', 'n', 'i', 't', '_'})
33
+ #define INIT_FUNCNAME(buf, file) do { \
34
+ const char *base = (file); \
35
+ const size_t flen = _init_funcname(&base); \
36
+ const size_t plen = sizeof(INIT_FUNC_PREFIX); \
37
+ char *const tmp = ALLOCA_N(char, plen + flen + 1); \
38
+ memcpy(tmp, INIT_FUNC_PREFIX, plen); \
39
+ memcpy(tmp+plen, base, flen); \
40
+ tmp[plen+flen] = '\0'; \
41
+ *(buf) = tmp; \
42
+ } while(0)
43
+
44
+ // adapted from Ruby's dln.c
45
+ static size_t _init_funcname(const char **file)
46
+ {
47
+ const char *p = *file,
48
+ *base,
49
+ *dot = NULL;
50
+
51
+ for (base = p; *p; p++) { /* Find position of last '/' */
52
+ if (*p == '.' && !dot) {
53
+ dot = p;
54
+ }
55
+ if (*p == '/') {
56
+ base = p + 1;
57
+ dot = NULL;
58
+ }
59
+ }
60
+ *file = base;
61
+ return (uintptr_t) ((dot ? dot : p) - base);
62
+ }
63
+
64
+ // adapted from Ruby's dln.c
65
+ static void *_dln_load(const char *file)
66
+ {
67
+ char *buf;
68
+ const char *error;
69
+ #define DLN_ERROR() (error = dlerror(), strcpy(ALLOCA_N(char, strlen(error) + 1), error))
70
+
71
+ void *handle;
72
+ void (*init_fct)(void);
73
+
74
+ INIT_FUNCNAME(&buf, file);
75
+
76
+ #ifndef RTLD_DEEPBIND
77
+ # define RTLD_DEEPBIND 0
78
+ #endif
79
+ /* Load file */
80
+ if ((handle = dlopen(file, RTLD_LAZY|RTLD_LOCAL|RTLD_DEEPBIND)) == NULL) {
81
+ DLN_ERROR();
82
+ goto failed;
83
+ }
84
+ #if defined(RUBY_EXPORT)
85
+ {
86
+ static const char incompatible[] = "incompatible library version";
87
+ void *ex = dlsym(handle, "ruby_xmalloc");
88
+ if (ex && ex != (void *) &ruby_xmalloc) {
89
+
90
+ # if defined __APPLE__
91
+ /* dlclose() segfaults */
92
+ rb_fatal("%s - %s", incompatible, file);
93
+ # else
94
+ dlclose(handle);
95
+ error = incompatible;
96
+ goto failed;
97
+ #endif
98
+ }
99
+ }
100
+ # endif
101
+
102
+ init_fct = (void (*)(void)) dlsym(handle, buf);
103
+ if (init_fct == NULL) {
104
+ error = DLN_ERROR();
105
+ dlclose(handle);
106
+ goto failed;
107
+ }
108
+
109
+ /* Call the init code */
110
+ (*init_fct)();
111
+
112
+ return handle;
113
+
114
+ failed:
115
+ rb_raise(rb_eLoadError, "%s", error);
116
+ }
117
+
118
+ __attribute__((visibility("default"))) void Init_mini_racer_loader()
119
+ {
120
+ VALUE mMiniRacer = rb_define_module("MiniRacer");
121
+ VALUE mLoader = rb_define_module_under(mMiniRacer, "Loader");
122
+ rb_define_singleton_method(mLoader, "load", _load_shared_lib, 1);
123
+ }
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MiniRacer
2
- VERSION = "0.1.0"
4
+ VERSION = "0.5.0"
5
+ LIBV8_NODE_VERSION = "~> 16.10.0.0"
3
6
  end
data/lib/mini_racer.rb CHANGED
@@ -1,13 +1,40 @@
1
1
  require "mini_racer/version"
2
- require "mini_racer_extension"
2
+ require "mini_racer_loader"
3
+ require "pathname"
4
+
5
+ ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
6
+ ext_path = Gem.loaded_specs['mini_racer'].require_paths
7
+ .map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
8
+ ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
9
+
10
+ raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
11
+ MiniRacer::Loader.load(ext_found.to_s)
12
+
3
13
  require "thread"
14
+ require "json"
4
15
 
5
16
  module MiniRacer
6
17
 
7
- class EvalError < StandardError; end
18
+ MARSHAL_STACKDEPTH_DEFAULT = 2**9-2
19
+ MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2
8
20
 
9
- class ScriptTerminatedError < EvalError; end
21
+ class Error < ::StandardError; end
22
+
23
+ class ContextDisposedError < Error; end
24
+ class SnapshotError < Error; end
25
+ class PlatformAlreadyInitialized < Error; end
26
+
27
+ class EvalError < Error; end
10
28
  class ParseError < EvalError; end
29
+ class ScriptTerminatedError < EvalError; end
30
+ class V8OutOfMemoryError < EvalError; end
31
+
32
+ class FailedV8Conversion
33
+ attr_reader :info
34
+ def initialize(info)
35
+ @info = info
36
+ end
37
+ end
11
38
 
12
39
  class RuntimeError < EvalError
13
40
  def initialize(message)
@@ -18,7 +45,6 @@ module MiniRacer
18
45
  else
19
46
  @js_backtrace = nil
20
47
  end
21
-
22
48
  super(message)
23
49
  end
24
50
 
@@ -31,7 +57,6 @@ module MiniRacer
31
57
  val
32
58
  end
33
59
  end
34
-
35
60
  end
36
61
 
37
62
  # helper class returned when we have a JavaScript function
@@ -41,44 +66,388 @@ module MiniRacer
41
66
  end
42
67
  end
43
68
 
69
+ class Isolate
70
+ def initialize(snapshot = nil)
71
+ unless snapshot.nil? || snapshot.is_a?(Snapshot)
72
+ raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}"
73
+ end
74
+
75
+ # defined in the C class
76
+ init_with_snapshot(snapshot)
77
+ end
78
+ end
79
+
80
+ class Platform
81
+ class << self
82
+ def set_flags!(*args, **kwargs)
83
+ flags_to_strings([args, kwargs]).each do |flag|
84
+ # defined in the C class
85
+ set_flag_as_str!(flag)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def flags_to_strings(flags)
92
+ flags.flatten.map { |flag| flag_to_string(flag) }.flatten
93
+ end
94
+
95
+ # normalize flags to strings, and adds leading dashes if needed
96
+ def flag_to_string(flag)
97
+ if flag.is_a?(Hash)
98
+ flag.map do |key, value|
99
+ "#{flag_to_string(key)} #{value}"
100
+ end
101
+ else
102
+ str = flag.to_s
103
+ str = "--#{str}" unless str.start_with?('--')
104
+ str
105
+ end
106
+ end
107
+ end
108
+ end
109
+
44
110
  # eval is defined in the C class
45
111
  class Context
46
112
 
47
113
  class ExternalFunction
48
114
  def initialize(name, callback, parent)
49
- @name = name
115
+ unless String === name
116
+ raise ArgumentError, "parent_object must be a String"
117
+ end
118
+ parent_object, _ , @name = name.rpartition(".")
50
119
  @callback = callback
51
120
  @parent = parent
121
+ @parent_object_eval = nil
122
+ @parent_object = nil
123
+
124
+ unless parent_object.empty?
125
+ @parent_object = parent_object
126
+
127
+ @parent_object_eval = ""
128
+ prev = ""
129
+ first = true
130
+ parent_object.split(".").each do |obj|
131
+ prev << obj
132
+ if first
133
+ @parent_object_eval << "if (typeof #{prev} === 'undefined') { #{prev} = {} };\n"
134
+ else
135
+ @parent_object_eval << "#{prev} = #{prev} || {};\n"
136
+ end
137
+ prev << "."
138
+ first = false
139
+ end
140
+ @parent_object_eval << "#{parent_object};"
141
+ end
52
142
  notify_v8
53
143
  end
54
144
  end
55
145
 
56
- def initialize(options = nil)
146
+ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil)
147
+ options ||= {}
148
+
149
+ 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)
150
+
57
151
  @functions = {}
58
- @lock = Mutex.new
59
152
  @timeout = nil
153
+ @max_memory = nil
60
154
  @current_exception = nil
155
+ @timeout = timeout
156
+ @max_memory = max_memory
157
+ @marshal_stack_depth = marshal_stack_depth
158
+
159
+ # false signals it should be fetched if requested
160
+ @isolate = isolate || false
161
+
162
+ @ensure_gc_after_idle = ensure_gc_after_idle
61
163
 
62
- if options
63
- @timeout = options[:timeout]
164
+ if @ensure_gc_after_idle
165
+ @last_eval = nil
166
+ @ensure_gc_thread = nil
167
+ @ensure_gc_mutex = Mutex.new
64
168
  end
65
169
 
170
+ @disposed = false
171
+
172
+ @callback_mutex = Mutex.new
173
+ @callback_running = false
174
+ @thread_raise_called = false
175
+ @eval_thread = nil
176
+
177
+ # defined in the C class
178
+ init_unsafe(isolate, snapshot)
179
+ end
180
+
181
+ def isolate
182
+ return @isolate if @isolate != false
183
+ # defined in the C class
184
+ @isolate = create_isolate_value
185
+ end
186
+
187
+ def load(filename)
188
+ # TODO do this native cause no need to allocate VALUE here
189
+ eval(File.read(filename))
66
190
  end
67
191
 
68
- def eval(str)
69
- @lock.synchronize do
192
+ def write_heap_snapshot(file_or_io)
193
+ f = nil
194
+ implicit = false
195
+
196
+
197
+ if String === file_or_io
198
+ f = File.open(file_or_io, "w")
199
+ implicit = true
200
+ else
201
+ f = file_or_io
202
+ end
203
+
204
+ if !(File === f)
205
+ raise ArgumentError("file_or_io")
206
+ end
207
+
208
+ write_heap_snapshot_unsafe(f)
209
+
210
+ ensure
211
+ f.close if implicit
212
+ end
213
+
214
+ def eval(str, options=nil)
215
+ raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
216
+
217
+ filename = options && options[:filename].to_s
218
+
219
+ @eval_thread = Thread.current
220
+ isolate_mutex.synchronize do
70
221
  @current_exception = nil
71
- eval_unsafe(str)
222
+ timeout do
223
+ eval_unsafe(str, filename)
224
+ end
225
+ end
226
+ ensure
227
+ @eval_thread = nil
228
+ ensure_gc_thread if @ensure_gc_after_idle
229
+ end
230
+
231
+ def call(function_name, *arguments)
232
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
233
+
234
+ @eval_thread = Thread.current
235
+ isolate_mutex.synchronize do
236
+ timeout do
237
+ call_unsafe(function_name, *arguments)
238
+ end
72
239
  end
240
+ ensure
241
+ @eval_thread = nil
242
+ ensure_gc_thread if @ensure_gc_after_idle
73
243
  end
74
244
 
245
+ def dispose
246
+ return if @disposed
247
+ isolate_mutex.synchronize do
248
+ return if @disposed
249
+ dispose_unsafe
250
+ @disposed = true
251
+ @isolate = nil # allow it to be garbage collected, if set
252
+ end
253
+ end
254
+
255
+
75
256
  def attach(name, callback)
76
- @lock.synchronize do
77
- external = ExternalFunction.new(name, callback, self)
78
- @functions[name.to_s] = external
257
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
258
+
259
+ wrapped = lambda do |*args|
260
+ begin
261
+
262
+ r = nil
263
+
264
+ begin
265
+ @callback_mutex.synchronize{
266
+ @callback_running = true
267
+ }
268
+ r = callback.call(*args)
269
+ ensure
270
+ @callback_mutex.synchronize{
271
+ @callback_running = false
272
+ }
273
+ end
274
+
275
+ # wait up to 2 seconds for this to be interrupted
276
+ # will very rarely be called cause #raise is called
277
+ # in another mutex
278
+ @callback_mutex.synchronize {
279
+ if @thread_raise_called
280
+ sleep 2
281
+ end
282
+ }
283
+
284
+ r
285
+
286
+ ensure
287
+ @callback_mutex.synchronize {
288
+ @thread_raise_called = false
289
+ }
290
+ end
291
+ end
292
+
293
+ isolate_mutex.synchronize do
294
+ external = ExternalFunction.new(name, wrapped, self)
295
+ @functions["#{name}"] = external
296
+ end
297
+ end
298
+
299
+ private
300
+
301
+ def ensure_gc_thread
302
+ @last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
303
+ @ensure_gc_mutex.synchronize do
304
+ @ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
305
+ @ensure_gc_thread ||= Thread.new do
306
+ ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0
307
+ done = false
308
+ while !done
309
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
310
+
311
+ if @disposed
312
+ @ensure_gc_thread = nil
313
+ break
314
+ end
315
+
316
+ if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval
317
+ @ensure_gc_mutex.synchronize do
318
+ isolate_mutex.synchronize do
319
+ if !@eval_thread
320
+ isolate.low_memory_notification if !@disposed
321
+ @ensure_gc_thread = nil
322
+ done = true
323
+ end
324
+ end
325
+ end
326
+ end
327
+ sleep ensure_gc_after_idle_seconds if !done
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ def stop_attached
334
+ @callback_mutex.synchronize{
335
+ if @callback_running
336
+ @eval_thread.raise ScriptTerminatedError, "Terminated during callback"
337
+ @thread_raise_called = true
338
+ end
339
+ }
340
+ end
341
+
342
+ def timeout(&blk)
343
+ return blk.call unless @timeout
344
+
345
+ mutex = Mutex.new
346
+ done = false
347
+
348
+ rp,wp = IO.pipe
349
+
350
+ t = Thread.new do
351
+ begin
352
+ result = IO.select([rp],[],[],(@timeout/1000.0))
353
+ if !result
354
+ mutex.synchronize do
355
+ stop unless done
356
+ end
357
+ end
358
+ rescue => e
359
+ STDERR.puts e
360
+ STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
361
+ end
362
+ end
363
+
364
+ rval = blk.call
365
+ mutex.synchronize do
366
+ done = true
367
+ end
368
+
369
+ wp.write("done")
370
+
371
+ # ensure we do not leak a thread in state
372
+ t.join
373
+ t = nil
374
+
375
+ rval
376
+ ensure
377
+ # exceptions need to be handled
378
+ if t && wp
379
+ wp.write("done")
380
+ t.join
381
+ end
382
+ wp.close if wp
383
+ rp.close if rp
384
+ end
385
+
386
+ def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)
387
+ assert_option_is_nil_or_a('isolate', isolate, Isolate)
388
+ assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
389
+
390
+ assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1)
391
+ assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE)
392
+ assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
393
+ assert_numeric_or_nil('timeout', timeout, min_value: 1)
394
+
395
+ if isolate && snapshot
396
+ raise ArgumentError, 'can only pass one of isolate and snapshot options'
79
397
  end
80
398
  end
81
399
 
400
+ def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil)
401
+ if max_value && object.is_a?(Numeric) && object > max_value
402
+ raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}"
403
+ end
404
+
405
+ if object.is_a?(Numeric) && object < min_value
406
+ raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}"
407
+ end
408
+
409
+ if !object.nil? && !object.is_a?(Numeric)
410
+ raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}"
411
+ end
412
+ end
413
+
414
+ def assert_option_is_nil_or_a(option_name, object, klass)
415
+ unless object.nil? || object.is_a?(klass)
416
+ raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}"
417
+ end
418
+ end
82
419
  end
83
420
 
421
+ # `size` and `warmup!` public methods are defined in the C class
422
+ class Snapshot
423
+ def initialize(str = '')
424
+ # ensure it first can load
425
+ begin
426
+ ctx = MiniRacer::Context.new
427
+ ctx.eval(str)
428
+ rescue MiniRacer::RuntimeError => e
429
+ raise MiniRacer::SnapshotError.new, e.message, e.backtrace
430
+ end
431
+
432
+ @source = str
433
+
434
+ # defined in the C class
435
+ load(str)
436
+ end
437
+
438
+ def warmup!(src)
439
+ # we have to do something here
440
+ # we are bloating memory a bit but it is more correct
441
+ # than hitting an exception when attempty to compile invalid source
442
+ begin
443
+ ctx = MiniRacer::Context.new
444
+ ctx.eval(@source)
445
+ ctx.eval(src)
446
+ rescue MiniRacer::RuntimeError => e
447
+ raise MiniRacer::SnapshotError.new, e.message, e.backtrace
448
+ end
449
+
450
+ warmup_unsafe!(src)
451
+ end
452
+ end
84
453
  end
data/mini_racer.gemspec CHANGED
@@ -11,23 +11,29 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{Minimal embedded v8 for Ruby}
13
13
  spec.description = %q{Minimal embedded v8 engine for Ruby}
14
- spec.homepage = ""
14
+ spec.homepage = "https://github.com/discourse/mini_racer"
15
15
  spec.license = "MIT"
16
16
 
17
+ spec.metadata = {
18
+ "bug_tracker_uri" => "https://github.com/discourse/mini_racer/issues",
19
+ "changelog_uri" => "https://github.com/discourse/mini_racer/blob/v#{spec.version}/CHANGELOG",
20
+ "documentation_uri" => "https://www.rubydoc.info/gems/mini_racer/#{spec.version}",
21
+ "source_code_uri" => "https://github.com/discourse/mini_racer/tree/v#{spec.version}",
22
+ }
17
23
 
18
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(benchmark|test|spec|features)/}) }
19
- spec.bindir = "exe"
20
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(benchmark|test|spec|features|examples)/}) }
21
25
  spec.require_paths = ["lib"]
22
26
 
23
- spec.add_development_dependency "bundler", "~> 1.12"
24
- spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "bundler"
28
+ spec.add_development_dependency "rake", ">= 12.3.3"
25
29
  spec.add_development_dependency "minitest", "~> 5.0"
26
30
  spec.add_development_dependency "rake-compiler"
31
+ spec.add_development_dependency "m"
27
32
 
28
- spec.add_dependency 'libv8', '~> 5.0'
33
+ spec.add_dependency 'libv8-node', MiniRacer::LIBV8_NODE_VERSION
29
34
  spec.require_paths = ["lib", "ext"]
30
35
 
31
- spec.extensions = ["ext/mini_racer_extension/extconf.rb"]
36
+ spec.extensions = ["ext/mini_racer_loader/extconf.rb", "ext/mini_racer_extension/extconf.rb"]
32
37
 
38
+ spec.required_ruby_version = '>= 2.6'
33
39
  end