Skip to content

Commit

Permalink
Add runtime using GraalJS on TruffleRuby
Browse files Browse the repository at this point in the history
* 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")'
  • Loading branch information
eregon committed Sep 23, 2021
1 parent 73a6717 commit 6c16c8f
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 1 deletion.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
149 changes: 149 additions & 0 deletions lib/execjs/graaljs_runtime.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/execjs/runtimes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,6 +14,8 @@ module Runtimes

RubyRhino = RubyRhinoRuntime.new

GraalJS = GraalJSRuntime.new

MiniRacer = MiniRacerRuntime.new

Node = ExternalRuntime.new(
Expand Down Expand Up @@ -82,6 +85,7 @@ def self.names
def self.runtimes
@runtimes ||= [
RubyRhino,
GraalJS,
Duktape,
MiniRacer,
Node,
Expand Down

0 comments on commit 6c16c8f

Please sign in to comment.