mini_racer 0.1.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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