FEATURE: Implement MiniRacer for TruffleRuby (#253)

This commit is contained in:
Brandon Fish 2022-07-21 00:02:32 -05:00 committed by GitHub
parent 7419fd154e
commit e0f5a7ac66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 436 additions and 23 deletions

View File

@ -3,6 +3,26 @@ on:
- push
jobs:
test-truffleruby:
name: Test TruffleRuby
runs-on: ubuntu-20.04
env:
TRUFFLERUBYOPT: "--jvm --polyglot"
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: truffleruby+graalvm-head
- name: Install GraalVM js component
run: if ! gu list | grep '^js '; then gu install js; fi
- name: Bundle
run: bundle install
- name: Compile
run: bundle exec rake compile
- name: Test
run: bundle exec rake test
test-darwin:
strategy:
fail-fast: false

View File

@ -1,6 +1,5 @@
require "bundler/gem_tasks"
require "rake/testtask"
require "rake/extensiontask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
@ -11,8 +10,21 @@ end
task :default => [:compile, :test]
gem = Gem::Specification.load( File.dirname(__FILE__) + '/mini_racer.gemspec' )
Rake::ExtensionTask.new( 'mini_racer_loader', gem )
Rake::ExtensionTask.new( 'mini_racer_extension', gem )
if RUBY_ENGINE == "truffleruby"
task :compile do
# noop
end
task :clean do
# noop
end
else
require 'rake/extensiontask'
Rake::ExtensionTask.new( 'mini_racer_loader', gem )
Rake::ExtensionTask.new( 'mini_racer_extension', gem )
end
# via http://blog.flavorjon.es/2009/06/easily-valgrind-gdb-your-ruby-c.html

View File

@ -1,4 +1,10 @@
require 'mkmf'
if RUBY_ENGINE == "truffleruby"
File.write("Makefile", dummy_makefile($srcdir).join(""))
return
end
require_relative '../../lib/mini_racer/version'
gem 'libv8-node', MiniRacer::LIBV8_NODE_VERSION
require 'libv8-node'

View File

@ -1,5 +1,10 @@
require 'mkmf'
if RUBY_ENGINE == "truffleruby"
File.write("Makefile", dummy_makefile($srcdir).join(""))
return
end
extension_name = 'mini_racer_loader'
dir_config extension_name

View File

@ -1,14 +1,18 @@
require "mini_racer/version"
require "mini_racer_loader"
require "pathname"
ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
ext_path = Gem.loaded_specs['mini_racer'].require_paths
.map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
if RUBY_ENGINE == "truffleruby"
require "mini_racer/truffleruby"
else
require "mini_racer_loader"
ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
ext_path = Gem.loaded_specs['mini_racer'].require_paths
.map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
MiniRacer::Loader.load(ext_found.to_s)
raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
MiniRacer::Loader.load(ext_found.to_s)
end
require "thread"
require "json"

View File

@ -0,0 +1,353 @@
# frozen_string_literal: true
module MiniRacer
class Context
class ExternalFunction
private
def notify_v8
name = @name.encode(::Encoding::UTF_8)
wrapped = lambda do |*args|
converted = @parent.send(:convert_js_to_ruby, args)
begin
result = @callback.call(*converted)
rescue Polyglot::ForeignException => e
e = RuntimeError.new(e.message)
e.set_backtrace(e.backtrace)
@parent.instance_variable_set(:@current_exception, e)
raise e
rescue => e
@parent.instance_variable_set(:@current_exception, e)
raise e
end
@parent.send(:convert_ruby_to_js, result)
end
if @parent_object.nil?
# set global name to proc
result = @parent.eval_in_context('this')
result[name] = wrapped
else
parent_object_eval = @parent_object_eval.encode(::Encoding::UTF_8)
begin
result = @parent.eval_in_context(parent_object_eval)
rescue Polyglot::ForeignException, StandardError => e
raise ParseError, "Was expecting #{@parent_object} to be an object", e.backtrace
end
result[name] = wrapped
# set evaluated object results name to proc
end
end
end
def heap_stats
{
total_physical_size: 0,
total_heap_size_executable: 0,
total_heap_size: 0,
used_heap_size: 0,
heap_size_limit: 0,
}
end
def stop
if @entered
@context.stop
@stopped = true
stop_attached
end
end
private
@context_initialized = false
@use_strict = false
def init_unsafe(isolate, snapshot)
unless defined?(Polyglot::InnerContext)
raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version"
end
unless Polyglot.languages.include? "js"
warn "You also need to install the 'js' component with 'gu install js' on GraalVM 22.2+", uplevel: 0 if $VERBOSE
end
@context = Polyglot::InnerContext.new(on_cancelled: -> {
raise ScriptTerminatedError, 'JavaScript was terminated (either by timeout or explicitly)'
})
Context.instance_variable_set(:@context_initialized, true)
@js_object = @context.eval('js', 'Object')
@isolate_mutex = Mutex.new
@stopped = false
@entered = false
@has_entered = false
@current_exception = nil
if isolate && snapshot
isolate.instance_variable_set(:@snapshot, snapshot)
end
if snapshot
@snapshot = snapshot
elsif isolate
@snapshot = isolate.instance_variable_get(:@snapshot)
else
@snapshot = nil
end
@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
[
(x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
(x) => { return x instanceof Date },
(x) => { return x.getTime(x) },
(x) => { return typeof x === 'symbol' },
(x) => { var r = x.description; return r === undefined ? 'undefined' : r },
(x) => { return new Date(x) },
(x) => { return new Array(x) },
]
CODE
end
def dispose_unsafe
@context.close
end
def eval_unsafe(str, filename)
@entered = true
if !@has_entered && @snapshot
snapshot_src = encode(@snapshot.instance_variable_get(:@source))
begin
eval_in_context(snapshot_src, filename)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
raise TypeError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.is_a?(String)
str = encode(str)
begin
translate do
eval_in_context(str, filename)
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
rescue ::RuntimeError => e
if @current_exception
e = @current_exception
@current_exception = nil
raise e
else
raise e, e.message
end
end
ensure
@entered = false
end
def call_unsafe(function_name, *arguments)
@entered = true
if !@has_entered && @snapshot
src = encode(@snapshot.instance_variable_get(:source))
begin
eval_in_context(src)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
begin
translate do
function = eval_in_context(function_name)
function.call(*convert_ruby_to_js(arguments))
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
ensure
@entered = false
end
def create_isolate_value
# Returning a dummy object since TruffleRuby does not have a 1-1 concept with isolate.
# However, code and ASTs are shared between contexts.
Isolate.new
end
def isolate_mutex
@isolate_mutex
end
def translate
convert_js_to_ruby yield
rescue Object => e
message = e.message
if @current_exception
raise @current_exception
elsif e.message && e.message.start_with?('SyntaxError:')
error_class = MiniRacer::ParseError
elsif e.is_a?(MiniRacer::ScriptTerminatedError)
error_class = MiniRacer::ScriptTerminatedError
else
error_class = MiniRacer::RuntimeError
end
if error_class == MiniRacer::RuntimeError
bls = e.backtrace_locations&.select { |bl| bl&.source_location&.language == 'js' }
if bls && !bls.empty?
if '(eval)' != bls[0].path
message = "#{e.message}\n at #{bls[0]}\n" + bls[1..].map(&:to_s).join("\n")
else
message = "#{e.message}\n" + bls.map(&:to_s).join("\n")
end
end
raise error_class, message
else
raise error_class, message, e.backtrace
end
end
def convert_js_to_ruby(value)
case value
when true, false, Integer, Float
value
else
if value.nil?
nil
elsif value.respond_to?(:call)
MiniRacer::JavaScriptFunction.new
elsif value.respond_to?(:to_str)
value.to_str.dup
elsif value.respond_to?(:to_ary)
value.to_ary.map do |e|
if e.respond_to?(:call)
nil
else
convert_js_to_ruby(e)
end
end
elsif time?(value)
js_date_to_time(value)
elsif symbol?(value)
js_symbol_to_symbol(value)
else
object = value
h = {}
object.instance_variables.each do |member|
v = object[member]
unless v.respond_to?(:call)
h[member.to_s] = convert_js_to_ruby(v)
end
end
h
end
end
end
def object_or_array?(val)
@is_object_or_array_func.call(val)
end
def time?(value)
@is_time_func.call(value)
end
def js_date_to_time(value)
millis = @js_date_to_time_func.call(value)
Time.at(Rational(millis, 1000))
end
def symbol?(value)
@is_symbol_func.call(value)
end
def js_symbol_to_symbol(value)
@js_symbol_to_symbol_func.call(value).to_s.to_sym
end
def js_new_date(value)
@js_new_date_func.call(value)
end
def js_new_array(size)
@js_new_array_func.call(size)
end
def convert_ruby_to_js(value)
case value
when nil, true, false, Integer, Float
value
when Array
ary = js_new_array(value.size)
value.each_with_index do |v, i|
ary[i] = convert_ruby_to_js(v)
end
ary
when Hash
h = @js_object.new
value.each_pair do |k, v|
h[convert_ruby_to_js(k.to_s)] = convert_ruby_to_js(v)
end
h
when String, Symbol
Truffle::Interop.as_truffle_string value
when Time
js_new_date(value.to_f * 1000)
when DateTime
js_new_date(value.to_time.to_f * 1000)
else
"Undefined Conversion"
end
end
def encode(string)
raise ArgumentError unless string
string.encode(::Encoding::UTF_8)
end
class_eval <<-'RUBY', "(mini_racer)", 1
def eval_in_context(code, file = nil); code = ('"use strict";' + code) if Context.instance_variable_get(:@use_strict); @context.eval('js', code, file || '(mini_racer)'); end
RUBY
end
class Isolate
def init_with_snapshot(snapshot)
# TruffleRuby does not have a 1-1 concept with isolate.
# However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
@snapshot = snapshot
end
def low_memory_notification
GC.start
end
def idle_notification(idle_time)
true
end
end
class Platform
def self.set_flag_as_str!(flag)
raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
end
end
class Snapshot
def load(str)
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
end
def warmup_unsafe!(src)
raise TypeError, "wrong type argument #{src.class} (should be a string)" unless src.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
# by replaying snapshot source before the first eval/call
self
end
end
end

View File

@ -35,7 +35,8 @@ class MiniRacerFunctionTest < Minitest::Test
context.call('f', 1)
end
assert_equal err.message, 'Error: foo bar'
assert_match(/1:23/, err.backtrace[0])
assert_match(/1:23/, err.backtrace[0]) unless RUBY_ENGINE == "truffleruby"
assert_match(/1:/, err.backtrace[0]) if RUBY_ENGINE == "truffleruby"
end
def test_args_types

View File

@ -10,6 +10,7 @@ class MiniRacerTest < Minitest::Test
def test_locale
skip "TruffleRuby does not have all js timezone by default" if RUBY_ENGINE == "truffleruby"
val = MiniRacer::Context.new.eval("new Date('April 28 2021').toLocaleDateString('es-MX');")
assert_equal '28/4/2021', val
@ -88,7 +89,7 @@ class MiniRacerTest < Minitest::Test
begin
Thread.new do
sleep 0.001
sleep 0.01
context.stop
end
context.eval('while(true){}')
@ -102,6 +103,7 @@ class MiniRacerTest < Minitest::Test
end
def test_it_can_timeout_during_serialization
skip "TruffleRuby needs a fix for timing out during translation" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(timeout: 500)
assert_raises(MiniRacer::ScriptTerminatedError) do
@ -302,12 +304,14 @@ raise FooError, "I like foos"
end
def test_max_memory
skip "TruffleRuby does not yet implement max_memory" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(max_memory: 200_000_000)
assert_raises(MiniRacer::V8OutOfMemoryError) { context.eval('let s = 1000; var a = new Array(s); a.fill(0); while(true) {s *= 1.1; let n = new Array(Math.floor(s)); n.fill(0); a = a.concat(n); };') }
end
def test_max_memory_for_call
skip "TruffleRuby does not yet implement max_memory" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(max_memory: 100_000_000)
context.eval(<<~JS)
let s;
@ -399,6 +403,7 @@ raise FooError, "I like foos"
end
def test_snapshot_size
skip "TruffleRuby does not yet implement snapshots" if RUBY_ENGINE == "truffleruby"
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
# for some reason sizes seem to change across runs, so we just
@ -407,6 +412,7 @@ raise FooError, "I like foos"
end
def test_snapshot_dump
skip "TruffleRuby does not yet implement snapshots" if RUBY_ENGINE == "truffleruby"
snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
dump = snapshot.dump
@ -581,13 +587,11 @@ raise FooError, "I like foos"
def test_concurrent_access_over_the_same_isolate_2
isolate = MiniRacer::Isolate.new
equals_after_sleep = {}
# workaround Rubies prior to commit 475c8701d74ebebe
# (Make SecureRandom support Ractor, 2020-09-04)
SecureRandom.hex
(1..10).map do |i|
equals_after_sleep = (1..10).map do |i|
Thread.new {
random = SecureRandom.hex
context = MiniRacer::Context.new(isolate: isolate)
@ -598,12 +602,12 @@ raise FooError, "I like foos"
# cruby hashes are thread safe as long as you don't mess with the
# same key in different threads
equals_after_sleep[i] = context.eval('a') == random
context.eval('a') == random
}
end.each(&:join)
end.map(&:value)
assert_equal 10, equals_after_sleep.size
assert equals_after_sleep.values.all?
assert equals_after_sleep.all?
end
def test_platform_set_flags_raises_an_exception_if_already_initialized
@ -711,6 +715,7 @@ raise FooError, "I like foos"
end
def test_estimated_size
skip "TruffleRuby does not yet implement heap_stats" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(timeout: 5)
context.eval("let a='testing';")
@ -780,7 +785,7 @@ raise FooError, "I like foos"
def test_estimated_size_when_disposed
context = MiniRacer::Context.new(timeout: 5)
context = MiniRacer::Context.new(timeout: 50)
context.eval("let a='testing';")
context.dispose
@ -805,7 +810,7 @@ raise FooError, "I like foos"
end
def test_attached_recursion
context = MiniRacer::Context.new(timeout: 20)
context = MiniRacer::Context.new(timeout: 200)
context.attach("a", proc{|a| a})
context.attach("b", proc{|a| a})
@ -842,6 +847,7 @@ raise FooError, "I like foos"
end
def test_heap_dump
skip "TruffleRuby does not yet implement heap_dump" if RUBY_ENGINE == "truffleruby"
f = Tempfile.new("heap")
path = f.path
f.unlink
@ -873,6 +879,7 @@ raise FooError, "I like foos"
end
def test_cyclical_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -880,6 +887,7 @@ raise FooError, "I like foos"
end
def test_cyclical_array_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -887,6 +895,7 @@ raise FooError, "I like foos"
end
def test_cyclical_elem_in_array_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -894,6 +903,7 @@ raise FooError, "I like foos"
end
def test_infinite_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -911,6 +921,7 @@ raise FooError, "I like foos"
end
def test_deep_object_js
skip "TruffleRuby does not yet implement marshal_stack_depth" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new(marshal_stack_depth: 5)
context.attach("a", proc{|a| a})
@ -966,6 +977,7 @@ raise FooError, "I like foos"
end
def test_webassembly
skip "TruffleRuby does not enable WebAssembly by default" if RUBY_ENGINE == "truffleruby"
context = MiniRacer::Context.new()
context.eval("let instance = null;")
filename = File.expand_path("../support/add.wasm", __FILE__)

View File

@ -23,6 +23,6 @@ trigger_gc
MiniRacer::Context.new.dispose
Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" }
if Process.respond_to?(:fork)
Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" }
end