From 6c16c8f6ed40f5c67f1687bb8bcc84fbc7dcb79c Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sun, 22 Nov 2020 16:20:48 +0100 Subject: [PATCH] Add runtime using GraalJS on TruffleRuby * Use Truffle inner contexts to provide correct isolation between ExecJS::Context * To run the tests: TRUFFLERUBYOPT="--jvm --polyglot" bundle exec rake test:graaljs TESTOPTS="--seed=0 --verbose" * Full command without subprocess: TRUFFLERUBYOPT="--jvm --polyglot" jt -u jvm-js ruby -w -Ilib:test -I $PWD/vendor/bundle/truffleruby/*/gems/rake-13.0.1/lib $PWD/vendor/bundle/truffleruby/*/gems/rake-13.0.1/lib/rake/rake_test_loader.rb test/test_execjs.rb --seed=0 --verbose * Try command: TRUFFLERUBYOPT="--jvm --polyglot" jt -u jvm-js ruby -Ilib -rexecjs -e 'p ExecJS.eval("2 + 3")' --- .github/workflows/ci.yml | 7 +- lib/execjs/graaljs_runtime.rb | 149 ++++++++++++++++++++++++++++++++++ lib/execjs/runtimes.rb | 4 + 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 lib/execjs/graaljs_runtime.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a80c190..02fb178 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ '3.0', '2.7', '2.6', '2.5', 'jruby', 'truffleruby' ] + ruby: [ '3.0', '2.7', '2.6', '2.5', 'jruby', 'truffleruby', 'truffleruby+graalvm-head' ] runs-on: ubuntu-latest steps: - name: Checkout @@ -18,12 +18,17 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + - name: Update Rubygems run: gem update --system - name: Install bundler run: gem install bundler -v '2.2.16' - name: Install dependencies run: bundle install + + - name: Set TRUFFLERUBYOPT + run: echo "TRUFFLERUBYOPT=--jvm --polyglot" >> $GITHUB_ENV + if: matrix.ruby == 'truffleruby+graalvm-head' - name: Run test run: rake - name: Install gem diff --git a/lib/execjs/graaljs_runtime.rb b/lib/execjs/graaljs_runtime.rb new file mode 100644 index 0000000..f8e8818 --- /dev/null +++ b/lib/execjs/graaljs_runtime.rb @@ -0,0 +1,149 @@ +require "execjs/runtime" + +module ExecJS + class GraalJSRuntime < Runtime + class Context < Runtime::Context + def initialize(runtime, source = "", options = {}) + @context = Polyglot::InnerContext.new + @context.eval('js', 'delete this.console') + @js_object = @context.eval('js', 'Object') + + source = encode(source) + + @source = source + unless source.empty? + translate do + eval_in_context(source) + end + end + end + + def exec(source, options = {}) + source = encode(source) + source = "(function(){#{source}})()" if /\S/.match?(source) + source = "#{@source};\n#{source}" unless @source.empty? + + translate do + eval_in_context(source) + end + end + + def eval(source, options = {}) + source = encode(source) + source = "(#{source})" if /\S/.match?(source) + source = "#{@source};\n#{source}" unless @source.empty? + + translate do + eval_in_context(source) + end + end + + def call(source, *args) + source = encode(source) + source = "(#{source})" if /\S/.match?(source) + source = "#{@source};\n#{source}" unless @source.empty? + + translate do + function = eval_in_context(source) + function.call(*convert_ruby_to_js(args)) + end + end + + private + + def translate + begin + convert_js_to_ruby yield + rescue ::RuntimeError => e + if e.message.start_with?('SyntaxError:') + error_class = ExecJS::RuntimeError + else + error_class = ExecJS::ProgramError + end + + backtrace = e.backtrace.map { |line| line.sub('(eval)', '(execjs)') } + raise error_class, e.message, 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) + nil + 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 + else + raise TypeError, "Unknown how to convert to JS: #{value.inspect}" + end + end + + class_eval <<-'RUBY', "(execjs)", 1 + def eval_in_context(code); @context.eval('js', code); end + RUBY + end + + def name + "GraalVM (Graal.js)" + end + + def available? + return @available if defined?(@available) + + unless RUBY_ENGINE == "truffleruby" + return @available = false + end + + unless defined?(Polyglot::InnerContext) + warn "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version", uplevel: 0 + return @available = false + end + + unless Polyglot.languages.include? "js" + warn "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`", uplevel: 0 + warn "Note that you need TruffleRuby+GraalVM and not just the TruffleRuby standalone to use #{self.class}", uplevel: 0 + return @available = false + end + + @available = true + end + end +end diff --git a/lib/execjs/runtimes.rb b/lib/execjs/runtimes.rb index 19d9d96..615ecd6 100644 --- a/lib/execjs/runtimes.rb +++ b/lib/execjs/runtimes.rb @@ -4,6 +4,7 @@ require "execjs/external_runtime" require "execjs/ruby_rhino_runtime" require "execjs/mini_racer_runtime" +require "execjs/graaljs_runtime" module ExecJS module Runtimes @@ -13,6 +14,8 @@ module Runtimes RubyRhino = RubyRhinoRuntime.new + GraalJS = GraalJSRuntime.new + MiniRacer = MiniRacerRuntime.new Node = ExternalRuntime.new( @@ -82,6 +85,7 @@ def self.names def self.runtimes @runtimes ||= [ RubyRhino, + GraalJS, Duktape, MiniRacer, Node,