diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf514dd..aeff40da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,77 +3,95 @@ on: - push jobs: - test-darwin: - strategy: - fail-fast: false - matrix: - os: - - '10.15' - - '11.0' - platform: - - x86_64 - # arm64 - name: Test (darwin) - runs-on: macos-${{ matrix.os }} + 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: Bundle run: bundle install - - name: Compile - run: bundle exec rake compile + # - name: Compile + # run: bundle exec rake compile - name: Test run: bundle exec rake test - test-linux: - strategy: - fail-fast: false - matrix: - ruby: - - '2.6' - - '2.7' - - '3.0' - - '3.1' - platform: - - amd64 - - arm64 - # arm - # ppc64le - # s390x - libc: - - gnu - - musl - name: Test (linux) - runs-on: ubuntu-20.04 - steps: - - name: Enable ${{ matrix.platform }} platform - id: qemu - if: ${{ matrix.platform != 'amd64' }} - run: | - docker run --privileged --rm tonistiigi/binfmt:latest --install ${{ matrix.platform }} | tee platforms.json - echo "::set-output name=platforms::$(cat platforms.json)" - - name: Start container - id: container - run: | - case ${{ matrix.libc }} in - gnu) - echo 'ruby:${{ matrix.ruby }}' - ;; - musl) - echo 'ruby:${{ matrix.ruby }}-alpine' - ;; - esac > container_image - echo "::set-output name=image::$(cat container_image)" - docker run --rm -d -v "${PWD}":"${PWD}" -w "${PWD}" --platform linux/${{ matrix.platform }} $(cat container_image) /bin/sleep 64d | tee container_id - docker exec -w "${PWD}" $(cat container_id) uname -a - echo "::set-output name=id::$(cat container_id)" - - name: Install Alpine system dependencies - if: ${{ matrix.libc == 'musl' }} - run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base linux-headers bash python2 python3 git curl tar clang binutils-gold - - name: Checkout - uses: actions/checkout@v2 - - name: Bundle - run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle install - - name: Compile - run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle exec rake compile - - name: Test - run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle exec rake test + # test-darwin: + # strategy: + # fail-fast: false + # matrix: + # os: + # - '10.15' + # - '11.0' + # platform: + # - x86_64 + # # arm64 + # name: Test (darwin) + # runs-on: macos-${{ matrix.os }} + # steps: + # - name: Checkout + # uses: actions/checkout@v2 + # - name: Bundle + # run: bundle install + # - name: Compile + # run: bundle exec rake compile + # - name: Test + # run: bundle exec rake test + # test-linux: + # strategy: + # fail-fast: false + # matrix: + # ruby: + # - '2.6' + # - '2.7' + # - '3.0' + # - '3.1' + # platform: + # - amd64 + # - arm64 + # # arm + # # ppc64le + # # s390x + # libc: + # - gnu + # - musl + # name: Test (linux) + # runs-on: ubuntu-20.04 + # steps: + # - name: Enable ${{ matrix.platform }} platform + # id: qemu + # if: ${{ matrix.platform != 'amd64' }} + # run: | + # docker run --privileged --rm tonistiigi/binfmt:latest --install ${{ matrix.platform }} | tee platforms.json + # echo "::set-output name=platforms::$(cat platforms.json)" + # - name: Start container + # id: container + # run: | + # case ${{ matrix.libc }} in + # gnu) + # echo 'ruby:${{ matrix.ruby }}' + # ;; + # musl) + # echo 'ruby:${{ matrix.ruby }}-alpine' + # ;; + # esac > container_image + # echo "::set-output name=image::$(cat container_image)" + # docker run --rm -d -v "${PWD}":"${PWD}" -w "${PWD}" --platform linux/${{ matrix.platform }} $(cat container_image) /bin/sleep 64d | tee container_id + # docker exec -w "${PWD}" $(cat container_id) uname -a + # echo "::set-output name=id::$(cat container_id)" + # - name: Install Alpine system dependencies + # if: ${{ matrix.libc == 'musl' }} + # run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base linux-headers bash python2 python3 git curl tar clang binutils-gold + # - name: Checkout + # uses: actions/checkout@v2 + # - name: Bundle + # run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle install + # - name: Compile + # run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle exec rake compile + # - name: Test + # run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle exec rake test diff --git a/ext/mini_racer_extension/extconf.rb b/ext/mini_racer_extension/extconf.rb index 3d3b21cf..6b617a4f 100644 --- a/ext/mini_racer_extension/extconf.rb +++ b/ext/mini_racer_extension/extconf.rb @@ -1,3 +1,11 @@ +if defined?(TruffleRuby) + File.open("Makefile", "w") do |mf| + mf.puts "# Dummy makefile for TruffleRuby" + mf.puts "all install::\n" + end + return +end + require 'mkmf' require_relative '../../lib/mini_racer/version' gem 'libv8-node', MiniRacer::LIBV8_NODE_VERSION diff --git a/ext/mini_racer_loader/extconf.rb b/ext/mini_racer_loader/extconf.rb index 15d8ddf0..ac8865f5 100644 --- a/ext/mini_racer_loader/extconf.rb +++ b/ext/mini_racer_loader/extconf.rb @@ -1,3 +1,11 @@ +if defined?(TruffleRuby) + File.open("Makefile", "w") do |mf| + mf.puts "# Dummy makefile for TruffleRuby" + mf.puts "all install::\n" + end + return +end + require 'mkmf' extension_name = 'mini_racer_loader' diff --git a/lib/mini_racer.rb b/lib/mini_racer.rb index 7f57d6b4..469c093d 100644 --- a/lib/mini_racer.rb +++ b/lib/mini_racer.rb @@ -1,14 +1,16 @@ require "mini_racer/version" -require "mini_racer_loader" +require "mini_racer_loader" unless defined?(TruffleRuby) 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? } +unless defined?(TruffleRuby) + 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" @@ -451,3 +453,5 @@ def warmup!(src) end end end + +require "mini_racer/truffleruby" if defined?(TruffleRuby) diff --git a/lib/mini_racer/truffleruby.rb b/lib/mini_racer/truffleruby.rb new file mode 100644 index 00000000..3a0a0222 --- /dev/null +++ b/lib/mini_racer/truffleruby.rb @@ -0,0 +1,272 @@ +# frozen_string_literal: true + +require "monitor" + +module MiniRacer + + class Context + 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 @context.respond_to?(:stop) + if @entered + @context.stop + @stopped = true + stop_attached + end + 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" + raise "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`" + + "Note that you need TruffleRuby+GraalVM and not just the TruffleRuby standalone to use #{self.class}" + end + + @context = Polyglot::InnerContext.new + @@context_initialized = true + @js_object = @context.eval('js', 'Object') + @isolate_mutex = Monitor.new + @stopped = false + @entered = false + @has_entered = false + 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 + end + + def dispose_unsafe + @context.close + end + + def eval_unsafe(str, filename) + @entered = true + if !@has_entered && @snapshot + eval_in_context('"use strict;"') if @@use_strict + snapshot_src = encode(@snapshot.instance_variable_get(:@source)) + begin + eval_in_context(snapshot_src) + rescue RuntimeError => e + if e.message == "Polyglot::InnerContext was terminated forcefully" + raise ScriptTerminatedError + else + raise e + end + end + end + @has_entered = true + raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped + raise ArgumentError, "wrong type argument #{str.class} (should be a string)" unless str.kind_of?(String) + raise ArgumentError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.kind_of?(String) + + str = encode(str) + begin + translate do + eval_in_context(str) + end + rescue RuntimeError => e + if e.message == "Polyglot::InnerContext was terminated forcefully" + raise ScriptTerminatedError + else + raise e + end + end + ensure + @entered = false + end + + def call_unsafe(function_name, *arguments) + @entered = true + if !@has_entered && @snapshot + eval_in_context("use strict;") if @@use_strict + src = encode(@snapshot.instance_variable_get(:source)) + begin + eval_in_context(src) + rescue RuntimeError => e + raise e unless e.message == "Polyglot::InnerContext was terminated forcefully" + 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 RuntimeError => e + raise e unless e.message == "Polyglot::InnerContext was terminated forcefully" + 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 + + class ExternalFunction + private + def notify_v8 + name = @name.encode('UTF-8') + if @parent_object.nil? + # set global name to proc + result = @parent.eval_in_context('this') + result[name] = @callback + else + parent_object_eval = @parent_object_eval.encode('UTF-8') + result = @parent.eval_in_context(parent_object_eval) + result[name] = @callback + # set evaluated object results name to proc + end + end + end + + def translate + convert_js_to_ruby yield + rescue ::RuntimeError => e + if e.message.start_with?('SyntaxError:') + error_class = MiniRacer::ParseError + else + error_class = MiniRacer::RuntimeError + end + + backtrace = e.backtrace.map { |line| line.sub('(eval)', '(mini_racer)') } + raise error_class, e.message, backtrace + 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 + 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 + 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 convert_ruby_to_js(value) + case value + when nil, true, false, Integer, Float, String + value + when Array + value.map { |e| convert_ruby_to_js(e) } + when Hash + h = @js_object.new + value.each_pair do |k,v| + h[convert_ruby_to_js(k)] = convert_ruby_to_js(v) + end + h + when Symbol # ? + value.to_s + when Time + value.to_f + when DateTime + value.to_time.to_f + else + "Undefined Conversion" + end + end + + def encode(string) + raise ArgumentError unless string + string.encode('UTF-8') + string.encode(::Encoding::UTF_8) + end + + class_eval <<-'RUBY', "(mini_racer)", 1 + def eval_in_context(code); @context.eval('js', code); 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 napshot, 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 ArgumentError, "wrong type argument #{flag.class} (should be a string)" unless flag.kind_of?(String) + raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.class_variable_get(:@@context_initialized) + Context.class_variable_set(:@@use_strict, true) if "--use_strict" == flag + end + end + + class Snapshot + def load(str) + raise ArgumentError, "wrong type argument #{str.class} (should be a string)" unless str.kind_of?(String) + # Intentionally noop since TruffleRuby mocks the snapshot API + end + + def warmup_unsafe!(src) + # Intentionally noop since TruffleRuby mocks the snapshot API + # by replaying snapshot source before the first eval/call + self + end + end +end \ No newline at end of file diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 44469332..85b5f5a1 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -99,7 +99,7 @@ def test_it_can_stop assert_equal MiniRacer::ScriptTerminatedError, exp.class assert_match(/terminated/, exp.message) - end + end unless defined?(TruffleRuby) && !Polyglot::InnerContext.instance_methods.include?(:stop) def test_it_can_timeout_during_serialization context = MiniRacer::Context.new(timeout: 500) @@ -107,7 +107,7 @@ def test_it_can_timeout_during_serialization assert_raises(MiniRacer::ScriptTerminatedError) do context.eval 'var a = {get a(){ while(true); }}; a' end - end + end unless defined?(TruffleRuby) #&& !Polyglot::InnerContext.instance_methods.include?(:stop) def test_it_can_automatically_time_out_context # 2 millisecs is a very short timeout but we don't want test running forever @@ -115,7 +115,7 @@ def test_it_can_automatically_time_out_context assert_raises do context.eval('while(true){}') end - end + end unless defined?(TruffleRuby) && !Polyglot::InnerContext.instance_methods.include?(:stop) def test_returns_javascript_function context = MiniRacer::Context.new @@ -302,12 +302,14 @@ def test_return_unknown end def test_max_memory + skip "TruffleRuby does not yet implement max_memory" if defined?(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 defined?(TruffleRuby) context = MiniRacer::Context.new(max_memory: 100_000_000) context.eval(<<~JS) let s; @@ -399,6 +401,7 @@ def test_it_can_use_snapshots end def test_snapshot_size + skip "TruffleRuby does not yet implement snapshots" if defined?(TruffleRuby) snapshot = MiniRacer::Snapshot.new('var foo = "bar";') # for some reason sizes seem to change across runs, so we just @@ -407,6 +410,7 @@ def test_snapshot_size end def test_snapshot_dump + skip "TruffleRuby does not yet implement snapshots" if defined?(TruffleRuby) snapshot = MiniRacer::Snapshot.new('var foo = "bar";') dump = snapshot.dump @@ -604,7 +608,7 @@ def test_concurrent_access_over_the_same_isolate_2 assert_equal 10, equals_after_sleep.size assert equals_after_sleep.values.all? - end + end unless defined?(TruffleRuby) # unsafe hash concurrent access def test_platform_set_flags_raises_an_exception_if_already_initialized # makes sure it's initialized @@ -842,6 +846,7 @@ def test_isolate_is_nil_after_disposal end def test_heap_dump + skip "TruffleRuby does not yet implement heap_dump" if defined?(TruffleRuby) f = Tempfile.new("heap") path = f.path f.unlink @@ -1021,5 +1026,5 @@ def test_timeout doit(); JS end - end + end unless defined?(TruffleRuby) && !Polyglot::InnerContext.instance_methods.include?(:stop) end diff --git a/test/test_forking.rb b/test/test_forking.rb index c849f0ed..05bc5d45 100644 --- a/test/test_forking.rb +++ b/test/test_forking.rb @@ -23,6 +23,6 @@ def trigger_gc MiniRacer::Context.new.dispose -Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" } +Process.wait fork { puts @ctx.eval("a"); @ctx.dispose; puts Process.pid; trigger_gc; puts "done #{Process.pid}" } if Process.respond_to?(:fork)